From dcf7137ccc81986091b6c76624855bb5c32185f7 Mon Sep 17 00:00:00 2001 From: the-nose-knows Date: Fri, 8 Mar 2019 23:01:42 -0800 Subject: [PATCH] upstream sync (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ERR/TST: Add pytest idiom to dtypes/test_cast.py (#24847) * fix MacPython pandas-wheels failue (#24851) * DEPS: Bump pyarrow min version to 0.9.0 (#24854) Closes gh-24767 * DOC: Document AttributeError for accessor (#24855) Closes https://github.com/pandas-dev/pandas/issues/20579 * Start whatsnew for 0.24.1 and 0.25.0 (#24848) * DEPR/API: Non-ns precision in Index constructors (#24806) * BUG: Format mismatch doesn't coerce to NaT (#24815) * BUG: Properly parse unicode usecols names in CSV (#24856) * CLN: fix typo in asv eval.Query suite (#24865) * BUG: DataFrame respects dtype with masked recarray (#24874) * REF/CLN: Move private method (#24875) * BUG : ValueError in case on NaN value in groupby columns (#24850) * BUG: fix floating precision formatting in presence of inf (#24863) * DOC: Creating top-level user guide section, and moving pages inside (#24677) * DOC: Creating top-level development section, and moving pages inside (#24691) * DOC: Creating top-level getting started section, and moving pages inside (#24678) * DOC: Implementing redirect system, and adding user_guide redirects (#24715) * DOC: Implementing redirect system, and adding user_guide redirects * Using relative urls for the redirect * Validating that no file is overwritten by a redirect * Adding redirects for getting started and development sections * DOC: fixups (#24888) * Fixed heading on whatnew * Remove empty scalars.rst * CLN: fix typo in ctors.SeriesDtypesConstructors setup (#24894) * DOC: No clean in sphinx_build (#24902) Closes https://github.com/pandas-dev/pandas/issues/24727 * BUG (output formatting): use fixed with for truncation column instead of inferring from last column (#24905) * DOC: also redirect old whatsnew url (#24906) * Revert BUG-24212 fix usage of Index.take in pd.merge (#24904) * Revert BUG-24212 fix usage of Index.take in pd.merge xref https://github.com/pandas-dev/pandas/pull/24733/ xref https://github.com/pandas-dev/pandas/issues/24897 * test 0.23.4 output * added note about buggy test * DOC: Add experimental note to DatetimeArray and TimedeltaArray (#24882) * DOC: Add experimental note to DatetimeArray and TimedeltaArray * Disable M8 in nanops (#24907) * Disable M8 in nanops Closes https://github.com/pandas-dev/pandas/issues/24752 * CLN: fix typo in asv benchmark of non_unique_sorted, which was not sorted (#24917) * API/VIS: remove misc plotting methods from plot accessor (revert #23811) (#24912) * DOC: some 0.24.0 whatsnew clean-up (#24911) * DOC: Final reorganization of documentation pages (#24890) * DOC: Final reorganization of documentation pages * Move ecosystem to top level * DOC: Adding redirects to API moved pages (#24909) * DOC: Adding redirects to API moved pages * DOC: Making home page links more compact and clearer (#24928) * DOC: 0.24 release date (#24930) * DOC: Adding version to the whatsnew section in the home page (#24929) * API: Remove IntervalArray from top-level (#24926) * RLS: 0.24.0 * DEV: Start 0.25 cycle * DOC: State that we support scalars in to_numeric (#24944) We support it and test it already. xref gh-24910. * DOC: Minor what's new fix (#24933) * TST: GH#23922 Add missing match params to pytest.raises (#24937) * Add tests for NaT when performing dt.to_period (#24921) * DOC: switch headline whatsnew to 0.25 (#24941) * BUG-24212 fix regression in #24897 (#24916) * CLN: reduce overhead in setup for categoricals benchmarks in asv (#24913) * Excel Reader Refactor - Base Class Introduction (#24829) * TST/REF: Add pytest idiom to test_numeric.py (#24946) * BLD: silence npy_no_deprecated warnings with numpy>=1.16.0 (#24864) * CLN: Refactor cython to use memory views (#24932) * DOC: Clean sort_values and sort_index docstrings (#24843) * STY: use pytest.raises context syntax (indexing) (#24960) * Fixed itertuples usage in to_dict (#24965) * Fixed itertuples usage in to_dict Closes https://github.com/pandas-dev/pandas/issues/24940 Closes https://github.com/pandas-dev/pandas/issues/24939 * STY: use pytest.raises context manager (resample) (#24977) * DOC: Document breaking change to read_csv (#24989) * DEPR: Fixed warning for implicit registration (#24964) * STY: use pytest.raises context manager (indexes/datetimes) (#24995) * DOC: move whatsnew note of #24916 (#24999) * BUG: Fix broken links (#25002) The previous location of contributing.rst file was /doc/source/contributing.rst but has been moved to /doc/source/development/contributing.rst * fix for BUG: grouping with tz-aware: Values falls after last bin (#24973) * REGR: Preserve order by default in Index.difference (#24967) Closes https://github.com/pandas-dev/pandas/issues/24959 * CLN: do not use .repeat asv setting for storing benchmark data (#25015) * CLN: isort asv_bench/benchmark/algorithms.py (#24958) * fix+test to_timedelta('NaT', box=False) (#24961) * PERF: significant speedup in sparse init and ops by using numpy in check_integrity (#24985) * BUG: Fixed merging on tz-aware (#25033) * Test nested PandasArray (#24993) * DOC: fix error in documentation #24981 (#25038) * BUG: support dtypes in column_dtypes for to_records() (#24895) * Makes example from docstring work (#25035) * CLN: typo fixups (#25028) * BUG: to_datetime(strs, utc=True) used previous UTC offset (#25020) * BUG: Better handle larger numbers in to_numeric (#24956) * BUG: Better handle larger numbers in to_numeric * Warn about lossiness when passing really large numbers that exceed (u)int64 ranges. * Coerce negative numbers to float when requested instead of crashing and returning object. * Consistently parse numbers as integers / floats, even if we know that the resulting container has to be float. This is to ensure consistent error behavior when inputs numbers are too large. Closes gh-24910. * MAINT: Address comments * BUG: avoid usage in_qtconsole for recent IPython versions (#25039) * Drop IPython<4.0 compat * Revert "Drop IPython<4.0 compat" This reverts commit 0cb0452b31431143ba22b7ad41bf4d3d9d878522. * update a * whatsnew * REGR: fix read_sql delegation for queries on MySQL/pymysql (#25024) * DOC: Start 0.24.2.rst (#25026) [ci skip] * REGR: rename_axis with None should remove axis name (#25069) * clarified the documentation for DF.drop_duplicates (#25056) * Clarification in docstring of Series.value_counts (#25062) * ENH: Support fold argument in Timestamp.replace (#25046) * CLN: to_pickle internals (#25044) * Implement+Test Tick.__rtruediv__ (#24832) * API: change Index set ops sort=True -> sort=None (#25063) * BUG: to_clipboard text truncated for Python 3 on Windows for UTF-16 text (#25040) * PERF: use new to_records() argument in to_stata() (#25045) * DOC: Cleanup 0.24.1 whatsnew (#25084) * Fix quotes position in pandas.core, typos and misspelled parameters. (#25093) * CLN: Remove sentinel_factory() in favor of object() (#25074) * TST: remove DST transition scenarios from tc #24689 (#24736) * BLD: remove spellcheck from Makefile (#25111) * DOC: small clean-up of 0.24.1 whatsnew (#25096) * DOC: small doc fix to Series.repeat (#25115) * TST: tests for categorical apply (#25095) * CLN: use dtype in constructor (#25098) * DOC: frame.py doctest fixing (#25097) * DOC: 0.24.1 release (#25125) [ci skip] * Revert set_index inspection/error handling for 0.24.1 (#25085) * DOC: Minor what's new fix (#24933) * Backport PR #24916: BUG-24212 fix regression in #24897 (#24951) * Revert "Backport PR #24916: BUG-24212 fix regression in #24897 (#24951)" This reverts commit 84056c52e3f20ab44921b86a8e0f05275bf8ddb4. * DOC/CLN: Timezone section in timeseries.rst (#24825) * DOC: Improve timezone documentation in timeseries.rst * edit some of the examples * Address review * DOC: Fix validation type error RT04 (#25107) (#25129) * Reading a HDF5 created in py2 (#25058) * BUG: Fixing regression in DataFrame.all and DataFrame.any with bool_only=True (#25102) * Removal of return variable names (#25123) * DOC: Improve docstring of Series.mul (#25136) * TST/REF: collect DataFrame reduction tests (#24914) * Fix validation error type `SS05` and check in CI (#25133) * Fixed tuple to List Conversion in Dataframe class (#25089) * STY: use pytest.raises context manager (indexes/multi) (#25175) * DOC: Updates to Timestamp document (#25163) * BLD: pin cython language level to '2' (#25145) Not explicitly pinning the language level has been producing future warnings from cython. The next release of cython is going to change the default level to '3str' under which the pandas cython extensions do not compile. The long term solution is to update the cython files to the next language level, but this is a stop-gap to keep pandas building. * CLN: Use ABCs in set_index (#25128) * DOC: update docstring for series.nunique (#25116) * DEPR: remove PanelGroupBy, disable DataFrame.to_panel (#25047) * BUG: DataFrame.merge(suffixes=) does not respect None (#24819) * fix MacPython pandas-wheels failure (#25186) * modernize compat imports (#25192) * TST: follow-up to Test nested pandas array #24993 (#25155) * revert changes to tests in gh-24993 * Test nested PandasArray * isort test_numpy.py * change NP_VERSION_INFO * use LooseVersion * add _np_version_under1p16 * remove blank line from merge master * add doctstrings to fixtures * DOC/CLN: Fix errors in Series docstrings (#24945) * REF: Add more pytest idiom to test_holiday.py (#25204) * DOC: Fix validation type error SA05 (#25208) Create check for SA05 errors in CI * BUG: Fix Series.is_unique with single occurrence of NaN (#25182) * REF: Remove many Panel tests (#25191) * DOC: Fixes to docstrings and add PR10 (space before colon) to validation (#25109) * DOC: exclude autogenerated c/cpp/html files from 'trailing whitespace' checks (#24549) * STY: use pytest.raises context manager (indexes/period) (#25199) * fix ci failures (#25225) * DEPR: remove tm.makePanel and all usages (#25231) * DEPR: Remove Panel-specific parts of io.pytables (#25233) * DEPR: Add Deprecated warning for timedelta with passed units M and Y (#23264) * BUG-25061 fix printing indices with NaNs (#25202) * BUG: Fix regression in DataFrame.apply causing RecursionError (#25230) * BUG: Fix regression in DataFrame.apply causing RecursionError * Add feedback from PR * Add feedback after further code review * Add feedback after further code review 2 * BUG: Fix read_json orient='table' without index (#25170) (#25171) * BLD: prevent asv from calling sys.stdin.close() by using different launch method (#25237) * (Closes #25029) Removed extra bracket from cheatsheet code example. (#25032) * CLN: For loops, boolean conditions, misc. (#25206) * Refactor groupby group_add from tempita to fused types (#24954) * CLN: Remove ipython 2.x compat (#25150) * CLN: Remove ipython 2.x compat * trivial change to trigger asv * Update v0.25.0.rst * revert whatsnew * BUG: Duplicated returns boolean dataframe (#25234) * REF/TST: resample/test_base.py (#25262) * Revert "BLD: prevent asv from calling sys.stdin.close() by using different launch method (#25237)" (#25253) This reverts commit f67b7fd2c75db951e63513003b1c6d3d1161a4b2. * BUG: pandas Timestamp tz_localize and tz_convert do not preserve `freq` attribute (#25247) * DEPR: remove assert_panel_equal (#25238) * PR04 errors fix (#25157) * Split Excel IO Into Sub-Directory (#25153) * API: Ensure DatetimeTZDtype standardizes pytz timezones (#25254) * API: Ensure DatetimeTZDtype standardizes pytz timezones * Add whatsnew * BUG: Fix exceptions when Series.interpolate's `order` parameter is missing or invalid (#25246) * BUG: raise accurate exception from Series.interpolate (#24014) * Actually validate `order` before use in spline * Remove unnecessary check and dead code * Clean up comparison/tests based on feedback * Include invalid order value in exception * Check for NaN order in spline validation * Add whatsnew entry for bug fix * CLN: Make unit tests assert one error at a time * CLN: break test into distinct test case * PEP8 fix in test module * CLN: Test fixture for interpolate methods * BUG: DataFrame.join on tz-aware DatetimeIndex (#25260) * REF: use _constructor and ABCFoo to avoid runtime imports (#25272) * Refactor groupby group_prod, group_var, group_mean, group_ohlc (#25249) * Fix typo in Cheat sheet with regex (#25215) * Edit parameter type in pandas.core.frame.py DataFrame.count (#25198) * TST/CLN: remove test_slice_ints_with_floats_raises (#25277) * Removed Panel class from HDF ASVs (#25281) * DOC: Fix minor typo in docstring (#25285) * DOC/CLN: Fix errors in DataFrame docstrings (#24952) * Skipped broken Py2 / Windows test (#25323) * Rt05 documentation error fix issue 25108 (#25309) * Fix typos in docs (#25305) * Doc: corrects spelling in generic.py (#25333) * BUG: groupby.transform retains timezone information (#25264) * Fixes Formatting Exception (#25088) * Bug: OverflowError in resample.agg with tz data (#25297) * DOC/CLN: Fix various docstring errors (#25295) * COMPAT: alias .to_numpy() for timestamp and timedelta scalars (#25142) * ENH: Support times with timezones in at_time (#25280) * BUG: Fix passing of numeric_only argument for categorical reduce (#25304) * TST: use a fixed seed to have the same uniques across python versions (#25346) TST: add pytest-mock to handle mocker fixture * TST: xfail excel styler tests, xref GH25351 (#25352) * TST: xfail excel styler tests, xref GH25351 * CI: cleanup .c files for cpplint>1.4 * DOC: Correct doc mistake in combiner func (#25360) Closes gh-25359. * DOC/BLD: fix --no-api option (#25209) * DOC: modify typos in Contributing section (#25365) * Remove spurious MultiIndex creation in `_set_axis_name` (#25371) * Resovles #25370 * Introduced by #22969 * #23049: test for Fatal Stack Overflow stemming From Misuse of astype('category') (#25366) * 9236: test for the DataFrame.groupby with MultiIndex having pd.NaT (#25310) * [BUG] exception handling of MultiIndex.__contains__ too narrow (#25268) * 14873: test for groupby.agg coercing booleans (#25327) * BUG/ENH: Timestamp.strptime (#25124) * BUG: constructor Timestamp.strptime() does not support %z. * Add doc string to NaT and Timestamp * updated the error message * Updated whatsnew entry. * Interval dtype fix (#25338) * [CLN] Excel Module Cleanups (#25275) Closes gh-25153 Authored-By: tdamsma * ENH: indexing and __getitem__ of dataframe and series accept zerodim integer np.array as int (#24924) * REGR: fix TimedeltaIndex sum and datetime subtraction with NaT (#25282, #25317) (#25329) * edited whatsnew typo (#25381) * fix typo of see also in DataFrame stat funcs (#25388) * API: more consistent error message for MultiIndex.from_arrays (#25189) * CLN: (re-)enable infer_dtype to catch complex (#25382) * DOC: Edited docstring of Interval (#25410) The docstring contained a repeated segment, which I removed. * Mark test_pct_max_many_rows as high memory (#25400) Fixes issue #25384 * Correct a typo of version number for interpolate() (#25418) * DEP: add pytest-mock to environment.yml (#25417) * BUG: Fix type coercion in read_json orient='table' (#21345) (#25219) * ERR: doc update for ParsingError (#25414) Closes gh-22881 * ENH: Add in sort keyword to DatetimeIndex.union (#25110) * DOC: Rewriting of ParserError doc + minor spacing (#25421) Follow-up to gh-25414. * API/ERR: allow iterators in df.set_index & improve errors (#24984) * BUG: Indexing with UTC offset string no longer ignored (#25263) * PERF/REF: improve performance of Series.searchsorted, PandasArray.searchsorted, collect functionality (#22034) * TST: remove never-used singleton fixtures (#24885) * BUG: fixed merging with empty frame containing an Int64 column (#25183) (#25289) * DOC: fixed geo accessor example in extending.rst (#25420) I realised "lon" and "lat" had just been switched with "longitude" and "latitude" in the following code block. So I used those names here as well. * TST: numpy RuntimeWarning with Series.round() (#25432) * CI: add __init__.py to isort skip list (#25455) * DOC: CategoricalIndex doc string (#24852) * DataFrame.drop Raises KeyError definition (#25474) * BUG: Keep column level name in resample nunique (#25469) Closes gh-23222 xref gh-23645 * ERR: Correct error message in to_datetime (#25467) * ERR: Correct error message in to_datetime Closes gh-23830 xref gh-23969 * Fix minor typo (#25458) Signed-off-by: Philippe Ombredanne * CI: Set pytest minversion to 4.0.2 (#25402) * CI: Set pytest minversion to 4.0.2 * STY: use pytest.raises context manager (indexes) (#25447) * STY: use pytest.raises context manager (tests/test_*) (#25452) * STY: use pytest.raises context manager (tests/test_*) * fix ci failures * skip py2 ci failure * Fix minor error in dynamic load function (#25256) * Cythonized GroupBy Quantile (#20405) * BUG: Fix regression on DataFrame.replace for regex (#25266) * BUG: Fix regression on DataFrame.replace for regex The commit ensures that the replacement for regex is not confined to the beginning of the string but spans all the characters within. The behaviour is then consistent with versions prior to 0.24.0. One test has been added to account for character replacement when the character is not at the beginning of the string. * Correct contribution guide docbuild instruction (#25479) * TST/REF: Add pytest idiom to test_frequencies.py (#25430) * BUG: Fix index type casting in read_json with orient='table' and float index (#25433) (#25434) * BUG: Groupby.agg with reduction function with tz aware data (#25308) * BUG: Groupby.agg cannot reduce with tz aware data * Handle output always as UTC * Add whatsnew * isort and add another fixed groupby.first/last issue * bring condition at a higher level * Add try for _try_cast * Add comments * Don't pass the utc_dtype explicitly * Remove unused import * Use string dtype instead * DOC: Fix docstring for read_sql_table (#25465) * ENH: Add Series.str.casefold (#25419) * Fix PR10 error and Clean up docstrings from functions related to RT05 errors (#25132) * Fix unreliable test (#25496) * DOC: Clarifying doc/make.py --single parameter (#25482) * fix MacPython / pandas-wheels ci failures (#25505) * DOC: Reword Series.interpolate docstring for clarity (#25491) * Changed insertion order to sys.path (#25486) * TST: xfail non-writeable pytables tests with numpy 1.16x (#25517) * STY: use pytest.raises context manager (arithmetic, arrays, computati… (#25504) * BUG: Fix RecursionError during IntervalTree construction (#25498) * STY: use pytest.raises context manager (plotting, reductions, scalar...) (#25483) * STY: use pytest.raises context manager (plotting, reductions, scalar...) * revert removed testing in test_timedelta.py * remove TODO from test_frame.py * skip py2 ci failure * BUG: Fix potential segfault after pd.Categorical(pd.Series(...), categories=...) (#25368) * Make DataFrame.to_html output full content (#24841) * BUG-16807-1 SparseFrame fills with default_fill_value if data is None (#24842) Closes gh-16807. * DOC: Add conda uninstall pandas to contributing guide (#25490) * fix #25487 add modify documentation * fix segfault when running with cython coverage enabled, xref cython#2879 (#25529) * TST: inline empty_frame = DataFrame({}) fixture (#24886) * DOC: Polishing typos out of doc/source/user_guide/indexing.rst (#25528) * STY: use pytest.raises context manager (frame) (#25516) * DOC: Fix #24268 by updating description for keep in Series.nlargest (#25358) * DOC: Fix #24268 by updating description for keep * fix MacPython / pandas-wheels ci failures (#25537) * TST/CLN: Remove more Panel tests (#25550) * BUG: caught typeError in series.at (#25506) (#25533) * ENH: Add errors parameter to DataFrame.rename (#25535) * ENH: GH13473 Add errors parameter to DataFrame.rename * TST: Skip IntervalTree construction overflow test on 32bit (#25558) * DOC: Small fixes to 0.24.2 whatsnew (#25559) * minor typo error (#25574) * BUG: in error message raised when invalid axis parameter (#25553) * BLD: Fixed pip install with no numpy (#25568) * Document the behavior of `axis=None` with `style.background_gradient` (#25551) * fix minor typos in dsintro.rst (#25579) * BUG: Handle readonly arrays in period_array (#25556) * BUG: Handle readonly arrays in period_array Closes #25403 * DOC: Fix typo in tz_localize (#25598) * BUG: secondary y axis could not be set to log scale (#25545) (#25586) * TST: add test for groupby on list of empty list (#25589) * TYPING: Small fixes to make stubgen happy (#25576) * CLN: Parmeterize test cases (#25355) --- .coveragerc | 27 - .github/CODE_OF_CONDUCT.md | 63 + .github/CONTRIBUTING.md | 27 +- .github/ISSUE_TEMPLATE.md | 13 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +- .gitignore | 13 +- .pep8speaks.yml | 19 + .travis.yml | 109 +- AUTHORS.md | 57 + LICENSE | 106 +- LICENSES/DATEUTIL_LICENSE | 54 + LICENSES/MUSL_LICENSE | 132 + LICENSES/XARRAY_LICENSE | 191 + MANIFEST.in | 36 +- Makefile | 3 + README.md | 156 +- appveyor.yml | 89 - asv_bench/asv.conf.json | 19 +- asv_bench/benchmarks/__init__.py | 1 + asv_bench/benchmarks/algorithms.py | 197 +- asv_bench/benchmarks/attrs_caching.py | 13 +- asv_bench/benchmarks/binary_ops.py | 139 +- asv_bench/benchmarks/categoricals.py | 303 +- asv_bench/benchmarks/ctors.py | 103 +- asv_bench/benchmarks/dtypes.py | 39 + asv_bench/benchmarks/eval.py | 65 +- asv_bench/benchmarks/frame_ctor.py | 163 +- asv_bench/benchmarks/frame_methods.py | 745 +- asv_bench/benchmarks/gil.py | 458 +- asv_bench/benchmarks/groupby.py | 914 +- asv_bench/benchmarks/hdfstore_bench.py | 122 - asv_bench/benchmarks/index_object.py | 279 +- asv_bench/benchmarks/indexing.py | 433 +- asv_bench/benchmarks/indexing_engines.py | 64 + asv_bench/benchmarks/inference.py | 149 +- .../benchmarks/io}/__init__.py | 0 asv_bench/benchmarks/io/csv.py | 236 + asv_bench/benchmarks/io/excel.py | 36 + asv_bench/benchmarks/io/hdf.py | 122 + asv_bench/benchmarks/io/json.py | 127 + asv_bench/benchmarks/io/msgpack.py | 27 + asv_bench/benchmarks/io/pickle.py | 27 + asv_bench/benchmarks/io/sas.py | 20 + asv_bench/benchmarks/io/sql.py | 127 + asv_bench/benchmarks/io/stata.py | 39 + asv_bench/benchmarks/io_bench.py | 194 - asv_bench/benchmarks/io_sql.py | 105 - asv_bench/benchmarks/join_merge.py | 444 +- asv_bench/benchmarks/multiindex_object.py | 129 + asv_bench/benchmarks/offset.py | 118 + asv_bench/benchmarks/packers.py | 316 - asv_bench/benchmarks/pandas_vb_common.py | 72 +- asv_bench/benchmarks/panel_ctor.py | 87 +- asv_bench/benchmarks/panel_methods.py | 31 +- asv_bench/benchmarks/parser_vb.py | 121 - asv_bench/benchmarks/period.py | 128 +- asv_bench/benchmarks/plotting.py | 91 +- asv_bench/benchmarks/reindex.py | 213 +- asv_bench/benchmarks/replace.py | 92 +- asv_bench/benchmarks/reshape.py | 236 +- asv_bench/benchmarks/rolling.py | 116 + asv_bench/benchmarks/series_methods.py | 244 +- asv_bench/benchmarks/sparse.py | 202 +- asv_bench/benchmarks/stat_ops.py | 319 +- asv_bench/benchmarks/strings.py | 189 +- asv_bench/benchmarks/timedelta.py | 155 +- asv_bench/benchmarks/timeseries.py | 603 +- asv_bench/benchmarks/timestamp.py | 140 + asv_bench/vbench_to_asv.py | 163 - azure-pipelines.yml | 119 + bench/alignment.py | 22 - bench/bench_dense_to_sparse.py | 14 - bench/bench_get_put_value.py | 56 - bench/bench_groupby.py | 66 - bench/bench_join_panel.py | 85 - bench/bench_khash_dict.py | 89 - bench/bench_merge.R | 161 - bench/bench_merge.py | 105 - bench/bench_merge_sqlite.py | 87 - bench/bench_pivot.R | 27 - bench/bench_pivot.py | 16 - bench/bench_take_indexing.py | 55 - bench/bench_unique.py | 278 - bench/bench_with_subset.R | 53 - bench/bench_with_subset.py | 116 - bench/better_unique.py | 80 - bench/duplicated.R | 22 - bench/io_roundtrip.py | 116 - bench/serialize.py | 89 - bench/test.py | 70 - bench/zoo_bench.R | 71 - bench/zoo_bench.py | 36 - ci/README.txt | 17 - ci/azure/posix.yml | 100 + ci/azure/windows.yml | 59 + ci/before_install_travis.sh | 15 - ci/before_script_travis.sh | 11 + ci/build_docs.sh | 62 +- ci/code_checks.sh | 259 + ci/deps/azure-27-compat.yaml | 28 + ci/deps/azure-27-locale.yaml | 30 + ci/deps/azure-36-locale_slow.yaml | 35 + ci/deps/azure-37-locale.yaml | 34 + ci/deps/azure-37-numpydev.yaml | 19 + ci/deps/azure-macos-35.yaml | 31 + ci/deps/azure-windows-27.yaml | 33 + ci/deps/azure-windows-36.yaml | 30 + ci/deps/travis-27.yaml | 51 + ci/deps/travis-36-doc.yaml | 46 + ci/deps/travis-36-locale.yaml | 37 + ci/deps/travis-36-slow.yaml | 33 + ci/deps/travis-36.yaml | 47 + ci/deps/travis-37.yaml | 22 + ci/incremental/build.cmd | 9 + ci/incremental/build.sh | 16 + ci/incremental/install_miniconda.sh | 19 + ci/incremental/setup_conda_environment.cmd | 21 + ci/incremental/setup_conda_environment.sh | 52 + ci/install.ps1 | 92 - ci/install_circle.sh | 85 - ci/install_db_circle.sh | 8 - ci/install_travis.sh | 109 +- ci/lint.sh | 68 - ci/print_skipped.py | 6 +- ci/print_versions.py | 28 - ci/requirements-2.7.build | 6 - ci/requirements-2.7.pip | 8 - ci/requirements-2.7.run | 22 - ci/requirements-2.7.sh | 7 - ci/requirements-2.7_BUILD_TEST.build | 6 - ci/requirements-2.7_COMPAT.build | 5 - ci/requirements-2.7_COMPAT.pip | 2 - ci/requirements-2.7_COMPAT.run | 16 - ci/requirements-2.7_LOCALE.build | 5 - ci/requirements-2.7_LOCALE.pip | 1 - ci/requirements-2.7_LOCALE.run | 14 - ci/requirements-2.7_SLOW.build | 5 - ci/requirements-2.7_SLOW.run | 20 - ci/requirements-2.7_WIN.run | 18 - ci/requirements-3.4.build | 4 - ci/requirements-3.4.pip | 2 - ci/requirements-3.4.run | 18 - ci/requirements-3.4_SLOW.build | 6 - ci/requirements-3.4_SLOW.run | 20 - ci/requirements-3.4_SLOW.sh | 7 - ci/requirements-3.5.build | 6 - ci/requirements-3.5.pip | 2 - ci/requirements-3.5.run | 21 - ci/requirements-3.5.sh | 7 - ci/requirements-3.5_ASCII.build | 6 - ci/requirements-3.5_ASCII.run | 3 - ci/requirements-3.5_DOC.build | 5 - ci/requirements-3.5_DOC.run | 21 - ci/requirements-3.5_DOC.sh | 11 - ci/requirements-3.5_OSX.build | 4 - ci/requirements-3.5_OSX.pip | 1 - ci/requirements-3.5_OSX.run | 16 - ci/requirements-3.5_OSX.sh | 7 - ci/requirements-3.6.build | 6 - ci/requirements-3.6.run | 22 - ci/requirements-3.6_NUMPY_DEV.build | 4 - ci/requirements-3.6_NUMPY_DEV.build.sh | 14 - ci/requirements-3.6_NUMPY_DEV.run | 2 - ci/requirements-3.6_WIN.run | 13 - ci/requirements_all.txt | 26 - ci/requirements_dev.txt | 7 - ci/run_build_docs.sh | 10 - ci/run_circle.sh | 9 - ci/run_tests.sh | 58 + ci/script_multi.sh | 36 - ci/script_single.sh | 29 - ci/show_circle.sh | 8 - ci/upload_coverage.sh | 12 - circle.yml | 38 - codecov.yml | 2 + conda.recipe/meta.yaml | 28 +- doc/README.rst | 170 +- doc/_templates/api_redirect.html | 13 +- doc/cheatsheet/Pandas_Cheat_Sheet.pdf | Bin 685284 -> 345905 bytes doc/cheatsheet/Pandas_Cheat_Sheet.pptx | Bin 105196 -> 105278 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf | Bin 0 -> 420632 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx | Bin 0 -> 82563 bytes doc/cheatsheet/README.txt | 4 + doc/make.py | 761 +- doc/plots/stats/moment_plots.py | 30 - doc/plots/stats/moments_ewma.py | 15 - doc/plots/stats/moments_ewmvol.py | 23 - doc/plots/stats/moments_expw.py | 35 - doc/plots/stats/moments_rolling.py | 24 - doc/plots/stats/moments_rolling_binary.py | 30 - doc/redirects.csv | 1581 ++++ doc/source/_static/banklist.html | 110 +- doc/source/_static/ci.png | Bin 0 -> 35295 bytes doc/source/_static/favicon.ico | Bin 0 -> 3902 bytes doc/source/_static/print_df_new.png | Bin 0 -> 77202 bytes doc/source/_static/print_df_old.png | Bin 0 -> 89239 bytes doc/source/_static/reshaping_melt.png | Bin 0 -> 52900 bytes doc/source/_static/reshaping_pivot.png | Bin 0 -> 52132 bytes doc/source/_static/reshaping_stack.png | Bin 0 -> 54479 bytes doc/source/_static/reshaping_unstack.png | Bin 0 -> 53895 bytes doc/source/_static/reshaping_unstack_0.png | Bin 0 -> 58533 bytes doc/source/_static/reshaping_unstack_1.png | Bin 0 -> 57978 bytes doc/source/_static/style-excel.png | Bin 0 -> 58167 bytes doc/source/api.rst | 1860 ---- doc/source/conf.py | 470 +- doc/source/contributing.rst | 978 -- doc/source/development/contributing.rst | 1291 +++ .../development/contributing_docstring.rst | 1006 ++ doc/source/development/developer.rst | 164 + doc/source/development/extending.rst | 399 + doc/source/development/index.rst | 15 + doc/source/development/internals.rst | 108 + doc/source/ecosystem.rst | 274 +- doc/source/{ => getting_started}/10min.rst | 314 +- doc/source/{ => getting_started}/basics.rst | 936 +- .../comparison}/comparison_with_r.rst | 114 +- .../comparison}/comparison_with_sas.rst | 232 +- .../comparison}/comparison_with_sql.rst | 37 +- .../comparison/comparison_with_stata.rst | 678 ++ .../getting_started/comparison/index.rst | 15 + doc/source/{ => getting_started}/dsintro.rst | 434 +- doc/source/getting_started/index.rst | 17 + doc/source/getting_started/overview.rst | 178 + doc/source/getting_started/tutorials.rst | 109 + doc/source/index.rst.template | 168 +- doc/source/install.rst | 209 +- doc/source/internals.rst | 241 - doc/source/options.rst | 541 -- doc/source/overview.rst | 120 - doc/source/r_interface.rst | 76 - doc/source/reference/arrays.rst | 403 + doc/source/reference/extensions.rst | 22 + doc/source/reference/frame.rst | 351 + doc/source/reference/general_functions.rst | 87 + .../reference/general_utility_functions.rst | 109 + doc/source/reference/groupby.rst | 133 + doc/source/reference/index.rst | 80 + doc/source/reference/indexing.rst | 481 + doc/source/reference/io.rst | 140 + doc/source/reference/offset_frequency.rst | 1387 +++ doc/source/reference/panel.rst | 208 + doc/source/reference/plotting.rst | 22 + doc/source/reference/resampling.rst | 66 + doc/source/reference/series.rst | 593 ++ doc/source/reference/style.rst | 66 + doc/source/reference/window.rst | 69 + doc/source/release.rst | 5353 ----------- doc/source/remote_data.rst | 31 - doc/source/style.rst | 10 - doc/source/styled.xlsx | Bin 0 -> 5682 bytes .../themes/nature_with_gtoc/layout.html | 11 + .../nature_with_gtoc/static/nature.css_t | 51 +- doc/source/timeseries.rst | 2177 ----- doc/source/tutorials.rst | 180 - doc/source/{ => user_guide}/advanced.rst | 629 +- doc/source/{ => user_guide}/categorical.rst | 676 +- doc/source/{ => user_guide}/computation.rst | 309 +- doc/source/{ => user_guide}/cookbook.rst | 714 +- doc/source/{ => user_guide}/enhancingperf.rst | 276 +- doc/source/{ => user_guide}/gotchas.rst | 189 +- doc/source/{ => user_guide}/groupby.rst | 609 +- doc/source/user_guide/index.rst | 40 + doc/source/{ => user_guide}/indexing.rst | 702 +- doc/source/user_guide/integer_na.rst | 107 + doc/source/{ => user_guide}/io.rst | 2597 +++--- doc/source/{ => user_guide}/merging.rst | 536 +- doc/source/{ => user_guide}/missing_data.rst | 294 +- doc/source/user_guide/options.rst | 554 ++ doc/source/{ => user_guide}/reshaping.rst | 406 +- doc/source/{ => user_guide}/sparse.rst | 76 +- .../style.ipynb} | 543 +- doc/source/user_guide/templates/myhtml.tpl | 5 + .../templates/template_structure.html | 57 + doc/source/{ => user_guide}/text.rst | 214 +- doc/source/{ => user_guide}/timedeltas.rst | 168 +- doc/source/user_guide/timeseries.rst | 2445 +++++ doc/source/{ => user_guide}/visualization.rst | 334 +- doc/source/whatsnew.rst | 87 - doc/source/whatsnew/index.rst | 203 + .../whatsnew/{v0.10.0.txt => v0.10.0.rst} | 146 +- .../whatsnew/{v0.10.1.txt => v0.10.1.rst} | 128 +- .../whatsnew/{v0.11.0.txt => v0.11.0.rst} | 77 +- .../whatsnew/{v0.12.0.txt => v0.12.0.rst} | 80 +- .../whatsnew/{v0.13.0.txt => v0.13.0.rst} | 586 +- doc/source/whatsnew/v0.13.1.rst | 382 + doc/source/whatsnew/v0.13.1.txt | 288 - .../whatsnew/{v0.14.0.txt => v0.14.0.rst} | 304 +- .../whatsnew/{v0.14.1.txt => v0.14.1.rst} | 61 +- .../whatsnew/{v0.15.0.txt => v0.15.0.rst} | 251 +- .../whatsnew/{v0.15.1.txt => v0.15.1.rst} | 40 +- .../whatsnew/{v0.15.2.txt => v0.15.2.rst} | 29 +- .../whatsnew/{v0.16.0.txt => v0.16.0.rst} | 124 +- .../whatsnew/{v0.16.1.txt => v0.16.1.rst} | 196 +- .../whatsnew/{v0.16.2.txt => v0.16.2.rst} | 33 +- .../whatsnew/{v0.17.0.txt => v0.17.0.rst} | 142 +- .../whatsnew/{v0.17.1.txt => v0.17.1.rst} | 33 +- .../whatsnew/{v0.18.0.txt => v0.18.0.rst} | 76 +- .../whatsnew/{v0.18.1.txt => v0.18.1.rst} | 83 +- .../whatsnew/{v0.19.0.txt => v0.19.0.rst} | 162 +- .../whatsnew/{v0.19.1.txt => v0.19.1.rst} | 20 +- .../whatsnew/{v0.19.2.txt => v0.19.2.rst} | 20 +- .../whatsnew/{v0.20.0.txt => v0.20.0.rst} | 1141 ++- doc/source/whatsnew/v0.20.2.rst | 143 + doc/source/whatsnew/v0.20.3.rst | 76 + doc/source/whatsnew/v0.21.0.rst | 1197 +++ doc/source/whatsnew/v0.21.1.rst | 187 + doc/source/whatsnew/v0.22.0.rst | 259 + doc/source/whatsnew/v0.23.0.rst | 1434 +++ doc/source/whatsnew/v0.23.1.rst | 151 + doc/source/whatsnew/v0.23.2.rst | 123 + doc/source/whatsnew/v0.23.3.rst | 16 + doc/source/whatsnew/v0.23.4.rst | 47 + doc/source/whatsnew/v0.24.0.rst | 1936 ++++ doc/source/whatsnew/v0.24.1.rst | 85 + doc/source/whatsnew/v0.24.2.rst | 111 + doc/source/whatsnew/v0.25.0.rst | 267 + .../whatsnew/{v0.4.x.txt => v0.4.x.rst} | 10 +- .../whatsnew/{v0.5.0.txt => v0.5.0.rst} | 16 + .../whatsnew/{v0.6.0.txt => v0.6.0.rst} | 18 +- .../whatsnew/{v0.6.1.txt => v0.6.1.rst} | 12 +- .../whatsnew/{v0.7.0.txt => v0.7.0.rst} | 23 +- .../whatsnew/{v0.7.1.txt => v0.7.1.rst} | 11 + .../whatsnew/{v0.7.2.txt => v0.7.2.rst} | 11 + .../whatsnew/{v0.7.3.txt => v0.7.3.rst} | 37 +- .../whatsnew/{v0.8.0.txt => v0.8.0.rst} | 35 +- .../whatsnew/{v0.8.1.txt => v0.8.1.rst} | 13 +- .../whatsnew/{v0.9.0.txt => v0.9.0.rst} | 24 +- .../whatsnew/{v0.9.1.txt => v0.9.1.rst} | 40 +- .../whatsnew/whatsnew_0171_html_table.html | 927 +- doc/sphinxext/README.rst | 2 +- doc/sphinxext/announce.py | 140 + doc/sphinxext/contributors.py | 53 + .../ipython_console_highlighting.py | 116 - .../ipython_sphinxext/ipython_directive.py | 1093 --- doc/sphinxext/numpydoc/LICENSE.txt | 94 - doc/sphinxext/numpydoc/README.rst | 51 - doc/sphinxext/numpydoc/__init__.py | 3 - doc/sphinxext/numpydoc/comment_eater.py | 169 - doc/sphinxext/numpydoc/compiler_unparse.py | 865 -- doc/sphinxext/numpydoc/docscrape.py | 527 -- doc/sphinxext/numpydoc/docscrape_sphinx.py | 277 - doc/sphinxext/numpydoc/linkcode.py | 83 - doc/sphinxext/numpydoc/numpydoc.py | 191 - doc/sphinxext/numpydoc/phantom_import.py | 167 - doc/sphinxext/numpydoc/plot_directive.py | 642 -- .../numpydoc/tests/test_docscrape.py | 767 -- doc/sphinxext/numpydoc/tests/test_linkcode.py | 5 - .../numpydoc/tests/test_phantom_import.py | 5 - .../numpydoc/tests/test_plot_directive.py | 5 - .../numpydoc/tests/test_traitsdoc.py | 5 - doc/sphinxext/numpydoc/traitsdoc.py | 142 - environment.yml | 57 + pandas/__init__.py | 51 +- pandas/_libs/__init__.py | 9 +- pandas/_libs/algos.pxd | 16 +- pandas/_libs/algos.pyx | 668 +- pandas/_libs/algos_common_helper.pxi.in | 509 +- pandas/_libs/algos_rank_helper.pxi.in | 149 +- pandas/_libs/algos_take_helper.pxi.in | 64 +- pandas/_libs/groupby.pxd | 6 + pandas/_libs/groupby.pyx | 882 +- pandas/_libs/groupby_helper.pxi.in | 984 +- pandas/{tools => _libs}/hashing.pyx | 64 +- pandas/_libs/hashtable.pxd | 13 +- pandas/_libs/hashtable.pyx | 101 +- pandas/_libs/hashtable_class_helper.pxi.in | 865 +- pandas/_libs/hashtable_func_helper.pxi.in | 84 +- pandas/_libs/index.pyx | 490 +- pandas/_libs/index_class_helper.pxi.in | 87 +- pandas/_libs/indexing.pyx | 23 + pandas/_libs/internals.pyx | 466 + pandas/_libs/interval.pyx | 484 + pandas/_libs/intervaltree.pxi.in | 413 + pandas/_libs/join.pyx | 834 +- pandas/_libs/join_func_helper.pxi.in | 373 - pandas/_libs/join_helper.pxi.in | 408 - pandas/_libs/khash.pxd | 141 + pandas/_libs/lib.pxd | 4 - pandas/_libs/lib.pyx | 2804 +++--- pandas/_libs/missing.pxd | 11 + pandas/_libs/missing.pyx | 285 + pandas/_libs/ops.pyx | 295 + pandas/{io => _libs}/parsers.pyx | 873 +- pandas/_libs/period.pyx | 1258 --- pandas/_libs/properties.pyx | 69 + .../_libs/{src/reduce.pyx => reduction.pyx} | 74 +- pandas/_libs/reshape.pyx | 112 +- pandas/_libs/reshape_helper.pxi.in | 81 - pandas/_libs/skiplist.pxd | 45 + pandas/_libs/{src => }/skiplist.pyx | 58 +- pandas/{sparse => _libs}/sparse.pyx | 190 +- .../{sparse => _libs}/sparse_op_helper.pxi.in | 107 +- pandas/_libs/src/compat_helper.h | 17 +- pandas/_libs/src/datetime.pxd | 195 - pandas/_libs/src/datetime/np_datetime.h | 127 - pandas/_libs/src/datetime_helper.h | 36 - pandas/_libs/src/headers/cmath | 36 + pandas/_libs/src/headers/math.h | 11 - pandas/_libs/src/headers/portable.h | 6 + pandas/_libs/src/inference.pyx | 1587 ---- .../_libs/src/{helper.h => inline_helper.h} | 10 +- pandas/_libs/src/khash.pxd | 140 - pandas/_libs/src/klib/khash.h | 13 +- pandas/_libs/src/klib/khash_python.h | 31 +- pandas/_libs/src/klib/ktypes.h | 6 - pandas/_libs/src/klib/kvec.h | 151 - pandas/_libs/src/msgpack/unpack_template.h | 2 +- pandas/_libs/src/numpy.pxd | 984 -- pandas/_libs/src/numpy_helper.h | 162 - pandas/_libs/src/offsets.pyx | 367 - pandas/_libs/src/parse_helper.h | 17 +- pandas/_libs/src/parser/.gitignore | 2 - pandas/_libs/src/parser/Makefile | 13 - pandas/_libs/src/parser/io.c | 144 +- pandas/_libs/src/parser/io.h | 30 +- pandas/_libs/src/parser/tokenizer.c | 242 +- pandas/_libs/src/parser/tokenizer.h | 61 +- pandas/_libs/src/period_helper.c | 1518 --- pandas/_libs/src/period_helper.h | 191 - pandas/_libs/src/properties.pyx | 65 - pandas/_libs/src/skiplist.h | 13 +- pandas/_libs/src/skiplist.pxd | 22 - pandas/_libs/src/ujson/lib/ultrajson.h | 11 +- pandas/_libs/src/ujson/lib/ultrajsonenc.c | 19 +- pandas/_libs/src/ujson/python/JSONtoObj.c | 16 +- pandas/_libs/src/ujson/python/objToJSON.c | 108 +- pandas/_libs/src/ujson/python/ujson.c | 10 +- pandas/_libs/src/util.pxd | 128 - pandas/{util => _libs}/testing.pyx | 13 +- pandas/_libs/tslib.pxd | 10 - pandas/_libs/tslib.pyx | 6152 ++---------- pandas/_libs/tslibs/__init__.py | 9 + pandas/_libs/tslibs/ccalendar.pxd | 12 + pandas/_libs/tslibs/ccalendar.pyx | 226 + pandas/_libs/tslibs/conversion.pxd | 34 + pandas/_libs/tslibs/conversion.pyx | 1335 +++ pandas/_libs/tslibs/fields.pyx | 669 ++ pandas/_libs/tslibs/frequencies.pxd | 9 + pandas/_libs/tslibs/frequencies.pyx | 512 + pandas/_libs/tslibs/nattype.pxd | 20 + pandas/_libs/tslibs/nattype.pyx | 745 ++ pandas/_libs/tslibs/np_datetime.pxd | 76 + pandas/_libs/tslibs/np_datetime.pyx | 203 + pandas/_libs/tslibs/offsets.pxd | 3 + pandas/_libs/tslibs/offsets.pyx | 1143 +++ pandas/_libs/tslibs/parsing.pyx | 750 ++ pandas/_libs/tslibs/period.pyx | 2554 +++++ pandas/_libs/tslibs/resolution.pyx | 355 + .../{ => tslibs}/src/datetime/np_datetime.c | 508 +- .../_libs/tslibs/src/datetime/np_datetime.h | 80 + .../src/datetime/np_datetime_strings.c | 689 +- .../src/datetime/np_datetime_strings.h | 53 +- pandas/_libs/tslibs/strptime.pyx | 668 ++ pandas/_libs/tslibs/timedeltas.pxd | 8 + pandas/_libs/tslibs/timedeltas.pyx | 1547 +++ pandas/_libs/tslibs/timestamps.pxd | 8 + pandas/_libs/tslibs/timestamps.pyx | 1432 +++ pandas/_libs/tslibs/timezones.pxd | 16 + pandas/_libs/tslibs/timezones.pyx | 359 + pandas/_libs/tslibs/util.pxd | 229 + pandas/_libs/util.pxd | 114 + pandas/{core => _libs}/window.pyx | 1113 ++- pandas/_libs/writers.pyx | 167 + pandas/_version.py | 66 +- pandas/api/__init__.py | 1 + pandas/api/extensions/__init__.py | 10 + pandas/api/types/__init__.py | 9 +- pandas/arrays/__init__.py | 23 + pandas/compat/__init__.py | 154 +- pandas/compat/chainmap_impl.py | 13 +- pandas/compat/numpy/__init__.py | 49 +- pandas/compat/numpy/function.py | 74 +- pandas/compat/openpyxl_compat.py | 35 - pandas/compat/pickle_compat.py | 142 +- pandas/computation/api.py | 14 - pandas/conftest.py | 655 +- pandas/core/accessor.py | 285 + pandas/core/algorithms.py | 1582 ++-- pandas/core/api.py | 84 +- pandas/core/apply.py | 411 + pandas/core/arrays/__init__.py | 13 + pandas/core/arrays/_ranges.py | 188 + pandas/core/arrays/array_.py | 274 + pandas/core/arrays/base.py | 1120 +++ pandas/core/arrays/categorical.py | 2704 ++++++ pandas/core/arrays/datetimelike.py | 1598 ++++ pandas/core/arrays/datetimes.py | 2148 +++++ pandas/core/arrays/integer.py | 706 ++ pandas/core/arrays/interval.py | 1104 +++ pandas/core/arrays/numpy_.py | 465 + pandas/core/arrays/period.py | 956 ++ pandas/core/arrays/sparse.py | 2028 ++++ pandas/core/arrays/timedeltas.py | 1066 +++ pandas/core/base.py | 1259 ++- pandas/core/categorical.py | 2136 +---- pandas/core/common.py | 627 +- .../{formats => core/computation}/__init__.py | 0 pandas/{ => core}/computation/align.py | 36 +- pandas/core/computation/api.py | 3 + .../__init__.py => core/computation/check.py} | 11 +- pandas/{ => core}/computation/common.py | 4 +- pandas/{ => core}/computation/engines.py | 22 +- pandas/{ => core}/computation/eval.py | 153 +- pandas/{ => core}/computation/expr.py | 99 +- pandas/{ => core}/computation/expressions.py | 87 +- pandas/{ => core}/computation/ops.py | 55 +- pandas/{ => core}/computation/pytables.py | 101 +- pandas/{ => core}/computation/scope.py | 27 +- pandas/core/config.py | 104 +- pandas/core/config_init.py | 269 +- pandas/core/datetools.py | 51 - pandas/{indexes => core/dtypes}/__init__.py | 0 pandas/core/dtypes/api.py | 14 + pandas/core/dtypes/base.py | 294 + pandas/{types => core/dtypes}/cast.py | 785 +- pandas/core/dtypes/common.py | 2057 ++++ pandas/core/dtypes/concat.py | 581 ++ pandas/core/dtypes/dtypes.py | 990 ++ pandas/{types => core/dtypes}/generic.py | 27 +- pandas/core/dtypes/inference.py | 499 + pandas/core/dtypes/missing.py | 529 ++ pandas/core/frame.py | 7526 +++++++++------ pandas/core/generic.py | 8281 +++++++++++++---- pandas/core/groupby.py | 4345 --------- pandas/core/groupby/__init__.py | 4 + pandas/core/groupby/base.py | 158 + pandas/core/groupby/categorical.py | 100 + pandas/core/groupby/generic.py | 1588 ++++ pandas/core/groupby/groupby.py | 2206 +++++ pandas/core/groupby/grouper.py | 635 ++ pandas/core/groupby/ops.py | 898 ++ pandas/core/index.py | 4 +- pandas/{sparse => core/indexes}/__init__.py | 0 pandas/core/indexes/accessors.py | 325 + pandas/core/indexes/api.py | 286 + pandas/{ => core}/indexes/base.py | 5477 +++++++---- pandas/core/indexes/category.py | 903 ++ pandas/core/indexes/datetimelike.py | 724 ++ pandas/core/indexes/datetimes.py | 1706 ++++ pandas/{ => core}/indexes/frozen.py | 86 +- pandas/core/indexes/interval.py | 1315 +++ pandas/{ => core}/indexes/multi.py | 2174 +++-- pandas/{ => core}/indexes/numeric.py | 202 +- pandas/core/indexes/period.py | 966 ++ pandas/{ => core}/indexes/range.py | 352 +- pandas/core/indexes/timedeltas.py | 809 ++ pandas/core/indexing.py | 1496 ++- pandas/core/internals.py | 5258 ----------- pandas/core/internals/__init__.py | 13 + pandas/core/internals/arrays.py | 55 + pandas/core/internals/blocks.py | 3237 +++++++ pandas/core/internals/concat.py | 485 + pandas/core/internals/construction.py | 715 ++ pandas/core/internals/managers.py | 2073 +++++ pandas/core/missing.py | 462 +- pandas/core/nanops.py | 743 +- pandas/core/ops.py | 2970 ++++-- pandas/core/panel.py | 579 +- pandas/core/panel4d.py | 60 - pandas/core/panelnd.py | 132 - pandas/core/resample.py | 1760 ++++ pandas/core/reshape.py | 1411 --- pandas/{stats => core/reshape}/__init__.py | 0 pandas/core/reshape/api.py | 8 + pandas/{tools => core/reshape}/concat.py | 222 +- pandas/core/reshape/melt.py | 461 + pandas/{tools => core/reshape}/merge.py | 779 +- pandas/{tools => core/reshape}/pivot.py | 356 +- pandas/core/reshape/reshape.py | 1046 +++ pandas/core/reshape/tile.py | 550 ++ pandas/core/reshape/util.py | 57 + pandas/core/series.py | 3792 +++++--- pandas/core/sorting.py | 189 +- pandas/core/sparse.py | 10 - .../formats => core/sparse}/__init__.py | 0 pandas/core/sparse/api.py | 5 + pandas/{ => core}/sparse/frame.py | 395 +- pandas/{ => core}/sparse/scipy_sparse.py | 31 +- pandas/core/sparse/series.py | 592 ++ pandas/core/strings.py | 2342 +++-- .../{tests/types => core/tools}/__init__.py | 0 pandas/core/tools/datetimes.py | 901 ++ .../{tools/util.py => core/tools/numeric.py} | 129 +- pandas/core/tools/timedeltas.py | 172 + pandas/{tools => core/util}/__init__.py | 0 pandas/{tools => core/util}/hashing.py | 111 +- pandas/core/window.py | 1744 +++- pandas/errors/__init__.py | 192 + pandas/formats/format.py | 2800 ------ pandas/formats/printing.py | 235 - pandas/formats/style.py | 1007 -- pandas/indexes/api.py | 121 - pandas/indexes/category.py | 632 -- pandas/io/api.py | 31 +- pandas/{util => io}/clipboard/__init__.py | 29 +- pandas/{util => io}/clipboard/clipboards.py | 23 +- pandas/{util => io}/clipboard/exceptions.py | 2 +- pandas/{util => io}/clipboard/windows.py | 17 +- pandas/io/{clipboard.py => clipboards.py} | 61 +- pandas/io/common.py | 297 +- pandas/io/data.py | 6 - pandas/io/date_converters.py | 14 +- pandas/io/excel.py | 1644 ---- pandas/io/excel/__init__.py | 16 + pandas/io/excel/_base.py | 852 ++ pandas/io/excel/_openpyxl.py | 453 + pandas/io/excel/_util.py | 265 + pandas/io/excel/_xlrd.py | 126 + pandas/io/excel/_xlsxwriter.py | 218 + pandas/io/excel/_xlwt.py | 132 + pandas/io/feather_format.py | 84 +- pandas/{types => io/formats}/__init__.py | 0 pandas/io/formats/console.py | 121 + pandas/io/formats/css.py | 250 + pandas/io/formats/csvs.py | 315 + pandas/io/formats/excel.py | 664 ++ pandas/io/formats/format.py | 1626 ++++ pandas/io/formats/html.py | 540 ++ pandas/io/formats/latex.py | 246 + pandas/io/formats/printing.py | 435 + pandas/io/formats/style.py | 1373 +++ pandas/io/formats/templates/html.tpl | 70 + pandas/{util => io/formats}/terminal.py | 65 +- pandas/io/gbq.py | 177 +- pandas/io/gcs.py | 16 + pandas/io/html.py | 625 +- pandas/io/json/json.py | 520 +- pandas/io/json/normalize.py | 72 +- pandas/io/json/table_schema.py | 193 +- pandas/io/msgpack/_packer.pyx | 59 +- pandas/io/msgpack/_unpacker.pyx | 95 +- pandas/io/packers.py | 172 +- pandas/io/parquet.py | 283 + pandas/io/parsers.py | 1608 ++-- pandas/io/pickle.py | 202 +- pandas/io/pytables.py | 1347 +-- pandas/io/s3.py | 17 +- pandas/io/sas/sas.pyx | 71 +- pandas/io/sas/sas7bdat.py | 129 +- pandas/io/sas/sas_constants.py | 104 +- pandas/io/sas/sas_xport.py | 33 +- pandas/io/sas/sasreader.py | 24 +- pandas/io/sql.py | 592 +- pandas/io/stata.py | 1208 ++- pandas/io/wb.py | 6 - pandas/json.py | 7 - pandas/lib.py | 7 - pandas/parser.py | 8 - pandas/plotting/__init__.py | 20 + pandas/plotting/_compat.py | 25 + pandas/plotting/_converter.py | 1154 +++ .../{tools/plotting.py => plotting/_core.py} | 2730 +++--- pandas/plotting/_misc.py | 641 ++ pandas/plotting/_style.py | 168 + pandas/plotting/_timeseries.py | 353 + pandas/plotting/_tools.py | 382 + pandas/sparse/api.py | 6 - pandas/sparse/array.py | 808 -- pandas/sparse/list.py | 151 - pandas/sparse/series.py | 832 -- pandas/stats/api.py | 7 - pandas/stats/moments.py | 854 -- pandas/testing.py | 8 + pandas/tests/api/test_api.py | 221 +- pandas/tests/api/test_types.py | 42 + .../tests/arithmetic/__init__.py | 0 pandas/tests/arithmetic/conftest.py | 192 + pandas/tests/arithmetic/test_datetime64.py | 2352 +++++ pandas/tests/arithmetic/test_numeric.py | 1076 +++ pandas/tests/arithmetic/test_object.py | 314 + pandas/tests/arithmetic/test_period.py | 1213 +++ pandas/tests/arithmetic/test_timedelta64.py | 2009 ++++ .../tests/arrays/__init__.py | 0 .../tests/arrays/categorical/__init__.py | 0 pandas/tests/arrays/categorical/common.py | 10 + pandas/tests/arrays/categorical/conftest.py | 13 + pandas/tests/arrays/categorical/test_algos.py | 142 + .../arrays/categorical/test_analytics.py | 312 + pandas/tests/arrays/categorical/test_api.py | 508 + .../arrays/categorical/test_constructors.py | 586 ++ .../tests/arrays/categorical/test_dtypes.py | 177 + .../tests/arrays/categorical/test_indexing.py | 264 + .../tests/arrays/categorical/test_missing.py | 87 + .../arrays/categorical/test_operators.py | 369 + pandas/tests/arrays/categorical/test_repr.py | 529 ++ .../tests/arrays/categorical/test_sorting.py | 124 + .../tests/arrays/categorical/test_subclass.py | 25 + .../tests/arrays/categorical/test_warnings.py | 31 + .../tests/arrays/interval/__init__.py | 0 pandas/tests/arrays/interval/test_interval.py | 68 + pandas/tests/arrays/interval/test_ops.py | 82 + pandas/tests/arrays/sparse/__init__.py | 0 .../{ => arrays}/sparse/test_arithmetics.py | 131 +- pandas/tests/arrays/sparse/test_array.py | 1203 +++ pandas/tests/arrays/sparse/test_dtype.py | 161 + .../{ => arrays}/sparse/test_libsparse.py | 252 +- pandas/tests/arrays/test_array.py | 305 + pandas/tests/arrays/test_datetimelike.py | 657 ++ pandas/tests/arrays/test_datetimes.py | 292 + pandas/tests/arrays/test_integer.py | 713 ++ pandas/tests/arrays/test_numpy.py | 206 + pandas/tests/arrays/test_period.py | 317 + pandas/tests/arrays/test_timedeltas.py | 161 + pandas/tests/computation/test_compat.py | 22 +- pandas/tests/computation/test_eval.py | 774 +- pandas/tests/dtypes/__init__.py | 0 pandas/tests/dtypes/cast/__init__.py | 0 .../dtypes/cast/test_construct_from_scalar.py | 22 + .../dtypes/cast/test_construct_ndarray.py | 20 + .../dtypes/cast/test_construct_object_arr.py | 22 + .../tests/dtypes/cast/test_convert_objects.py | 15 + pandas/tests/dtypes/cast/test_downcast.py | 82 + .../dtypes/cast/test_find_common_type.py | 108 + .../dtypes/cast/test_infer_datetimelike.py | 22 + pandas/tests/dtypes/cast/test_infer_dtype.py | 160 + pandas/tests/dtypes/test_common.py | 656 ++ pandas/tests/dtypes/test_concat.py | 53 + pandas/tests/dtypes/test_dtypes.py | 904 ++ pandas/tests/dtypes/test_generic.py | 92 + pandas/tests/dtypes/test_inference.py | 1364 +++ pandas/tests/dtypes/test_missing.py | 489 + pandas/tests/extension/__init__.py | 0 pandas/tests/extension/arrow/__init__.py | 0 pandas/tests/extension/arrow/bool.py | 144 + pandas/tests/extension/arrow/test_bool.py | 68 + pandas/tests/extension/base/__init__.py | 56 + pandas/tests/extension/base/base.py | 10 + pandas/tests/extension/base/casting.py | 23 + pandas/tests/extension/base/constructors.py | 77 + pandas/tests/extension/base/dtype.py | 91 + pandas/tests/extension/base/getitem.py | 248 + pandas/tests/extension/base/groupby.py | 78 + pandas/tests/extension/base/interface.py | 68 + pandas/tests/extension/base/io.py | 23 + pandas/tests/extension/base/methods.py | 335 + pandas/tests/extension/base/missing.py | 130 + pandas/tests/extension/base/ops.py | 166 + pandas/tests/extension/base/printing.py | 44 + pandas/tests/extension/base/reduce.py | 61 + pandas/tests/extension/base/reshaping.py | 271 + pandas/tests/extension/base/setitem.py | 188 + pandas/tests/extension/conftest.py | 167 + pandas/tests/extension/decimal/__init__.py | 4 + pandas/tests/extension/decimal/array.py | 166 + .../tests/extension/decimal/test_decimal.py | 401 + pandas/tests/extension/json/__init__.py | 3 + pandas/tests/extension/json/array.py | 199 + pandas/tests/extension/json/test_json.py | 304 + pandas/tests/extension/test_categorical.py | 243 + pandas/tests/extension/test_common.py | 86 + pandas/tests/extension/test_datetime.py | 237 + pandas/tests/extension/test_external_block.py | 76 + pandas/tests/extension/test_integer.py | 224 + pandas/tests/extension/test_interval.py | 162 + pandas/tests/extension/test_numpy.py | 430 + pandas/tests/extension/test_period.py | 166 + pandas/tests/extension/test_sparse.py | 369 + pandas/tests/formats/data/unicode_series.csv | 18 - pandas/tests/formats/test_printing.py | 195 - pandas/tests/formats/test_style.py | 719 -- pandas/tests/formats/test_to_csv.py | 216 - pandas/tests/formats/test_to_html.py | 1861 ---- pandas/tests/frame/common.py | 18 +- pandas/tests/frame/conftest.py | 159 + pandas/tests/frame/test_alter_axes.py | 1522 ++- pandas/tests/frame/test_analytics.py | 2697 +++--- pandas/tests/frame/test_api.py | 547 ++ pandas/tests/frame/test_apply.py | 1015 +- pandas/tests/frame/test_arithmetic.py | 636 ++ pandas/tests/frame/test_asof.py | 32 +- .../tests/frame/test_axis_select_reindex.py | 582 +- pandas/tests/frame/test_block_internals.py | 382 +- pandas/tests/frame/test_combine_concat.py | 254 +- pandas/tests/frame/test_constructors.py | 1021 +- pandas/tests/frame/test_convert_to.py | 483 +- pandas/tests/frame/test_dtypes.py | 654 +- pandas/tests/frame/test_duplicates.py | 466 + pandas/tests/frame/test_indexing.py | 1665 +++- pandas/tests/frame/test_join.py | 57 +- pandas/tests/frame/test_misc_api.py | 393 - pandas/tests/frame/test_missing.py | 330 +- pandas/tests/frame/test_mutate_columns.py | 132 +- pandas/tests/frame/test_nonunique_indexes.py | 64 +- pandas/tests/frame/test_operators.py | 961 +- pandas/tests/frame/test_period.py | 52 +- pandas/tests/frame/test_quantile.py | 108 +- pandas/tests/frame/test_query_eval.py | 376 +- pandas/tests/frame/test_rank.py | 164 +- pandas/tests/frame/test_replace.py | 361 +- pandas/tests/frame/test_repr_info.py | 220 +- pandas/tests/frame/test_reshape.py | 420 +- .../frame/test_sort_values_level_as_str.py | 96 + pandas/tests/frame/test_sorting.py | 436 +- pandas/tests/frame/test_subclass.py | 431 +- pandas/tests/frame/test_timeseries.py | 564 +- pandas/tests/frame/test_timezones.py | 198 + pandas/tests/frame/test_to_csv.py | 582 +- pandas/tests/frame/test_validate.py | 53 +- pandas/tests/generic/__init__.py | 0 pandas/tests/generic/test_frame.py | 271 + pandas/tests/generic/test_generic.py | 955 ++ .../generic/test_label_or_level_utils.py | 406 + pandas/tests/generic/test_series.py | 247 + pandas/tests/groupby/aggregate/__init__.py | 0 .../tests/groupby/aggregate/test_aggregate.py | 305 + pandas/tests/groupby/aggregate/test_cython.py | 218 + pandas/tests/groupby/aggregate/test_other.py | 529 ++ pandas/tests/groupby/common.py | 62 - pandas/tests/groupby/conftest.py | 78 + pandas/tests/groupby/test_aggregate.py | 792 -- pandas/tests/groupby/test_apply.py | 542 ++ pandas/tests/groupby/test_bin_groupby.py | 69 +- pandas/tests/groupby/test_categorical.py | 1435 ++- pandas/tests/groupby/test_counting.py | 224 + pandas/tests/groupby/test_filters.py | 1191 ++- pandas/tests/groupby/test_function.py | 1201 +++ pandas/tests/groupby/test_groupby.py | 5796 ++++-------- pandas/tests/groupby/test_grouping.py | 814 ++ pandas/tests/groupby/test_index_as_string.py | 68 + pandas/tests/groupby/test_nth.py | 436 + pandas/tests/groupby/test_rank.py | 306 + pandas/tests/groupby/test_timegrouper.py | 157 +- pandas/tests/groupby/test_transform.py | 1390 +-- pandas/tests/groupby/test_value_counts.py | 94 +- pandas/tests/groupby/test_whitelist.py | 190 +- pandas/tests/indexes/common.py | 764 +- pandas/tests/indexes/conftest.py | 49 + pandas/tests/indexes/data/mindex_073.pickle | Bin 670 -> 0 bytes .../tests/indexes/data/multiindex_v1.pickle | 149 - pandas/tests/indexes/datetimelike.py | 79 +- .../indexes/datetimes/test_arithmetic.py | 109 + pandas/tests/indexes/datetimes/test_astype.py | 311 +- .../indexes/datetimes/test_construction.py | 727 +- .../indexes/datetimes/test_date_range.py | 927 +- .../tests/indexes/datetimes/test_datetime.py | 642 +- .../indexes/datetimes/test_datetimelike.py | 63 +- .../tests/indexes/datetimes/test_formats.py | 221 + .../tests/indexes/datetimes/test_indexing.py | 481 +- pandas/tests/indexes/datetimes/test_misc.py | 359 +- .../tests/indexes/datetimes/test_missing.py | 90 +- pandas/tests/indexes/datetimes/test_ops.py | 1475 +-- .../indexes/datetimes/test_partial_slcing.py | 256 - .../indexes/datetimes/test_partial_slicing.py | 425 + .../indexes/datetimes/test_scalar_compat.py | 292 + pandas/tests/indexes/datetimes/test_setops.py | 457 +- .../tests/indexes/datetimes/test_timezones.py | 1163 +++ pandas/tests/indexes/datetimes/test_tools.py | 1919 ++-- pandas/tests/indexes/interval/__init__.py | 0 pandas/tests/indexes/interval/test_astype.py | 206 + .../indexes/interval/test_construction.py | 389 + .../tests/indexes/interval/test_interval.py | 1260 +++ .../indexes/interval/test_interval_new.py | 271 + .../indexes/interval/test_interval_range.py | 316 + .../indexes/interval/test_interval_tree.py | 184 + pandas/tests/indexes/multi/__init__.py | 0 pandas/tests/indexes/multi/conftest.py | 56 + pandas/tests/indexes/multi/test_analytics.py | 309 + pandas/tests/indexes/multi/test_astype.py | 32 + pandas/tests/indexes/multi/test_compat.py | 129 + .../tests/indexes/multi/test_constructor.py | 585 ++ pandas/tests/indexes/multi/test_contains.py | 106 + pandas/tests/indexes/multi/test_conversion.py | 224 + pandas/tests/indexes/multi/test_copy.py | 93 + pandas/tests/indexes/multi/test_drop.py | 136 + pandas/tests/indexes/multi/test_duplicates.py | 278 + .../tests/indexes/multi/test_equivalence.py | 221 + pandas/tests/indexes/multi/test_format.py | 132 + pandas/tests/indexes/multi/test_get_set.py | 457 + pandas/tests/indexes/multi/test_indexing.py | 393 + pandas/tests/indexes/multi/test_integrity.py | 296 + pandas/tests/indexes/multi/test_join.py | 96 + pandas/tests/indexes/multi/test_missing.py | 129 + pandas/tests/indexes/multi/test_monotonic.py | 213 + pandas/tests/indexes/multi/test_names.py | 124 + .../indexes/multi/test_partial_indexing.py | 98 + pandas/tests/indexes/multi/test_reindex.py | 108 + pandas/tests/indexes/multi/test_reshape.py | 126 + pandas/tests/indexes/multi/test_set_ops.py | 372 + pandas/tests/indexes/multi/test_sorting.py | 266 + .../tests/indexes/period/test_arithmetic.py | 108 + pandas/tests/indexes/period/test_asfreq.py | 180 +- pandas/tests/indexes/period/test_astype.py | 126 + .../tests/indexes/period/test_construction.py | 369 +- pandas/tests/indexes/period/test_formats.py | 220 + pandas/tests/indexes/period/test_indexing.py | 554 +- pandas/tests/indexes/period/test_ops.py | 1234 +-- .../indexes/period/test_partial_slicing.py | 57 +- pandas/tests/indexes/period/test_period.py | 626 +- .../tests/indexes/period/test_period_range.py | 95 + .../indexes/period/test_scalar_compat.py | 18 + pandas/tests/indexes/period/test_setops.py | 161 +- pandas/tests/indexes/period/test_tools.py | 504 +- pandas/tests/indexes/test_base.py | 3487 ++++--- pandas/tests/indexes/test_category.py | 768 +- pandas/tests/indexes/test_common.py | 355 + pandas/tests/indexes/test_frozen.py | 87 +- pandas/tests/indexes/test_multi.py | 2709 ------ pandas/tests/indexes/test_numeric.py | 661 +- pandas/tests/indexes/test_range.py | 650 +- .../indexes/timedeltas/test_arithmetic.py | 279 + .../tests/indexes/timedeltas/test_astype.py | 147 +- .../indexes/timedeltas/test_construction.py | 156 +- .../tests/indexes/timedeltas/test_formats.py | 96 + .../tests/indexes/timedeltas/test_indexing.py | 272 +- pandas/tests/indexes/timedeltas/test_ops.py | 1211 +-- .../timedeltas/test_partial_slicing.py | 30 +- .../indexes/timedeltas/test_scalar_compat.py | 64 + .../tests/indexes/timedeltas/test_setops.py | 23 +- .../indexes/timedeltas/test_timedelta.py | 467 +- .../timedeltas/test_timedelta_range.py | 42 +- pandas/tests/indexes/timedeltas/test_tools.py | 89 +- pandas/tests/indexing/common.py | 90 +- pandas/tests/indexing/conftest.py | 20 + pandas/tests/indexing/interval/__init__.py | 0 .../tests/indexing/interval/test_interval.py | 267 + .../indexing/interval/test_interval_new.py | 246 + pandas/tests/indexing/multiindex/__init__.py | 0 pandas/tests/indexing/multiindex/conftest.py | 31 + .../multiindex/test_chaining_and_caching.py | 65 + .../indexing/multiindex/test_datetime.py | 22 + .../tests/indexing/multiindex/test_getitem.py | 237 + pandas/tests/indexing/multiindex/test_iloc.py | 151 + .../indexing/multiindex/test_indexing_slow.py | 89 + pandas/tests/indexing/multiindex/test_ix.py | 56 + pandas/tests/indexing/multiindex/test_loc.py | 380 + .../indexing/multiindex/test_multiindex.py | 94 + .../tests/indexing/multiindex/test_partial.py | 183 + .../tests/indexing/multiindex/test_set_ops.py | 42 + .../tests/indexing/multiindex/test_setitem.py | 439 + .../tests/indexing/multiindex/test_slice.py | 577 ++ .../tests/indexing/multiindex/test_sorted.py | 92 + pandas/tests/indexing/multiindex/test_xs.py | 237 + pandas/tests/indexing/test_callable.py | 7 +- pandas/tests/indexing/test_categorical.py | 532 +- .../indexing/test_chaining_and_caching.py | 231 +- pandas/tests/indexing/test_coercion.py | 1553 ++-- pandas/tests/indexing/test_datetime.py | 136 +- pandas/tests/indexing/test_floats.py | 377 +- pandas/tests/indexing/test_iloc.py | 202 +- pandas/tests/indexing/test_indexing.py | 466 +- .../tests/indexing/test_indexing_engines.py | 169 + pandas/tests/indexing/test_indexing_slow.py | 86 +- pandas/tests/indexing/test_ix.py | 121 +- pandas/tests/indexing/test_loc.py | 345 +- pandas/tests/indexing/test_multiindex.py | 1250 --- pandas/tests/indexing/test_panel.py | 209 - pandas/tests/indexing/test_partial.py | 243 +- pandas/tests/indexing/test_scalar.py | 123 +- pandas/tests/indexing/test_timedelta.py | 83 +- pandas/tests/internals/__init__.py | 0 .../tests/{ => internals}/test_internals.py | 342 +- pandas/tests/io/conftest.py | 90 + pandas/tests/io/data/banklist.html | 111 +- pandas/tests/io/data/feather-0_3_1.feather | Bin 0 -> 672 bytes pandas/tests/io/data/fixed_width_format.txt | 3 + .../io/data/legacy_hdf/legacy_table_0.11.h5 | Bin 293877 -> 0 bytes .../data/legacy_hdf/legacy_table_fixed_py2.h5 | Bin 0 -> 1064200 bytes .../io/data/legacy_hdf/legacy_table_py2.h5 | Bin 0 -> 72279 bytes ...periodindex_0.20.1_x86_64_darwin_2.7.13.h5 | Bin 0 -> 7312 bytes .../0.16.2/0.16.2_AMD64_windows_2.7.14.pickle | Bin 0 -> 132692 bytes .../0.17.0/0.17.0_x86_64_darwin_3.5.3.pickle | Bin 0 -> 129175 bytes .../0.18.1/0.18.1_x86_64_darwin_3.5.2.pickle | Bin 125826 -> 127853 bytes .../0.19.2/0.19.2_AMD64_windows_2.7.14.pickle | Bin 0 -> 133468 bytes .../0.19.2/0.19.2_x86_64_darwin_2.7.14.pickle | Bin 0 -> 132762 bytes .../0.19.2/0.19.2_x86_64_darwin_3.6.1.pickle | Bin 125349 -> 126076 bytes .../0.20.3/0.20.3_x86_64_darwin_2.7.14.pickle | Bin 0 -> 132857 bytes pandas/tests/io/data/macau.html | 2624 +++--- pandas/tests/io/data/nyse_wsj.html | 2 +- pandas/tests/io/data/spam.html | 762 +- pandas/tests/io/data/stata13_dates.dta | Bin 0 -> 3386 bytes pandas/tests/io/data/stata16_118.dta | Bin 0 -> 4614 bytes pandas/tests/io/data/test1.xls | Bin 30720 -> 28672 bytes pandas/tests/io/data/test1.xlsm | Bin 45056 -> 13967 bytes pandas/tests/io/data/test1.xlsx | Bin 44929 -> 13878 bytes pandas/tests/io/data/testmultiindex.xls | Bin 28672 -> 39424 bytes pandas/tests/io/data/testmultiindex.xlsm | Bin 16249 -> 19133 bytes pandas/tests/io/data/testmultiindex.xlsx | Bin 16135 -> 18845 bytes pandas/tests/io/data/wikipedia_states.html | 3 +- pandas/tests/io/formats/__init__.py | 0 .../data/html/datetime64_hourformatter.html | 18 + .../data/html/datetime64_monthformatter.html | 18 + .../io/formats/data/html/escape_disabled.html | 21 + .../tests/io/formats/data/html/escaped.html | 21 + .../data/html/gh12031_expected_output.html | 22 + .../data/html/gh14882_expected_output_1.html | 274 + .../data/html/gh14882_expected_output_2.html | 258 + .../data/html/gh14998_expected_output.html | 12 + .../data/html/gh15019_expected_output.html | 30 + .../data/html/gh21625_expected_output.html | 14 + .../data/html/gh22270_expected_output.html | 14 + .../data/html/gh22579_expected_output.html | 76 + .../data/html/gh22783_expected_output.html | 27 + .../html/gh22783_named_columns_index.html | 30 + .../data/html/gh6131_expected_output.html | 46 + .../data/html/gh8452_expected_output.html | 28 + .../tests/io/formats/data/html/index_1.html | 30 + .../tests/io/formats/data/html/index_2.html | 26 + .../tests/io/formats/data/html/index_3.html | 36 + .../tests/io/formats/data/html/index_4.html | 33 + .../tests/io/formats/data/html/index_5.html | 40 + .../io/formats/data/html/index_formatter.html | 31 + ...index_named_multi_columns_named_multi.html | 34 + ...ex_named_multi_columns_named_standard.html | 29 + .../html/index_named_multi_columns_none.html | 23 + ...dex_named_multi_columns_unnamed_multi.html | 34 + ..._named_multi_columns_unnamed_standard.html | 29 + ...ex_named_standard_columns_named_multi.html | 30 + ...named_standard_columns_named_standard.html | 26 + .../index_named_standard_columns_none.html | 21 + ..._named_standard_columns_unnamed_multi.html | 30 + ...med_standard_columns_unnamed_standard.html | 26 + .../html/index_none_columns_named_multi.html | 25 + .../index_none_columns_named_standard.html | 21 + .../data/html/index_none_columns_none.html | 12 + .../index_none_columns_unnamed_multi.html | 21 + .../index_none_columns_unnamed_standard.html | 18 + ...dex_unnamed_multi_columns_named_multi.html | 28 + ..._unnamed_multi_columns_named_standard.html | 23 + .../index_unnamed_multi_columns_none.html | 15 + ...x_unnamed_multi_columns_unnamed_multi.html | 28 + ...nnamed_multi_columns_unnamed_standard.html | 23 + ..._unnamed_standard_columns_named_multi.html | 25 + ...named_standard_columns_named_standard.html | 21 + .../index_unnamed_standard_columns_none.html | 14 + ...nnamed_standard_columns_unnamed_multi.html | 25 + ...med_standard_columns_unnamed_standard.html | 21 + .../tests/io/formats/data/html/justify.html | 30 + .../io/formats/data/html/multiindex_1.html | 32 + .../io/formats/data/html/multiindex_2.html | 34 + .../data/html/multiindex_sparsify_1.html | 40 + .../data/html/multiindex_sparsify_2.html | 46 + ...tiindex_sparsify_false_multi_sparse_1.html | 42 + ...tiindex_sparsify_false_multi_sparse_2.html | 48 + .../formats/data/html/render_links_false.html | 24 + .../formats/data/html/render_links_true.html | 24 + ...index_named_multi_columns_named_multi.html | 88 + ...ex_named_multi_columns_named_standard.html | 72 + ...unc_df_index_named_multi_columns_none.html | 62 + ...dex_named_multi_columns_unnamed_multi.html | 88 + ..._named_multi_columns_unnamed_standard.html | 72 + ...ex_named_standard_columns_named_multi.html | 74 + ...named_standard_columns_named_standard.html | 62 + ..._df_index_named_standard_columns_none.html | 54 + ..._named_standard_columns_unnamed_multi.html | 74 + ...med_standard_columns_unnamed_standard.html | 62 + ...unc_df_index_none_columns_named_multi.html | 66 + ..._df_index_none_columns_named_standard.html | 54 + .../trunc_df_index_none_columns_none.html | 39 + ...c_df_index_none_columns_unnamed_multi.html | 58 + ...f_index_none_columns_unnamed_standard.html | 48 + ...dex_unnamed_multi_columns_named_multi.html | 78 + ..._unnamed_multi_columns_named_standard.html | 62 + ...c_df_index_unnamed_multi_columns_none.html | 50 + ...x_unnamed_multi_columns_unnamed_multi.html | 78 + ...nnamed_multi_columns_unnamed_standard.html | 62 + ..._unnamed_standard_columns_named_multi.html | 66 + ...named_standard_columns_named_standard.html | 54 + ...f_index_unnamed_standard_columns_none.html | 44 + ...nnamed_standard_columns_unnamed_multi.html | 66 + ...med_standard_columns_unnamed_standard.html | 54 + .../tests/io/formats/data/html/truncate.html | 86 + .../data/html/truncate_multi_index.html | 101 + .../html/truncate_multi_index_sparse_off.html | 105 + .../tests/io/formats/data/html/unicode_1.html | 50 + .../tests/io/formats/data/html/unicode_2.html | 14 + .../io/formats/data/html/with_classes.html | 9 + pandas/tests/io/formats/test_console.py | 92 + pandas/tests/io/formats/test_css.py | 187 + .../{ => io}/formats/test_eng_formatting.py | 31 +- pandas/tests/{ => io}/formats/test_format.py | 1365 +-- pandas/tests/io/formats/test_printing.py | 204 + pandas/tests/io/formats/test_style.py | 1315 +++ pandas/tests/io/formats/test_to_csv.py | 563 ++ pandas/tests/io/formats/test_to_excel.py | 278 + pandas/tests/io/formats/test_to_html.py | 625 ++ .../tests/{ => io}/formats/test_to_latex.py | 310 +- .../tests/io/generate_legacy_storage_files.py | 142 +- .../tests/io/json/data/tsframe_v012.json.zip | Bin 0 -> 436 bytes pandas/tests/io/json/test_compression.py | 120 + .../tests/io/json/test_json_table_schema.py | 471 +- pandas/tests/io/json/test_normalize.py | 187 +- pandas/tests/io/json/test_pandas.py | 484 +- pandas/tests/io/json/test_readlines.py | 172 + pandas/tests/io/json/test_ujson.py | 2192 ++--- pandas/tests/io/msgpack/common.py | 9 + pandas/tests/io/msgpack/data/frame.mp | Bin 0 -> 309 bytes pandas/tests/io/msgpack/test_buffer.py | 4 +- pandas/tests/io/msgpack/test_case.py | 4 +- pandas/tests/io/msgpack/test_except.py | 46 +- pandas/tests/io/msgpack/test_extension.py | 10 +- pandas/tests/io/msgpack/test_limits.py | 52 +- pandas/tests/io/msgpack/test_newspec.py | 2 +- pandas/tests/io/msgpack/test_obj.py | 24 +- pandas/tests/io/msgpack/test_pack.py | 32 +- pandas/tests/io/msgpack/test_read_size.py | 3 +- pandas/tests/io/msgpack/test_seq.py | 3 +- pandas/tests/io/msgpack/test_sequnpack.py | 55 +- pandas/tests/io/msgpack/test_subtype.py | 3 +- pandas/tests/io/msgpack/test_unpack.py | 11 +- pandas/tests/io/msgpack/test_unpack_raw.py | 1 + pandas/tests/io/parser/c_parser_only.py | 410 - pandas/tests/io/parser/comment.py | 118 - pandas/tests/io/parser/common.py | 1679 ---- pandas/tests/io/parser/compression.py | 171 - pandas/tests/io/parser/conftest.py | 85 + pandas/tests/io/parser/converters.py | 153 - pandas/tests/io/parser/data/items.jsonl | 2 + pandas/tests/io/parser/data/salaries.csv | 92 +- pandas/tests/io/parser/data/sub_char.csv | 2 + pandas/tests/io/parser/data/tar_csv.tar | Bin 0 -> 10240 bytes pandas/tests/io/parser/data/tar_csv.tar.gz | Bin 0 -> 117 bytes pandas/tests/io/parser/data/tips.csv.bz2 | Bin 0 -> 1316 bytes pandas/tests/io/parser/data/tips.csv.gz | Bin 0 -> 1740 bytes .../tests/io/parser/data/utf16_ex_small.zip | Bin 0 -> 285 bytes pandas/tests/io/parser/dialect.py | 78 - pandas/tests/io/parser/dtypes.py | 286 - pandas/tests/io/parser/header.py | 277 - pandas/tests/io/parser/index_col.py | 141 - pandas/tests/io/parser/multithread.py | 99 - pandas/tests/io/parser/na_values.py | 305 - pandas/tests/io/parser/parse_dates.py | 656 -- pandas/tests/io/parser/python_parser_only.py | 239 - pandas/tests/io/parser/quoting.py | 153 - pandas/tests/io/parser/skiprows.py | 225 - pandas/tests/io/parser/test_c_parser_only.py | 591 ++ pandas/tests/io/parser/test_comment.py | 136 + pandas/tests/io/parser/test_common.py | 1946 ++++ pandas/tests/io/parser/test_compression.py | 154 + pandas/tests/io/parser/test_converters.py | 158 + pandas/tests/io/parser/test_dialect.py | 135 + pandas/tests/io/parser/test_dtypes.py | 514 + pandas/tests/io/parser/test_header.py | 428 + pandas/tests/io/parser/test_index_col.py | 152 + pandas/tests/io/parser/test_mangle_dupes.py | 119 + pandas/tests/io/parser/test_multi_thread.py | 145 + pandas/tests/io/parser/test_na_values.py | 441 + pandas/tests/io/parser/test_network.py | 191 +- pandas/tests/io/parser/test_parse_dates.py | 849 ++ pandas/tests/io/parser/test_parsers.py | 101 - .../io/parser/test_python_parser_only.py | 301 + pandas/tests/io/parser/test_quoting.py | 158 + pandas/tests/io/parser/test_read_fwf.py | 681 +- pandas/tests/io/parser/test_skiprows.py | 222 + pandas/tests/io/parser/test_textreader.py | 188 +- pandas/tests/io/parser/test_unsupported.py | 125 +- pandas/tests/io/parser/test_usecols.py | 534 ++ pandas/tests/io/parser/usecols.py | 477 - pandas/tests/io/sas/data/cars.sas7bdat | Bin 0 -> 13312 bytes pandas/tests/io/sas/data/datetime.csv | 5 + .../data/datetime.sas7bdat} | Bin 238321 -> 131072 bytes pandas/tests/io/sas/data/load_log.sas7bdat | Bin 0 -> 589824 bytes pandas/tests/io/sas/data/many_columns.csv | 4 + .../tests/io/sas/data/many_columns.sas7bdat | Bin 0 -> 81920 bytes pandas/tests/io/sas/data/productsales.csv | 2880 +++--- .../data/zero_variables.sas7bdat} | Bin 211111 -> 149504 bytes pandas/tests/io/sas/test_sas.py | 22 +- pandas/tests/io/sas/test_sas7bdat.py | 145 +- pandas/tests/io/sas/test_xport.py | 19 +- pandas/tests/io/test_clipboard.py | 283 +- pandas/tests/io/test_common.py | 330 +- pandas/tests/io/test_compression.py | 116 + pandas/tests/io/test_date_converters.py | 43 + pandas/tests/io/test_excel.py | 3021 +++--- pandas/tests/io/test_feather.py | 120 +- pandas/tests/io/test_gbq.py | 93 +- pandas/tests/io/test_gcs.py | 72 + pandas/tests/io/test_html.py | 1041 ++- pandas/tests/io/test_packers.py | 341 +- pandas/tests/io/test_parquet.py | 541 ++ pandas/tests/io/test_pickle.py | 233 +- pandas/tests/io/test_pytables.py | 2663 +++--- pandas/tests/io/test_s3.py | 29 +- pandas/tests/io/test_sql.py | 1027 +- pandas/tests/io/test_stata.py | 868 +- pandas/tests/plotting/common.py | 168 +- pandas/tests/plotting/test_boxplot_method.py | 150 +- pandas/tests/plotting/test_converter.py | 346 + pandas/tests/plotting/test_datetimelike.py | 1250 +-- pandas/tests/plotting/test_frame.py | 1308 ++- pandas/tests/plotting/test_groupby.py | 13 +- pandas/tests/plotting/test_hist_method.py | 171 +- pandas/tests/plotting/test_misc.py | 226 +- pandas/tests/plotting/test_series.py | 555 +- pandas/tests/reductions/__init__.py | 4 + pandas/tests/reductions/test_reductions.py | 1161 +++ .../tests/reductions/test_stat_reductions.py | 202 + pandas/tests/resample/__init__.py | 0 pandas/tests/resample/conftest.py | 142 + pandas/tests/resample/test_base.py | 235 + pandas/tests/resample/test_datetime_index.py | 1478 +++ pandas/tests/resample/test_period_index.py | 772 ++ pandas/tests/resample/test_resample_api.py | 573 ++ .../tests/resample/test_resampler_grouper.py | 260 + pandas/tests/resample/test_time_grouper.py | 266 + pandas/tests/resample/test_timedelta.py | 128 + pandas/tests/reshape/__init__.py | 0 .../{tools => reshape}/data/cut_data.csv | 2 +- pandas/tests/reshape/merge/__init__.py | 0 .../merge}/data/allow_exact_matches.csv | 0 .../allow_exact_matches_and_tolerance.csv | 0 .../{tools => reshape/merge}/data/asof.csv | 0 .../{tools => reshape/merge}/data/asof2.csv | 0 .../{tools => reshape/merge}/data/quotes.csv | 0 .../{tools => reshape/merge}/data/quotes2.csv | 0 .../merge}/data/tolerance.csv | 0 .../{tools => reshape/merge}/data/trades.csv | 0 .../{tools => reshape/merge}/data/trades2.csv | 0 .../{tools => reshape/merge}/test_join.py | 369 +- pandas/tests/reshape/merge/test_merge.py | 1668 ++++ .../merge}/test_merge_asof.py | 318 +- .../merge/test_merge_index_as_string.py | 177 + .../merge}/test_merge_ordered.py | 43 +- pandas/tests/reshape/merge/test_multi.py | 668 ++ .../tests/{tools => reshape}/test_concat.py | 1290 ++- pandas/tests/reshape/test_cut.py | 458 + pandas/tests/reshape/test_melt.py | 718 ++ pandas/tests/{tools => reshape}/test_pivot.py | 868 +- pandas/tests/reshape/test_qcut.py | 199 + pandas/tests/reshape/test_reshape.py | 626 ++ .../test_union_categoricals.py | 55 +- pandas/tests/reshape/test_util.py | 53 + pandas/tests/scalar/interval/__init__.py | 0 pandas/tests/scalar/interval/test_interval.py | 225 + pandas/tests/scalar/interval/test_ops.py | 60 + pandas/tests/scalar/period/__init__.py | 0 .../test_asfreq.py} | 590 +- pandas/tests/scalar/period/test_period.py | 1498 +++ pandas/tests/scalar/test_nat.py | 492 +- pandas/tests/scalar/test_period.py | 1419 --- pandas/tests/scalar/test_timedelta.py | 699 -- pandas/tests/scalar/test_timestamp.py | 1527 --- pandas/tests/scalar/timedelta/__init__.py | 0 .../tests/scalar/timedelta/test_arithmetic.py | 691 ++ .../scalar/timedelta/test_construction.py | 210 + pandas/tests/scalar/timedelta/test_formats.py | 28 + .../tests/scalar/timedelta/test_timedelta.py | 759 ++ pandas/tests/scalar/timestamp/__init__.py | 0 .../tests/scalar/timestamp/test_arithmetic.py | 117 + .../scalar/timestamp/test_comparisons.py | 168 + .../tests/scalar/timestamp/test_rendering.py | 96 + .../tests/scalar/timestamp/test_timestamp.py | 988 ++ .../tests/scalar/timestamp/test_timezones.py | 389 + .../tests/scalar/timestamp/test_unary_ops.py | 377 + pandas/tests/series/common.py | 5 +- pandas/tests/series/conftest.py | 33 + pandas/tests/series/indexing/__init__.py | 0 pandas/tests/series/indexing/conftest.py | 8 + .../tests/series/indexing/test_alter_index.py | 564 ++ pandas/tests/series/indexing/test_boolean.py | 634 ++ pandas/tests/series/indexing/test_callable.py | 33 + pandas/tests/series/indexing/test_datetime.py | 714 ++ pandas/tests/series/indexing/test_iloc.py | 37 + pandas/tests/series/indexing/test_indexing.py | 841 ++ pandas/tests/series/indexing/test_loc.py | 168 + pandas/tests/series/indexing/test_numeric.py | 259 + pandas/tests/series/test_alter_axes.py | 306 +- pandas/tests/series/test_analytics.py | 1853 ++-- pandas/tests/series/test_api.py | 712 ++ pandas/tests/series/test_apply.py | 464 +- pandas/tests/series/test_arithmetic.py | 172 + pandas/tests/series/test_asof.py | 50 +- pandas/tests/series/test_block_internals.py | 43 + pandas/tests/series/test_combine_concat.py | 227 +- pandas/tests/series/test_constructors.py | 965 +- pandas/tests/series/test_datetime_values.py | 350 +- pandas/tests/series/test_dtypes.py | 538 +- pandas/tests/series/test_duplicates.py | 148 + pandas/tests/series/test_indexing.py | 2688 ------ pandas/tests/series/test_internals.py | 80 +- pandas/tests/series/test_io.py | 279 +- pandas/tests/series/test_misc_api.py | 350 - pandas/tests/series/test_missing.py | 704 +- pandas/tests/series/test_operators.py | 2040 +--- pandas/tests/series/test_period.py | 249 +- pandas/tests/series/test_quantile.py | 153 +- pandas/tests/series/test_rank.py | 307 +- pandas/tests/series/test_replace.py | 91 +- pandas/tests/series/test_repr.py | 380 +- pandas/tests/series/test_sorting.py | 187 +- pandas/tests/series/test_subclass.py | 44 +- pandas/tests/series/test_timeseries.py | 545 +- pandas/tests/series/test_timezones.py | 366 + pandas/tests/series/test_validate.py | 40 +- pandas/tests/sparse/common.py | 10 - pandas/tests/sparse/frame/__init__.py | 0 pandas/tests/sparse/frame/conftest.py | 115 + pandas/tests/sparse/frame/test_analytics.py | 39 + pandas/tests/sparse/frame/test_apply.py | 105 + pandas/tests/sparse/frame/test_frame.py | 1407 +++ pandas/tests/sparse/frame/test_indexing.py | 109 + pandas/tests/sparse/frame/test_to_csv.py | 21 + .../tests/sparse/frame/test_to_from_scipy.py | 185 + pandas/tests/sparse/series/__init__.py | 0 pandas/tests/sparse/series/test_indexing.py | 111 + .../tests/sparse/{ => series}/test_series.py | 645 +- pandas/tests/sparse/test_array.py | 812 -- pandas/tests/sparse/test_combine_concat.py | 359 +- pandas/tests/sparse/test_format.py | 59 +- pandas/tests/sparse/test_frame.py | 1319 --- pandas/tests/sparse/test_groupby.py | 50 +- pandas/tests/sparse/test_indexing.py | 343 +- pandas/tests/sparse/test_list.py | 112 - pandas/tests/sparse/test_pivot.py | 6 +- pandas/tests/sparse/test_reshape.py | 42 + pandas/tests/test_algos.py | 1278 ++- pandas/tests/test_base.py | 1299 ++- pandas/tests/test_categorical.py | 4427 --------- pandas/tests/test_common.py | 193 +- pandas/tests/test_compat.py | 59 +- pandas/tests/test_config.py | 356 +- pandas/tests/test_downstream.py | 135 + pandas/tests/test_errors.py | 74 + pandas/tests/test_expressions.py | 140 +- pandas/tests/test_generic.py | 2057 ---- pandas/tests/test_join.py | 64 +- pandas/tests/test_lib.py | 179 +- pandas/tests/test_multilevel.py | 1597 ++-- pandas/tests/test_nanops.py | 438 +- pandas/tests/test_panel.py | 2535 ----- pandas/tests/test_panel4d.py | 954 -- pandas/tests/test_panelnd.py | 102 - pandas/tests/test_register_accessor.py | 89 + pandas/tests/test_reshape.py | 957 -- pandas/tests/test_sorting.py | 187 +- pandas/tests/test_strings.py | 1356 ++- pandas/tests/test_take.py | 607 +- pandas/tests/test_testing.py | 778 -- pandas/tests/test_util.py | 403 - pandas/tests/test_window.py | 3115 ++++--- pandas/tests/tools/test_hashing.py | 234 - pandas/tests/tools/test_merge.py | 1405 --- pandas/tests/tools/test_numeric.py | 583 ++ pandas/tests/tools/test_tile.py | 427 - pandas/tests/tools/test_util.py | 485 - pandas/tests/tseries/frequencies/__init__.py | 0 .../tseries/frequencies/test_freq_code.py | 149 + .../tseries/frequencies/test_inference.py | 406 + .../tseries/frequencies/test_to_offset.py | 146 + pandas/tests/tseries/holiday/__init__.py | 0 pandas/tests/tseries/holiday/test_calendar.py | 77 + pandas/tests/tseries/holiday/test_federal.py | 36 + pandas/tests/tseries/holiday/test_holiday.py | 193 + .../tests/tseries/holiday/test_observance.py | 93 + pandas/tests/tseries/offsets/__init__.py | 1 + pandas/tests/tseries/offsets/common.py | 25 + pandas/tests/tseries/offsets/conftest.py | 21 + .../{ => offsets}/data/cday-0.14.1.pickle | Bin .../data/dateoffset_0_15_2.pickle | 0 pandas/tests/tseries/offsets/test_fiscal.py | 657 ++ pandas/tests/tseries/offsets/test_offsets.py | 3172 +++++++ .../offsets/test_offsets_properties.py | 108 + pandas/tests/tseries/offsets/test_ticks.py | 343 + .../tests/tseries/offsets/test_yqm_offsets.py | 1035 ++ pandas/tests/tseries/test_converter.py | 199 - pandas/tests/tseries/test_frequencies.py | 845 -- pandas/tests/tseries/test_holiday.py | 390 - pandas/tests/tseries/test_offsets.py | 4962 ---------- pandas/tests/tseries/test_resample.py | 3206 ------- pandas/tests/tseries/test_timezones.py | 1722 ---- pandas/tests/tslibs/__init__.py | 0 pandas/tests/tslibs/test_api.py | 40 + pandas/tests/tslibs/test_array_to_datetime.py | 156 + pandas/tests/tslibs/test_ccalendar.py | 25 + pandas/tests/tslibs/test_conversion.py | 68 + pandas/tests/tslibs/test_libfrequencies.py | 100 + pandas/tests/tslibs/test_liboffsets.py | 174 + pandas/tests/tslibs/test_normalize_date.py | 18 + pandas/tests/tslibs/test_parse_iso8601.py | 62 + pandas/tests/tslibs/test_parsing.py | 186 + pandas/tests/tslibs/test_period_asfreq.py | 87 + pandas/tests/tslibs/test_timedeltas.py | 29 + pandas/tests/tslibs/test_timezones.py | 101 + pandas/tests/types/test_cast.py | 320 - pandas/tests/types/test_common.py | 82 - pandas/tests/types/test_concat.py | 78 - pandas/tests/types/test_dtypes.py | 352 - pandas/tests/types/test_generic.py | 40 - pandas/tests/types/test_inference.py | 975 -- pandas/tests/types/test_io.py | 109 - pandas/tests/types/test_missing.py | 303 - pandas/tests/util/__init__.py | 0 pandas/tests/util/conftest.py | 26 + pandas/tests/util/test_assert_almost_equal.py | 350 + .../util/test_assert_categorical_equal.py | 92 + .../util/test_assert_extension_array_equal.py | 102 + pandas/tests/util/test_assert_frame_equal.py | 209 + pandas/tests/util/test_assert_index_equal.py | 179 + .../util/test_assert_interval_array_equal.py | 80 + .../util/test_assert_numpy_array_equal.py | 177 + pandas/tests/util/test_assert_series_equal.py | 185 + pandas/tests/util/test_deprecate.py | 63 + pandas/tests/util/test_deprecate_kwarg.py | 93 + pandas/tests/util/test_hashing.py | 327 + pandas/tests/util/test_locale.py | 94 + pandas/tests/util/test_move.py | 79 + pandas/tests/util/test_safe_import.py | 45 + pandas/tests/util/test_util.py | 127 + pandas/tests/util/test_validate_args.py | 76 + .../util/test_validate_args_and_kwargs.py | 105 + pandas/tests/util/test_validate_kwargs.py | 72 + pandas/tools/tile.py | 390 - pandas/tseries/api.py | 8 +- pandas/tseries/base.py | 867 -- pandas/tseries/common.py | 242 - pandas/tseries/converter.py | 1040 +-- pandas/tseries/frequencies.py | 995 +- pandas/tseries/holiday.py | 54 +- pandas/tseries/index.py | 2181 ----- pandas/tseries/interval.py | 35 - pandas/tseries/offsets.py | 2556 +++-- pandas/tseries/period.py | 1181 --- pandas/tseries/plotting.py | 345 +- pandas/tseries/resample.py | 1398 --- pandas/tseries/tdi.py | 989 -- pandas/tseries/timedeltas.py | 181 - pandas/tseries/tools.py | 785 -- pandas/tseries/util.py | 104 - pandas/tslib.py | 8 - pandas/types/api.py | 58 - pandas/types/common.py | 507 - pandas/types/concat.py | 488 - pandas/types/dtypes.py | 367 - pandas/types/inference.py | 106 - pandas/types/missing.py | 393 - pandas/util/__init__.py | 2 + pandas/util/{decorators.py => _decorators.py} | 242 +- .../util/{depr_module.py => _depr_module.py} | 41 +- pandas/util/{doctools.py => _doctools.py} | 36 +- pandas/util/_exceptions.py | 16 + .../{print_versions.py => _print_versions.py} | 48 +- pandas/util/_test_decorators.py | 210 + pandas/util/_tester.py | 32 +- pandas/util/{validators.py => _validators.py} | 148 +- pandas/util/move.c | 59 +- pandas/util/testing.py | 2046 ++-- requirements-dev.txt | 45 + scripts/api_rst_coverage.py | 43 - scripts/bench_join.R | 50 - scripts/bench_join.py | 211 - scripts/bench_join_multi.py | 32 - scripts/bench_refactor.py | 51 - scripts/boxplot_test.py | 14 - scripts/build_dist.sh | 4 +- scripts/build_dist_for_release.sh | 10 + scripts/count_code.sh | 1 - scripts/download_wheels.py | 47 + scripts/faster_xs.py | 15 - scripts/file_sizes.py | 208 - scripts/find_commits_touching_func.py | 177 +- scripts/find_undoc_args.py | 126 - scripts/gen_release_notes.py | 95 - scripts/generate_pip_deps_from_conda.py | 122 + scripts/git-mrb | 82 - scripts/git_code_churn.py | 34 - scripts/groupby_sample.py | 54 - scripts/groupby_speed.py | 35 - scripts/groupby_test.py | 145 - scripts/hdfstore_panel_perf.py | 17 - scripts/json_manip.py | 423 - scripts/leak.py | 13 - scripts/list_future_warnings.sh | 46 + scripts/{merge-py.py => merge-pr.py} | 69 +- scripts/parser_magic.py | 74 - scripts/preepoch_test.py | 23 - scripts/pypistats.py | 101 - scripts/roll_median_leak.py | 26 - scripts/runtests.py | 5 - scripts/test_py27.bat | 6 - scripts/testmed.py | 171 - scripts/tests/__init__.py | 0 scripts/tests/conftest.py | 3 + scripts/tests/test_validate_docstrings.py | 1131 +++ scripts/touchup_gh_issues.py | 44 - scripts/use_build_cache.py | 354 - scripts/validate_docstrings.py | 954 ++ scripts/winbuild_py27.bat | 2 - scripts/windows_builder/build_27-32.bat | 25 - scripts/windows_builder/build_27-64.bat | 25 - scripts/windows_builder/build_34-32.bat | 27 - scripts/windows_builder/build_34-64.bat | 27 - scripts/windows_builder/check_and_build.bat | 2 - scripts/windows_builder/check_and_build.py | 194 - scripts/windows_builder/readme.txt | 17 - setup.cfg | 152 +- setup.py | 767 +- test.bat | 2 +- test.sh | 2 +- test_fast.bat | 2 +- test_fast.sh | 2 +- test_perf.sh | 5 - tox.ini | 11 +- vb_suite/.gitignore | 4 - vb_suite/attrs_caching.py | 20 - vb_suite/binary_ops.py | 199 - vb_suite/categoricals.py | 16 - vb_suite/ctors.py | 39 - vb_suite/eval.py | 150 - vb_suite/frame_ctor.py | 123 - vb_suite/frame_methods.py | 525 -- vb_suite/generate_rst_files.py | 2 - vb_suite/gil.py | 110 - vb_suite/groupby.py | 620 -- vb_suite/hdfstore_bench.py | 278 - vb_suite/index_object.py | 173 - vb_suite/indexing.py | 292 - vb_suite/inference.py | 36 - vb_suite/io_bench.py | 150 - vb_suite/io_sql.py | 126 - vb_suite/join_merge.py | 270 - vb_suite/make.py | 167 - vb_suite/measure_memory_consumption.py | 55 - vb_suite/miscellaneous.py | 32 - vb_suite/packers.py | 252 - vb_suite/pandas_vb_common.py | 30 - vb_suite/panel_ctor.py | 76 - vb_suite/panel_methods.py | 28 - vb_suite/parser_vb.py | 112 - vb_suite/perf_HEAD.py | 243 - vb_suite/plotting.py | 25 - vb_suite/reindex.py | 225 - vb_suite/replace.py | 36 - vb_suite/reshape.py | 65 - vb_suite/run_suite.py | 15 - vb_suite/series_methods.py | 39 - vb_suite/source/conf.py | 225 - vb_suite/source/themes/agogo/layout.html | 95 - .../source/themes/agogo/static/agogo.css_t | 476 - .../source/themes/agogo/static/bgfooter.png | Bin 434 -> 0 bytes vb_suite/source/themes/agogo/static/bgtop.png | Bin 430 -> 0 bytes vb_suite/source/themes/agogo/theme.conf | 19 - vb_suite/sparse.py | 65 - vb_suite/stat_ops.py | 126 - vb_suite/strings.py | 59 - vb_suite/suite.py | 164 - vb_suite/test.py | 67 - vb_suite/test_perf.py | 616 -- vb_suite/timedelta.py | 32 - vb_suite/timeseries.py | 445 - versioneer.py | 20 +- 1539 files changed, 316215 insertions(+), 217635 deletions(-) delete mode 100644 .coveragerc create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .pep8speaks.yml create mode 100644 AUTHORS.md create mode 100644 LICENSES/DATEUTIL_LICENSE create mode 100644 LICENSES/MUSL_LICENSE create mode 100644 LICENSES/XARRAY_LICENSE delete mode 100644 appveyor.yml create mode 100644 asv_bench/benchmarks/dtypes.py delete mode 100644 asv_bench/benchmarks/hdfstore_bench.py create mode 100644 asv_bench/benchmarks/indexing_engines.py rename {doc/sphinxext/ipython_sphinxext => asv_bench/benchmarks/io}/__init__.py (100%) create mode 100644 asv_bench/benchmarks/io/csv.py create mode 100644 asv_bench/benchmarks/io/excel.py create mode 100644 asv_bench/benchmarks/io/hdf.py create mode 100644 asv_bench/benchmarks/io/json.py create mode 100644 asv_bench/benchmarks/io/msgpack.py create mode 100644 asv_bench/benchmarks/io/pickle.py create mode 100644 asv_bench/benchmarks/io/sas.py create mode 100644 asv_bench/benchmarks/io/sql.py create mode 100644 asv_bench/benchmarks/io/stata.py delete mode 100644 asv_bench/benchmarks/io_bench.py delete mode 100644 asv_bench/benchmarks/io_sql.py create mode 100644 asv_bench/benchmarks/multiindex_object.py create mode 100644 asv_bench/benchmarks/offset.py delete mode 100644 asv_bench/benchmarks/packers.py delete mode 100644 asv_bench/benchmarks/parser_vb.py create mode 100644 asv_bench/benchmarks/rolling.py create mode 100644 asv_bench/benchmarks/timestamp.py delete mode 100644 asv_bench/vbench_to_asv.py create mode 100644 azure-pipelines.yml delete mode 100644 bench/alignment.py delete mode 100644 bench/bench_dense_to_sparse.py delete mode 100644 bench/bench_get_put_value.py delete mode 100644 bench/bench_groupby.py delete mode 100644 bench/bench_join_panel.py delete mode 100644 bench/bench_khash_dict.py delete mode 100644 bench/bench_merge.R delete mode 100644 bench/bench_merge.py delete mode 100644 bench/bench_merge_sqlite.py delete mode 100644 bench/bench_pivot.R delete mode 100644 bench/bench_pivot.py delete mode 100644 bench/bench_take_indexing.py delete mode 100644 bench/bench_unique.py delete mode 100644 bench/bench_with_subset.R delete mode 100644 bench/bench_with_subset.py delete mode 100644 bench/better_unique.py delete mode 100644 bench/duplicated.R delete mode 100644 bench/io_roundtrip.py delete mode 100644 bench/serialize.py delete mode 100644 bench/test.py delete mode 100644 bench/zoo_bench.R delete mode 100644 bench/zoo_bench.py delete mode 100644 ci/README.txt create mode 100644 ci/azure/posix.yml create mode 100644 ci/azure/windows.yml delete mode 100755 ci/before_install_travis.sh create mode 100755 ci/before_script_travis.sh create mode 100755 ci/code_checks.sh create mode 100644 ci/deps/azure-27-compat.yaml create mode 100644 ci/deps/azure-27-locale.yaml create mode 100644 ci/deps/azure-36-locale_slow.yaml create mode 100644 ci/deps/azure-37-locale.yaml create mode 100644 ci/deps/azure-37-numpydev.yaml create mode 100644 ci/deps/azure-macos-35.yaml create mode 100644 ci/deps/azure-windows-27.yaml create mode 100644 ci/deps/azure-windows-36.yaml create mode 100644 ci/deps/travis-27.yaml create mode 100644 ci/deps/travis-36-doc.yaml create mode 100644 ci/deps/travis-36-locale.yaml create mode 100644 ci/deps/travis-36-slow.yaml create mode 100644 ci/deps/travis-36.yaml create mode 100644 ci/deps/travis-37.yaml create mode 100644 ci/incremental/build.cmd create mode 100755 ci/incremental/build.sh create mode 100755 ci/incremental/install_miniconda.sh create mode 100644 ci/incremental/setup_conda_environment.cmd create mode 100755 ci/incremental/setup_conda_environment.sh delete mode 100644 ci/install.ps1 delete mode 100755 ci/install_circle.sh delete mode 100755 ci/install_db_circle.sh delete mode 100755 ci/lint.sh delete mode 100755 ci/print_versions.py delete mode 100644 ci/requirements-2.7.build delete mode 100644 ci/requirements-2.7.pip delete mode 100644 ci/requirements-2.7.run delete mode 100644 ci/requirements-2.7.sh delete mode 100644 ci/requirements-2.7_BUILD_TEST.build delete mode 100644 ci/requirements-2.7_COMPAT.build delete mode 100644 ci/requirements-2.7_COMPAT.pip delete mode 100644 ci/requirements-2.7_COMPAT.run delete mode 100644 ci/requirements-2.7_LOCALE.build delete mode 100644 ci/requirements-2.7_LOCALE.pip delete mode 100644 ci/requirements-2.7_LOCALE.run delete mode 100644 ci/requirements-2.7_SLOW.build delete mode 100644 ci/requirements-2.7_SLOW.run delete mode 100644 ci/requirements-2.7_WIN.run delete mode 100644 ci/requirements-3.4.build delete mode 100644 ci/requirements-3.4.pip delete mode 100644 ci/requirements-3.4.run delete mode 100644 ci/requirements-3.4_SLOW.build delete mode 100644 ci/requirements-3.4_SLOW.run delete mode 100644 ci/requirements-3.4_SLOW.sh delete mode 100644 ci/requirements-3.5.build delete mode 100644 ci/requirements-3.5.pip delete mode 100644 ci/requirements-3.5.run delete mode 100644 ci/requirements-3.5.sh delete mode 100644 ci/requirements-3.5_ASCII.build delete mode 100644 ci/requirements-3.5_ASCII.run delete mode 100644 ci/requirements-3.5_DOC.build delete mode 100644 ci/requirements-3.5_DOC.run delete mode 100644 ci/requirements-3.5_DOC.sh delete mode 100644 ci/requirements-3.5_OSX.build delete mode 100644 ci/requirements-3.5_OSX.pip delete mode 100644 ci/requirements-3.5_OSX.run delete mode 100644 ci/requirements-3.5_OSX.sh delete mode 100644 ci/requirements-3.6.build delete mode 100644 ci/requirements-3.6.run delete mode 100644 ci/requirements-3.6_NUMPY_DEV.build delete mode 100644 ci/requirements-3.6_NUMPY_DEV.build.sh delete mode 100644 ci/requirements-3.6_NUMPY_DEV.run delete mode 100644 ci/requirements-3.6_WIN.run delete mode 100644 ci/requirements_all.txt delete mode 100644 ci/requirements_dev.txt delete mode 100755 ci/run_build_docs.sh delete mode 100755 ci/run_circle.sh create mode 100755 ci/run_tests.sh delete mode 100755 ci/script_multi.sh delete mode 100755 ci/script_single.sh delete mode 100755 ci/show_circle.sh delete mode 100755 ci/upload_coverage.sh delete mode 100644 circle.yml create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JA.pptx delete mode 100644 doc/plots/stats/moment_plots.py delete mode 100644 doc/plots/stats/moments_ewma.py delete mode 100644 doc/plots/stats/moments_ewmvol.py delete mode 100644 doc/plots/stats/moments_expw.py delete mode 100644 doc/plots/stats/moments_rolling.py delete mode 100644 doc/plots/stats/moments_rolling_binary.py create mode 100644 doc/redirects.csv create mode 100644 doc/source/_static/ci.png create mode 100644 doc/source/_static/favicon.ico create mode 100644 doc/source/_static/print_df_new.png create mode 100644 doc/source/_static/print_df_old.png create mode 100644 doc/source/_static/reshaping_melt.png create mode 100644 doc/source/_static/reshaping_pivot.png create mode 100644 doc/source/_static/reshaping_stack.png create mode 100644 doc/source/_static/reshaping_unstack.png create mode 100644 doc/source/_static/reshaping_unstack_0.png create mode 100644 doc/source/_static/reshaping_unstack_1.png create mode 100644 doc/source/_static/style-excel.png delete mode 100644 doc/source/api.rst delete mode 100644 doc/source/contributing.rst create mode 100644 doc/source/development/contributing.rst create mode 100644 doc/source/development/contributing_docstring.rst create mode 100644 doc/source/development/developer.rst create mode 100644 doc/source/development/extending.rst create mode 100644 doc/source/development/index.rst create mode 100644 doc/source/development/internals.rst rename doc/source/{ => getting_started}/10min.rst (64%) rename doc/source/{ => getting_started}/basics.rst (64%) rename doc/source/{ => getting_started/comparison}/comparison_with_r.rst (86%) rename doc/source/{ => getting_started/comparison}/comparison_with_sas.rst (70%) rename doc/source/{ => getting_started/comparison}/comparison_with_sql.rst (92%) create mode 100644 doc/source/getting_started/comparison/comparison_with_stata.rst create mode 100644 doc/source/getting_started/comparison/index.rst rename doc/source/{ => getting_started}/dsintro.rst (65%) create mode 100644 doc/source/getting_started/index.rst create mode 100644 doc/source/getting_started/overview.rst create mode 100644 doc/source/getting_started/tutorials.rst delete mode 100644 doc/source/internals.rst delete mode 100644 doc/source/options.rst delete mode 100644 doc/source/overview.rst delete mode 100644 doc/source/r_interface.rst create mode 100644 doc/source/reference/arrays.rst create mode 100644 doc/source/reference/extensions.rst create mode 100644 doc/source/reference/frame.rst create mode 100644 doc/source/reference/general_functions.rst create mode 100644 doc/source/reference/general_utility_functions.rst create mode 100644 doc/source/reference/groupby.rst create mode 100644 doc/source/reference/index.rst create mode 100644 doc/source/reference/indexing.rst create mode 100644 doc/source/reference/io.rst create mode 100644 doc/source/reference/offset_frequency.rst create mode 100644 doc/source/reference/panel.rst create mode 100644 doc/source/reference/plotting.rst create mode 100644 doc/source/reference/resampling.rst create mode 100644 doc/source/reference/series.rst create mode 100644 doc/source/reference/style.rst create mode 100644 doc/source/reference/window.rst delete mode 100644 doc/source/release.rst delete mode 100644 doc/source/remote_data.rst delete mode 100644 doc/source/style.rst create mode 100644 doc/source/styled.xlsx delete mode 100644 doc/source/timeseries.rst delete mode 100644 doc/source/tutorials.rst rename doc/source/{ => user_guide}/advanced.rst (51%) rename doc/source/{ => user_guide}/categorical.rst (53%) rename doc/source/{ => user_guide}/computation.rst (74%) rename doc/source/{ => user_guide}/cookbook.rst (57%) rename doc/source/{ => user_guide}/enhancingperf.rst (72%) rename doc/source/{ => user_guide}/gotchas.rst (56%) rename doc/source/{ => user_guide}/groupby.rst (60%) create mode 100644 doc/source/user_guide/index.rst rename doc/source/{ => user_guide}/indexing.rst (70%) create mode 100644 doc/source/user_guide/integer_na.rst rename doc/source/{ => user_guide}/io.rst (64%) rename doc/source/{ => user_guide}/merging.rst (63%) rename doc/source/{ => user_guide}/missing_data.rst (69%) create mode 100644 doc/source/user_guide/options.rst rename doc/source/{ => user_guide}/reshaping.rst (55%) rename doc/source/{ => user_guide}/sparse.rst (85%) rename doc/source/{html-styling.ipynb => user_guide/style.ipynb} (69%) create mode 100644 doc/source/user_guide/templates/myhtml.tpl create mode 100644 doc/source/user_guide/templates/template_structure.html rename doc/source/{ => user_guide}/text.rst (71%) rename doc/source/{ => user_guide}/timedeltas.rst (70%) create mode 100644 doc/source/user_guide/timeseries.rst rename doc/source/{ => user_guide}/visualization.rst (78%) delete mode 100644 doc/source/whatsnew.rst create mode 100644 doc/source/whatsnew/index.rst rename doc/source/whatsnew/{v0.10.0.txt => v0.10.0.rst} (79%) rename doc/source/whatsnew/{v0.10.1.txt => v0.10.1.rst} (64%) rename doc/source/whatsnew/{v0.11.0.txt => v0.11.0.rst} (82%) rename doc/source/whatsnew/{v0.12.0.txt => v0.12.0.rst} (90%) rename doc/source/whatsnew/{v0.13.0.txt => v0.13.0.rst} (55%) create mode 100644 doc/source/whatsnew/v0.13.1.rst delete mode 100644 doc/source/whatsnew/v0.13.1.txt rename doc/source/whatsnew/{v0.14.0.txt => v0.14.0.rst} (82%) rename doc/source/whatsnew/{v0.14.1.txt => v0.14.1.rst} (90%) rename doc/source/whatsnew/{v0.15.0.txt => v0.15.0.rst} (87%) rename doc/source/whatsnew/{v0.15.1.txt => v0.15.1.rst} (92%) rename doc/source/whatsnew/{v0.15.2.txt => v0.15.2.rst} (92%) rename doc/source/whatsnew/{v0.16.0.txt => v0.16.0.rst} (88%) rename doc/source/whatsnew/{v0.16.1.txt => v0.16.1.rst} (80%) rename doc/source/whatsnew/{v0.16.2.txt => v0.16.2.rst} (91%) rename doc/source/whatsnew/{v0.17.0.txt => v0.17.0.rst} (94%) rename doc/source/whatsnew/{v0.17.1.txt => v0.17.1.rst} (93%) rename doc/source/whatsnew/{v0.18.0.txt => v0.18.0.rst} (95%) rename doc/source/whatsnew/{v0.18.1.txt => v0.18.1.rst} (92%) rename doc/source/whatsnew/{v0.19.0.txt => v0.19.0.rst} (94%) rename doc/source/whatsnew/{v0.19.1.txt => v0.19.1.rst} (93%) rename doc/source/whatsnew/{v0.19.2.txt => v0.19.2.rst} (94%) rename doc/source/whatsnew/{v0.20.0.txt => v0.20.0.rst} (50%) create mode 100644 doc/source/whatsnew/v0.20.2.rst create mode 100644 doc/source/whatsnew/v0.20.3.rst create mode 100644 doc/source/whatsnew/v0.21.0.rst create mode 100644 doc/source/whatsnew/v0.21.1.rst create mode 100644 doc/source/whatsnew/v0.22.0.rst create mode 100644 doc/source/whatsnew/v0.23.0.rst create mode 100644 doc/source/whatsnew/v0.23.1.rst create mode 100644 doc/source/whatsnew/v0.23.2.rst create mode 100644 doc/source/whatsnew/v0.23.3.rst create mode 100644 doc/source/whatsnew/v0.23.4.rst create mode 100644 doc/source/whatsnew/v0.24.0.rst create mode 100644 doc/source/whatsnew/v0.24.1.rst create mode 100644 doc/source/whatsnew/v0.24.2.rst create mode 100644 doc/source/whatsnew/v0.25.0.rst rename doc/source/whatsnew/{v0.4.x.txt => v0.4.x.rst} (93%) rename doc/source/whatsnew/{v0.5.0.txt => v0.5.0.rst} (94%) rename doc/source/whatsnew/{v0.6.0.txt => v0.6.0.rst} (94%) rename doc/source/whatsnew/{v0.6.1.txt => v0.6.1.rst} (91%) rename doc/source/whatsnew/{v0.7.0.txt => v0.7.0.rst} (95%) rename doc/source/whatsnew/{v0.7.1.txt => v0.7.1.rst} (90%) rename doc/source/whatsnew/{v0.7.2.txt => v0.7.2.rst} (90%) rename doc/source/whatsnew/{v0.7.3.txt => v0.7.3.rst} (74%) rename doc/source/whatsnew/{v0.8.0.txt => v0.8.0.rst} (93%) rename doc/source/whatsnew/{v0.8.1.txt => v0.8.1.rst} (90%) rename doc/source/whatsnew/{v0.9.0.txt => v0.9.0.rst} (91%) rename doc/source/whatsnew/{v0.9.1.txt => v0.9.1.rst} (82%) create mode 100755 doc/sphinxext/announce.py create mode 100644 doc/sphinxext/contributors.py delete mode 100644 doc/sphinxext/ipython_sphinxext/ipython_console_highlighting.py delete mode 100644 doc/sphinxext/ipython_sphinxext/ipython_directive.py delete mode 100644 doc/sphinxext/numpydoc/LICENSE.txt delete mode 100755 doc/sphinxext/numpydoc/README.rst delete mode 100755 doc/sphinxext/numpydoc/__init__.py delete mode 100755 doc/sphinxext/numpydoc/comment_eater.py delete mode 100755 doc/sphinxext/numpydoc/compiler_unparse.py delete mode 100755 doc/sphinxext/numpydoc/docscrape.py delete mode 100755 doc/sphinxext/numpydoc/docscrape_sphinx.py delete mode 100644 doc/sphinxext/numpydoc/linkcode.py delete mode 100755 doc/sphinxext/numpydoc/numpydoc.py delete mode 100755 doc/sphinxext/numpydoc/phantom_import.py delete mode 100755 doc/sphinxext/numpydoc/plot_directive.py delete mode 100755 doc/sphinxext/numpydoc/tests/test_docscrape.py delete mode 100644 doc/sphinxext/numpydoc/tests/test_linkcode.py delete mode 100644 doc/sphinxext/numpydoc/tests/test_phantom_import.py delete mode 100644 doc/sphinxext/numpydoc/tests/test_plot_directive.py delete mode 100644 doc/sphinxext/numpydoc/tests/test_traitsdoc.py delete mode 100755 doc/sphinxext/numpydoc/traitsdoc.py create mode 100644 environment.yml create mode 100644 pandas/_libs/groupby.pxd rename pandas/{tools => _libs}/hashing.pyx (70%) create mode 100644 pandas/_libs/indexing.pyx create mode 100644 pandas/_libs/internals.pyx create mode 100644 pandas/_libs/interval.pyx create mode 100644 pandas/_libs/intervaltree.pxi.in delete mode 100644 pandas/_libs/join_func_helper.pxi.in delete mode 100644 pandas/_libs/join_helper.pxi.in create mode 100644 pandas/_libs/khash.pxd delete mode 100644 pandas/_libs/lib.pxd create mode 100644 pandas/_libs/missing.pxd create mode 100644 pandas/_libs/missing.pyx create mode 100644 pandas/_libs/ops.pyx rename pandas/{io => _libs}/parsers.pyx (79%) delete mode 100644 pandas/_libs/period.pyx create mode 100644 pandas/_libs/properties.pyx rename pandas/_libs/{src/reduce.pyx => reduction.pyx} (89%) delete mode 100644 pandas/_libs/reshape_helper.pxi.in create mode 100644 pandas/_libs/skiplist.pxd rename pandas/_libs/{src => }/skiplist.pyx (79%) rename pandas/{sparse => _libs}/sparse.pyx (86%) rename pandas/{sparse => _libs}/sparse_op_helper.pxi.in (75%) delete mode 100644 pandas/_libs/src/datetime.pxd delete mode 100644 pandas/_libs/src/datetime/np_datetime.h delete mode 100644 pandas/_libs/src/datetime_helper.h create mode 100644 pandas/_libs/src/headers/cmath delete mode 100644 pandas/_libs/src/headers/math.h delete mode 100644 pandas/_libs/src/inference.pyx rename pandas/_libs/src/{helper.h => inline_helper.h} (74%) delete mode 100644 pandas/_libs/src/khash.pxd delete mode 100644 pandas/_libs/src/klib/ktypes.h delete mode 100644 pandas/_libs/src/klib/kvec.h delete mode 100644 pandas/_libs/src/numpy.pxd delete mode 100644 pandas/_libs/src/numpy_helper.h delete mode 100644 pandas/_libs/src/offsets.pyx delete mode 100644 pandas/_libs/src/parser/.gitignore delete mode 100644 pandas/_libs/src/parser/Makefile delete mode 100644 pandas/_libs/src/period_helper.c delete mode 100644 pandas/_libs/src/period_helper.h delete mode 100644 pandas/_libs/src/properties.pyx delete mode 100644 pandas/_libs/src/skiplist.pxd delete mode 100644 pandas/_libs/src/util.pxd rename pandas/{util => _libs}/testing.pyx (97%) delete mode 100644 pandas/_libs/tslib.pxd create mode 100644 pandas/_libs/tslibs/__init__.py create mode 100644 pandas/_libs/tslibs/ccalendar.pxd create mode 100644 pandas/_libs/tslibs/ccalendar.pyx create mode 100644 pandas/_libs/tslibs/conversion.pxd create mode 100644 pandas/_libs/tslibs/conversion.pyx create mode 100644 pandas/_libs/tslibs/fields.pyx create mode 100644 pandas/_libs/tslibs/frequencies.pxd create mode 100644 pandas/_libs/tslibs/frequencies.pyx create mode 100644 pandas/_libs/tslibs/nattype.pxd create mode 100644 pandas/_libs/tslibs/nattype.pyx create mode 100644 pandas/_libs/tslibs/np_datetime.pxd create mode 100644 pandas/_libs/tslibs/np_datetime.pyx create mode 100644 pandas/_libs/tslibs/offsets.pxd create mode 100644 pandas/_libs/tslibs/offsets.pyx create mode 100644 pandas/_libs/tslibs/parsing.pyx create mode 100644 pandas/_libs/tslibs/period.pyx create mode 100644 pandas/_libs/tslibs/resolution.pyx rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime.c (63%) create mode 100644 pandas/_libs/tslibs/src/datetime/np_datetime.h rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime_strings.c (51%) rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime_strings.h (53%) create mode 100644 pandas/_libs/tslibs/strptime.pyx create mode 100644 pandas/_libs/tslibs/timedeltas.pxd create mode 100644 pandas/_libs/tslibs/timedeltas.pyx create mode 100644 pandas/_libs/tslibs/timestamps.pxd create mode 100644 pandas/_libs/tslibs/timestamps.pyx create mode 100644 pandas/_libs/tslibs/timezones.pxd create mode 100644 pandas/_libs/tslibs/timezones.pyx create mode 100644 pandas/_libs/tslibs/util.pxd create mode 100644 pandas/_libs/util.pxd rename pandas/{core => _libs}/window.pyx (54%) create mode 100644 pandas/_libs/writers.pyx create mode 100644 pandas/api/extensions/__init__.py create mode 100644 pandas/arrays/__init__.py delete mode 100644 pandas/compat/openpyxl_compat.py delete mode 100644 pandas/computation/api.py create mode 100644 pandas/core/accessor.py create mode 100644 pandas/core/apply.py create mode 100644 pandas/core/arrays/__init__.py create mode 100644 pandas/core/arrays/_ranges.py create mode 100644 pandas/core/arrays/array_.py create mode 100644 pandas/core/arrays/base.py create mode 100644 pandas/core/arrays/categorical.py create mode 100644 pandas/core/arrays/datetimelike.py create mode 100644 pandas/core/arrays/datetimes.py create mode 100644 pandas/core/arrays/integer.py create mode 100644 pandas/core/arrays/interval.py create mode 100644 pandas/core/arrays/numpy_.py create mode 100644 pandas/core/arrays/period.py create mode 100644 pandas/core/arrays/sparse.py create mode 100644 pandas/core/arrays/timedeltas.py rename pandas/{formats => core/computation}/__init__.py (100%) rename pandas/{ => core}/computation/align.py (83%) create mode 100644 pandas/core/computation/api.py rename pandas/{computation/__init__.py => core/computation/check.py} (75%) rename pandas/{ => core}/computation/common.py (99%) rename pandas/{ => core}/computation/engines.py (86%) rename pandas/{ => core}/computation/eval.py (65%) rename pandas/{ => core}/computation/expr.py (88%) rename pandas/{ => core}/computation/expressions.py (67%) rename pandas/{ => core}/computation/ops.py (92%) rename pandas/{ => core}/computation/pytables.py (84%) rename pandas/{ => core}/computation/scope.py (93%) delete mode 100644 pandas/core/datetools.py rename pandas/{indexes => core/dtypes}/__init__.py (100%) create mode 100644 pandas/core/dtypes/api.py create mode 100644 pandas/core/dtypes/base.py rename pandas/{types => core/dtypes}/cast.py (54%) create mode 100644 pandas/core/dtypes/common.py create mode 100644 pandas/core/dtypes/concat.py create mode 100644 pandas/core/dtypes/dtypes.py rename pandas/{types => core/dtypes}/generic.py (64%) create mode 100644 pandas/core/dtypes/inference.py create mode 100644 pandas/core/dtypes/missing.py delete mode 100644 pandas/core/groupby.py create mode 100644 pandas/core/groupby/__init__.py create mode 100644 pandas/core/groupby/base.py create mode 100644 pandas/core/groupby/categorical.py create mode 100644 pandas/core/groupby/generic.py create mode 100644 pandas/core/groupby/groupby.py create mode 100644 pandas/core/groupby/grouper.py create mode 100644 pandas/core/groupby/ops.py rename pandas/{sparse => core/indexes}/__init__.py (100%) create mode 100644 pandas/core/indexes/accessors.py create mode 100644 pandas/core/indexes/api.py rename pandas/{ => core}/indexes/base.py (51%) create mode 100644 pandas/core/indexes/category.py create mode 100644 pandas/core/indexes/datetimelike.py create mode 100644 pandas/core/indexes/datetimes.py rename pandas/{ => core}/indexes/frozen.py (59%) create mode 100644 pandas/core/indexes/interval.py rename pandas/{ => core}/indexes/multi.py (54%) rename pandas/{ => core}/indexes/numeric.py (66%) create mode 100644 pandas/core/indexes/period.py rename pandas/{ => core}/indexes/range.py (64%) create mode 100644 pandas/core/indexes/timedeltas.py delete mode 100644 pandas/core/internals.py create mode 100644 pandas/core/internals/__init__.py create mode 100644 pandas/core/internals/arrays.py create mode 100644 pandas/core/internals/blocks.py create mode 100644 pandas/core/internals/concat.py create mode 100644 pandas/core/internals/construction.py create mode 100644 pandas/core/internals/managers.py delete mode 100644 pandas/core/panel4d.py delete mode 100644 pandas/core/panelnd.py create mode 100644 pandas/core/resample.py delete mode 100644 pandas/core/reshape.py rename pandas/{stats => core/reshape}/__init__.py (100%) create mode 100644 pandas/core/reshape/api.py rename pandas/{tools => core/reshape}/concat.py (75%) create mode 100644 pandas/core/reshape/melt.py rename pandas/{tools => core/reshape}/merge.py (66%) rename pandas/{tools => core/reshape}/pivot.py (60%) create mode 100644 pandas/core/reshape/reshape.py create mode 100644 pandas/core/reshape/tile.py create mode 100644 pandas/core/reshape/util.py delete mode 100644 pandas/core/sparse.py rename pandas/{tests/formats => core/sparse}/__init__.py (100%) create mode 100644 pandas/core/sparse/api.py rename pandas/{ => core}/sparse/frame.py (71%) rename pandas/{ => core}/sparse/scipy_sparse.py (86%) create mode 100644 pandas/core/sparse/series.py rename pandas/{tests/types => core/tools}/__init__.py (100%) create mode 100644 pandas/core/tools/datetimes.py rename pandas/{tools/util.py => core/tools/numeric.py} (65%) create mode 100644 pandas/core/tools/timedeltas.py rename pandas/{tools => core/util}/__init__.py (100%) rename pandas/{tools => core/util}/hashing.py (74%) create mode 100644 pandas/errors/__init__.py delete mode 100644 pandas/formats/format.py delete mode 100644 pandas/formats/printing.py delete mode 100644 pandas/formats/style.py delete mode 100644 pandas/indexes/api.py delete mode 100644 pandas/indexes/category.py rename pandas/{util => io}/clipboard/__init__.py (75%) rename pandas/{util => io}/clipboard/clipboards.py (90%) rename pandas/{util => io}/clipboard/exceptions.py (78%) rename pandas/{util => io}/clipboard/windows.py (93%) rename pandas/io/{clipboard.py => clipboards.py} (61%) delete mode 100644 pandas/io/data.py delete mode 100644 pandas/io/excel.py create mode 100644 pandas/io/excel/__init__.py create mode 100644 pandas/io/excel/_base.py create mode 100644 pandas/io/excel/_openpyxl.py create mode 100644 pandas/io/excel/_util.py create mode 100644 pandas/io/excel/_xlrd.py create mode 100644 pandas/io/excel/_xlsxwriter.py create mode 100644 pandas/io/excel/_xlwt.py rename pandas/{types => io/formats}/__init__.py (100%) create mode 100644 pandas/io/formats/console.py create mode 100644 pandas/io/formats/css.py create mode 100644 pandas/io/formats/csvs.py create mode 100644 pandas/io/formats/excel.py create mode 100644 pandas/io/formats/format.py create mode 100644 pandas/io/formats/html.py create mode 100644 pandas/io/formats/latex.py create mode 100644 pandas/io/formats/printing.py create mode 100644 pandas/io/formats/style.py create mode 100644 pandas/io/formats/templates/html.tpl rename pandas/{util => io/formats}/terminal.py (69%) create mode 100644 pandas/io/gcs.py create mode 100644 pandas/io/parquet.py delete mode 100644 pandas/io/wb.py delete mode 100644 pandas/json.py delete mode 100644 pandas/lib.py delete mode 100644 pandas/parser.py create mode 100644 pandas/plotting/__init__.py create mode 100644 pandas/plotting/_compat.py create mode 100644 pandas/plotting/_converter.py rename pandas/{tools/plotting.py => plotting/_core.py} (60%) create mode 100644 pandas/plotting/_misc.py create mode 100644 pandas/plotting/_style.py create mode 100644 pandas/plotting/_timeseries.py create mode 100644 pandas/plotting/_tools.py delete mode 100644 pandas/sparse/api.py delete mode 100644 pandas/sparse/array.py delete mode 100644 pandas/sparse/list.py delete mode 100644 pandas/sparse/series.py delete mode 100644 pandas/stats/api.py delete mode 100644 pandas/stats/moments.py create mode 100644 pandas/testing.py create mode 100644 pandas/tests/api/test_types.py rename bench/larry.py => pandas/tests/arithmetic/__init__.py (100%) create mode 100644 pandas/tests/arithmetic/conftest.py create mode 100644 pandas/tests/arithmetic/test_datetime64.py create mode 100644 pandas/tests/arithmetic/test_numeric.py create mode 100644 pandas/tests/arithmetic/test_object.py create mode 100644 pandas/tests/arithmetic/test_period.py create mode 100644 pandas/tests/arithmetic/test_timedelta64.py rename ci/requirements-2.7_SLOW.pip => pandas/tests/arrays/__init__.py (100%) rename ci/requirements-3.6.pip => pandas/tests/arrays/categorical/__init__.py (100%) create mode 100644 pandas/tests/arrays/categorical/common.py create mode 100644 pandas/tests/arrays/categorical/conftest.py create mode 100644 pandas/tests/arrays/categorical/test_algos.py create mode 100644 pandas/tests/arrays/categorical/test_analytics.py create mode 100644 pandas/tests/arrays/categorical/test_api.py create mode 100644 pandas/tests/arrays/categorical/test_constructors.py create mode 100644 pandas/tests/arrays/categorical/test_dtypes.py create mode 100644 pandas/tests/arrays/categorical/test_indexing.py create mode 100644 pandas/tests/arrays/categorical/test_missing.py create mode 100644 pandas/tests/arrays/categorical/test_operators.py create mode 100644 pandas/tests/arrays/categorical/test_repr.py create mode 100644 pandas/tests/arrays/categorical/test_sorting.py create mode 100644 pandas/tests/arrays/categorical/test_subclass.py create mode 100644 pandas/tests/arrays/categorical/test_warnings.py rename vb_suite/source/_static/stub => pandas/tests/arrays/interval/__init__.py (100%) create mode 100644 pandas/tests/arrays/interval/test_interval.py create mode 100644 pandas/tests/arrays/interval/test_ops.py create mode 100644 pandas/tests/arrays/sparse/__init__.py rename pandas/tests/{ => arrays}/sparse/test_arithmetics.py (82%) create mode 100644 pandas/tests/arrays/sparse/test_array.py create mode 100644 pandas/tests/arrays/sparse/test_dtype.py rename pandas/tests/{ => arrays}/sparse/test_libsparse.py (76%) create mode 100644 pandas/tests/arrays/test_array.py create mode 100644 pandas/tests/arrays/test_datetimelike.py create mode 100644 pandas/tests/arrays/test_datetimes.py create mode 100644 pandas/tests/arrays/test_integer.py create mode 100644 pandas/tests/arrays/test_numpy.py create mode 100644 pandas/tests/arrays/test_period.py create mode 100644 pandas/tests/arrays/test_timedeltas.py create mode 100644 pandas/tests/dtypes/__init__.py create mode 100644 pandas/tests/dtypes/cast/__init__.py create mode 100644 pandas/tests/dtypes/cast/test_construct_from_scalar.py create mode 100644 pandas/tests/dtypes/cast/test_construct_ndarray.py create mode 100644 pandas/tests/dtypes/cast/test_construct_object_arr.py create mode 100644 pandas/tests/dtypes/cast/test_convert_objects.py create mode 100644 pandas/tests/dtypes/cast/test_downcast.py create mode 100644 pandas/tests/dtypes/cast/test_find_common_type.py create mode 100644 pandas/tests/dtypes/cast/test_infer_datetimelike.py create mode 100644 pandas/tests/dtypes/cast/test_infer_dtype.py create mode 100644 pandas/tests/dtypes/test_common.py create mode 100644 pandas/tests/dtypes/test_concat.py create mode 100644 pandas/tests/dtypes/test_dtypes.py create mode 100644 pandas/tests/dtypes/test_generic.py create mode 100644 pandas/tests/dtypes/test_inference.py create mode 100644 pandas/tests/dtypes/test_missing.py create mode 100644 pandas/tests/extension/__init__.py create mode 100644 pandas/tests/extension/arrow/__init__.py create mode 100644 pandas/tests/extension/arrow/bool.py create mode 100644 pandas/tests/extension/arrow/test_bool.py create mode 100644 pandas/tests/extension/base/__init__.py create mode 100644 pandas/tests/extension/base/base.py create mode 100644 pandas/tests/extension/base/casting.py create mode 100644 pandas/tests/extension/base/constructors.py create mode 100644 pandas/tests/extension/base/dtype.py create mode 100644 pandas/tests/extension/base/getitem.py create mode 100644 pandas/tests/extension/base/groupby.py create mode 100644 pandas/tests/extension/base/interface.py create mode 100644 pandas/tests/extension/base/io.py create mode 100644 pandas/tests/extension/base/methods.py create mode 100644 pandas/tests/extension/base/missing.py create mode 100644 pandas/tests/extension/base/ops.py create mode 100644 pandas/tests/extension/base/printing.py create mode 100644 pandas/tests/extension/base/reduce.py create mode 100644 pandas/tests/extension/base/reshaping.py create mode 100644 pandas/tests/extension/base/setitem.py create mode 100644 pandas/tests/extension/conftest.py create mode 100644 pandas/tests/extension/decimal/__init__.py create mode 100644 pandas/tests/extension/decimal/array.py create mode 100644 pandas/tests/extension/decimal/test_decimal.py create mode 100644 pandas/tests/extension/json/__init__.py create mode 100644 pandas/tests/extension/json/array.py create mode 100644 pandas/tests/extension/json/test_json.py create mode 100644 pandas/tests/extension/test_categorical.py create mode 100644 pandas/tests/extension/test_common.py create mode 100644 pandas/tests/extension/test_datetime.py create mode 100644 pandas/tests/extension/test_external_block.py create mode 100644 pandas/tests/extension/test_integer.py create mode 100644 pandas/tests/extension/test_interval.py create mode 100644 pandas/tests/extension/test_numpy.py create mode 100644 pandas/tests/extension/test_period.py create mode 100644 pandas/tests/extension/test_sparse.py delete mode 100644 pandas/tests/formats/data/unicode_series.csv delete mode 100644 pandas/tests/formats/test_printing.py delete mode 100644 pandas/tests/formats/test_style.py delete mode 100644 pandas/tests/formats/test_to_csv.py delete mode 100644 pandas/tests/formats/test_to_html.py create mode 100644 pandas/tests/frame/conftest.py create mode 100644 pandas/tests/frame/test_api.py create mode 100644 pandas/tests/frame/test_arithmetic.py create mode 100644 pandas/tests/frame/test_duplicates.py delete mode 100644 pandas/tests/frame/test_misc_api.py create mode 100644 pandas/tests/frame/test_sort_values_level_as_str.py create mode 100644 pandas/tests/frame/test_timezones.py create mode 100644 pandas/tests/generic/__init__.py create mode 100644 pandas/tests/generic/test_frame.py create mode 100644 pandas/tests/generic/test_generic.py create mode 100644 pandas/tests/generic/test_label_or_level_utils.py create mode 100644 pandas/tests/generic/test_series.py create mode 100644 pandas/tests/groupby/aggregate/__init__.py create mode 100644 pandas/tests/groupby/aggregate/test_aggregate.py create mode 100644 pandas/tests/groupby/aggregate/test_cython.py create mode 100644 pandas/tests/groupby/aggregate/test_other.py delete mode 100644 pandas/tests/groupby/common.py create mode 100644 pandas/tests/groupby/conftest.py delete mode 100644 pandas/tests/groupby/test_aggregate.py create mode 100644 pandas/tests/groupby/test_apply.py create mode 100644 pandas/tests/groupby/test_counting.py create mode 100644 pandas/tests/groupby/test_function.py create mode 100644 pandas/tests/groupby/test_grouping.py create mode 100644 pandas/tests/groupby/test_index_as_string.py create mode 100644 pandas/tests/groupby/test_nth.py create mode 100644 pandas/tests/groupby/test_rank.py create mode 100644 pandas/tests/indexes/conftest.py delete mode 100644 pandas/tests/indexes/data/mindex_073.pickle delete mode 100644 pandas/tests/indexes/data/multiindex_v1.pickle create mode 100644 pandas/tests/indexes/datetimes/test_arithmetic.py create mode 100644 pandas/tests/indexes/datetimes/test_formats.py delete mode 100644 pandas/tests/indexes/datetimes/test_partial_slcing.py create mode 100644 pandas/tests/indexes/datetimes/test_partial_slicing.py create mode 100644 pandas/tests/indexes/datetimes/test_scalar_compat.py create mode 100644 pandas/tests/indexes/datetimes/test_timezones.py create mode 100644 pandas/tests/indexes/interval/__init__.py create mode 100644 pandas/tests/indexes/interval/test_astype.py create mode 100644 pandas/tests/indexes/interval/test_construction.py create mode 100644 pandas/tests/indexes/interval/test_interval.py create mode 100644 pandas/tests/indexes/interval/test_interval_new.py create mode 100644 pandas/tests/indexes/interval/test_interval_range.py create mode 100644 pandas/tests/indexes/interval/test_interval_tree.py create mode 100644 pandas/tests/indexes/multi/__init__.py create mode 100644 pandas/tests/indexes/multi/conftest.py create mode 100644 pandas/tests/indexes/multi/test_analytics.py create mode 100644 pandas/tests/indexes/multi/test_astype.py create mode 100644 pandas/tests/indexes/multi/test_compat.py create mode 100644 pandas/tests/indexes/multi/test_constructor.py create mode 100644 pandas/tests/indexes/multi/test_contains.py create mode 100644 pandas/tests/indexes/multi/test_conversion.py create mode 100644 pandas/tests/indexes/multi/test_copy.py create mode 100644 pandas/tests/indexes/multi/test_drop.py create mode 100644 pandas/tests/indexes/multi/test_duplicates.py create mode 100644 pandas/tests/indexes/multi/test_equivalence.py create mode 100644 pandas/tests/indexes/multi/test_format.py create mode 100644 pandas/tests/indexes/multi/test_get_set.py create mode 100644 pandas/tests/indexes/multi/test_indexing.py create mode 100644 pandas/tests/indexes/multi/test_integrity.py create mode 100644 pandas/tests/indexes/multi/test_join.py create mode 100644 pandas/tests/indexes/multi/test_missing.py create mode 100644 pandas/tests/indexes/multi/test_monotonic.py create mode 100644 pandas/tests/indexes/multi/test_names.py create mode 100644 pandas/tests/indexes/multi/test_partial_indexing.py create mode 100644 pandas/tests/indexes/multi/test_reindex.py create mode 100644 pandas/tests/indexes/multi/test_reshape.py create mode 100644 pandas/tests/indexes/multi/test_set_ops.py create mode 100644 pandas/tests/indexes/multi/test_sorting.py create mode 100644 pandas/tests/indexes/period/test_arithmetic.py create mode 100644 pandas/tests/indexes/period/test_astype.py create mode 100644 pandas/tests/indexes/period/test_formats.py create mode 100644 pandas/tests/indexes/period/test_period_range.py create mode 100644 pandas/tests/indexes/period/test_scalar_compat.py create mode 100644 pandas/tests/indexes/test_common.py delete mode 100644 pandas/tests/indexes/test_multi.py create mode 100644 pandas/tests/indexes/timedeltas/test_arithmetic.py create mode 100644 pandas/tests/indexes/timedeltas/test_formats.py create mode 100644 pandas/tests/indexes/timedeltas/test_scalar_compat.py create mode 100644 pandas/tests/indexing/conftest.py create mode 100644 pandas/tests/indexing/interval/__init__.py create mode 100644 pandas/tests/indexing/interval/test_interval.py create mode 100644 pandas/tests/indexing/interval/test_interval_new.py create mode 100644 pandas/tests/indexing/multiindex/__init__.py create mode 100644 pandas/tests/indexing/multiindex/conftest.py create mode 100644 pandas/tests/indexing/multiindex/test_chaining_and_caching.py create mode 100644 pandas/tests/indexing/multiindex/test_datetime.py create mode 100644 pandas/tests/indexing/multiindex/test_getitem.py create mode 100644 pandas/tests/indexing/multiindex/test_iloc.py create mode 100644 pandas/tests/indexing/multiindex/test_indexing_slow.py create mode 100644 pandas/tests/indexing/multiindex/test_ix.py create mode 100644 pandas/tests/indexing/multiindex/test_loc.py create mode 100644 pandas/tests/indexing/multiindex/test_multiindex.py create mode 100644 pandas/tests/indexing/multiindex/test_partial.py create mode 100644 pandas/tests/indexing/multiindex/test_set_ops.py create mode 100644 pandas/tests/indexing/multiindex/test_setitem.py create mode 100644 pandas/tests/indexing/multiindex/test_slice.py create mode 100644 pandas/tests/indexing/multiindex/test_sorted.py create mode 100644 pandas/tests/indexing/multiindex/test_xs.py create mode 100644 pandas/tests/indexing/test_indexing_engines.py delete mode 100644 pandas/tests/indexing/test_multiindex.py delete mode 100644 pandas/tests/indexing/test_panel.py create mode 100644 pandas/tests/internals/__init__.py rename pandas/tests/{ => internals}/test_internals.py (84%) create mode 100644 pandas/tests/io/conftest.py create mode 100644 pandas/tests/io/data/feather-0_3_1.feather create mode 100644 pandas/tests/io/data/fixed_width_format.txt delete mode 100644 pandas/tests/io/data/legacy_hdf/legacy_table_0.11.h5 create mode 100644 pandas/tests/io/data/legacy_hdf/legacy_table_fixed_py2.h5 create mode 100644 pandas/tests/io/data/legacy_hdf/legacy_table_py2.h5 create mode 100644 pandas/tests/io/data/legacy_hdf/periodindex_0.20.1_x86_64_darwin_2.7.13.h5 create mode 100644 pandas/tests/io/data/legacy_pickle/0.16.2/0.16.2_AMD64_windows_2.7.14.pickle create mode 100644 pandas/tests/io/data/legacy_pickle/0.17.0/0.17.0_x86_64_darwin_3.5.3.pickle create mode 100644 pandas/tests/io/data/legacy_pickle/0.19.2/0.19.2_AMD64_windows_2.7.14.pickle create mode 100644 pandas/tests/io/data/legacy_pickle/0.19.2/0.19.2_x86_64_darwin_2.7.14.pickle create mode 100644 pandas/tests/io/data/legacy_pickle/0.20.3/0.20.3_x86_64_darwin_2.7.14.pickle create mode 100644 pandas/tests/io/data/stata13_dates.dta create mode 100644 pandas/tests/io/data/stata16_118.dta create mode 100644 pandas/tests/io/formats/__init__.py create mode 100644 pandas/tests/io/formats/data/html/datetime64_hourformatter.html create mode 100644 pandas/tests/io/formats/data/html/datetime64_monthformatter.html create mode 100644 pandas/tests/io/formats/data/html/escape_disabled.html create mode 100644 pandas/tests/io/formats/data/html/escaped.html create mode 100644 pandas/tests/io/formats/data/html/gh12031_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh14882_expected_output_1.html create mode 100644 pandas/tests/io/formats/data/html/gh14882_expected_output_2.html create mode 100644 pandas/tests/io/formats/data/html/gh14998_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh15019_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh21625_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh22270_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh22579_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh22783_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh22783_named_columns_index.html create mode 100644 pandas/tests/io/formats/data/html/gh6131_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/gh8452_expected_output.html create mode 100644 pandas/tests/io/formats/data/html/index_1.html create mode 100644 pandas/tests/io/formats/data/html/index_2.html create mode 100644 pandas/tests/io/formats/data/html/index_3.html create mode 100644 pandas/tests/io/formats/data/html/index_4.html create mode 100644 pandas/tests/io/formats/data/html/index_5.html create mode 100644 pandas/tests/io/formats/data/html/index_formatter.html create mode 100644 pandas/tests/io/formats/data/html/index_named_multi_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_named_multi_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_named_multi_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/index_named_multi_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_named_multi_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_named_standard_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_named_standard_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_named_standard_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/index_named_standard_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_named_standard_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_none_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_none_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_none_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/index_none_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_none_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_multi_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_multi_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_multi_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_multi_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_multi_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_standard_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_standard_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_standard_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_standard_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/index_unnamed_standard_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/justify.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_1.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_2.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_sparsify_1.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_sparsify_2.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_sparsify_false_multi_sparse_1.html create mode 100644 pandas/tests/io/formats/data/html/multiindex_sparsify_false_multi_sparse_2.html create mode 100644 pandas/tests/io/formats/data/html/render_links_false.html create mode 100644 pandas/tests/io/formats/data/html/render_links_true.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_multi_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_named_standard_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_none_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_none_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_none_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_none_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_none_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_multi_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_multi_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_multi_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_multi_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_multi_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_standard_columns_named_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_standard_columns_named_standard.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_standard_columns_none.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_standard_columns_unnamed_multi.html create mode 100644 pandas/tests/io/formats/data/html/trunc_df_index_unnamed_standard_columns_unnamed_standard.html create mode 100644 pandas/tests/io/formats/data/html/truncate.html create mode 100644 pandas/tests/io/formats/data/html/truncate_multi_index.html create mode 100644 pandas/tests/io/formats/data/html/truncate_multi_index_sparse_off.html create mode 100644 pandas/tests/io/formats/data/html/unicode_1.html create mode 100644 pandas/tests/io/formats/data/html/unicode_2.html create mode 100644 pandas/tests/io/formats/data/html/with_classes.html create mode 100644 pandas/tests/io/formats/test_console.py create mode 100644 pandas/tests/io/formats/test_css.py rename pandas/tests/{ => io}/formats/test_eng_formatting.py (92%) rename pandas/tests/{ => io}/formats/test_format.py (67%) create mode 100644 pandas/tests/io/formats/test_printing.py create mode 100644 pandas/tests/io/formats/test_style.py create mode 100644 pandas/tests/io/formats/test_to_csv.py create mode 100644 pandas/tests/io/formats/test_to_excel.py create mode 100644 pandas/tests/io/formats/test_to_html.py rename pandas/tests/{ => io}/formats/test_to_latex.py (57%) mode change 100644 => 100755 pandas/tests/io/generate_legacy_storage_files.py create mode 100644 pandas/tests/io/json/data/tsframe_v012.json.zip create mode 100644 pandas/tests/io/json/test_compression.py create mode 100644 pandas/tests/io/json/test_readlines.py create mode 100644 pandas/tests/io/msgpack/common.py create mode 100644 pandas/tests/io/msgpack/data/frame.mp delete mode 100644 pandas/tests/io/parser/c_parser_only.py delete mode 100644 pandas/tests/io/parser/comment.py delete mode 100644 pandas/tests/io/parser/common.py delete mode 100644 pandas/tests/io/parser/compression.py create mode 100644 pandas/tests/io/parser/conftest.py delete mode 100644 pandas/tests/io/parser/converters.py create mode 100644 pandas/tests/io/parser/data/items.jsonl create mode 100644 pandas/tests/io/parser/data/sub_char.csv create mode 100644 pandas/tests/io/parser/data/tar_csv.tar create mode 100644 pandas/tests/io/parser/data/tar_csv.tar.gz create mode 100644 pandas/tests/io/parser/data/tips.csv.bz2 create mode 100644 pandas/tests/io/parser/data/tips.csv.gz create mode 100644 pandas/tests/io/parser/data/utf16_ex_small.zip delete mode 100644 pandas/tests/io/parser/dialect.py delete mode 100644 pandas/tests/io/parser/dtypes.py delete mode 100644 pandas/tests/io/parser/header.py delete mode 100644 pandas/tests/io/parser/index_col.py delete mode 100644 pandas/tests/io/parser/multithread.py delete mode 100644 pandas/tests/io/parser/na_values.py delete mode 100644 pandas/tests/io/parser/parse_dates.py delete mode 100644 pandas/tests/io/parser/python_parser_only.py delete mode 100644 pandas/tests/io/parser/quoting.py delete mode 100644 pandas/tests/io/parser/skiprows.py create mode 100644 pandas/tests/io/parser/test_c_parser_only.py create mode 100644 pandas/tests/io/parser/test_comment.py create mode 100644 pandas/tests/io/parser/test_common.py create mode 100644 pandas/tests/io/parser/test_compression.py create mode 100644 pandas/tests/io/parser/test_converters.py create mode 100644 pandas/tests/io/parser/test_dialect.py create mode 100644 pandas/tests/io/parser/test_dtypes.py create mode 100644 pandas/tests/io/parser/test_header.py create mode 100644 pandas/tests/io/parser/test_index_col.py create mode 100644 pandas/tests/io/parser/test_mangle_dupes.py create mode 100644 pandas/tests/io/parser/test_multi_thread.py create mode 100644 pandas/tests/io/parser/test_na_values.py create mode 100644 pandas/tests/io/parser/test_parse_dates.py delete mode 100644 pandas/tests/io/parser/test_parsers.py create mode 100644 pandas/tests/io/parser/test_python_parser_only.py create mode 100644 pandas/tests/io/parser/test_quoting.py create mode 100644 pandas/tests/io/parser/test_skiprows.py create mode 100644 pandas/tests/io/parser/test_usecols.py delete mode 100644 pandas/tests/io/parser/usecols.py create mode 100644 pandas/tests/io/sas/data/cars.sas7bdat create mode 100644 pandas/tests/io/sas/data/datetime.csv rename pandas/tests/io/{data/legacy_hdf/legacy_0.10.h5 => sas/data/datetime.sas7bdat} (54%) create mode 100644 pandas/tests/io/sas/data/load_log.sas7bdat create mode 100644 pandas/tests/io/sas/data/many_columns.csv create mode 100644 pandas/tests/io/sas/data/many_columns.sas7bdat rename pandas/tests/io/{data/legacy_hdf/legacy_table.h5 => sas/data/zero_variables.sas7bdat} (69%) create mode 100644 pandas/tests/io/test_compression.py create mode 100644 pandas/tests/io/test_date_converters.py create mode 100644 pandas/tests/io/test_gcs.py create mode 100644 pandas/tests/io/test_parquet.py create mode 100644 pandas/tests/plotting/test_converter.py create mode 100644 pandas/tests/reductions/__init__.py create mode 100644 pandas/tests/reductions/test_reductions.py create mode 100644 pandas/tests/reductions/test_stat_reductions.py create mode 100644 pandas/tests/resample/__init__.py create mode 100644 pandas/tests/resample/conftest.py create mode 100644 pandas/tests/resample/test_base.py create mode 100644 pandas/tests/resample/test_datetime_index.py create mode 100644 pandas/tests/resample/test_period_index.py create mode 100644 pandas/tests/resample/test_resample_api.py create mode 100644 pandas/tests/resample/test_resampler_grouper.py create mode 100644 pandas/tests/resample/test_time_grouper.py create mode 100644 pandas/tests/resample/test_timedelta.py create mode 100644 pandas/tests/reshape/__init__.py rename pandas/tests/{tools => reshape}/data/cut_data.csv (99%) create mode 100644 pandas/tests/reshape/merge/__init__.py rename pandas/tests/{tools => reshape/merge}/data/allow_exact_matches.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/allow_exact_matches_and_tolerance.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/asof.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/asof2.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/quotes.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/quotes2.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/tolerance.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/trades.csv (100%) rename pandas/tests/{tools => reshape/merge}/data/trades2.csv (100%) rename pandas/tests/{tools => reshape/merge}/test_join.py (74%) create mode 100644 pandas/tests/reshape/merge/test_merge.py rename pandas/tests/{tools => reshape/merge}/test_merge_asof.py (79%) create mode 100644 pandas/tests/reshape/merge/test_merge_index_as_string.py rename pandas/tests/{tools => reshape/merge}/test_merge_ordered.py (72%) create mode 100644 pandas/tests/reshape/merge/test_multi.py rename pandas/tests/{tools => reshape}/test_concat.py (59%) create mode 100644 pandas/tests/reshape/test_cut.py create mode 100644 pandas/tests/reshape/test_melt.py rename pandas/tests/{tools => reshape}/test_pivot.py (67%) create mode 100644 pandas/tests/reshape/test_qcut.py create mode 100644 pandas/tests/reshape/test_reshape.py rename pandas/tests/{tools => reshape}/test_union_categoricals.py (90%) create mode 100644 pandas/tests/reshape/test_util.py create mode 100644 pandas/tests/scalar/interval/__init__.py create mode 100644 pandas/tests/scalar/interval/test_interval.py create mode 100644 pandas/tests/scalar/interval/test_ops.py create mode 100644 pandas/tests/scalar/period/__init__.py rename pandas/tests/scalar/{test_period_asfreq.py => period/test_asfreq.py} (57%) create mode 100644 pandas/tests/scalar/period/test_period.py delete mode 100644 pandas/tests/scalar/test_period.py delete mode 100644 pandas/tests/scalar/test_timedelta.py delete mode 100644 pandas/tests/scalar/test_timestamp.py create mode 100644 pandas/tests/scalar/timedelta/__init__.py create mode 100644 pandas/tests/scalar/timedelta/test_arithmetic.py create mode 100644 pandas/tests/scalar/timedelta/test_construction.py create mode 100644 pandas/tests/scalar/timedelta/test_formats.py create mode 100644 pandas/tests/scalar/timedelta/test_timedelta.py create mode 100644 pandas/tests/scalar/timestamp/__init__.py create mode 100644 pandas/tests/scalar/timestamp/test_arithmetic.py create mode 100644 pandas/tests/scalar/timestamp/test_comparisons.py create mode 100644 pandas/tests/scalar/timestamp/test_rendering.py create mode 100644 pandas/tests/scalar/timestamp/test_timestamp.py create mode 100644 pandas/tests/scalar/timestamp/test_timezones.py create mode 100644 pandas/tests/scalar/timestamp/test_unary_ops.py create mode 100644 pandas/tests/series/conftest.py create mode 100644 pandas/tests/series/indexing/__init__.py create mode 100644 pandas/tests/series/indexing/conftest.py create mode 100644 pandas/tests/series/indexing/test_alter_index.py create mode 100644 pandas/tests/series/indexing/test_boolean.py create mode 100644 pandas/tests/series/indexing/test_callable.py create mode 100644 pandas/tests/series/indexing/test_datetime.py create mode 100644 pandas/tests/series/indexing/test_iloc.py create mode 100644 pandas/tests/series/indexing/test_indexing.py create mode 100644 pandas/tests/series/indexing/test_loc.py create mode 100644 pandas/tests/series/indexing/test_numeric.py create mode 100644 pandas/tests/series/test_api.py create mode 100644 pandas/tests/series/test_arithmetic.py create mode 100644 pandas/tests/series/test_block_internals.py create mode 100644 pandas/tests/series/test_duplicates.py delete mode 100644 pandas/tests/series/test_indexing.py delete mode 100644 pandas/tests/series/test_misc_api.py create mode 100644 pandas/tests/series/test_timezones.py create mode 100644 pandas/tests/sparse/frame/__init__.py create mode 100644 pandas/tests/sparse/frame/conftest.py create mode 100644 pandas/tests/sparse/frame/test_analytics.py create mode 100644 pandas/tests/sparse/frame/test_apply.py create mode 100644 pandas/tests/sparse/frame/test_frame.py create mode 100644 pandas/tests/sparse/frame/test_indexing.py create mode 100644 pandas/tests/sparse/frame/test_to_csv.py create mode 100644 pandas/tests/sparse/frame/test_to_from_scipy.py create mode 100644 pandas/tests/sparse/series/__init__.py create mode 100644 pandas/tests/sparse/series/test_indexing.py rename pandas/tests/sparse/{ => series}/test_series.py (70%) delete mode 100644 pandas/tests/sparse/test_array.py delete mode 100644 pandas/tests/sparse/test_frame.py delete mode 100644 pandas/tests/sparse/test_list.py create mode 100644 pandas/tests/sparse/test_reshape.py delete mode 100644 pandas/tests/test_categorical.py create mode 100644 pandas/tests/test_downstream.py create mode 100644 pandas/tests/test_errors.py delete mode 100644 pandas/tests/test_generic.py mode change 100755 => 100644 pandas/tests/test_multilevel.py delete mode 100644 pandas/tests/test_panel.py delete mode 100644 pandas/tests/test_panel4d.py delete mode 100644 pandas/tests/test_panelnd.py create mode 100644 pandas/tests/test_register_accessor.py delete mode 100644 pandas/tests/test_reshape.py delete mode 100644 pandas/tests/test_testing.py delete mode 100644 pandas/tests/test_util.py delete mode 100644 pandas/tests/tools/test_hashing.py delete mode 100644 pandas/tests/tools/test_merge.py create mode 100644 pandas/tests/tools/test_numeric.py delete mode 100644 pandas/tests/tools/test_tile.py delete mode 100644 pandas/tests/tools/test_util.py create mode 100644 pandas/tests/tseries/frequencies/__init__.py create mode 100644 pandas/tests/tseries/frequencies/test_freq_code.py create mode 100644 pandas/tests/tseries/frequencies/test_inference.py create mode 100644 pandas/tests/tseries/frequencies/test_to_offset.py create mode 100644 pandas/tests/tseries/holiday/__init__.py create mode 100644 pandas/tests/tseries/holiday/test_calendar.py create mode 100644 pandas/tests/tseries/holiday/test_federal.py create mode 100644 pandas/tests/tseries/holiday/test_holiday.py create mode 100644 pandas/tests/tseries/holiday/test_observance.py create mode 100644 pandas/tests/tseries/offsets/__init__.py create mode 100644 pandas/tests/tseries/offsets/common.py create mode 100644 pandas/tests/tseries/offsets/conftest.py rename pandas/tests/tseries/{ => offsets}/data/cday-0.14.1.pickle (100%) rename pandas/tests/tseries/{ => offsets}/data/dateoffset_0_15_2.pickle (100%) create mode 100644 pandas/tests/tseries/offsets/test_fiscal.py create mode 100644 pandas/tests/tseries/offsets/test_offsets.py create mode 100644 pandas/tests/tseries/offsets/test_offsets_properties.py create mode 100644 pandas/tests/tseries/offsets/test_ticks.py create mode 100644 pandas/tests/tseries/offsets/test_yqm_offsets.py delete mode 100644 pandas/tests/tseries/test_converter.py delete mode 100644 pandas/tests/tseries/test_frequencies.py delete mode 100644 pandas/tests/tseries/test_holiday.py delete mode 100644 pandas/tests/tseries/test_offsets.py delete mode 100755 pandas/tests/tseries/test_resample.py delete mode 100644 pandas/tests/tseries/test_timezones.py create mode 100644 pandas/tests/tslibs/__init__.py create mode 100644 pandas/tests/tslibs/test_api.py create mode 100644 pandas/tests/tslibs/test_array_to_datetime.py create mode 100644 pandas/tests/tslibs/test_ccalendar.py create mode 100644 pandas/tests/tslibs/test_conversion.py create mode 100644 pandas/tests/tslibs/test_libfrequencies.py create mode 100644 pandas/tests/tslibs/test_liboffsets.py create mode 100644 pandas/tests/tslibs/test_normalize_date.py create mode 100644 pandas/tests/tslibs/test_parse_iso8601.py create mode 100644 pandas/tests/tslibs/test_parsing.py create mode 100644 pandas/tests/tslibs/test_period_asfreq.py create mode 100644 pandas/tests/tslibs/test_timedeltas.py create mode 100644 pandas/tests/tslibs/test_timezones.py delete mode 100644 pandas/tests/types/test_cast.py delete mode 100644 pandas/tests/types/test_common.py delete mode 100644 pandas/tests/types/test_concat.py delete mode 100644 pandas/tests/types/test_dtypes.py delete mode 100644 pandas/tests/types/test_generic.py delete mode 100644 pandas/tests/types/test_inference.py delete mode 100644 pandas/tests/types/test_io.py delete mode 100644 pandas/tests/types/test_missing.py create mode 100644 pandas/tests/util/__init__.py create mode 100644 pandas/tests/util/conftest.py create mode 100644 pandas/tests/util/test_assert_almost_equal.py create mode 100644 pandas/tests/util/test_assert_categorical_equal.py create mode 100644 pandas/tests/util/test_assert_extension_array_equal.py create mode 100644 pandas/tests/util/test_assert_frame_equal.py create mode 100644 pandas/tests/util/test_assert_index_equal.py create mode 100644 pandas/tests/util/test_assert_interval_array_equal.py create mode 100644 pandas/tests/util/test_assert_numpy_array_equal.py create mode 100644 pandas/tests/util/test_assert_series_equal.py create mode 100644 pandas/tests/util/test_deprecate.py create mode 100644 pandas/tests/util/test_deprecate_kwarg.py create mode 100644 pandas/tests/util/test_hashing.py create mode 100644 pandas/tests/util/test_locale.py create mode 100644 pandas/tests/util/test_move.py create mode 100644 pandas/tests/util/test_safe_import.py create mode 100644 pandas/tests/util/test_util.py create mode 100644 pandas/tests/util/test_validate_args.py create mode 100644 pandas/tests/util/test_validate_args_and_kwargs.py create mode 100644 pandas/tests/util/test_validate_kwargs.py delete mode 100644 pandas/tools/tile.py delete mode 100644 pandas/tseries/base.py delete mode 100644 pandas/tseries/common.py delete mode 100644 pandas/tseries/index.py delete mode 100644 pandas/tseries/interval.py delete mode 100644 pandas/tseries/period.py delete mode 100755 pandas/tseries/resample.py delete mode 100644 pandas/tseries/tdi.py delete mode 100644 pandas/tseries/timedeltas.py delete mode 100644 pandas/tseries/tools.py delete mode 100644 pandas/tseries/util.py delete mode 100644 pandas/tslib.py delete mode 100644 pandas/types/api.py delete mode 100644 pandas/types/common.py delete mode 100644 pandas/types/concat.py delete mode 100644 pandas/types/dtypes.py delete mode 100644 pandas/types/inference.py delete mode 100644 pandas/types/missing.py rename pandas/util/{decorators.py => _decorators.py} (50%) rename pandas/util/{depr_module.py => _depr_module.py} (63%) rename pandas/util/{doctools.py => _doctools.py} (87%) create mode 100644 pandas/util/_exceptions.py rename pandas/util/{print_versions.py => _print_versions.py} (77%) create mode 100644 pandas/util/_test_decorators.py rename pandas/util/{validators.py => _validators.py} (59%) create mode 100644 requirements-dev.txt delete mode 100644 scripts/api_rst_coverage.py delete mode 100644 scripts/bench_join.R delete mode 100644 scripts/bench_join.py delete mode 100644 scripts/bench_join_multi.py delete mode 100644 scripts/bench_refactor.py delete mode 100644 scripts/boxplot_test.py create mode 100755 scripts/build_dist_for_release.sh delete mode 100755 scripts/count_code.sh create mode 100644 scripts/download_wheels.py delete mode 100644 scripts/faster_xs.py delete mode 100644 scripts/file_sizes.py delete mode 100755 scripts/find_undoc_args.py delete mode 100644 scripts/gen_release_notes.py create mode 100755 scripts/generate_pip_deps_from_conda.py delete mode 100644 scripts/git-mrb delete mode 100644 scripts/git_code_churn.py delete mode 100644 scripts/groupby_sample.py delete mode 100644 scripts/groupby_speed.py delete mode 100644 scripts/groupby_test.py delete mode 100644 scripts/hdfstore_panel_perf.py delete mode 100644 scripts/json_manip.py delete mode 100644 scripts/leak.py create mode 100755 scripts/list_future_warnings.sh rename scripts/{merge-py.py => merge-pr.py} (80%) delete mode 100644 scripts/parser_magic.py delete mode 100644 scripts/preepoch_test.py delete mode 100644 scripts/pypistats.py delete mode 100644 scripts/roll_median_leak.py delete mode 100644 scripts/runtests.py delete mode 100644 scripts/test_py27.bat delete mode 100644 scripts/testmed.py create mode 100644 scripts/tests/__init__.py create mode 100644 scripts/tests/conftest.py create mode 100644 scripts/tests/test_validate_docstrings.py delete mode 100755 scripts/touchup_gh_issues.py delete mode 100755 scripts/use_build_cache.py create mode 100755 scripts/validate_docstrings.py delete mode 100644 scripts/winbuild_py27.bat delete mode 100644 scripts/windows_builder/build_27-32.bat delete mode 100644 scripts/windows_builder/build_27-64.bat delete mode 100644 scripts/windows_builder/build_34-32.bat delete mode 100644 scripts/windows_builder/build_34-64.bat delete mode 100644 scripts/windows_builder/check_and_build.bat delete mode 100644 scripts/windows_builder/check_and_build.py delete mode 100644 scripts/windows_builder/readme.txt delete mode 100755 test_perf.sh delete mode 100644 vb_suite/.gitignore delete mode 100644 vb_suite/attrs_caching.py delete mode 100644 vb_suite/binary_ops.py delete mode 100644 vb_suite/categoricals.py delete mode 100644 vb_suite/ctors.py delete mode 100644 vb_suite/eval.py delete mode 100644 vb_suite/frame_ctor.py delete mode 100644 vb_suite/frame_methods.py delete mode 100644 vb_suite/generate_rst_files.py delete mode 100644 vb_suite/gil.py delete mode 100644 vb_suite/groupby.py delete mode 100644 vb_suite/hdfstore_bench.py delete mode 100644 vb_suite/index_object.py delete mode 100644 vb_suite/indexing.py delete mode 100644 vb_suite/inference.py delete mode 100644 vb_suite/io_bench.py delete mode 100644 vb_suite/io_sql.py delete mode 100644 vb_suite/join_merge.py delete mode 100755 vb_suite/make.py delete mode 100755 vb_suite/measure_memory_consumption.py delete mode 100644 vb_suite/miscellaneous.py delete mode 100644 vb_suite/packers.py delete mode 100644 vb_suite/pandas_vb_common.py delete mode 100644 vb_suite/panel_ctor.py delete mode 100644 vb_suite/panel_methods.py delete mode 100644 vb_suite/parser_vb.py delete mode 100755 vb_suite/perf_HEAD.py delete mode 100644 vb_suite/plotting.py delete mode 100644 vb_suite/reindex.py delete mode 100644 vb_suite/replace.py delete mode 100644 vb_suite/reshape.py delete mode 100755 vb_suite/run_suite.py delete mode 100644 vb_suite/series_methods.py delete mode 100644 vb_suite/source/conf.py delete mode 100644 vb_suite/source/themes/agogo/layout.html delete mode 100644 vb_suite/source/themes/agogo/static/agogo.css_t delete mode 100644 vb_suite/source/themes/agogo/static/bgfooter.png delete mode 100644 vb_suite/source/themes/agogo/static/bgtop.png delete mode 100644 vb_suite/source/themes/agogo/theme.conf delete mode 100644 vb_suite/sparse.py delete mode 100644 vb_suite/stat_ops.py delete mode 100644 vb_suite/strings.py delete mode 100644 vb_suite/suite.py delete mode 100644 vb_suite/test.py delete mode 100755 vb_suite/test_perf.py delete mode 100644 vb_suite/timedelta.py delete mode 100644 vb_suite/timeseries.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3f630aa6cf8f5..0000000000000 --- a/.coveragerc +++ /dev/null @@ -1,27 +0,0 @@ -# .coveragerc to control coverage.py -[run] -branch = False -omit = */tests/* - -[report] -# Regexes for lines to exclude from consideration -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain about missing debug-only code: - def __repr__ - if self\.debug - - # Don't complain if tests don't hit defensive assertion code: - raise AssertionError - raise NotImplementedError - - # Don't complain if non-runnable code isn't run: - if 0: - if __name__ == .__main__.: - -ignore_errors = False - -[html] -directory = coverage_html_report diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000..a1fbece3284ec --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,63 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of +fostering an open and welcoming community, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating +documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free +experience for everyone, regardless of level of experience, gender, gender +identity and expression, sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic + addresses, without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to +fairly and consistently applying these principles to every aspect of managing +this project. Project maintainers who do not follow or enforce the Code of +Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +A working group of community members is committed to promptly addressing any +reported issues. The working group is made up of pandas contributors and users. +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the working group by e-mail (pandas-coc@googlegroups.com). +Messages sent to this e-mail address will not be publicly visible but only to +the working group members. The working group currently includes + +- Safia Abdalla +- Tom Augspurger +- Joris Van den Bossche +- Camille Scott +- Nathaniel Smith + +All complaints will be reviewed and investigated and will result in a response +that is deemed necessary and appropriate to the circumstances. Maintainers are +obligated to maintain confidentiality with regard to the reporter of an +incident. + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.3.0, available at +[http://contributor-covenant.org/version/1/3/0/][version], +and the [Swift Code of Conduct][swift]. + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/3/0/ +[swift]: https://swift.org/community/#code-of-conduct + diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 95729f845ff5c..faff68b636109 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,24 +1,23 @@ -Contributing to pandas -====================== +# Contributing to pandas Whether you are a novice or experienced software developer, all contributions and suggestions are welcome! -Our main contribution docs can be found [here](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst), but if you do not want to read it in its entirety, we will summarize the main ways in which you can contribute and point to relevant places in the docs for further information. +Our main contributing guide can be found [in this repo](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst) or [on the website](https://pandas-docs.github.io/pandas-docs-travis/contributing.html). If you do not want to read it in its entirety, we will summarize the main ways in which you can contribute and point to relevant sections of that document for further information. + +## Getting Started -Getting Started ---------------- If you are looking to contribute to the *pandas* codebase, the best place to start is the [GitHub "issues" tab](https://github.com/pandas-dev/pandas/issues). This is also a great place for filing bug reports and making suggestions for ways in which we can improve the code and documentation. -If you have additional questions, feel free to ask them on the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata) or on [Gitter](https://gitter.im/pydata/pandas). Further information can also be found in our [Getting Started](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#where-to-start) section of our main contribution doc. +If you have additional questions, feel free to ask them on the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata) or on [Gitter](https://gitter.im/pydata/pandas). Further information can also be found in the "[Where to start?](https://github.com/pandas-dev/pandas/blob/master/doc/source/development/contributing.rst#where-to-start)" section. + +## Filing Issues + +If you notice a bug in the code or documentation, or have suggestions for how we can improve either, feel free to create an issue on the [GitHub "issues" tab](https://github.com/pandas-dev/pandas/issues) using [GitHub's "issue" form](https://github.com/pandas-dev/pandas/issues/new). The form contains some questions that will help us best address your issue. For more information regarding how to file issues against *pandas*, please refer to the "[Bug reports and enhancement requests](https://github.com/pandas-dev/pandas/blob/master/doc/source/development/contributing.rst#bug-reports-and-enhancement-requests)" section. -Filing Issues -------------- -If you notice a bug in the code or in docs or have suggestions for how we can improve either, feel free to create an issue on the [GitHub "issues" tab](https://github.com/pandas-dev/pandas/issues) using [GitHub's "issue" form](https://github.com/pandas-dev/pandas/issues/new). The form contains some questions that will help us best address your issue. For more information regarding how to file issues against *pandas*, please refer to the [Bug reports and enhancement requests](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#bug-reports-and-enhancement-requests) section of our main contribution doc. +## Contributing to the Codebase -Contributing to the Codebase ----------------------------- -The code is hosted on [GitHub](https://www.github.com/pandas-dev/pandas), so you will need to use [Git](http://git-scm.com/) to clone the project and make changes to the codebase. Once you have obtained a copy of the code, you should create a development environment that is separate from your existing Python environment so that you can make and test changes without compromising your own work environment. For more information, please refer to our [Working with the code](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#working-with-the-code) section of our main contribution docs. +The code is hosted on [GitHub](https://www.github.com/pandas-dev/pandas), so you will need to use [Git](http://git-scm.com/) to clone the project and make changes to the codebase. Once you have obtained a copy of the code, you should create a development environment that is separate from your existing Python environment so that you can make and test changes without compromising your own work environment. For more information, please refer to the "[Working with the code](https://github.com/pandas-dev/pandas/blob/master/doc/source/development/contributing.rst#working-with-the-code)" section. -Before submitting your changes for review, make sure to check that your changes do not break any tests. You can find more information about our test suites can be found [here](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#test-driven-development-code-writing). We also have guidelines regarding coding style that will be enforced during testing. Details about coding style can be found [here](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#code-standards). +Before submitting your changes for review, make sure to check that your changes do not break any tests. You can find more information about our test suites in the "[Test-driven development/code writing](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#test-driven-development-code-writing)" section. We also have guidelines regarding coding style that will be enforced during testing, which can be found in the "[Code standards](https://github.com/pandas-dev/pandas/blob/master/doc/source/development/contributing.rst#code-standards)" section. -Once your changes are ready to be submitted, make sure to push your changes to GitHub before creating a pull request. Details about how to do that can be found in the [Contributing your changes to pandas](https://github.com/pandas-dev/pandas/blob/master/doc/source/contributing.rst#contributing-your-changes-to-pandas) section of our main contribution docs. We will review your changes, and you will most likely be asked to make additional changes before it is finally ready to merge. However, once it's ready, we will merge it, and you will have successfully contributed to the codebase! +Once your changes are ready to be submitted, make sure to push your changes to GitHub before creating a pull request. Details about how to do that can be found in the "[Contributing your changes to pandas](https://github.com/pandas-dev/pandas/blob/master/doc/source/development/contributing.rst#contributing-your-changes-to-pandas)" section. We will review your changes, and you will most likely be asked to make additional changes before it is finally ready to merge. However, once it's ready, we will merge it, and you will have successfully contributed to the codebase! diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1f614b54b1f71..e33835c462511 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -8,11 +8,22 @@ [this should explain **why** the current behaviour is a problem and why the expected output is a better solution.] +**Note**: We receive a lot of issues on our GitHub tracker, so it is very possible that your issue has been posted before. Please check first before submitting so that we do not have to handle and close duplicates! + +**Note**: Many problems can be resolved by simply upgrading `pandas` to the latest version. Before submitting, please check if that solution works for you. If possible, you may want to check if `master` addresses this issue, but that is not necessary. + +For documentation-related issues, you can check the latest versions of the docs on `master` here: + +https://pandas-docs.github.io/pandas-docs-travis/ + +If the issue has not been resolved there, go ahead and file it in the issue tracker. + #### Expected Output #### Output of ``pd.show_versions()``
-# Paste the output here pd.show_versions() here + +[paste the output of ``pd.show_versions()`` here below this line]
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9281c51059087..4e1e9ce017408 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ - - [ ] closes #xxxx - - [ ] tests added / passed - - [ ] passes ``git diff upstream/master --name-only -- '*.py' | flake8 --diff`` - - [ ] whatsnew entry +- [ ] closes #xxxx +- [ ] tests added / passed +- [ ] passes `git diff upstream/master -u -- "*.py" | flake8 --diff` +- [ ] whatsnew entry diff --git a/.gitignore b/.gitignore index a509fcf736ea8..816aff376fc83 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *$ *.bak *flymake* +*.iml *.kdev4 *.log *.swp @@ -20,6 +21,7 @@ .ipynb_checkpoints .tags .cache/ +.vscode/ # Compiled source # ################### @@ -59,6 +61,9 @@ dist .coverage coverage.xml coverage_html_report +*.pytest_cache +# hypothesis test database +.hypothesis/ # OS generated files # ###################### @@ -86,8 +91,8 @@ scikits *.c *.cpp -# Performance Testing # -####################### +# Unit / Performance Testing # +############################## asv_bench/env/ asv_bench/html/ asv_bench/results/ @@ -96,6 +101,8 @@ asv_bench/pandas/ # Documentation generated files # ################################# doc/source/generated +doc/source/user_guide/styled.xlsx +doc/source/reference/api doc/source/_static doc/source/vbench doc/source/vbench.rst @@ -103,3 +110,5 @@ doc/source/index.rst doc/build/html/index.html # Windows specific leftover: doc/tmp.sv +env/ +doc/source/savefig/ diff --git a/.pep8speaks.yml b/.pep8speaks.yml new file mode 100644 index 0000000000000..cbcb098c47125 --- /dev/null +++ b/.pep8speaks.yml @@ -0,0 +1,19 @@ +# File : .pep8speaks.yml + +scanner: + diff_only: True # If True, errors caused by only the patch are shown + +# Opened issue in pep8speaks, so we can directly use the config in setup.cfg +# (and avoid having to duplicate it here): +# https://github.com/OrkoHunter/pep8speaks/issues/95 + +pycodestyle: + max-line-length: 79 + ignore: + - W503, # line break before binary operator + - W504, # line break after binary operator + - E402, # module level import not at top of file + - E731, # do not assign a lambda expression, use a def + - C406, # Unnecessary list literal - rewrite as a dict literal. + - C408, # Unnecessary dict call - rewrite as a literal. + - C409 # Unnecessary list passed to tuple() - rewrite as a tuple literal. diff --git a/.travis.yml b/.travis.yml index d864b755541de..e478d71a5c350 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: 3.5 # set NOCACHE-true # To delete caches go to https://travis-ci.org/OWNER/REPOSITORY/caches or run # travis cache --delete inside the project directory from the travis command line client -# The cash directories will be deleted if anything in ci/ changes in a commit +# The cache directories will be deleted if anything in ci/ changes in a commit cache: ccache: true directories: @@ -16,80 +16,64 @@ cache: env: global: - - # pandas-docs-travis GH - - secure: "YvvTc+FrSYHgdxqoxn9s8VOaCWjvZzlkaf6k55kkmQqCYR9dPiLMsot1F96/N7o3YlD1s0znPQCak93Du8HHi/8809zAXloTaMSZrWz4R4qn96xlZFRE88O/w/Z1t3VVYpKX3MHlCggBc8MtXrqmvWKJMAqXyysZ4TTzoiJDPvE=" + # create a github personal access token + # cd pandas-dev/pandas + # travis encrypt 'PANDAS_GH_TOKEN=personal_access_token' -r pandas-dev/pandas + - secure: "EkWLZhbrp/mXJOx38CHjs7BnjXafsqHtwxPQrqWy457VDFWhIY1DMnIR/lOWG+a20Qv52sCsFtiZEmMfUjf0pLGXOqurdxbYBGJ7/ikFLk9yV2rDwiArUlVM9bWFnFxHvdz9zewBH55WurrY4ShZWyV+x2dWjjceWG5VpWeI6sA=" git: # for cloning - depth: 1000 + depth: 2000 matrix: fast_finish: true exclude: # Exclude the default Python 3.5 build - python: 3.5 + include: - - os: osx - language: generic - env: - - JOB="3.5_OSX" TEST_ARGS="--skip-slow --skip-network" - - os: linux + - dist: trusty env: - - JOB="2.7_LOCALE" TEST_ARGS="--only-slow --skip-network" LOCALE_OVERRIDE="zh_CN.UTF-8" - addons: - apt: - packages: - - language-pack-zh-hans - - os: linux + - JOB="3.7" ENV_FILE="ci/deps/travis-37.yaml" PATTERN="(not slow and not network)" + + - dist: trusty env: - - JOB="2.7" TEST_ARGS="--skip-slow" LINT=true + - JOB="2.7" ENV_FILE="ci/deps/travis-27.yaml" PATTERN="(not slow or (single and db))" addons: apt: packages: - python-gtk2 - - os: linux - env: - - JOB="3.5" TEST_ARGS="--skip-slow --skip-network" COVERAGE=true - addons: - apt: - packages: - - xsel - - os: linux - env: - - JOB="3.6" TEST_ARGS="--skip-slow --skip-network" PANDAS_TESTING_MODE="deprecate" CONDA_FORGE=true - # In allow_failures - - os: linux + + - dist: trusty env: - - JOB="2.7_SLOW" TEST_ARGS="--only-slow --skip-network" - # In allow_failures - - os: linux + - JOB="3.6, locale" ENV_FILE="ci/deps/travis-36-locale.yaml" PATTERN="((not slow and not network) or (single and db))" LOCALE_OVERRIDE="zh_CN.UTF-8" + + - dist: trusty env: - - JOB="2.7_BUILD_TEST" TEST_ARGS="--skip-slow" BUILD_TEST=true + - JOB="3.6, coverage" ENV_FILE="ci/deps/travis-36.yaml" PATTERN="((not slow and not network) or (single and db))" PANDAS_TESTING_MODE="deprecate" COVERAGE=true + # In allow_failures - - os: linux + - dist: trusty env: - - JOB="3.6_NUMPY_DEV" TEST_ARGS="--skip-slow --skip-network" PANDAS_TESTING_MODE="deprecate" + - JOB="3.6, slow" ENV_FILE="ci/deps/travis-36-slow.yaml" PATTERN="slow" + # In allow_failures - - os: linux + - dist: trusty env: - - JOB="3.5_DOC" DOC=true + - JOB="3.6, doc" ENV_FILE="ci/deps/travis-36-doc.yaml" DOC=true allow_failures: - - os: linux + - dist: trusty env: - - JOB="2.7_SLOW" TEST_ARGS="--only-slow --skip-network" - - os: linux + - JOB="3.6, slow" ENV_FILE="ci/deps/travis-36-slow.yaml" PATTERN="slow" + - dist: trusty env: - - JOB="2.7_BUILD_TEST" TEST_ARGS="--skip-slow" BUILD_TEST=true - - os: linux - env: - - JOB="3.6_NUMPY_DEV" TEST_ARGS="--skip-slow --skip-network" PANDAS_TESTING_MODE="deprecate" - - os: linux - env: - - JOB="3.5_DOC" DOC=true + - JOB="3.6, doc" ENV_FILE="ci/deps/travis-36-doc.yaml" DOC=true before_install: - echo "before_install" + # set non-blocking IO on travis + # https://github.com/travis-ci/travis-ci/issues/8920#issuecomment-352661024 + - python -c 'import os,sys,fcntl; flags = fcntl.fcntl(sys.stdout, fcntl.F_GETFL); fcntl.fcntl(sys.stdout, fcntl.F_SETFL, flags&~os.O_NONBLOCK);' - source ci/travis_process_gbq_encryption.sh - export PATH="$HOME/miniconda3/bin:$PATH" - df -h @@ -97,8 +81,12 @@ before_install: - uname -a - git --version - git tag - - ci/before_install_travis.sh - - export DISPLAY=":99.0" + # Because travis runs on Google Cloud and has a /etc/boto.cfg, + # it breaks moto import, see: + # https://github.com/spulec/moto/issues/1771 + # https://github.com/boto/boto/issues/3741 + # This overrides travis and tells it to look nowhere. + - export BOTO_CONFIG=/dev/null install: - echo "install start" @@ -109,25 +97,22 @@ install: before_script: - ci/install_db_travis.sh + - export DISPLAY=":99.0" + - ci/before_script_travis.sh script: - echo "script start" - - ci/run_build_docs.sh - - ci/script_single.sh - - ci/script_multi.sh - - ci/lint.sh - - echo "script done" - -after_success: - - ci/upload_coverage.sh + - source activate pandas-dev + - ci/build_docs.sh + - ci/run_tests.sh after_script: - echo "after_script start" - - source activate pandas && python -c "import pandas; pandas.show_versions();" - - if [ -e /tmp/single.xml ]; then - ci/print_skipped.py /tmp/single.xml; + - source activate pandas-dev && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd + - if [ -e test-data-single.xml ]; then + ci/print_skipped.py test-data-single.xml; fi - - if [ -e /tmp/multiple.xml ]; then - ci/print_skipped.py /tmp/multiple.xml; + - if [ -e test-data-multiple.xml ]; then + ci/print_skipped.py test-data-multiple.xml; fi - echo "after_script done" diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000000000..dcaaea101f4c8 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,57 @@ +About the Copyright Holders +=========================== + +* Copyright (c) 2008-2011 AQR Capital Management, LLC + + AQR Capital Management began pandas development in 2008. Development was + led by Wes McKinney. AQR released the source under this license in 2009. +* Copyright (c) 2011-2012, Lambda Foundry, Inc. + + Wes is now an employee of Lambda Foundry, and remains the pandas project + lead. +* Copyright (c) 2011-2012, PyData Development Team + + The PyData Development Team is the collection of developers of the PyData + project. This includes all of the PyData sub-projects, including pandas. The + core team that coordinates development on GitHub can be found here: + http://github.com/pydata. + +Full credits for pandas contributors can be found in the documentation. + +Our Copyright Policy +==================== + +PyData uses a shared copyright model. Each contributor maintains copyright +over their contributions to PyData. However, it is important to note that +these contributions are typically only changes to the repositories. Thus, +the PyData source code, in its entirety, is not the copyright of any single +person or institution. Instead, it is the collective copyright of the +entire PyData Development Team. If individual contributors want to maintain +a record of what changes/contributions they have specific copyright on, +they should indicate their copyright in the commit message of the change +when they commit the change to one of the PyData repositories. + +With this in mind, the following banner should be used in any source code +file to indicate the copyright and license terms: + +``` +#----------------------------------------------------------------------------- +# Copyright (c) 2012, PyData Development Team +# All rights reserved. +# +# Distributed under the terms of the BSD Simplified License. +# +# The full license is in the LICENSE file, distributed with this software. +#----------------------------------------------------------------------------- +``` + +Other licenses can be found in the LICENSES directory. + +License +======= + +pandas is distributed under a 3-clause ("Simplified" or "New") BSD +license. Parts of NumPy, SciPy, numpydoc, bottleneck, which all have +BSD-compatible licenses, are included. Their licenses follow the pandas +license. + diff --git a/LICENSE b/LICENSE index c9b8834e8774b..924de26253bf4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,87 +1,29 @@ -======= -License -======= +BSD 3-Clause License -pandas is distributed under a 3-clause ("Simplified" or "New") BSD -license. Parts of NumPy, SciPy, numpydoc, bottleneck, which all have -BSD-compatible licenses, are included. Their licenses follow the pandas -license. - -pandas license -============== - -Copyright (c) 2011-2012, Lambda Foundry, Inc. and PyData Development Team -All rights reserved. - -Copyright (c) 2008-2011 AQR Capital Management, LLC +Copyright (c) 2008-2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team All rights reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of the copyright holder nor the names of any - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -About the Copyright Holders -=========================== - -AQR Capital Management began pandas development in 2008. Development was -led by Wes McKinney. AQR released the source under this license in 2009. -Wes is now an employee of Lambda Foundry, and remains the pandas project -lead. - -The PyData Development Team is the collection of developers of the PyData -project. This includes all of the PyData sub-projects, including pandas. The -core team that coordinates development on GitHub can be found here: -http://github.com/pydata. - -Full credits for pandas contributors can be found in the documentation. - -Our Copyright Policy -==================== - -PyData uses a shared copyright model. Each contributor maintains copyright -over their contributions to PyData. However, it is important to note that -these contributions are typically only changes to the repositories. Thus, -the PyData source code, in its entirety, is not the copyright of any single -person or institution. Instead, it is the collective copyright of the -entire PyData Development Team. If individual contributors want to maintain -a record of what changes/contributions they have specific copyright on, -they should indicate their copyright in the commit message of the change -when they commit the change to one of the PyData repositories. - -With this in mind, the following banner should be used in any source code -file to indicate the copyright and license terms: - -#----------------------------------------------------------------------------- -# Copyright (c) 2012, PyData Development Team -# All rights reserved. -# -# Distributed under the terms of the BSD Simplified License. -# -# The full license is in the LICENSE file, distributed with this software. -#----------------------------------------------------------------------------- - -Other licenses can be found in the LICENSES directory. \ No newline at end of file diff --git a/LICENSES/DATEUTIL_LICENSE b/LICENSES/DATEUTIL_LICENSE new file mode 100644 index 0000000000000..6053d35cfc60b --- /dev/null +++ b/LICENSES/DATEUTIL_LICENSE @@ -0,0 +1,54 @@ +Copyright 2017- Paul Ganssle +Copyright 2017- dateutil contributors (see AUTHORS file) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The above license applies to all contributions after 2017-12-01, as well as +all contributions that have been re-licensed (see AUTHORS file for the list of +contributors who have re-licensed their code). +-------------------------------------------------------------------------------- +dateutil - Extensions to the standard Python datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer +Copyright (c) 2012-2014 - Tomi Pieviläinen +Copyright (c) 2014-2016 - Yaron de Leeuw +Copyright (c) 2015- - Paul Ganssle +Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The above BSD License Applies to all code, even that also covered by Apache 2.0. diff --git a/LICENSES/MUSL_LICENSE b/LICENSES/MUSL_LICENSE new file mode 100644 index 0000000000000..a8833d4bc4744 --- /dev/null +++ b/LICENSES/MUSL_LICENSE @@ -0,0 +1,132 @@ +musl as a whole is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2005-2014 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- + +Authors/contributors include: + +Anthony G. Basile +Arvid Picciani +Bobby Bingham +Boris Brezillon +Brent Cook +Chris Spiegel +Clément Vasseur +Emil Renner Berthing +Hiltjo Posthuma +Isaac Dunham +Jens Gustedt +Jeremy Huntwork +John Spencer +Justin Cormack +Luca Barbato +Luka Perkov +M Farkas-Dyck (Strake) +Michael Forney +Nicholas J. Kain +orc +Pascal Cuoq +Pierre Carrier +Rich Felker +Richard Pennington +sin +Solar Designer +Stefan Kristiansson +Szabolcs Nagy +Timo Teräs +Valentin Ochs +William Haddon + +Portions of this software are derived from third-party works licensed +under terms compatible with the above MIT license: + +The TRE regular expression implementation (src/regex/reg* and +src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed +under a 2-clause BSD license (license text in the source files). The +included version has been heavily modified by Rich Felker in 2012, in +the interests of size, simplicity, and namespace cleanliness. + +Much of the math library code (src/math/* and src/complex/*) is +Copyright © 1993,2004 Sun Microsystems or +Copyright © 2003-2011 David Schultz or +Copyright © 2003-2009 Steven G. Kargl or +Copyright © 2003-2009 Bruce D. Evans or +Copyright © 2008 Stephen L. Moshier +and labelled as such in comments in the individual source files. All +have been licensed under extremely permissive terms. + +The ARM memcpy code (src/string/armel/memcpy.s) is Copyright © 2008 +The Android Open Source Project and is licensed under a two-clause BSD +license. It was taken from Bionic libc, used on Android. + +The implementation of DES for crypt (src/misc/crypt_des.c) is +Copyright © 1994 David Burren. It is licensed under a BSD license. + +The implementation of blowfish crypt (src/misc/crypt_blowfish.c) was +originally written by Solar Designer and placed into the public +domain. The code also comes with a fallback permissive license for use +in jurisdictions that may not recognize the public domain. + +The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011 +Valentin Ochs and is licensed under an MIT-style license. + +The BSD PRNG implementation (src/prng/random.c) and XSI search API +(src/search/*.c) functions are Copyright © 2011 Szabolcs Nagy and +licensed under following terms: "Permission to use, copy, modify, +and/or distribute this code for any purpose with or without fee is +hereby granted. There is no warranty." + +The x86_64 port was written by Nicholas J. Kain. Several files (crt) +were released into the public domain; others are licensed under the +standard MIT license terms at the top of this file. See individual +files for their copyright status. + +The mips and microblaze ports were originally written by Richard +Pennington for use in the ellcc project. The original code was adapted +by Rich Felker for build system and code conventions during upstream +integration. It is licensed under the standard MIT terms. + +The powerpc port was also originally written by Richard Pennington, +and later supplemented and integrated by John Spencer. It is licensed +under the standard MIT terms. + +All other files which have no copyright comments are original works +produced specifically for use as part of this library, written either +by Rich Felker, the main author of the library, or by one or more +contibutors listed above. Details on authorship of individual files +can be found in the git version control history of the project. The +omission of copyright and license comments in each file is in the +interest of source tree size. + +All public header files (include/* and arch/*/bits/*) should be +treated as Public Domain as they intentionally contain no content +which can be covered by copyright. Some source modules may fall in +this category as well. If you believe that a file is so trivial that +it should be in the Public Domain, please contact the authors and +request an explicit statement releasing it from copyright. + +The following files are trivial, believed not to be copyrightable in +the first place, and hereby explicitly released to the Public Domain: + +All public headers: include/*, arch/*/bits/* +Startup files: crt/* diff --git a/LICENSES/XARRAY_LICENSE b/LICENSES/XARRAY_LICENSE new file mode 100644 index 0000000000000..37ec93a14fdcd --- /dev/null +++ b/LICENSES/XARRAY_LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in index b7a7e6039ac9a..b417b8890fa24 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,27 +1,41 @@ include MANIFEST.in include LICENSE include RELEASE.md -include README.rst +include README.md include setup.py graft doc prune doc/build +graft LICENSES + graft pandas -global-exclude *.so -global-exclude *.pyd +global-exclude *.bz2 +global-exclude *.csv +global-exclude *.dta +global-exclude *.gz +global-exclude *.h5 +global-exclude *.html +global-exclude *.json +global-exclude *.msgpack +global-exclude *.pickle +global-exclude *.png global-exclude *.pyc +global-exclude *.pyd +global-exclude *.sas7bdat +global-exclude *.so +global-exclude *.xls +global-exclude *.xlsm +global-exclude *.xlsx +global-exclude *.xpt +global-exclude *.xz +global-exclude *.zip global-exclude *~ -global-exclude \#* -global-exclude .git* global-exclude .DS_Store -global-exclude *.png +global-exclude .git* +global-exclude \#* -# include examples/data/* -# recursive-include examples *.py -# recursive-include doc/source * -# recursive-include doc/sphinxext * -# recursive-include LICENSES * include versioneer.py include pandas/_version.py +include pandas/io/formats/templates/*.tpl diff --git a/Makefile b/Makefile index 194a8861715b7..956ff52338839 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,9 @@ clean_pyc: build: clean_pyc python setup.py build_ext --inplace +lint-diff: + git diff upstream/master --name-only -- "*.py" | xargs flake8 + develop: build -python setup.py develop diff --git a/README.md b/README.md index e05f1405419fc..ce22818705865 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,33 @@ - + - + - + + - + @@ -33,52 +48,40 @@ - - - - - - - - - - + - - + +
Latest Releaselatest release + + latest release + +
latest release + + latest release + +
Package Statusstatus + + status
Licenselicense + + license + +
Build Status
- - circleci build status - -
- - appveyor build status + + Azure Pipelines build status
Coveragecoverage
Conda - - conda default downloads +   + + coverage
Conda-forgeDownloads - + conda-forge downloads
PyPI - - pypi downloads - - Gitter + + +
-[![https://gitter.im/pydata/pandas](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pydata/pandas?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -## What is it + +## What is it? **pandas** is a Python package providing fast, flexible, and expressive data structures designed to make working with "relational" or "labeled" data both @@ -86,7 +89,7 @@ easy and intuitive. It aims to be the fundamental high-level building block for doing practical, **real world** data analysis in Python. Additionally, it has the broader goal of becoming **the most powerful and flexible open source data analysis / manipulation tool available in any language**. It is already well on -its way toward this goal. +its way towards this goal. ## Main Features Here are just a few of the things that pandas does well: @@ -123,31 +126,31 @@ Here are just a few of the things that pandas does well: moving window linear regressions, date shifting and lagging, etc. - [missing-data]: http://pandas.pydata.org/pandas-docs/stable/missing_data.html#working-with-missing-data - [insertion-deletion]: http://pandas.pydata.org/pandas-docs/stable/dsintro.html#column-selection-addition-deletion - [alignment]: http://pandas.pydata.org/pandas-docs/stable/dsintro.html?highlight=alignment#intro-to-data-structures - [groupby]: http://pandas.pydata.org/pandas-docs/stable/groupby.html#group-by-split-apply-combine - [conversion]: http://pandas.pydata.org/pandas-docs/stable/dsintro.html#dataframe - [slicing]: http://pandas.pydata.org/pandas-docs/stable/indexing.html#slicing-ranges - [fancy-indexing]: http://pandas.pydata.org/pandas-docs/stable/indexing.html#advanced-indexing-with-ix - [subsetting]: http://pandas.pydata.org/pandas-docs/stable/indexing.html#boolean-indexing - [merging]: http://pandas.pydata.org/pandas-docs/stable/merging.html#database-style-dataframe-joining-merging - [joining]: http://pandas.pydata.org/pandas-docs/stable/merging.html#joining-on-index - [reshape]: http://pandas.pydata.org/pandas-docs/stable/reshaping.html#reshaping-and-pivot-tables - [pivot-table]: http://pandas.pydata.org/pandas-docs/stable/reshaping.html#pivot-tables-and-cross-tabulations - [mi]: http://pandas.pydata.org/pandas-docs/stable/indexing.html#hierarchical-indexing-multiindex - [flat-files]: http://pandas.pydata.org/pandas-docs/stable/io.html#csv-text-files - [excel]: http://pandas.pydata.org/pandas-docs/stable/io.html#excel-files - [db]: http://pandas.pydata.org/pandas-docs/stable/io.html#sql-queries - [hdfstore]: http://pandas.pydata.org/pandas-docs/stable/io.html#hdf5-pytables - [timeseries]: http://pandas.pydata.org/pandas-docs/stable/timeseries.html#time-series-date-functionality + [missing-data]: https://pandas.pydata.org/pandas-docs/stable/missing_data.html#working-with-missing-data + [insertion-deletion]: https://pandas.pydata.org/pandas-docs/stable/dsintro.html#column-selection-addition-deletion + [alignment]: https://pandas.pydata.org/pandas-docs/stable/dsintro.html?highlight=alignment#intro-to-data-structures + [groupby]: https://pandas.pydata.org/pandas-docs/stable/groupby.html#group-by-split-apply-combine + [conversion]: https://pandas.pydata.org/pandas-docs/stable/dsintro.html#dataframe + [slicing]: https://pandas.pydata.org/pandas-docs/stable/indexing.html#slicing-ranges + [fancy-indexing]: https://pandas.pydata.org/pandas-docs/stable/indexing.html#advanced-indexing-with-ix + [subsetting]: https://pandas.pydata.org/pandas-docs/stable/indexing.html#boolean-indexing + [merging]: https://pandas.pydata.org/pandas-docs/stable/merging.html#database-style-dataframe-joining-merging + [joining]: https://pandas.pydata.org/pandas-docs/stable/merging.html#joining-on-index + [reshape]: https://pandas.pydata.org/pandas-docs/stable/reshaping.html#reshaping-and-pivot-tables + [pivot-table]: https://pandas.pydata.org/pandas-docs/stable/reshaping.html#pivot-tables-and-cross-tabulations + [mi]: https://pandas.pydata.org/pandas-docs/stable/indexing.html#hierarchical-indexing-multiindex + [flat-files]: https://pandas.pydata.org/pandas-docs/stable/io.html#csv-text-files + [excel]: https://pandas.pydata.org/pandas-docs/stable/io.html#excel-files + [db]: https://pandas.pydata.org/pandas-docs/stable/io.html#sql-queries + [hdfstore]: https://pandas.pydata.org/pandas-docs/stable/io.html#hdf5-pytables + [timeseries]: https://pandas.pydata.org/pandas-docs/stable/timeseries.html#time-series-date-functionality ## Where to get it The source code is currently hosted on GitHub at: -http://github.com/pandas-dev/pandas +https://github.com/pandas-dev/pandas Binary installers for the latest released version are available at the [Python -package index](http://pypi.python.org/pypi/pandas/) and on conda. +package index](https://pypi.org/project/pandas) and on conda. ```sh # conda @@ -160,12 +163,11 @@ pip install pandas ``` ## Dependencies -- [NumPy](http://www.numpy.org): 1.7.0 or higher -- [python-dateutil](http://labix.org/python-dateutil): 1.5 or higher -- [pytz](http://pytz.sourceforge.net) - - Needed for time zone support with ``pandas.date_range`` +- [NumPy](https://www.numpy.org): 1.12.0 or higher +- [python-dateutil](https://labix.org/python-dateutil): 2.5.0 or higher +- [pytz](https://pythonhosted.org/pytz): 2011k or higher -See the [full installation instructions](http://pandas.pydata.org/pandas-docs/stable/install.html#dependencies) +See the [full installation instructions](https://pandas.pydata.org/pandas-docs/stable/install.html#dependencies) for recommended and optional dependencies. ## Installation from sources @@ -197,32 +199,36 @@ mode](https://pip.pypa.io/en/latest/reference/pip_install.html#editable-installs pip install -e . ``` -On Windows, you will need to install MinGW and execute: - -```sh -python setup.py build --compiler=mingw32 -python setup.py install -``` - -See http://pandas.pydata.org/ for more information. +See the full instructions for [installing from source](https://pandas.pydata.org/pandas-docs/stable/install.html#installing-from-source). ## License -BSD +[BSD 3](LICENSE) ## Documentation -The official documentation is hosted on PyData.org: http://pandas.pydata.org/ - -The Sphinx documentation should provide a good starting point for learning how -to use the library. Expect the docs to continue to expand as time goes on. +The official documentation is hosted on PyData.org: https://pandas.pydata.org/pandas-docs/stable ## Background Work on ``pandas`` started at AQR (a quantitative hedge fund) in 2008 and has been under active development since then. +## Getting Help + +For usage questions, the best place to go to is [StackOverflow](https://stackoverflow.com/questions/tagged/pandas). +Further, general questions and discussions can also take place on the [pydata mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata). + ## Discussion and Development -Since pandas development is related to a number of other scientific -Python projects, questions are welcome on the scipy-user mailing -list. Specialized discussions or design issues should take place on -the PyData mailing list / Google group: +Most development discussion is taking place on github in this repo. Further, the [pandas-dev mailing list](https://mail.python.org/mailman/listinfo/pandas-dev) can also be used for specialized discussions or design issues, and a [Gitter channel](https://gitter.im/pydata/pandas) is available for quick development related questions. + +## Contributing to pandas [![Open Source Helpers](https://www.codetriage.com/pandas-dev/pandas/badges/users.svg)](https://www.codetriage.com/pandas-dev/pandas) + +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. + +A detailed overview on how to contribute can be found in the **[contributing guide](https://pandas-docs.github.io/pandas-docs-travis/contributing.html)**. There is also an [overview](.github/CONTRIBUTING.md) on GitHub. + +If you are simply looking to start working with the pandas codebase, navigate to the [GitHub "issues" tab](https://github.com/pandas-dev/pandas/issues) and start looking through interesting issues. There are a number of issues listed under [Docs](https://github.com/pandas-dev/pandas/issues?labels=Docs&sort=updated&state=open) and [good first issue](https://github.com/pandas-dev/pandas/issues?labels=good+first+issue&sort=updated&state=open) where you could start out. + +You can also triage issues which may include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to pandas on CodeTriage](https://www.codetriage.com/pandas-dev/pandas). + +Or maybe through using pandas you have an idea of your own or are looking for something in the documentation and thinking ‘this can be improved’...you can do something about it! -https://groups.google.com/forum/#!forum/pydata +Feel free to ask questions on the [mailing list](https://groups.google.com/forum/?fromgroups#!forum/pydata) or on [Gitter](https://gitter.im/pydata/pandas). diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index db729b3005be6..0000000000000 --- a/appveyor.yml +++ /dev/null @@ -1,89 +0,0 @@ -# With infos from -# http://tjelvarolsson.com/blog/how-to-continuously-test-your-python-code-on-windows-using-appveyor/ -# https://packaging.python.org/en/latest/appveyor/ -# https://github.com/rmcgibbo/python-appveyor-conda-example - -# Backslashes in quotes need to be escaped: \ -> "\\" - -matrix: - fast_finish: true # immediately finish build once one of the jobs fails. - -environment: - global: - # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the - # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 - CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\run_with_env.cmd" - clone_folder: C:\projects\pandas - - matrix: - - - CONDA_ROOT: "C:\\Miniconda3_64" - PYTHON_VERSION: "3.6" - PYTHON_ARCH: "64" - CONDA_PY: "36" - CONDA_NPY: "112" - - - CONDA_ROOT: "C:\\Miniconda3_64" - PYTHON_VERSION: "2.7" - PYTHON_ARCH: "64" - CONDA_PY: "27" - CONDA_NPY: "110" - -# We always use a 64-bit machine, but can build x86 distributions -# with the PYTHON_ARCH variable (which is used by CMD_IN_ENV). -platform: - - x64 - -# all our python builds have to happen in tests_script... -build: false - -install: - # cancel older builds for the same PR - - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` - https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` - Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` - throw "There are newer queued builds for this pull request, failing early." } - - # this installs the appropriate Miniconda (Py2/Py3, 32/64 bit) - # updates conda & installs: conda-build jinja2 anaconda-client - - powershell .\ci\install.ps1 - - SET PATH=%CONDA_ROOT%;%CONDA_ROOT%\Scripts;%PATH% - - echo "install" - - cd - - ls -ltr - - git tag --sort v:refname - - # this can conflict with git - - cmd: rmdir C:\cygwin /s /q - - # install our build environment - - cmd: conda config --set show_channel_urls true --set always_yes true --set changeps1 false - - cmd: conda update -q conda - - cmd: conda config --set ssl_verify false - - # add the pandas channel *before* defaults to have defaults take priority - - cmd: conda config --add channels conda-forge - - cmd: conda config --add channels pandas - - cmd: conda config --remove channels defaults - - cmd: conda config --add channels defaults - - # this is now the downloaded conda... - - cmd: conda info -a - - # create our env - - cmd: conda create -n pandas python=%PYTHON_VERSION% cython pytest - - cmd: activate pandas - - SET REQ=ci\requirements-%PYTHON_VERSION%_WIN.run - - cmd: echo "installing requirements from %REQ%" - - cmd: conda install -n pandas --file=%REQ% - - cmd: conda list -n pandas - - cmd: echo "installing requirements from %REQ% - done" - - # build em using the local source checkout in the correct windows env - - cmd: '%CMD_IN_ENV% python setup.py build_ext --inplace' - -test_script: - # tests - - cmd: activate pandas - - cmd: test.bat diff --git a/asv_bench/asv.conf.json b/asv_bench/asv.conf.json index 4fc6f9f634426..fa098e2455683 100644 --- a/asv_bench/asv.conf.json +++ b/asv_bench/asv.conf.json @@ -26,7 +26,7 @@ // The Pythons you'd like to test against. If not provided, defaults // to the current version of Python used to run `asv`. // "pythons": ["2.7", "3.4"], - "pythons": ["2.7"], + "pythons": ["3.6"], // The matrix of dependencies to test. Each key is the name of a // package (in PyPI) and the values are version numbers. An empty @@ -46,12 +46,14 @@ "numexpr": [], "pytables": [null, ""], // platform dependent, see excludes below "tables": [null, ""], - "libpython": [null, ""], "openpyxl": [], "xlsxwriter": [], "xlrd": [], "xlwt": [], "pytest": [], + // If using Windows with python 2.7 and want to build using the + // mingw toolchain (rather than MSVC), uncomment the following line. + // "libpython": [], }, // Combinations of libraries/python versions can be excluded/included @@ -80,10 +82,6 @@ {"environment_type": "conda", "pytables": null}, {"environment_type": "(?!conda).*", "tables": null}, {"environment_type": "(?!conda).*", "pytables": ""}, - // On conda&win32, install libpython - {"sys_platform": "(?!win32).*", "libpython": ""}, - {"environment_type": "conda", "sys_platform": "win32", "libpython": null}, - {"environment_type": "(?!conda).*", "libpython": ""} ], "include": [], @@ -119,8 +117,9 @@ // with results. If the commit is `null`, regression detection is // skipped for the matching benchmark. // - // "regressions_first_commits": { - // "some_benchmark": "352cdf", // Consider regressions only after this commit - // "another_benchmark": null, // Skip regression detection altogether - // } + "regressions_first_commits": { + ".*": "0409521665" + }, + "regression_thresholds": { + } } diff --git a/asv_bench/benchmarks/__init__.py b/asv_bench/benchmarks/__init__.py index e69de29bb2d1d..eada147852fe1 100644 --- a/asv_bench/benchmarks/__init__.py +++ b/asv_bench/benchmarks/__init__.py @@ -0,0 +1 @@ +"""Pandas benchmarks.""" diff --git a/asv_bench/benchmarks/algorithms.py b/asv_bench/benchmarks/algorithms.py index fe657936c403e..74849d330f2bc 100644 --- a/asv_bench/benchmarks/algorithms.py +++ b/asv_bench/benchmarks/algorithms.py @@ -1,115 +1,144 @@ +from importlib import import_module + import numpy as np + import pandas as pd from pandas.util import testing as tm +for imp in ['pandas.util', 'pandas.tools.hashing']: + try: + hashing = import_module(imp) + break + except (ImportError, TypeError, ValueError): + pass + + +class Factorize(object): + + params = [[True, False], ['int', 'uint', 'float', 'string']] + param_names = ['sort', 'dtype'] + + def setup(self, sort, dtype): + N = 10**5 + data = {'int': pd.Int64Index(np.arange(N).repeat(5)), + 'uint': pd.UInt64Index(np.arange(N).repeat(5)), + 'float': pd.Float64Index(np.random.randn(N).repeat(5)), + 'string': tm.makeStringIndex(N).repeat(5)} + self.idx = data[dtype] + + def time_factorize(self, sort, dtype): + self.idx.factorize(sort=sort) + -class Algorithms(object): - goal_time = 0.2 +class FactorizeUnique(object): - def setup(self): - N = 100000 - np.random.seed(1234) + params = [[True, False], ['int', 'uint', 'float', 'string']] + param_names = ['sort', 'dtype'] - self.int_unique = pd.Int64Index(np.arange(N * 5)) + def setup(self, sort, dtype): + N = 10**5 + data = {'int': pd.Int64Index(np.arange(N)), + 'uint': pd.UInt64Index(np.arange(N)), + 'float': pd.Float64Index(np.arange(N)), + 'string': tm.makeStringIndex(N)} + self.idx = data[dtype] + assert self.idx.is_unique + + def time_factorize(self, sort, dtype): + self.idx.factorize(sort=sort) + + +class Duplicated(object): + + params = [['first', 'last', False], ['int', 'uint', 'float', 'string']] + param_names = ['keep', 'dtype'] + + def setup(self, keep, dtype): + N = 10**5 + data = {'int': pd.Int64Index(np.arange(N).repeat(5)), + 'uint': pd.UInt64Index(np.arange(N).repeat(5)), + 'float': pd.Float64Index(np.random.randn(N).repeat(5)), + 'string': tm.makeStringIndex(N).repeat(5)} + self.idx = data[dtype] # cache is_unique - self.int_unique.is_unique + self.idx.is_unique - self.int = pd.Int64Index(np.arange(N).repeat(5)) - self.float = pd.Float64Index(np.random.randn(N).repeat(5)) + def time_duplicated(self, keep, dtype): + self.idx.duplicated(keep=keep) - # Convenience naming. - self.checked_add = pd.core.algorithms.checked_add_with_arr - self.arr = np.arange(1000000) - self.arrpos = np.arange(1000000) - self.arrneg = np.arange(-1000000, 0) - self.arrmixed = np.array([1, -1]).repeat(500000) - self.strings = tm.makeStringIndex(100000) +class DuplicatedUniqueIndex(object): - self.arr_nan = np.random.choice([True, False], size=1000000) - self.arrmixed_nan = np.random.choice([True, False], size=1000000) + params = ['int', 'uint', 'float', 'string'] + param_names = ['dtype'] - # match - self.uniques = tm.makeStringIndex(1000).values - self.all = self.uniques.repeat(10) + def setup(self, dtype): + N = 10**5 + data = {'int': pd.Int64Index(np.arange(N)), + 'uint': pd.UInt64Index(np.arange(N)), + 'float': pd.Float64Index(np.random.randn(N)), + 'string': tm.makeStringIndex(N)} + self.idx = data[dtype] + # cache is_unique + self.idx.is_unique - def time_factorize_string(self): - self.strings.factorize() + def time_duplicated_unique(self, dtype): + self.idx.duplicated() - def time_factorize_int(self): - self.int.factorize() - def time_factorize_float(self): - self.int.factorize() +class Hashing(object): - def time_duplicated_int_unique(self): - self.int_unique.duplicated() + def setup_cache(self): + N = 10**5 - def time_duplicated_int(self): - self.int.duplicated() + df = pd.DataFrame( + {'strings': pd.Series(tm.makeStringIndex(10000).take( + np.random.randint(0, 10000, size=N))), + 'floats': np.random.randn(N), + 'ints': np.arange(N), + 'dates': pd.date_range('20110101', freq='s', periods=N), + 'timedeltas': pd.timedelta_range('1 day', freq='s', periods=N)}) + df['categories'] = df['strings'].astype('category') + df.iloc[10:20] = np.nan + return df - def time_duplicated_float(self): - self.float.duplicated() + def time_frame(self, df): + hashing.hash_pandas_object(df) - def time_match_strings(self): - pd.match(self.all, self.uniques) + def time_series_int(self, df): + hashing.hash_pandas_object(df['ints']) - def time_add_overflow_pos_scalar(self): - self.checked_add(self.arr, 1) + def time_series_string(self, df): + hashing.hash_pandas_object(df['strings']) - def time_add_overflow_neg_scalar(self): - self.checked_add(self.arr, -1) + def time_series_float(self, df): + hashing.hash_pandas_object(df['floats']) - def time_add_overflow_zero_scalar(self): - self.checked_add(self.arr, 0) + def time_series_categorical(self, df): + hashing.hash_pandas_object(df['categories']) - def time_add_overflow_pos_arr(self): - self.checked_add(self.arr, self.arrpos) + def time_series_timedeltas(self, df): + hashing.hash_pandas_object(df['timedeltas']) - def time_add_overflow_neg_arr(self): - self.checked_add(self.arr, self.arrneg) + def time_series_dates(self, df): + hashing.hash_pandas_object(df['dates']) - def time_add_overflow_mixed_arr(self): - self.checked_add(self.arr, self.arrmixed) - def time_add_overflow_first_arg_nan(self): - self.checked_add(self.arr, self.arrmixed, arr_mask=self.arr_nan) +class Quantile(object): + params = [[0, 0.5, 1], + ['linear', 'nearest', 'lower', 'higher', 'midpoint'], + ['float', 'int', 'uint']] + param_names = ['quantile', 'interpolation', 'dtype'] - def time_add_overflow_second_arg_nan(self): - self.checked_add(self.arr, self.arrmixed, b_mask=self.arrmixed_nan) + def setup(self, quantile, interpolation, dtype): + N = 10**5 + data = {'int': np.arange(N), + 'uint': np.arange(N).astype(np.uint64), + 'float': np.random.randn(N)} + self.idx = pd.Series(data[dtype].repeat(5)) - def time_add_overflow_both_arg_nan(self): - self.checked_add(self.arr, self.arrmixed, arr_mask=self.arr_nan, - b_mask=self.arrmixed_nan) + def time_quantile(self, quantile, interpolation, dtype): + self.idx.quantile(quantile, interpolation=interpolation) -class Hashing(object): - goal_time = 0.2 - - def setup(self): - N = 100000 - - self.df = pd.DataFrame( - {'A': pd.Series(tm.makeStringIndex(100).take( - np.random.randint(0, 100, size=N))), - 'B': pd.Series(tm.makeStringIndex(10000).take( - np.random.randint(0, 10000, size=N))), - 'D': np.random.randn(N), - 'E': np.arange(N), - 'F': pd.date_range('20110101', freq='s', periods=N), - 'G': pd.timedelta_range('1 day', freq='s', periods=N), - }) - self.df['C'] = self.df['B'].astype('category') - self.df.iloc[10:20] = np.nan - - def time_frame(self): - self.df.hash() - - def time_series_int(self): - self.df.E.hash() - - def time_series_string(self): - self.df.B.hash() - - def time_series_categorical(self): - self.df.C.hash() +from .pandas_vb_common import setup # noqa: F401 isort:skip diff --git a/asv_bench/benchmarks/attrs_caching.py b/asv_bench/benchmarks/attrs_caching.py index 9210f1f2878d4..d061755208c9e 100644 --- a/asv_bench/benchmarks/attrs_caching.py +++ b/asv_bench/benchmarks/attrs_caching.py @@ -1,9 +1,12 @@ -from .pandas_vb_common import * -from pandas.util.decorators import cache_readonly +import numpy as np +from pandas import DataFrame +try: + from pandas.util import cache_readonly +except ImportError: + from pandas.util.decorators import cache_readonly class DataFrameAttributes(object): - goal_time = 0.2 def setup(self): self.df = DataFrame(np.random.randn(10, 6)) @@ -17,7 +20,6 @@ def time_set_index(self): class CacheReadonly(object): - goal_time = 0.2 def setup(self): @@ -30,3 +32,6 @@ def prop(self): def time_cache_readonly(self): self.obj.prop + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/binary_ops.py b/asv_bench/benchmarks/binary_ops.py index 72700c3de282e..22b8ed80f3d07 100644 --- a/asv_bench/benchmarks/binary_ops.py +++ b/asv_bench/benchmarks/binary_ops.py @@ -1,9 +1,13 @@ -from .pandas_vb_common import * -import pandas.computation.expressions as expr +import numpy as np +from pandas import DataFrame, Series, date_range +from pandas.core.algorithms import checked_add_with_arr +try: + import pandas.core.computation.expressions as expr +except ImportError: + import pandas.computation.expressions as expr class Ops(object): - goal_time = 0.2 params = [[True, False], ['default', 1]] param_names = ['use_numexpr', 'threads'] @@ -17,18 +21,17 @@ def setup(self, use_numexpr, threads): if not use_numexpr: expr.set_use_numexpr(False) - def time_frame_add(self, use_numexpr, threads): - (self.df + self.df2) + self.df + self.df2 def time_frame_mult(self, use_numexpr, threads): - (self.df * self.df2) + self.df * self.df2 def time_frame_multi_and(self, use_numexpr, threads): - self.df[((self.df > 0) & (self.df2 > 0))] + self.df[(self.df > 0) & (self.df2 > 0)] def time_frame_comparison(self, use_numexpr, threads): - (self.df > self.df2) + self.df > self.df2 def teardown(self, use_numexpr, threads): expr.set_use_numexpr(True) @@ -36,75 +39,117 @@ def teardown(self, use_numexpr, threads): class Ops2(object): - goal_time = 0.2 def setup(self): - self.df = DataFrame(np.random.randn(1000, 1000)) - self.df2 = DataFrame(np.random.randn(1000, 1000)) + N = 10**3 + self.df = DataFrame(np.random.randn(N, N)) + self.df2 = DataFrame(np.random.randn(N, N)) + + self.df_int = DataFrame(np.random.randint(np.iinfo(np.int16).min, + np.iinfo(np.int16).max, + size=(N, N))) + self.df2_int = DataFrame(np.random.randint(np.iinfo(np.int16).min, + np.iinfo(np.int16).max, + size=(N, N))) - self.df_int = DataFrame( - np.random.random_integers(np.iinfo(np.int16).min, - np.iinfo(np.int16).max, - size=(1000, 1000))) - self.df2_int = DataFrame( - np.random.random_integers(np.iinfo(np.int16).min, - np.iinfo(np.int16).max, - size=(1000, 1000))) + self.s = Series(np.random.randn(N)) - ## Division + # Division def time_frame_float_div(self): - (self.df // self.df2) + self.df // self.df2 def time_frame_float_div_by_zero(self): - (self.df / 0) + self.df / 0 def time_frame_float_floor_by_zero(self): - (self.df // 0) + self.df // 0 def time_frame_int_div_by_zero(self): - (self.df_int / 0) + self.df_int / 0 - ## Modulo + # Modulo def time_frame_int_mod(self): - (self.df / self.df2) + self.df_int % self.df2_int def time_frame_float_mod(self): - (self.df / self.df2) + self.df % self.df2 + + # Dot product + + def time_frame_dot(self): + self.df.dot(self.df2) + + def time_series_dot(self): + self.s.dot(self.s) + + def time_frame_series_dot(self): + self.df.dot(self.s) class Timeseries(object): - goal_time = 0.2 - def setup(self): - self.N = 1000000 - self.halfway = ((self.N // 2) - 1) - self.s = Series(date_range('20010101', periods=self.N, freq='T')) - self.ts = self.s[self.halfway] + params = [None, 'US/Eastern'] + param_names = ['tz'] - self.s2 = Series(date_range('20010101', periods=self.N, freq='s')) + def setup(self, tz): + N = 10**6 + halfway = (N // 2) - 1 + self.s = Series(date_range('20010101', periods=N, freq='T', tz=tz)) + self.ts = self.s[halfway] - def time_series_timestamp_compare(self): - (self.s <= self.ts) + self.s2 = Series(date_range('20010101', periods=N, freq='s', tz=tz)) - def time_timestamp_series_compare(self): - (self.ts >= self.s) + def time_series_timestamp_compare(self, tz): + self.s <= self.ts - def time_timestamp_ops_diff1(self): + def time_timestamp_series_compare(self, tz): + self.ts >= self.s + + def time_timestamp_ops_diff(self, tz): self.s2.diff() - def time_timestamp_ops_diff2(self): - (self.s - self.s.shift()) + def time_timestamp_ops_diff_with_shift(self, tz): + self.s - self.s.shift() + +class AddOverflowScalar(object): + params = [1, -1, 0] + param_names = ['scalar'] -class TimeseriesTZ(Timeseries): + def setup(self, scalar): + N = 10**6 + self.arr = np.arange(N) + + def time_add_overflow_scalar(self, scalar): + checked_add_with_arr(self.arr, scalar) + + +class AddOverflowArray(object): def setup(self): - self.N = 1000000 - self.halfway = ((self.N // 2) - 1) - self.s = Series(date_range('20010101', periods=self.N, freq='T', tz='US/Eastern')) - self.ts = self.s[self.halfway] + N = 10**6 + self.arr = np.arange(N) + self.arr_rev = np.arange(-N, 0) + self.arr_mixed = np.array([1, -1]).repeat(N / 2) + self.arr_nan_1 = np.random.choice([True, False], size=N) + self.arr_nan_2 = np.random.choice([True, False], size=N) + + def time_add_overflow_arr_rev(self): + checked_add_with_arr(self.arr, self.arr_rev) + + def time_add_overflow_arr_mask_nan(self): + checked_add_with_arr(self.arr, self.arr_mixed, arr_mask=self.arr_nan_1) + + def time_add_overflow_b_mask_nan(self): + checked_add_with_arr(self.arr, self.arr_mixed, + b_mask=self.arr_nan_1) + + def time_add_overflow_both_arg_nan(self): + checked_add_with_arr(self.arr, self.arr_mixed, arr_mask=self.arr_nan_1, + b_mask=self.arr_nan_2) + - self.s2 = Series(date_range('20010101', periods=self.N, freq='s', tz='US/Eastern')) +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/categoricals.py b/asv_bench/benchmarks/categoricals.py index 153107911ca2c..4b5b2848f7e0f 100644 --- a/asv_bench/benchmarks/categoricals.py +++ b/asv_bench/benchmarks/categoricals.py @@ -1,99 +1,296 @@ -from .pandas_vb_common import * +import warnings + +import numpy as np +import pandas as pd +import pandas.util.testing as tm try: - from pandas.types.concat import union_categoricals + from pandas.api.types import union_categoricals except ImportError: - pass + try: + from pandas.types.concat import union_categoricals + except ImportError: + pass -class Categoricals(object): - goal_time = 0.2 +class Concat(object): def setup(self): - N = 100000 - self.s = pd.Series((list('aabbcd') * N)).astype('category') + N = 10**5 + self.s = pd.Series(list('aabbcd') * N).astype('category') + + self.a = pd.Categorical(list('aabbcd') * N) + self.b = pd.Categorical(list('bbcdjk') * N) + + def time_concat(self): + pd.concat([self.s, self.s]) + + def time_union(self): + union_categoricals([self.a, self.b]) + - self.a = pd.Categorical((list('aabbcd') * N)) - self.b = pd.Categorical((list('bbcdjk') * N)) +class Constructor(object): + def setup(self): + N = 10**5 self.categories = list('abcde') - self.cat_idx = Index(self.categories) + self.cat_idx = pd.Index(self.categories) self.values = np.tile(self.categories, N) self.codes = np.tile(range(len(self.categories)), N) - self.datetimes = pd.Series(pd.date_range( - '1995-01-01 00:00:00', periods=10000, freq='s')) + self.datetimes = pd.Series(pd.date_range('1995-01-01 00:00:00', + periods=N / 10, + freq='s')) + self.datetimes_with_nat = self.datetimes.copy() + self.datetimes_with_nat.iloc[-1] = pd.NaT - def time_concat(self): - concat([self.s, self.s]) + self.values_some_nan = list(np.tile(self.categories + [np.nan], N)) + self.values_all_nan = [np.nan] * len(self.values) + self.values_all_int8 = np.ones(N, 'int8') + self.categorical = pd.Categorical(self.values, self.categories) + self.series = pd.Series(self.categorical) - def time_union(self): - union_categoricals([self.a, self.b]) + def time_regular(self): + pd.Categorical(self.values, self.categories) - def time_constructor_regular(self): - Categorical(self.values, self.categories) + def time_fastpath(self): + pd.Categorical(self.codes, self.cat_idx, fastpath=True) - def time_constructor_fastpath(self): - Categorical(self.codes, self.cat_idx, fastpath=True) + def time_datetimes(self): + pd.Categorical(self.datetimes) - def time_constructor_datetimes(self): - Categorical(self.datetimes) + def time_datetimes_with_nat(self): + pd.Categorical(self.datetimes_with_nat) - def time_constructor_datetimes_with_nat(self): - t = self.datetimes - t.iloc[-1] = pd.NaT - Categorical(t) + def time_with_nan(self): + pd.Categorical(self.values_some_nan) + def time_all_nan(self): + pd.Categorical(self.values_all_nan) -class Categoricals2(object): - goal_time = 0.2 + def time_from_codes_all_int8(self): + pd.Categorical.from_codes(self.values_all_int8, self.categories) + + def time_existing_categorical(self): + pd.Categorical(self.categorical) + + def time_existing_series(self): + pd.Categorical(self.series) - def setup(self): - n = 500000 - np.random.seed(2718281) - arr = ['s%04d' % i for i in np.random.randint(0, n // 10, size=n)] - self.ts = Series(arr).astype('category') - self.sel = self.ts.loc[[0]] +class ValueCounts(object): - def time_value_counts(self): - self.ts.value_counts(dropna=False) + params = [True, False] + param_names = ['dropna'] - def time_value_counts_dropna(self): - self.ts.value_counts(dropna=True) + def setup(self, dropna): + n = 5 * 10**5 + arr = ['s{:04d}'.format(i) for i in np.random.randint(0, n // 10, + size=n)] + self.ts = pd.Series(arr).astype('category') + + def time_value_counts(self, dropna): + self.ts.value_counts(dropna=dropna) + + +class Repr(object): + + def setup(self): + self.sel = pd.Series(['s1234']).astype('category') def time_rendering(self): str(self.sel) -class Categoricals3(object): - goal_time = 0.2 +class SetCategories(object): + + def setup(self): + n = 5 * 10**5 + arr = ['s{:04d}'.format(i) for i in np.random.randint(0, n // 10, + size=n)] + self.ts = pd.Series(arr).astype('category') + + def time_set_categories(self): + self.ts.cat.set_categories(self.ts.cat.categories[::2]) + + +class RemoveCategories(object): + + def setup(self): + n = 5 * 10**5 + arr = ['s{:04d}'.format(i) for i in np.random.randint(0, n // 10, + size=n)] + self.ts = pd.Series(arr).astype('category') + + def time_remove_categories(self): + self.ts.cat.remove_categories(self.ts.cat.categories[::2]) + + +class Rank(object): def setup(self): - N = 100000 + N = 10**5 ncats = 100 - self.s1 = Series(np.array(tm.makeCategoricalIndex(N, ncats))) - self.s1_cat = self.s1.astype('category') - self.s1_cat_ordered = self.s1.astype('category', ordered=True) + self.s_str = pd.Series(tm.makeCategoricalIndex(N, ncats)).astype(str) + self.s_str_cat = self.s_str.astype('category') + with warnings.catch_warnings(record=True): + self.s_str_cat_ordered = self.s_str.astype('category', + ordered=True) - self.s2 = Series(np.random.randint(0, ncats, size=N)) - self.s2_cat = self.s2.astype('category') - self.s2_cat_ordered = self.s2.astype('category', ordered=True) + self.s_int = pd.Series(np.random.randint(0, ncats, size=N)) + self.s_int_cat = self.s_int.astype('category') + with warnings.catch_warnings(record=True): + self.s_int_cat_ordered = self.s_int.astype('category', + ordered=True) def time_rank_string(self): - self.s1.rank() + self.s_str.rank() def time_rank_string_cat(self): - self.s1_cat.rank() + self.s_str_cat.rank() def time_rank_string_cat_ordered(self): - self.s1_cat_ordered.rank() + self.s_str_cat_ordered.rank() def time_rank_int(self): - self.s2.rank() + self.s_int.rank() def time_rank_int_cat(self): - self.s2_cat.rank() + self.s_int_cat.rank() def time_rank_int_cat_ordered(self): - self.s2_cat_ordered.rank() + self.s_int_cat_ordered.rank() + + +class Isin(object): + + params = ['object', 'int64'] + param_names = ['dtype'] + + def setup(self, dtype): + np.random.seed(1234) + n = 5 * 10**5 + sample_size = 100 + arr = [i for i in np.random.randint(0, n // 10, size=n)] + if dtype == 'object': + arr = ['s{:04d}'.format(i) for i in arr] + self.sample = np.random.choice(arr, sample_size) + self.series = pd.Series(arr).astype('category') + + def time_isin_categorical(self, dtype): + self.series.isin(self.sample) + + +class IsMonotonic(object): + + def setup(self): + N = 1000 + self.c = pd.CategoricalIndex(list('a' * N + 'b' * N + 'c' * N)) + self.s = pd.Series(self.c) + + def time_categorical_index_is_monotonic_increasing(self): + self.c.is_monotonic_increasing + + def time_categorical_index_is_monotonic_decreasing(self): + self.c.is_monotonic_decreasing + + def time_categorical_series_is_monotonic_increasing(self): + self.s.is_monotonic_increasing + + def time_categorical_series_is_monotonic_decreasing(self): + self.s.is_monotonic_decreasing + + +class Contains(object): + + def setup(self): + N = 10**5 + self.ci = tm.makeCategoricalIndex(N) + self.c = self.ci.values + self.key = self.ci.categories[0] + + def time_categorical_index_contains(self): + self.key in self.ci + + def time_categorical_contains(self): + self.key in self.c + + +class CategoricalSlicing(object): + + params = ['monotonic_incr', 'monotonic_decr', 'non_monotonic'] + param_names = ['index'] + + def setup(self, index): + N = 10**6 + categories = ['a', 'b', 'c'] + values = [0] * N + [1] * N + [2] * N + if index == 'monotonic_incr': + self.data = pd.Categorical.from_codes(values, + categories=categories) + elif index == 'monotonic_decr': + self.data = pd.Categorical.from_codes(list(reversed(values)), + categories=categories) + elif index == 'non_monotonic': + self.data = pd.Categorical.from_codes([0, 1, 2] * N, + categories=categories) + else: + raise ValueError('Invalid index param: {}'.format(index)) + + self.scalar = 10000 + self.list = list(range(10000)) + self.cat_scalar = 'b' + + def time_getitem_scalar(self, index): + self.data[self.scalar] + + def time_getitem_slice(self, index): + self.data[:self.scalar] + + def time_getitem_list_like(self, index): + self.data[[self.scalar]] + + def time_getitem_list(self, index): + self.data[self.list] + + def time_getitem_bool_array(self, index): + self.data[self.data == self.cat_scalar] + + +class Indexing(object): + + def setup(self): + N = 10**5 + self.index = pd.CategoricalIndex(range(N), range(N)) + self.series = pd.Series(range(N), index=self.index).sort_index() + self.category = self.index[500] + + def time_get_loc(self): + self.index.get_loc(self.category) + + def time_shape(self): + self.index.shape + + def time_shallow_copy(self): + self.index._shallow_copy() + + def time_align(self): + pd.DataFrame({'a': self.series, 'b': self.series[:500]}) + + def time_intersection(self): + self.index[:750].intersection(self.index[250:]) + + def time_unique(self): + self.index.unique() + + def time_reindex(self): + self.index.reindex(self.index[:500]) + + def time_reindex_missing(self): + self.index.reindex(['a', 'b', 'c', 'd']) + + def time_sort_values(self): + self.index.sort_values(ascending=False) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/ctors.py b/asv_bench/benchmarks/ctors.py index b5694a3a21502..5715c4fb2d0d4 100644 --- a/asv_bench/benchmarks/ctors.py +++ b/asv_bench/benchmarks/ctors.py @@ -1,30 +1,103 @@ -from .pandas_vb_common import * +import numpy as np +import pandas.util.testing as tm +from pandas import Series, Index, DatetimeIndex, Timestamp, MultiIndex -class Constructors(object): - goal_time = 0.2 +def no_change(arr): + return arr + + +def list_of_str(arr): + return list(arr.astype(str)) + + +def gen_of_str(arr): + return (x for x in arr.astype(str)) + + +def arr_dict(arr): + return dict(zip(range(len(arr)), arr)) + + +def list_of_tuples(arr): + return [(i, -i) for i in arr] + + +def gen_of_tuples(arr): + return ((i, -i) for i in arr) + + +def list_of_lists(arr): + return [[i, -i] for i in arr] + + +def list_of_tuples_with_none(arr): + return [(i, -i) for i in arr][:-1] + [None] - def setup(self): - self.arr = np.random.randn(100, 100) - self.arr_str = np.array(['foo', 'bar', 'baz'], dtype=object) - self.data = np.random.randn(100) - self.index = Index(np.arange(100)) +def list_of_lists_with_none(arr): + return [[i, -i] for i in arr][:-1] + [None] - self.s = Series(([Timestamp('20110101'), Timestamp('20120101'), - Timestamp('20130101')] * 1000)) - def time_frame_from_ndarray(self): - DataFrame(self.arr) +class SeriesConstructors(object): - def time_series_from_ndarray(self): - pd.Series(self.data, index=self.index) + param_names = ["data_fmt", "with_index", "dtype"] + params = [[no_change, + list, + list_of_str, + gen_of_str, + arr_dict, + list_of_tuples, + gen_of_tuples, + list_of_lists, + list_of_tuples_with_none, + list_of_lists_with_none], + [False, True], + ['float', 'int']] + + def setup(self, data_fmt, with_index, dtype): + N = 10**4 + if dtype == 'float': + arr = np.random.randn(N) + else: + arr = np.arange(N) + self.data = data_fmt(arr) + self.index = np.arange(N) if with_index else None + + def time_series_constructor(self, data_fmt, with_index, dtype): + Series(self.data, index=self.index) + + +class SeriesDtypesConstructors(object): + + def setup(self): + N = 10**4 + self.arr = np.random.randn(N) + self.arr_str = np.array(['foo', 'bar', 'baz'], dtype=object) + self.s = Series([Timestamp('20110101'), Timestamp('20120101'), + Timestamp('20130101')] * N * 10) def time_index_from_array_string(self): Index(self.arr_str) + def time_index_from_array_floats(self): + Index(self.arr) + def time_dtindex_from_series(self): DatetimeIndex(self.s) - def time_dtindex_from_series2(self): + def time_dtindex_from_index_with_series(self): Index(self.s) + + +class MultiIndexConstructor(object): + + def setup(self): + N = 10**4 + self.iterables = [tm.makeStringIndex(N), range(20)] + + def time_multiindex_from_iterables(self): + MultiIndex.from_product(self.iterables) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/dtypes.py b/asv_bench/benchmarks/dtypes.py new file mode 100644 index 0000000000000..e59154cd99965 --- /dev/null +++ b/asv_bench/benchmarks/dtypes.py @@ -0,0 +1,39 @@ +from pandas.api.types import pandas_dtype + +import numpy as np +from .pandas_vb_common import ( + numeric_dtypes, datetime_dtypes, string_dtypes, extension_dtypes) + + +_numpy_dtypes = [np.dtype(dtype) + for dtype in (numeric_dtypes + + datetime_dtypes + + string_dtypes)] +_dtypes = _numpy_dtypes + extension_dtypes + + +class Dtypes(object): + params = (_dtypes + + list(map(lambda dt: dt.name, _dtypes))) + param_names = ['dtype'] + + def time_pandas_dtype(self, dtype): + pandas_dtype(dtype) + + +class DtypesInvalid(object): + param_names = ['dtype'] + params = ['scalar-string', 'scalar-int', 'list-string', 'array-string'] + data_dict = {'scalar-string': 'foo', + 'scalar-int': 1, + 'list-string': ['foo'] * 1000, + 'array-string': np.array(['foo'] * 1000)} + + def time_pandas_dtype_invalid(self, dtype): + try: + pandas_dtype(self.data_dict[dtype]) + except TypeError: + pass + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/eval.py b/asv_bench/benchmarks/eval.py index a0819e33dc254..68df38cd50742 100644 --- a/asv_bench/benchmarks/eval.py +++ b/asv_bench/benchmarks/eval.py @@ -1,67 +1,64 @@ -from .pandas_vb_common import * +import numpy as np import pandas as pd -import pandas.computation.expressions as expr +try: + import pandas.core.computation.expressions as expr +except ImportError: + import pandas.computation.expressions as expr class Eval(object): - goal_time = 0.2 params = [['numexpr', 'python'], [1, 'all']] param_names = ['engine', 'threads'] def setup(self, engine, threads): - self.df = DataFrame(np.random.randn(20000, 100)) - self.df2 = DataFrame(np.random.randn(20000, 100)) - self.df3 = DataFrame(np.random.randn(20000, 100)) - self.df4 = DataFrame(np.random.randn(20000, 100)) + self.df = pd.DataFrame(np.random.randn(20000, 100)) + self.df2 = pd.DataFrame(np.random.randn(20000, 100)) + self.df3 = pd.DataFrame(np.random.randn(20000, 100)) + self.df4 = pd.DataFrame(np.random.randn(20000, 100)) if threads == 1: expr.set_numexpr_threads(1) def time_add(self, engine, threads): - df, df2, df3, df4 = self.df, self.df2, self.df3, self.df4 - pd.eval('df + df2 + df3 + df4', engine=engine) + pd.eval('self.df + self.df2 + self.df3 + self.df4', engine=engine) def time_and(self, engine, threads): - df, df2, df3, df4 = self.df, self.df2, self.df3, self.df4 - pd.eval('(df > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)', engine=engine) + pd.eval('(self.df > 0) & (self.df2 > 0) & ' + '(self.df3 > 0) & (self.df4 > 0)', engine=engine) def time_chained_cmp(self, engine, threads): - df, df2, df3, df4 = self.df, self.df2, self.df3, self.df4 - pd.eval('df < df2 < df3 < df4', engine=engine) + pd.eval('self.df < self.df2 < self.df3 < self.df4', engine=engine) def time_mult(self, engine, threads): - df, df2, df3, df4 = self.df, self.df2, self.df3, self.df4 - pd.eval('df * df2 * df3 * df4', engine=engine) + pd.eval('self.df * self.df2 * self.df3 * self.df4', engine=engine) def teardown(self, engine, threads): expr.set_numexpr_threads() class Query(object): - goal_time = 0.2 def setup(self): - self.N = 1000000 - self.halfway = ((self.N // 2) - 1) - self.index = date_range('20010101', periods=self.N, freq='T') - self.s = Series(self.index) - self.ts = self.s.iloc[self.halfway] - self.df = DataFrame({'a': np.random.randn(self.N), }, index=self.index) - self.df2 = DataFrame({'dates': self.s.values,}) - - self.df3 = DataFrame({'a': np.random.randn(self.N),}) - self.min_val = self.df3['a'].min() - self.max_val = self.df3['a'].max() + N = 10**6 + halfway = (N // 2) - 1 + index = pd.date_range('20010101', periods=N, freq='T') + s = pd.Series(index) + self.ts = s.iloc[halfway] + self.df = pd.DataFrame({'a': np.random.randn(N), 'dates': index}, + index=index) + data = np.random.randn(N) + self.min_val = data.min() + self.max_val = data.max() def time_query_datetime_index(self): - ts = self.ts - self.df.query('index < @ts') + self.df.query('index < @self.ts') - def time_query_datetime_series(self): - ts = self.ts - self.df2.query('dates < @ts') + def time_query_datetime_column(self): + self.df.query('dates < @self.ts') def time_query_with_boolean_selection(self): - min_val, max_val = self.min_val, self.max_val - self.df.query('(a >= @min_val) & (a <= @max_val)') + self.df.query('(a >= @self.min_val) & (a <= @self.max_val)') + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/frame_ctor.py b/asv_bench/benchmarks/frame_ctor.py index 05c1a27fdf8ca..dfb6ab5b189b2 100644 --- a/asv_bench/benchmarks/frame_ctor.py +++ b/asv_bench/benchmarks/frame_ctor.py @@ -1,138 +1,107 @@ -from .pandas_vb_common import * +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, Series, MultiIndex, Timestamp, date_range try: - from pandas.tseries.offsets import * -except: - from pandas.core.datetools import * + from pandas.tseries.offsets import Nano, Hour +except ImportError: + # For compatibility with older versions + from pandas.core.datetools import * # noqa -#---------------------------------------------------------------------- -# Creation from nested dict - class FromDicts(object): - goal_time = 0.2 def setup(self): - (N, K) = (5000, 50) + N, K = 5000, 50 self.index = tm.makeStringIndex(N) self.columns = tm.makeStringIndex(K) - self.frame = DataFrame(np.random.randn(N, K), index=self.index, columns=self.columns) - try: - self.data = self.frame.to_dict() - except: - self.data = self.frame.toDict() - self.some_dict = self.data.values()[0] - self.dict_list = [dict(zip(self.columns, row)) for row in self.frame.values] - - self.data2 = dict( - ((i, dict(((j, float(j)) for j in range(100)))) for i in - xrange(2000))) - - def time_frame_ctor_list_of_dict(self): + frame = DataFrame(np.random.randn(N, K), index=self.index, + columns=self.columns) + self.data = frame.to_dict() + self.dict_list = frame.to_dict(orient='records') + self.data2 = {i: {j: float(j) for j in range(100)} + for i in range(2000)} + + def time_list_of_dict(self): DataFrame(self.dict_list) - def time_frame_ctor_nested_dict(self): + def time_nested_dict(self): DataFrame(self.data) - def time_series_ctor_from_dict(self): - Series(self.some_dict) + def time_nested_dict_index(self): + DataFrame(self.data, index=self.index) - def time_frame_ctor_nested_dict_int64(self): - # nested dict, integer indexes, regression described in #621 - DataFrame(self.data) + def time_nested_dict_columns(self): + DataFrame(self.data, columns=self.columns) + def time_nested_dict_index_columns(self): + DataFrame(self.data, index=self.index, columns=self.columns) -# from a mi-series + def time_nested_dict_int64(self): + # nested dict, integer indexes, regression described in #621 + DataFrame(self.data2) -class frame_from_series(object): - goal_time = 0.2 + +class FromSeries(object): def setup(self): - self.mi = MultiIndex.from_tuples([(x, y) for x in range(100) for y in range(100)]) - self.s = Series(randn(10000), index=self.mi) + mi = MultiIndex.from_product([range(100), range(100)]) + self.s = Series(np.random.randn(10000), index=mi) - def time_frame_from_mi_series(self): + def time_mi_series(self): DataFrame(self.s) -#---------------------------------------------------------------------- -# get_numeric_data - -class frame_get_numeric_data(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(randn(10000, 25)) - self.df['foo'] = 'bar' - self.df['bar'] = 'baz' - self.df = self.df.consolidate() - - def time_frame_get_numeric_data(self): - self.df._get_numeric_data() +class FromDictwithTimestamp(object): + params = [Nano(1), Hour(1)] + param_names = ['offset'] -# ---------------------------------------------------------------------- -# From dict with DatetimeIndex with all offsets - -# dynamically generate benchmarks for every offset -# -# get_period_count & get_index_for_offset are there because blindly taking each -# offset times 1000 can easily go out of Timestamp bounds and raise errors. + def setup(self, offset): + N = 10**3 + np.random.seed(1234) + idx = date_range(Timestamp('1/1/1900'), freq=offset, periods=N) + df = DataFrame(np.random.randn(N, 10), index=idx) + self.d = df.to_dict() + def time_dict_with_timestamp_offsets(self, offset): + DataFrame(self.d) -def get_period_count(start_date, off): - ten_offsets_in_days = ((start_date + (off * 10)) - start_date).days - if (ten_offsets_in_days == 0): - return 1000 - else: - return min((9 * ((Timestamp.max - start_date).days // ten_offsets_in_days)), 1000) +class FromRecords(object): -def get_index_for_offset(off): - start_date = Timestamp('1/1/1900') - return date_range(start_date, periods=min(1000, get_period_count( - start_date, off)), freq=off) + params = [None, 1000] + param_names = ['nrows'] + def setup(self, nrows): + N = 100000 + self.gen = ((x, (x * 20), (x * 100)) for x in range(N)) -all_offsets = offsets.__all__ -# extra cases -for off in ['FY5253', 'FY5253Quarter']: - all_offsets.pop(all_offsets.index(off)) - all_offsets.extend([off + '_1', off + '_2']) + def time_frame_from_records_generator(self, nrows): + # issue-6700 + self.df = DataFrame.from_records(self.gen, nrows=nrows) -class FrameConstructorDTIndexFromOffsets(object): +class FromNDArray(object): - params = [all_offsets, [1, 2]] - param_names = ['offset', 'n_steps'] + def setup(self): + N = 100000 + self.data = np.random.randn(N) - offset_kwargs = {'WeekOfMonth': {'weekday': 1, 'week': 1}, - 'LastWeekOfMonth': {'weekday': 1, 'week': 1}, - 'FY5253': {'startingMonth': 1, 'weekday': 1}, - 'FY5253Quarter': {'qtr_with_extra_week': 1, 'startingMonth': 1, 'weekday': 1}} + def time_frame_from_ndarray(self): + self.df = DataFrame(self.data) - offset_extra_cases = {'FY5253': {'variation': ['nearest', 'last']}, - 'FY5253Quarter': {'variation': ['nearest', 'last']}} - def setup(self, offset, n_steps): +class FromLists(object): - extra = False - if offset.endswith("_", None, -1): - extra = int(offset[-1]) - offset = offset[:-2] + goal_time = 0.2 - kwargs = {} - if offset in self.offset_kwargs: - kwargs = self.offset_kwargs[offset] + def setup(self): + N = 1000 + M = 100 + self.data = [[j for j in range(M)] for i in range(N)] - if extra: - extras = self.offset_extra_cases[offset] - for extra_arg in extras: - kwargs[extra_arg] = extras[extra_arg][extra -1] + def time_frame_from_lists(self): + self.df = DataFrame(self.data) - offset = getattr(offsets, offset) - self.idx = get_index_for_offset(offset(n_steps, **kwargs)) - self.df = DataFrame(np.random.randn(len(self.idx), 10), index=self.idx) - self.d = dict([(col, self.df[col]) for col in self.df.columns]) - def time_frame_ctor(self, offset, n_steps): - DataFrame(self.d) +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/frame_methods.py b/asv_bench/benchmarks/frame_methods.py index 9f491302a4d6f..ba2e63c20d3f8 100644 --- a/asv_bench/benchmarks/frame_methods.py +++ b/asv_bench/benchmarks/frame_methods.py @@ -1,20 +1,36 @@ -from .pandas_vb_common import * import string +import numpy as np -#---------------------------------------------------------------------- -# lookup +from pandas import ( + DataFrame, MultiIndex, NaT, Series, date_range, isnull, period_range) +import pandas.util.testing as tm -class frame_fancy_lookup(object): - goal_time = 0.2 + +class GetNumericData(object): + + def setup(self): + self.df = DataFrame(np.random.randn(10000, 25)) + self.df['foo'] = 'bar' + self.df['bar'] = 'baz' + self.df = self.df._consolidate() + + def time_frame_get_numeric_data(self): + self.df._get_numeric_data() + + +class Lookup(object): def setup(self): - self.df = DataFrame(np.random.randn(10000, 8), columns=list('abcdefgh')) + self.df = DataFrame(np.random.randn(10000, 8), + columns=list('abcdefgh')) self.df['foo'] = 'bar' self.row_labels = list(self.df.index[::10])[:900] - self.col_labels = (list(self.df.columns) * 100) - self.row_labels_all = np.array((list(self.df.index) * len(self.df.columns)), dtype='object') - self.col_labels_all = np.array((list(self.df.columns) * len(self.df.index)), dtype='object') + self.col_labels = list(self.df.columns) * 100 + self.row_labels_all = np.array( + list(self.df.index) * len(self.df.columns), dtype='object') + self.col_labels_all = np.array( + list(self.df.columns) * len(self.df.index), dtype='object') def time_frame_fancy_lookup(self): self.df.lookup(self.row_labels, self.col_labels) @@ -23,25 +39,18 @@ def time_frame_fancy_lookup_all(self): self.df.lookup(self.row_labels_all, self.col_labels_all) -#---------------------------------------------------------------------- -# reindex - class Reindex(object): - goal_time = 0.2 def setup(self): - self.df = DataFrame(randn(10000, 1000)) - self.idx = np.arange(4000, 7000) - + N = 10**3 + self.df = DataFrame(np.random.randn(N * 10, N)) + self.idx = np.arange(4 * N, 7 * N) self.df2 = DataFrame( - dict([(c, {0: randint(0, 2, 1000).astype(np.bool_), - 1: randint(0, 1000, 1000).astype( - np.int16), - 2: randint(0, 1000, 1000).astype( - np.int32), - 3: randint(0, 1000, 1000).astype( - np.int64),}[randint(0, 4)]) for c in - range(1000)])) + {c: {0: np.random.randint(0, 2, N).astype(np.bool_), + 1: np.random.randint(0, N, N).astype(np.int16), + 2: np.random.randint(0, N, N).astype(np.int32), + 3: np.random.randint(0, N, N).astype(np.int64)} + [np.random.randint(0, 4)] for c in range(N)}) def time_reindex_axis0(self): self.df.reindex(self.idx) @@ -52,82 +61,167 @@ def time_reindex_axis1(self): def time_reindex_both_axes(self): self.df.reindex(index=self.idx, columns=self.idx) - def time_reindex_both_axes_ix(self): - self.df.ix[(self.idx, self.idx)] - def time_reindex_upcast(self): - self.df2.reindex(permutation(range(1200))) + self.df2.reindex(np.random.permutation(range(1200))) -#---------------------------------------------------------------------- -# iteritems (monitor no-copying behaviour) +class Rename(object): + + def setup(self): + N = 10**3 + self.df = DataFrame(np.random.randn(N * 10, N)) + self.idx = np.arange(4 * N, 7 * N) + self.dict_idx = {k: k for k in self.idx} + self.df2 = DataFrame( + {c: {0: np.random.randint(0, 2, N).astype(np.bool_), + 1: np.random.randint(0, N, N).astype(np.int16), + 2: np.random.randint(0, N, N).astype(np.int32), + 3: np.random.randint(0, N, N).astype(np.int64)} + [np.random.randint(0, 4)] for c in range(N)}) + + def time_rename_single(self): + self.df.rename({0: 0}) + + def time_rename_axis0(self): + self.df.rename(self.dict_idx) + + def time_rename_axis1(self): + self.df.rename(columns=self.dict_idx) + + def time_rename_both_axes(self): + self.df.rename(index=self.dict_idx, columns=self.dict_idx) + + def time_dict_rename_both_axes(self): + self.df.rename(index=self.dict_idx, columns=self.dict_idx) + class Iteration(object): - goal_time = 0.2 def setup(self): - self.df = DataFrame(randn(10000, 1000)) - self.df2 = DataFrame(np.random.randn(50000, 10)) - self.df3 = pd.DataFrame(np.random.randn(1000,5000), - columns=['C'+str(c) for c in range(5000)]) + N = 1000 + self.df = DataFrame(np.random.randn(N * 10, N)) + self.df2 = DataFrame(np.random.randn(N * 50, 10)) + self.df3 = DataFrame(np.random.randn(N, 5 * N), + columns=['C' + str(c) for c in range(N * 5)]) + self.df4 = DataFrame(np.random.randn(N * 1000, 10)) - def f(self): + def time_iteritems(self): + # (monitor no-copying behaviour) if hasattr(self.df, '_item_cache'): self.df._item_cache.clear() - for (name, col) in self.df.iteritems(): + for name, col in self.df.iteritems(): pass - def g(self): - for (name, col) in self.df.iteritems(): + def time_iteritems_cached(self): + for name, col in self.df.iteritems(): pass - def time_iteritems(self): - self.f() + def time_iteritems_indexing(self): + for col in self.df3: + self.df3[col] - def time_iteritems_cached(self): - self.g() + def time_itertuples_start(self): + self.df4.itertuples() - def time_iteritems_indexing(self): - df = self.df3 - for col in df: - df[col] + def time_itertuples_read_first(self): + next(self.df4.itertuples()) def time_itertuples(self): - for row in self.df2.itertuples(): + for row in self.df4.itertuples(): pass + def time_itertuples_to_list(self): + list(self.df4.itertuples()) -#---------------------------------------------------------------------- -# to_string, to_html, repr + def mem_itertuples_start(self): + return self.df4.itertuples() -class Formatting(object): - goal_time = 0.2 + def peakmem_itertuples_start(self): + self.df4.itertuples() - def setup(self): - self.df = DataFrame(randn(100, 10)) + def mem_itertuples_read_first(self): + return next(self.df4.itertuples()) + + def peakmem_itertuples(self): + for row in self.df4.itertuples(): + pass + + def mem_itertuples_to_list(self): + return list(self.df4.itertuples()) + + def peakmem_itertuples_to_list(self): + list(self.df4.itertuples()) + + def time_itertuples_raw_start(self): + self.df4.itertuples(index=False, name=None) + + def time_itertuples_raw_read_first(self): + next(self.df4.itertuples(index=False, name=None)) + + def time_itertuples_raw_tuples(self): + for row in self.df4.itertuples(index=False, name=None): + pass + + def time_itertuples_raw_tuples_to_list(self): + list(self.df4.itertuples(index=False, name=None)) + + def mem_itertuples_raw_start(self): + return self.df4.itertuples(index=False, name=None) + + def peakmem_itertuples_raw_start(self): + self.df4.itertuples(index=False, name=None) + + def peakmem_itertuples_raw_read_first(self): + next(self.df4.itertuples(index=False, name=None)) + + def peakmem_itertuples_raw(self): + for row in self.df4.itertuples(index=False, name=None): + pass + + def mem_itertuples_raw_to_list(self): + return list(self.df4.itertuples(index=False, name=None)) - self.nrows = 500 - self.df2 = DataFrame(randn(self.nrows, 10)) - self.df2[0] = period_range('2000', '2010', self.nrows) - self.df2[1] = range(self.nrows) + def peakmem_itertuples_raw_to_list(self): + list(self.df4.itertuples(index=False, name=None)) - self.nrows = 10000 - self.data = randn(self.nrows, 10) - self.idx = MultiIndex.from_arrays(np.tile(randn(3, int(self.nrows / 100)), 100)) - self.df3 = DataFrame(self.data, index=self.idx) - self.idx = randn(self.nrows) - self.df4 = DataFrame(self.data, index=self.idx) + def time_iterrows(self): + for row in self.df.iterrows(): + pass - self.df_tall = pandas.DataFrame(np.random.randn(10000, 10)) - self.df_wide = pandas.DataFrame(np.random.randn(10, 10000)) +class ToString(object): + + def setup(self): + self.df = DataFrame(np.random.randn(100, 10)) def time_to_string_floats(self): self.df.to_string() + +class ToHTML(object): + + def setup(self): + nrows = 500 + self.df2 = DataFrame(np.random.randn(nrows, 10)) + self.df2[0] = period_range('2000', periods=nrows) + self.df2[1] = range(nrows) + def time_to_html_mixed(self): self.df2.to_html() + +class Repr(object): + + def setup(self): + nrows = 10000 + data = np.random.randn(nrows, 10) + arrays = np.tile(np.random.randn(3, int(nrows / 100)), 100) + idx = MultiIndex.from_arrays(arrays) + self.df3 = DataFrame(data, index=idx) + self.df4 = DataFrame(data, index=np.random.randn(nrows)) + self.df_tall = DataFrame(np.random.randn(nrows, 10)) + self.df_wide = DataFrame(np.random.randn(10, nrows)) + def time_html_repr_trunc_mi(self): self.df3._repr_html_() @@ -141,21 +235,14 @@ def time_frame_repr_wide(self): repr(self.df_wide) -#---------------------------------------------------------------------- -# nulls/masking - - -## masking - -class frame_mask_bools(object): - goal_time = 0.2 +class MaskBool(object): def setup(self): - self.data = np.random.randn(1000, 500) - self.df = DataFrame(self.data) - self.df = self.df.where((self.df > 0)) - self.bools = (self.df > 0) - self.mask = isnull(self.df) + data = np.random.randn(1000, 500) + df = DataFrame(data) + df = df.where(df > 0) + self.bools = df > 0 + self.mask = isnull(df) def time_frame_mask_bools(self): self.bools.mask(self.mask) @@ -164,31 +251,24 @@ def time_frame_mask_floats(self): self.bools.astype(float).mask(self.mask) -## isnull - -class FrameIsnull(object): - goal_time = 0.2 +class Isnull(object): def setup(self): - self.df_no_null = DataFrame(np.random.randn(1000, 1000)) - - np.random.seed(1234) - self.sample = np.array([np.nan, 1.0]) - self.data = np.random.choice(self.sample, (1000, 1000)) - self.df = DataFrame(self.data) - - np.random.seed(1234) - self.sample = np.array(list(string.ascii_lowercase) + - list(string.ascii_uppercase) + - list(string.whitespace)) - self.data = np.random.choice(self.sample, (1000, 1000)) - self.df_strings= DataFrame(self.data) - - np.random.seed(1234) - self.sample = np.array([NaT, np.nan, None, np.datetime64('NaT'), - np.timedelta64('NaT'), 0, 1, 2.0, '', 'abcd']) - self.data = np.random.choice(self.sample, (1000, 1000)) - self.df_obj = DataFrame(self.data) + N = 10**3 + self.df_no_null = DataFrame(np.random.randn(N, N)) + + sample = np.array([np.nan, 1.0]) + data = np.random.choice(sample, (N, N)) + self.df = DataFrame(data) + + sample = np.array(list(string.ascii_letters + string.whitespace)) + data = np.random.choice(sample, (N, N)) + self.df_strings = DataFrame(data) + + sample = np.array([NaT, np.nan, None, np.datetime64('NaT'), + np.timedelta64('NaT'), 0, 1, 2.0, '', 'abcd']) + data = np.random.choice(sample, (N, N)) + self.df_obj = DataFrame(data) def time_isnull_floats_no_null(self): isnull(self.df_no_null) @@ -203,126 +283,97 @@ def time_isnull_obj(self): isnull(self.df_obj) -# ---------------------------------------------------------------------- -# fillna in place - -class frame_fillna_inplace(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(randn(10000, 100)) - self.df.values[::2] = np.nan - - def time_frame_fillna_inplace(self): - self.df.fillna(0, inplace=True) +class Fillna(object): + params = ([True, False], ['pad', 'bfill']) + param_names = ['inplace', 'method'] + def setup(self, inplace, method): + values = np.random.randn(10000, 100) + values[::2] = np.nan + self.df = DataFrame(values) -class frame_fillna_many_columns_pad(object): - goal_time = 0.2 - - def setup(self): - self.values = np.random.randn(1000, 1000) - self.values[::2] = np.nan - self.df = DataFrame(self.values) - - def time_frame_fillna_many_columns_pad(self): - self.df.fillna(method='pad') - + def time_frame_fillna(self, inplace, method): + self.df.fillna(inplace=inplace, method=method) class Dropna(object): - goal_time = 0.2 - def setup(self): - self.data = np.random.randn(10000, 1000) - self.df = DataFrame(self.data) + params = (['all', 'any'], [0, 1]) + param_names = ['how', 'axis'] + + def setup(self, how, axis): + self.df = DataFrame(np.random.randn(10000, 1000)) self.df.ix[50:1000, 20:50] = np.nan self.df.ix[2000:3000] = np.nan self.df.ix[:, 60:70] = np.nan self.df_mixed = self.df.copy() self.df_mixed['foo'] = 'bar' - self.df_mi = self.df.copy() - self.df_mi.index = MultiIndex.from_tuples(self.df_mi.index.map((lambda x: (x, x)))) - self.df_mi.columns = MultiIndex.from_tuples(self.df_mi.columns.map((lambda x: (x, x)))) - - self.df_mixed_mi = self.df_mixed.copy() - self.df_mixed_mi.index = MultiIndex.from_tuples(self.df_mixed_mi.index.map((lambda x: (x, x)))) - self.df_mixed_mi.columns = MultiIndex.from_tuples(self.df_mixed_mi.columns.map((lambda x: (x, x)))) - - def time_dropna_axis0_all(self): - self.df.dropna(how='all', axis=0) - - def time_dropna_axis0_any(self): - self.df.dropna(how='any', axis=0) - - def time_dropna_axis1_all(self): - self.df.dropna(how='all', axis=1) + def time_dropna(self, how, axis): + self.df.dropna(how=how, axis=axis) - def time_dropna_axis1_any(self): - self.df.dropna(how='any', axis=1) + def time_dropna_axis_mixed_dtypes(self, how, axis): + self.df_mixed.dropna(how=how, axis=axis) - def time_dropna_axis0_all_mixed_dtypes(self): - self.df_mixed.dropna(how='all', axis=0) - def time_dropna_axis0_any_mixed_dtypes(self): - self.df_mixed.dropna(how='any', axis=0) +class Count(object): - def time_dropna_axis1_all_mixed_dtypes(self): - self.df_mixed.dropna(how='all', axis=1) + params = [0, 1] + param_names = ['axis'] - def time_dropna_axis1_any_mixed_dtypes(self): - self.df_mixed.dropna(how='any', axis=1) - - def time_count_level_axis0_multi(self): - self.df_mi.count(axis=0, level=1) + def setup(self, axis): + self.df = DataFrame(np.random.randn(10000, 1000)) + self.df.ix[50:1000, 20:50] = np.nan + self.df.ix[2000:3000] = np.nan + self.df.ix[:, 60:70] = np.nan + self.df_mixed = self.df.copy() + self.df_mixed['foo'] = 'bar' - def time_count_level_axis1_multi(self): - self.df_mi.count(axis=1, level=1) + self.df.index = MultiIndex.from_arrays([self.df.index, self.df.index]) + self.df.columns = MultiIndex.from_arrays([self.df.columns, + self.df.columns]) + self.df_mixed.index = MultiIndex.from_arrays([self.df_mixed.index, + self.df_mixed.index]) + self.df_mixed.columns = MultiIndex.from_arrays([self.df_mixed.columns, + self.df_mixed.columns]) - def time_count_level_axis0_mixed_dtypes_multi(self): - self.df_mixed_mi.count(axis=0, level=1) + def time_count_level_multi(self, axis): + self.df.count(axis=axis, level=1) - def time_count_level_axis1_mixed_dtypes_multi(self): - self.df_mixed_mi.count(axis=1, level=1) + def time_count_level_mixed_dtypes_multi(self, axis): + self.df_mixed.count(axis=axis, level=1) class Apply(object): - goal_time = 0.2 def setup(self): self.df = DataFrame(np.random.randn(1000, 100)) self.s = Series(np.arange(1028.0)) self.df2 = DataFrame({i: self.s for i in range(1028)}) - self.df3 = DataFrame(np.random.randn(1000, 3), columns=list('ABC')) def time_apply_user_func(self): - self.df2.apply((lambda x: np.corrcoef(x, self.s)[(0, 1)])) + self.df2.apply(lambda x: np.corrcoef(x, self.s)[(0, 1)]) def time_apply_axis_1(self): - self.df.apply((lambda x: (x + 1)), axis=1) + self.df.apply(lambda x: x + 1, axis=1) def time_apply_lambda_mean(self): - self.df.apply((lambda x: x.mean())) + self.df.apply(lambda x: x.mean()) def time_apply_np_mean(self): self.df.apply(np.mean) def time_apply_pass_thru(self): - self.df.apply((lambda x: x)) + self.df.apply(lambda x: x) def time_apply_ref_by_name(self): - self.df3.apply((lambda x: (x['A'] + x['B'])), axis=1) + self.df3.apply(lambda x: x['A'] + x['B'], axis=1) -#---------------------------------------------------------------------- -# dtypes - -class frame_dtypes(object): - goal_time = 0.2 +class Dtypes(object): def setup(self): self.df = DataFrame(np.random.randn(1000, 1000)) @@ -330,331 +381,205 @@ def setup(self): def time_frame_dtypes(self): self.df.dtypes -#---------------------------------------------------------------------- -# equals class Equals(object): - goal_time = 0.2 def setup(self): - self.float_df = DataFrame(np.random.randn(1000, 1000)) - self.object_df = DataFrame(([(['foo'] * 1000)] * 1000)) - self.nonunique_cols = self.object_df.copy() - self.nonunique_cols.columns = (['A'] * len(self.nonunique_cols.columns)) - self.pairs = dict([(name, self.make_pair(frame)) for (name, frame) in ( - ('float_df', self.float_df), ('object_df', self.object_df), - ('nonunique_cols', self.nonunique_cols))]) + N = 10**3 + self.float_df = DataFrame(np.random.randn(N, N)) + self.float_df_nan = self.float_df.copy() + self.float_df_nan.iloc[-1, -1] = np.nan - def make_pair(self, frame): - self.df = frame - self.df2 = self.df.copy() - self.df2.ix[((-1), (-1))] = np.nan - return (self.df, self.df2) + self.object_df = DataFrame('foo', index=range(N), columns=range(N)) + self.object_df_nan = self.object_df.copy() + self.object_df_nan.iloc[-1, -1] = np.nan - def test_equal(self, name): - (self.df, self.df2) = self.pairs[name] - return self.df.equals(self.df) - - def test_unequal(self, name): - (self.df, self.df2) = self.pairs[name] - return self.df.equals(self.df2) + self.nonunique_cols = self.object_df.copy() + self.nonunique_cols.columns = ['A'] * len(self.nonunique_cols.columns) + self.nonunique_cols_nan = self.nonunique_cols.copy() + self.nonunique_cols_nan.iloc[-1, -1] = np.nan def time_frame_float_equal(self): - self.test_equal('float_df') + self.float_df.equals(self.float_df) def time_frame_float_unequal(self): - self.test_unequal('float_df') + self.float_df.equals(self.float_df_nan) def time_frame_nonunique_equal(self): - self.test_equal('nonunique_cols') + self.nonunique_cols.equals(self.nonunique_cols) def time_frame_nonunique_unequal(self): - self.test_unequal('nonunique_cols') + self.nonunique_cols.equals(self.nonunique_cols_nan) def time_frame_object_equal(self): - self.test_equal('object_df') + self.object_df.equals(self.object_df) def time_frame_object_unequal(self): - self.test_unequal('object_df') + self.object_df.equals(self.object_df_nan) class Interpolate(object): - goal_time = 0.2 - def setup(self): + params = [None, 'infer'] + param_names = ['downcast'] + + def setup(self, downcast): + N = 10000 # this is the worst case, where every column has NaNs. - self.df = DataFrame(randn(10000, 100)) + self.df = DataFrame(np.random.randn(N, 100)) self.df.values[::2] = np.nan - self.df2 = DataFrame( - {'A': np.arange(0, 10000), 'B': np.random.randint(0, 100, 10000), - 'C': randn(10000), 'D': randn(10000),}) + self.df2 = DataFrame({'A': np.arange(0, N), + 'B': np.random.randint(0, 100, N), + 'C': np.random.randn(N), + 'D': np.random.randn(N)}) self.df2.loc[1::5, 'A'] = np.nan self.df2.loc[1::5, 'C'] = np.nan - def time_interpolate(self): - self.df.interpolate() + def time_interpolate(self, downcast): + self.df.interpolate(downcast=downcast) - def time_interpolate_some_good(self): - self.df2.interpolate() - - def time_interpolate_some_good_infer(self): - self.df2.interpolate(downcast='infer') + def time_interpolate_some_good(self, downcast): + self.df2.interpolate(downcast=downcast) class Shift(object): # frame shift speedup issue-5609 - goal_time = 0.2 + params = [0, 1] + param_names = ['axis'] - def setup(self): + def setup(self, axis): self.df = DataFrame(np.random.rand(10000, 500)) - def time_shift_axis0(self): - self.df.shift(1, axis=0) - - def time_shift_axis_1(self): - self.df.shift(1, axis=1) - - -#----------------------------------------------------------------------------- -# from_records issue-6700 - -class frame_from_records_generator(object): - goal_time = 0.2 - - def get_data(self, n=100000): - return ((x, (x * 20), (x * 100)) for x in range(n)) - - def time_frame_from_records_generator(self): - self.df = DataFrame.from_records(self.get_data()) - - def time_frame_from_records_generator_nrows(self): - self.df = DataFrame.from_records(self.get_data(), nrows=1000) - + def time_shift(self, axis): + self.df.shift(1, axis=axis) -#----------------------------------------------------------------------------- -# nunique - -class frame_nunique(object): +class Nunique(object): def setup(self): - self.data = np.random.randn(10000, 1000) - self.df = DataFrame(self.data) + self.df = DataFrame(np.random.randn(10000, 1000)) def time_frame_nunique(self): self.df.nunique() - -#----------------------------------------------------------------------------- -# duplicated - -class frame_duplicated(object): - goal_time = 0.2 +class Duplicated(object): def setup(self): - self.n = (1 << 20) - self.t = date_range('2015-01-01', freq='S', periods=(self.n // 64)) - self.xs = np.random.randn((self.n // 64)).round(2) - self.df = DataFrame({'a': np.random.randint(((-1) << 8), (1 << 8), self.n), 'b': np.random.choice(self.t, self.n), 'c': np.random.choice(self.xs, self.n), }) - - self.df2 = DataFrame(np.random.randn(1000, 100).astype(str)) + n = (1 << 20) + t = date_range('2015-01-01', freq='S', periods=(n // 64)) + xs = np.random.randn(n // 64).round(2) + self.df = DataFrame({'a': np.random.randint(-1 << 8, 1 << 8, n), + 'b': np.random.choice(t, n), + 'c': np.random.choice(xs, n)}) + self.df2 = DataFrame(np.random.randn(1000, 100).astype(str)).T def time_frame_duplicated(self): self.df.duplicated() def time_frame_duplicated_wide(self): - self.df2.T.duplicated() - + self.df2.duplicated() +class XS(object): + params = [0, 1] + param_names = ['axis'] + def setup(self, axis): + self.N = 10**4 + self.df = DataFrame(np.random.randn(self.N, self.N)) + def time_frame_xs(self, axis): + self.df.xs(self.N / 2, axis=axis) +class SortValues(object): + params = [True, False] + param_names = ['ascending'] + def setup(self, ascending): + self.df = DataFrame(np.random.randn(1000000, 2), columns=list('AB')) + def time_frame_sort_values(self, ascending): + self.df.sort_values(by='A', ascending=ascending) - - - - -class frame_xs_col(object): - goal_time = 0.2 +class SortIndexByColumns(object): def setup(self): - self.df = DataFrame(randn(1, 100000)) + N = 10000 + K = 10 + self.df = DataFrame({'key1': tm.makeStringIndex(N).values.repeat(K), + 'key2': tm.makeStringIndex(N).values.repeat(K), + 'value': np.random.randn(N * K)}) - def time_frame_xs_col(self): - self.df.xs(50000, axis=1) + def time_frame_sort_values_by_columns(self): + self.df.sort_values(by=['key1', 'key2']) -class frame_xs_row(object): - goal_time = 0.2 +class Quantile(object): - def setup(self): - self.df = DataFrame(randn(100000, 1)) + params = [0, 1] + param_names = ['axis'] - def time_frame_xs_row(self): - self.df.xs(50000) + def setup(self, axis): + self.df = DataFrame(np.random.randn(1000, 3), columns=list('ABC')) + def time_frame_quantile(self, axis): + self.df.quantile([0.1, 0.5], axis=axis) -class frame_sort_index(object): - goal_time = 0.2 +class GetDtypeCounts(object): + # 2807 def setup(self): - self.df = DataFrame(randn(1000000, 2), columns=list('AB')) - - def time_frame_sort_index(self): - self.df.sort_index() + self.df = DataFrame(np.random.randn(10, 10000)) + def time_frame_get_dtype_counts(self): + self.df.get_dtype_counts() -class frame_sort_index_by_columns(object): - goal_time = 0.2 + def time_info(self): + self.df.info() - def setup(self): - self.N = 10000 - self.K = 10 - self.key1 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.key2 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.df = DataFrame({'key1': self.key1, 'key2': self.key2, 'value': np.random.randn((self.N * self.K)), }) - self.col_array_list = list(self.df.values.T) - def time_frame_sort_index_by_columns(self): - self.df.sort_index(by=['key1', 'key2']) +class NSort(object): + params = ['first', 'last', 'all'] + param_names = ['keep'] -class frame_quantile_axis1(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(1000, 3), + def setup(self, keep): + self.df = DataFrame(np.random.randn(100000, 3), columns=list('ABC')) - def time_frame_quantile_axis1(self): - self.df.quantile([0.1, 0.5], axis=1) - - -#---------------------------------------------------------------------- -# boolean indexing + def time_nlargest_one_column(self, keep): + self.df.nlargest(100, 'A', keep=keep) -class frame_boolean_row_select(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(randn(10000, 100)) - self.bool_arr = np.zeros(10000, dtype=bool) - self.bool_arr[:1000] = True - - def time_frame_boolean_row_select(self): - self.df[self.bool_arr] - -class frame_getitem_single_column(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(randn(10000, 1000)) - self.df2 = DataFrame(randn(3000, 1), columns=['A']) - self.df3 = DataFrame(randn(3000, 1)) - - def h(self): - for i in range(10000): - self.df2['A'] - - def j(self): - for i in range(10000): - self.df3[0] - - def time_frame_getitem_single_column(self): - self.h() - - def time_frame_getitem_single_column2(self): - self.j() + def time_nlargest_two_columns(self, keep): + self.df.nlargest(100, ['A', 'B'], keep=keep) + def time_nsmallest_one_column(self, keep): + self.df.nsmallest(100, 'A', keep=keep) -#---------------------------------------------------------------------- -# assignment - -class frame_assign_timeseries_index(object): - goal_time = 0.2 - - def setup(self): - self.idx = date_range('1/1/2000', periods=100000, freq='D') - self.df = DataFrame(randn(100000, 1), columns=['A'], index=self.idx) - - def time_frame_assign_timeseries_index(self): - self.f(self.df) - - def f(self, df): - self.x = self.df.copy() - self.x['date'] = self.x.index + def time_nsmallest_two_columns(self, keep): + self.df.nsmallest(100, ['A', 'B'], keep=keep) - -# insert many columns - -class frame_insert_100_columns_begin(object): - goal_time = 0.2 +class Describe(object): def setup(self): - self.N = 1000 - - def f(self, K=100): - self.df = DataFrame(index=range(self.N)) - self.new_col = np.random.randn(self.N) - for i in range(K): - self.df.insert(0, i, self.new_col) - - def g(self, K=500): - self.df = DataFrame(index=range(self.N)) - self.new_col = np.random.randn(self.N) - for i in range(K): - self.df[i] = self.new_col - - def time_frame_insert_100_columns_begin(self): - self.f() + self.df = DataFrame({ + 'a': np.random.randint(0, 100, int(1e6)), + 'b': np.random.randint(0, 100, int(1e6)), + 'c': np.random.randint(0, 100, int(1e6)) + }) - def time_frame_insert_500_columns_end(self): - self.g() + def time_series_describe(self): + self.df['a'].describe() + def time_dataframe_describe(self): + self.df.describe() -#---------------------------------------------------------------------- -# strings methods, #2602 - -class series_string_vector_slice(object): - goal_time = 0.2 - - def setup(self): - self.s = Series((['abcdefg', np.nan] * 500000)) - - def time_series_string_vector_slice(self): - self.s.str[:5] - - -#---------------------------------------------------------------------- -# df.info() and get_dtype_counts() # 2807 - -class frame_get_dtype_counts(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(10, 10000)) - - def time_frame_get_dtype_counts(self): - self.df.get_dtype_counts() - - -class frame_nlargest(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(1000, 3), - columns=list('ABC')) - - def time_frame_nlargest(self): - self.df.nlargest(100, 'A') +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/gil.py b/asv_bench/benchmarks/gil.py index 1c5e59672cb57..6819a296c81df 100644 --- a/asv_bench/benchmarks/gil.py +++ b/asv_bench/benchmarks/gil.py @@ -1,235 +1,134 @@ -from .pandas_vb_common import * -from pandas.core import common as com - +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, Series, read_csv, factorize, date_range +from pandas.core.algorithms import take_1d try: - from cStringIO import StringIO + from pandas import (rolling_median, rolling_mean, rolling_min, rolling_max, + rolling_var, rolling_skew, rolling_kurt, rolling_std) + have_rolling_methods = True except ImportError: - from io import StringIO - + have_rolling_methods = False +try: + from pandas._libs import algos +except ImportError: + from pandas import algos try: from pandas.util.testing import test_parallel - have_real_test_parallel = True except ImportError: have_real_test_parallel = False - def test_parallel(num_threads=1): - def wrapper(fname): return fname - return wrapper +from .pandas_vb_common import BaseIO -class NoGilGroupby(object): - goal_time = 0.2 - def setup(self): - self.N = 1000000 - self.ngroups = 1000 - np.random.seed(1234) - self.df = DataFrame({'key': np.random.randint(0, self.ngroups, size=self.N), 'data': np.random.randn(self.N), }) +class ParallelGroupbyMethods(object): - np.random.seed(1234) - self.size = 2 ** 22 - self.ngroups = 100 - self.data = Series(np.random.randint(0, self.ngroups, size=self.size)) + params = ([2, 4, 8], ['count', 'last', 'max', 'mean', 'min', 'prod', + 'sum', 'var']) + param_names = ['threads', 'method'] - if (not have_real_test_parallel): + def setup(self, threads, method): + if not have_real_test_parallel: raise NotImplementedError + N = 10**6 + ngroups = 10**3 + df = DataFrame({'key': np.random.randint(0, ngroups, size=N), + 'data': np.random.randn(N)}) - @test_parallel(num_threads=2) - def _pg2_count(self): - self.df.groupby('key')['data'].count() - - def time_count_2(self): - self._pg2_count() - - @test_parallel(num_threads=2) - def _pg2_last(self): - self.df.groupby('key')['data'].last() - - def time_last_2(self): - self._pg2_last() - - @test_parallel(num_threads=2) - def _pg2_max(self): - self.df.groupby('key')['data'].max() - - def time_max_2(self): - self._pg2_max() - - @test_parallel(num_threads=2) - def _pg2_mean(self): - self.df.groupby('key')['data'].mean() - - def time_mean_2(self): - self._pg2_mean() - - @test_parallel(num_threads=2) - def _pg2_min(self): - self.df.groupby('key')['data'].min() - - def time_min_2(self): - self._pg2_min() - - @test_parallel(num_threads=2) - def _pg2_prod(self): - self.df.groupby('key')['data'].prod() - - def time_prod_2(self): - self._pg2_prod() - - @test_parallel(num_threads=2) - def _pg2_sum(self): - self.df.groupby('key')['data'].sum() - - def time_sum_2(self): - self._pg2_sum() - - @test_parallel(num_threads=4) - def _pg4_sum(self): - self.df.groupby('key')['data'].sum() - - def time_sum_4(self): - self._pg4_sum() - - def time_sum_4_notp(self): - for i in range(4): - self.df.groupby('key')['data'].sum() - - def _f_sum(self): - self.df.groupby('key')['data'].sum() - - @test_parallel(num_threads=8) - def _pg8_sum(self): - self._f_sum() - - def time_sum_8(self): - self._pg8_sum() - - def time_sum_8_notp(self): - for i in range(8): - self._f_sum() - - @test_parallel(num_threads=2) - def _pg2_var(self): - self.df.groupby('key')['data'].var() - - def time_var_2(self): - self._pg2_var() - - # get groups - - def _groups(self): - self.data.groupby(self.data).groups - - @test_parallel(num_threads=2) - def _pg2_groups(self): - self._groups() - - def time_groups_2(self): - self._pg2_groups() - - @test_parallel(num_threads=4) - def _pg4_groups(self): - self._groups() + @test_parallel(num_threads=threads) + def parallel(): + getattr(df.groupby('key')['data'], method)() + self.parallel = parallel - def time_groups_4(self): - self._pg4_groups() + def loop(): + getattr(df.groupby('key')['data'], method)() + self.loop = loop - @test_parallel(num_threads=8) - def _pg8_groups(self): - self._groups() + def time_parallel(self, threads, method): + self.parallel() - def time_groups_8(self): - self._pg8_groups() + def time_loop(self, threads, method): + for i in range(threads): + self.loop() +class ParallelGroups(object): -class nogil_take1d_float64(object): - goal_time = 0.2 + params = [2, 4, 8] + param_names = ['threads'] - def setup(self): - self.N = 1000000 - self.ngroups = 1000 - np.random.seed(1234) - self.df = DataFrame({'key': np.random.randint(0, self.ngroups, size=self.N), 'data': np.random.randn(self.N), }) - if (not have_real_test_parallel): + def setup(self, threads): + if not have_real_test_parallel: raise NotImplementedError - self.N = 10000000.0 - self.df = DataFrame({'int64': np.arange(self.N, dtype='int64'), 'float64': np.arange(self.N, dtype='float64'), }) - self.indexer = np.arange(100, (len(self.df) - 100)) + size = 2**22 + ngroups = 10**3 + data = Series(np.random.randint(0, ngroups, size=size)) - def time_nogil_take1d_float64(self): - self.take_1d_pg2_int64() + @test_parallel(num_threads=threads) + def get_groups(): + data.groupby(data).groups + self.get_groups = get_groups - @test_parallel(num_threads=2) - def take_1d_pg2_int64(self): - com.take_1d(self.df.int64.values, self.indexer) + def time_get_groups(self, threads): + self.get_groups() - @test_parallel(num_threads=2) - def take_1d_pg2_float64(self): - com.take_1d(self.df.float64.values, self.indexer) +class ParallelTake1D(object): -class nogil_take1d_int64(object): - goal_time = 0.2 + params = ['int64', 'float64'] + param_names = ['dtype'] - def setup(self): - self.N = 1000000 - self.ngroups = 1000 - np.random.seed(1234) - self.df = DataFrame({'key': np.random.randint(0, self.ngroups, size=self.N), 'data': np.random.randn(self.N), }) - if (not have_real_test_parallel): + def setup(self, dtype): + if not have_real_test_parallel: raise NotImplementedError - self.N = 10000000.0 - self.df = DataFrame({'int64': np.arange(self.N, dtype='int64'), 'float64': np.arange(self.N, dtype='float64'), }) - self.indexer = np.arange(100, (len(self.df) - 100)) + N = 10**6 + df = DataFrame({'col': np.arange(N, dtype=dtype)}) + indexer = np.arange(100, len(df) - 100) - def time_nogil_take1d_int64(self): - self.take_1d_pg2_float64() + @test_parallel(num_threads=2) + def parallel_take1d(): + take_1d(df['col'].values, indexer) + self.parallel_take1d = parallel_take1d - @test_parallel(num_threads=2) - def take_1d_pg2_int64(self): - com.take_1d(self.df.int64.values, self.indexer) + def time_take1d(self, dtype): + self.parallel_take1d() - @test_parallel(num_threads=2) - def take_1d_pg2_float64(self): - com.take_1d(self.df.float64.values, self.indexer) +class ParallelKth(object): -class nogil_kth_smallest(object): number = 1 repeat = 5 def setup(self): - if (not have_real_test_parallel): + if not have_real_test_parallel: raise NotImplementedError - np.random.seed(1234) - self.N = 10000000 - self.k = 500000 - self.a = np.random.randn(self.N) - self.b = self.a.copy() - self.kwargs_list = [{'arr': self.a}, {'arr': self.b}] + N = 10**7 + k = 5 * 10**5 + kwargs_list = [{'arr': np.random.randn(N)}, + {'arr': np.random.randn(N)}] - def time_nogil_kth_smallest(self): - @test_parallel(num_threads=2, kwargs_list=self.kwargs_list) - def run(arr): - algos.kth_smallest(arr, self.k) - run() + @test_parallel(num_threads=2, kwargs_list=kwargs_list) + def parallel_kth_smallest(arr): + algos.kth_smallest(arr, k) + self.parallel_kth_smallest = parallel_kth_smallest + def time_kth_smallest(self): + self.parallel_kth_smallest() -class nogil_datetime_fields(object): - goal_time = 0.2 + +class ParallelDatetimeFields(object): def setup(self): - self.N = 100000000 - self.dti = pd.date_range('1900-01-01', periods=self.N, freq='D') - self.period = self.dti.to_period('D') - if (not have_real_test_parallel): + if not have_real_test_parallel: raise NotImplementedError + N = 10**6 + self.dti = date_range('1900-01-01', periods=N, freq='T') + self.period = self.dti.to_period('D') def time_datetime_field_year(self): @test_parallel(num_threads=2) @@ -268,149 +167,106 @@ def run(period): run(self.period) -class nogil_rolling_algos_slow(object): - goal_time = 0.2 - - def setup(self): - self.win = 100 - np.random.seed(1234) - self.arr = np.random.rand(100000) - if (not have_real_test_parallel): - raise NotImplementedError - - def time_nogil_rolling_median(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_median(arr, win) - run(self.arr, self.win) - +class ParallelRolling(object): -class nogil_rolling_algos_fast(object): - goal_time = 0.2 + params = ['median', 'mean', 'min', 'max', 'var', 'skew', 'kurt', 'std'] + param_names = ['method'] - def setup(self): - self.win = 100 - np.random.seed(1234) - self.arr = np.random.rand(1000000) - if (not have_real_test_parallel): + def setup(self, method): + if not have_real_test_parallel: + raise NotImplementedError + win = 100 + arr = np.random.rand(100000) + if hasattr(DataFrame, 'rolling'): + df = DataFrame(arr).rolling(win) + + @test_parallel(num_threads=2) + def parallel_rolling(): + getattr(df, method)() + self.parallel_rolling = parallel_rolling + elif have_rolling_methods: + rolling = {'median': rolling_median, + 'mean': rolling_mean, + 'min': rolling_min, + 'max': rolling_max, + 'var': rolling_var, + 'skew': rolling_skew, + 'kurt': rolling_kurt, + 'std': rolling_std} + + @test_parallel(num_threads=2) + def parallel_rolling(): + rolling[method](arr, win) + self.parallel_rolling = parallel_rolling + else: raise NotImplementedError - def time_nogil_rolling_mean(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_mean(arr, win) - run(self.arr, self.win) - - def time_nogil_rolling_min(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_min(arr, win) - run(self.arr, self.win) - - def time_nogil_rolling_max(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_max(arr, win) - run(self.arr, self.win) - - def time_nogil_rolling_var(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_var(arr, win) - run(self.arr, self.win) - - def time_nogil_rolling_skew(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_skew(arr, win) - run(self.arr, self.win) - - def time_nogil_rolling_kurt(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_kurt(arr, win) - run(self.arr, self.win) + def time_rolling(self, method): + self.parallel_rolling() - def time_nogil_rolling_std(self): - @test_parallel(num_threads=2) - def run(arr, win): - rolling_std(arr, win) - run(self.arr, self.win) +class ParallelReadCSV(BaseIO): -class nogil_read_csv(object): number = 1 repeat = 5 + params = ['float', 'object', 'datetime'] + param_names = ['dtype'] - def setup(self): - if (not have_real_test_parallel): + def setup(self, dtype): + if not have_real_test_parallel: raise NotImplementedError - # Using the values - self.df = DataFrame(np.random.randn(10000, 50)) - self.df.to_csv('__test__.csv') + rows = 10000 + cols = 50 + data = {'float': DataFrame(np.random.randn(rows, cols)), + 'datetime': DataFrame(np.random.randn(rows, cols), + index=date_range('1/1/2000', + periods=rows)), + 'object': DataFrame('foo', + index=range(rows), + columns=['object%03d'.format(i) + for i in range(5)])} + + self.fname = '__test_{}__.csv'.format(dtype) + df = data[dtype] + df.to_csv(self.fname) - self.rng = date_range('1/1/2000', periods=10000) - self.df_date_time = DataFrame(np.random.randn(10000, 50), index=self.rng) - self.df_date_time.to_csv('__test_datetime__.csv') - - self.df_object = DataFrame('foo', index=self.df.index, columns=self.create_cols('object')) - self.df_object.to_csv('__test_object__.csv') - - def create_cols(self, name): - return [('%s%03d' % (name, i)) for i in range(5)] - - @test_parallel(num_threads=2) - def pg_read_csv(self): - read_csv('__test__.csv', sep=',', header=None, float_precision=None) - - def time_read_csv(self): - self.pg_read_csv() - - @test_parallel(num_threads=2) - def pg_read_csv_object(self): - read_csv('__test_object__.csv', sep=',') - - def time_read_csv_object(self): - self.pg_read_csv_object() + @test_parallel(num_threads=2) + def parallel_read_csv(): + read_csv(self.fname) + self.parallel_read_csv = parallel_read_csv - @test_parallel(num_threads=2) - def pg_read_csv_datetime(self): - read_csv('__test_datetime__.csv', sep=',', header=None) + def time_read_csv(self, dtype): + self.parallel_read_csv() - def time_read_csv_datetime(self): - self.pg_read_csv_datetime() +class ParallelFactorize(object): -class nogil_factorize(object): number = 1 repeat = 5 + params = [2, 4, 8] + param_names = ['threads'] - def setup(self): - if (not have_real_test_parallel): + def setup(self, threads): + if not have_real_test_parallel: raise NotImplementedError - np.random.seed(1234) - self.strings = tm.makeStringIndex(100000) + strings = tm.makeStringIndex(100000) - def factorize_strings(self): - pd.factorize(self.strings) + @test_parallel(num_threads=threads) + def parallel(): + factorize(strings) + self.parallel = parallel - @test_parallel(num_threads=4) - def _pg_factorize_strings_4(self): - self.factorize_strings() + def loop(): + factorize(strings) + self.loop = loop - def time_factorize_strings_4(self): - for i in range(2): - self._pg_factorize_strings_4() + def time_parallel(self, threads): + self.parallel() - @test_parallel(num_threads=2) - def _pg_factorize_strings_2(self): - self.factorize_strings() + def time_loop(self, threads): + for i in range(threads): + self.loop() - def time_factorize_strings_2(self): - for i in range(4): - self._pg_factorize_strings_2() - def time_factorize_strings(self): - for i in range(8): - self.factorize_strings() +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/groupby.py b/asv_bench/benchmarks/groupby.py index b8d8e8b7912d7..27d279bb90a31 100644 --- a/asv_bench/benchmarks/groupby.py +++ b/asv_bench/benchmarks/groupby.py @@ -1,129 +1,54 @@ -from .pandas_vb_common import * -from string import ascii_letters, digits +from functools import partial from itertools import product +from string import ascii_letters +import warnings +import numpy as np -class groupby_agg_builtins(object): - goal_time = 0.2 +from pandas import ( + Categorical, DataFrame, MultiIndex, Series, TimeGrouper, Timestamp, + date_range, period_range) +import pandas.util.testing as tm - def setup(self): - np.random.seed(27182) - self.n = 100000 - self.df = DataFrame(np.random.randint(1, (self.n / 100), (self.n, 3)), columns=['jim', 'joe', 'jolie']) - - def time_groupby_agg_builtins1(self): - self.df.groupby('jim').agg([sum, min, max]) - - def time_groupby_agg_builtins2(self): - self.df.groupby(['jim', 'joe']).agg([sum, min, max]) -#---------------------------------------------------------------------- -# dict return values +method_blacklist = { + 'object': {'median', 'prod', 'sem', 'cumsum', 'sum', 'cummin', 'mean', + 'max', 'skew', 'cumprod', 'cummax', 'rank', 'pct_change', 'min', + 'var', 'mad', 'describe', 'std', 'quantile'}, + 'datetime': {'median', 'prod', 'sem', 'cumsum', 'sum', 'mean', 'skew', + 'cumprod', 'cummax', 'pct_change', 'var', 'mad', 'describe', + 'std'} +} -class groupby_apply_dict_return(object): - goal_time = 0.2 +class ApplyDictReturn(object): def setup(self): self.labels = np.arange(1000).repeat(10) - self.data = Series(randn(len(self.labels))) - self.f = (lambda x: {'first': x.values[0], 'last': x.values[(-1)], }) + self.data = Series(np.random.randn(len(self.labels))) def time_groupby_apply_dict_return(self): - self.data.groupby(self.labels).apply(self.f) - - -#---------------------------------------------------------------------- -# groups - -class Groups(object): - goal_time = 0.1 - - size = 2 ** 22 - data = { - 'int64_small': Series(np.random.randint(0, 100, size=size)), - 'int64_large' : Series(np.random.randint(0, 10000, size=size)), - 'object_small': Series(tm.makeStringIndex(100).take(np.random.randint(0, 100, size=size))), - 'object_large': Series(tm.makeStringIndex(10000).take(np.random.randint(0, 10000, size=size))) - } - - param_names = ['df'] - params = ['int64_small', 'int64_large', 'object_small', 'object_large'] - - def setup(self, df): - self.df = self.data[df] - - def time_groupby_groups(self, df): - self.df.groupby(self.df).groups - - -#---------------------------------------------------------------------- -# First / last functions - -class FirstLast(object): - goal_time = 0.2 - - param_names = ['dtype'] - params = ['float32', 'float64', 'datetime', 'object'] - - # with datetimes (GH7555) + self.data.groupby(self.labels).apply(lambda x: {'first': x.values[0], + 'last': x.values[-1]}) - def setup(self, dtype): - - if dtype == 'datetime': - self.df = DataFrame( - {'values': date_range('1/1/2011', periods=100000, freq='s'), - 'key': range(100000),}) - elif dtype == 'object': - self.df = DataFrame( - {'values': (['foo'] * 100000), - 'key': range(100000)}) - else: - labels = np.arange(10000).repeat(10) - data = Series(randn(len(labels)), dtype=dtype) - data[::3] = np.nan - data[1::3] = np.nan - labels = labels.take(np.random.permutation(len(labels))) - self.df = DataFrame({'values': data, 'key': labels}) - def time_groupby_first(self, dtype): - self.df.groupby('key').first() - - def time_groupby_last(self, dtype): - self.df.groupby('key').last() - - def time_groupby_nth_any(self, dtype): - self.df.groupby('key').nth(0, dropna='all') - - def time_groupby_nth_none(self, dtype): - self.df.groupby('key').nth(0) +class Apply(object): + def setup_cache(self): + N = 10**4 + labels = np.random.randint(0, 2000, size=N) + labels2 = np.random.randint(0, 3, size=N) + df = DataFrame({'key': labels, + 'key2': labels2, + 'value1': np.random.randn(N), + 'value2': ['foo', 'bar', 'baz', 'qux'] * (N // 4) + }) + return df -#---------------------------------------------------------------------- -# DataFrame Apply overhead + def time_scalar_function_multi_col(self, df): + df.groupby(['key', 'key2']).apply(lambda x: 1) -class groupby_frame_apply(object): - goal_time = 0.2 - - def setup(self): - self.N = 10000 - self.labels = np.random.randint(0, 2000, size=self.N) - self.labels2 = np.random.randint(0, 3, size=self.N) - self.df = DataFrame({ - 'key': self.labels, - 'key2': self.labels2, - 'value1': np.random.randn(self.N), - 'value2': (['foo', 'bar', 'baz', 'qux'] * (self.N // 4)), - }) - - @staticmethod - def scalar_function(g): - return 1 - - def time_groupby_frame_apply_scalar_function(self): - self.df.groupby(['key', 'key2']).apply(self.scalar_function) - - def time_groupby_frame_apply_scalar_function_overhead(self): - self.df.groupby('key').apply(self.scalar_function) + def time_scalar_function_single_col(self, df): + df.groupby('key').apply(lambda x: 1) @staticmethod def df_copy_function(g): @@ -131,374 +56,329 @@ def df_copy_function(g): g.name return g.copy() - def time_groupby_frame_df_copy_function(self): - self.df.groupby(['key', 'key2']).apply(self.df_copy_function) - - def time_groupby_frame_apply_df_copy_overhead(self): - self.df.groupby('key').apply(self.df_copy_function) - - -#---------------------------------------------------------------------- -# 2d grouping, aggregate many columns + def time_copy_function_multi_col(self, df): + df.groupby(['key', 'key2']).apply(self.df_copy_function) -class groupby_frame_cython_many_columns(object): - goal_time = 0.2 - - def setup(self): - self.labels = np.random.randint(0, 100, size=1000) - self.df = DataFrame(randn(1000, 1000)) - - def time_sum(self): - self.df.groupby(self.labels).sum() + def time_copy_overhead_single_col(self, df): + df.groupby('key').apply(self.df_copy_function) -#---------------------------------------------------------------------- -# single key, long, integer key - -class groupby_frame_singlekey_integer(object): - goal_time = 0.2 - - def setup(self): - self.data = np.random.randn(100000, 1) - self.labels = np.random.randint(0, 1000, size=100000) - self.df = DataFrame(self.data) - - def time_sum(self): - self.df.groupby(self.labels).sum() - - -#---------------------------------------------------------------------- -# DataFrame nth - -class groupby_nth(object): - goal_time = 0.2 +class Groups(object): - def setup(self): - self.df = DataFrame(np.random.randint(1, 100, (10000, 2))) + param_names = ['key'] + params = ['int64_small', 'int64_large', 'object_small', 'object_large'] - def time_groupby_frame_nth_any(self): - self.df.groupby(0).nth(0, dropna='any') + def setup_cache(self): + size = 10**6 + data = {'int64_small': Series(np.random.randint(0, 100, size=size)), + 'int64_large': Series(np.random.randint(0, 10000, size=size)), + 'object_small': Series( + tm.makeStringIndex(100).take( + np.random.randint(0, 100, size=size))), + 'object_large': Series( + tm.makeStringIndex(10000).take( + np.random.randint(0, 10000, size=size)))} + return data - def time_groupby_frame_nth_none(self): - self.df.groupby(0).nth(0) + def setup(self, data, key): + self.ser = data[key] - def time_groupby_series_nth_any(self): - self.df[1].groupby(self.df[0]).nth(0, dropna='any') + def time_series_groups(self, data, key): + self.ser.groupby(self.ser).groups - def time_groupby_series_nth_none(self): - self.df[1].groupby(self.df[0]).nth(0) +class GroupManyLabels(object): -#---------------------------------------------------------------------- -# groupby_indices replacement, chop up Series + params = [1, 1000] + param_names = ['ncols'] -class groupby_indices(object): - goal_time = 0.2 + def setup(self, ncols): + N = 1000 + data = np.random.randn(N, ncols) + self.labels = np.random.randint(0, 100, size=N) + self.df = DataFrame(data) - def setup(self): - try: - self.rng = date_range('1/1/2000', '12/31/2005', freq='H') - (self.year, self.month, self.day) = (self.rng.year, self.rng.month, self.rng.day) - except: - self.rng = date_range('1/1/2000', '12/31/2000', offset=datetools.Hour()) - self.year = self.rng.map((lambda x: x.year)) - self.month = self.rng.map((lambda x: x.month)) - self.day = self.rng.map((lambda x: x.day)) - self.ts = Series(np.random.randn(len(self.rng)), index=self.rng) - - def time_groupby_indices(self): - len(self.ts.groupby([self.year, self.month, self.day])) + def time_sum(self, ncols): + self.df.groupby(self.labels).sum() -class groupby_int64_overflow(object): - goal_time = 0.2 +class Nth(object): - def setup(self): - self.arr = np.random.randint(((-1) << 12), (1 << 12), ((1 << 17), 5)) - self.i = np.random.choice(len(self.arr), (len(self.arr) * 5)) - self.arr = np.vstack((self.arr, self.arr[self.i])) - self.i = np.random.permutation(len(self.arr)) - self.arr = self.arr[self.i] - self.df = DataFrame(self.arr, columns=list('abcde')) - (self.df['jim'], self.df['joe']) = (np.random.randn(2, len(self.df)) * 10) + param_names = ['dtype'] + params = ['float32', 'float64', 'datetime', 'object'] - def time_groupby_int64_overflow(self): - self.df.groupby(list('abcde')).max() + def setup(self, dtype): + N = 10**5 + # with datetimes (GH7555) + if dtype == 'datetime': + values = date_range('1/1/2011', periods=N, freq='s') + elif dtype == 'object': + values = ['foo'] * N + else: + values = np.arange(N).astype(dtype) + key = np.arange(N) + self.df = DataFrame({'key': key, 'values': values}) + self.df.iloc[1, 1] = np.nan # insert missing data -#---------------------------------------------------------------------- -# count() speed + def time_frame_nth_any(self, dtype): + self.df.groupby('key').nth(0, dropna='any') -class groupby_multi_count(object): - goal_time = 0.2 + def time_groupby_nth_all(self, dtype): + self.df.groupby('key').nth(0, dropna='all') - def setup(self): - self.n = 10000 - self.offsets = np.random.randint(self.n, size=self.n).astype('timedelta64[ns]') - self.dates = (np.datetime64('now') + self.offsets) - self.dates[(np.random.rand(self.n) > 0.5)] = np.datetime64('nat') - self.offsets[(np.random.rand(self.n) > 0.5)] = np.timedelta64('nat') - self.value2 = np.random.randn(self.n) - self.value2[(np.random.rand(self.n) > 0.5)] = np.nan - self.obj = np.random.choice(list('ab'), size=self.n).astype(object) - self.obj[(np.random.randn(self.n) > 0.5)] = np.nan - self.df = DataFrame({'key1': np.random.randint(0, 500, size=self.n), - 'key2': np.random.randint(0, 100, size=self.n), - 'dates': self.dates, - 'value2': self.value2, - 'value3': np.random.randn(self.n), - 'ints': np.random.randint(0, 1000, size=self.n), - 'obj': self.obj, - 'offsets': self.offsets, }) - - def time_groupby_multi_count(self): - self.df.groupby(['key1', 'key2']).count() - - -class groupby_int_count(object): - goal_time = 0.2 + def time_frame_nth(self, dtype): + self.df.groupby('key').nth(0) - def setup(self): - self.n = 10000 - self.df = DataFrame({'key1': randint(0, 500, size=self.n), - 'key2': randint(0, 100, size=self.n), - 'ints': randint(0, 1000, size=self.n), - 'ints2': randint(0, 1000, size=self.n), }) + def time_series_nth_any(self, dtype): + self.df['values'].groupby(self.df['key']).nth(0, dropna='any') - def time_groupby_int_count(self): - self.df.groupby(['key1', 'key2']).count() + def time_series_nth_all(self, dtype): + self.df['values'].groupby(self.df['key']).nth(0, dropna='all') + def time_series_nth(self, dtype): + self.df['values'].groupby(self.df['key']).nth(0) -#---------------------------------------------------------------------- -# nunique() speed -class groupby_nunique(object): +class DateAttributes(object): def setup(self): - self.n = 10000 - self.df = DataFrame({'key1': randint(0, 500, size=self.n), - 'key2': randint(0, 100, size=self.n), - 'ints': randint(0, 1000, size=self.n), - 'ints2': randint(0, 1000, size=self.n), }) - - def time_groupby_nunique(self): - self.df.groupby(['key1', 'key2']).nunique() + rng = date_range('1/1/2000', '12/31/2005', freq='H') + self.year, self.month, self.day = rng.year, rng.month, rng.day + self.ts = Series(np.random.randn(len(rng)), index=rng) + def time_len_groupby_object(self): + len(self.ts.groupby([self.year, self.month, self.day])) -#---------------------------------------------------------------------- -# group with different functions per column -class groupby_agg_multi(object): - goal_time = 0.2 +class Int64(object): def setup(self): - self.fac1 = np.array(['A', 'B', 'C'], dtype='O') - self.fac2 = np.array(['one', 'two'], dtype='O') - self.df = DataFrame({'key1': self.fac1.take(np.random.randint(0, 3, size=100000)), 'key2': self.fac2.take(np.random.randint(0, 2, size=100000)), 'value1': np.random.randn(100000), 'value2': np.random.randn(100000), 'value3': np.random.randn(100000), }) - - def time_groupby_multi_different_functions(self): - self.df.groupby(['key1', 'key2']).agg({'value1': 'mean', 'value2': 'var', 'value3': 'sum'}) - - def time_groupby_multi_different_numpy_functions(self): - self.df.groupby(['key1', 'key2']).agg({'value1': np.mean, 'value2': np.var, 'value3': np.sum}) - - -class groupby_multi_index(object): - goal_time = 0.2 + arr = np.random.randint(-1 << 12, 1 << 12, (1 << 17, 5)) + i = np.random.choice(len(arr), len(arr) * 5) + arr = np.vstack((arr, arr[i])) + i = np.random.permutation(len(arr)) + arr = arr[i] + self.cols = list('abcde') + self.df = DataFrame(arr, columns=self.cols) + self.df['jim'], self.df['joe'] = np.random.randn(2, len(self.df)) * 10 + + def time_overflow(self): + self.df.groupby(self.cols).max() + + +class CountMultiDtype(object): + + def setup_cache(self): + n = 10000 + offsets = np.random.randint(n, size=n).astype('timedelta64[ns]') + dates = np.datetime64('now') + offsets + dates[np.random.rand(n) > 0.5] = np.datetime64('nat') + offsets[np.random.rand(n) > 0.5] = np.timedelta64('nat') + value2 = np.random.randn(n) + value2[np.random.rand(n) > 0.5] = np.nan + obj = np.random.choice(list('ab'), size=n).astype(object) + obj[np.random.randn(n) > 0.5] = np.nan + df = DataFrame({'key1': np.random.randint(0, 500, size=n), + 'key2': np.random.randint(0, 100, size=n), + 'dates': dates, + 'value2': value2, + 'value3': np.random.randn(n), + 'ints': np.random.randint(0, 1000, size=n), + 'obj': obj, + 'offsets': offsets}) + return df + + def time_multi_count(self, df): + df.groupby(['key1', 'key2']).count() + + +class CountMultiInt(object): + + def setup_cache(self): + n = 10000 + df = DataFrame({'key1': np.random.randint(0, 500, size=n), + 'key2': np.random.randint(0, 100, size=n), + 'ints': np.random.randint(0, 1000, size=n), + 'ints2': np.random.randint(0, 1000, size=n)}) + return df + + def time_multi_int_count(self, df): + df.groupby(['key1', 'key2']).count() + + def time_multi_int_nunique(self, df): + df.groupby(['key1', 'key2']).nunique() + + +class AggFunctions(object): + + def setup_cache(self): + N = 10**5 + fac1 = np.array(['A', 'B', 'C'], dtype='O') + fac2 = np.array(['one', 'two'], dtype='O') + df = DataFrame({'key1': fac1.take(np.random.randint(0, 3, size=N)), + 'key2': fac2.take(np.random.randint(0, 2, size=N)), + 'value1': np.random.randn(N), + 'value2': np.random.randn(N), + 'value3': np.random.randn(N)}) + return df + + def time_different_str_functions(self, df): + df.groupby(['key1', 'key2']).agg({'value1': 'mean', + 'value2': 'var', + 'value3': 'sum'}) + + def time_different_numpy_functions(self, df): + df.groupby(['key1', 'key2']).agg({'value1': np.mean, + 'value2': np.var, + 'value3': np.sum}) + + def time_different_python_functions_multicol(self, df): + df.groupby(['key1', 'key2']).agg([sum, min, max]) + + def time_different_python_functions_singlecol(self, df): + df.groupby('key1').agg([sum, min, max]) + + +class GroupStrings(object): def setup(self): - self.n = (((5 * 7) * 11) * (1 << 9)) - self.alpha = list(map(''.join, product((ascii_letters + digits), repeat=4))) - self.f = (lambda k: np.repeat(np.random.choice(self.alpha, (self.n // k)), k)) - self.df = DataFrame({'a': self.f(11), 'b': self.f(7), 'c': self.f(5), 'd': self.f(1), }) + n = 2 * 10**5 + alpha = list(map(''.join, product(ascii_letters, repeat=4))) + data = np.random.choice(alpha, (n // 5, 4), replace=False) + data = np.repeat(data, 5, axis=0) + self.df = DataFrame(data, columns=list('abcd')) self.df['joe'] = (np.random.randn(len(self.df)) * 10).round(3) - self.i = np.random.permutation(len(self.df)) - self.df = self.df.iloc[self.i].reset_index(drop=True).copy() + self.df = self.df.sample(frac=1).reset_index(drop=True) - def time_groupby_multi_index(self): + def time_multi_columns(self): self.df.groupby(list('abcd')).max() -class groupby_multi(object): - goal_time = 0.2 - - def setup(self): - self.N = 100000 - self.ngroups = 100 - self.df = DataFrame({'key1': self.get_test_data(ngroups=self.ngroups), 'key2': self.get_test_data(ngroups=self.ngroups), 'data1': np.random.randn(self.N), 'data2': np.random.randn(self.N), }) - self.simple_series = Series(np.random.randn(self.N)) - self.key1 = self.df['key1'] - - def get_test_data(self, ngroups=100, n=100000): - self.unique_groups = range(self.ngroups) - self.arr = np.asarray(np.tile(self.unique_groups, (n / self.ngroups)), dtype=object) - if (len(self.arr) < n): - self.arr = np.asarray((list(self.arr) + self.unique_groups[:(n - len(self.arr))]), dtype=object) - random.shuffle(self.arr) - return self.arr - - def f(self): - self.df.groupby(['key1', 'key2']).agg((lambda x: x.values.sum())) +class MultiColumn(object): - def time_groupby_multi_cython(self): - self.df.groupby(['key1', 'key2']).sum() + def setup_cache(self): + N = 10**5 + key1 = np.tile(np.arange(100, dtype=object), 1000) + key2 = key1.copy() + np.random.shuffle(key1) + np.random.shuffle(key2) + df = DataFrame({'key1': key1, + 'key2': key2, + 'data1': np.random.randn(N), + 'data2': np.random.randn(N)}) + return df - def time_groupby_multi_python(self): - self.df.groupby(['key1', 'key2'])['data1'].agg((lambda x: x.values.sum())) + def time_lambda_sum(self, df): + df.groupby(['key1', 'key2']).agg(lambda x: x.values.sum()) - def time_groupby_multi_series_op(self): - self.df.groupby(['key1', 'key2'])['data1'].agg(np.std) + def time_cython_sum(self, df): + df.groupby(['key1', 'key2']).sum() - def time_groupby_series_simple_cython(self): - self.simple_series.groupby(self.key1).sum() + def time_col_select_lambda_sum(self, df): + df.groupby(['key1', 'key2'])['data1'].agg(lambda x: x.values.sum()) - def time_groupby_series_simple_rank(self): - self.df.groupby('key1').rank(pct=True) + def time_col_select_numpy_sum(self, df): + df.groupby(['key1', 'key2'])['data1'].agg(np.sum) -#---------------------------------------------------------------------- -# size() speed - -class groupby_size(object): - goal_time = 0.2 +class Size(object): def setup(self): - self.n = 100000 - self.offsets = np.random.randint(self.n, size=self.n).astype('timedelta64[ns]') - self.dates = (np.datetime64('now') + self.offsets) - self.df = DataFrame({'key1': np.random.randint(0, 500, size=self.n), 'key2': np.random.randint(0, 100, size=self.n), 'value1': np.random.randn(self.n), 'value2': np.random.randn(self.n), 'value3': np.random.randn(self.n), 'dates': self.dates, }) - - def time_groupby_multi_size(self): + n = 10**5 + offsets = np.random.randint(n, size=n).astype('timedelta64[ns]') + dates = np.datetime64('now') + offsets + self.df = DataFrame({'key1': np.random.randint(0, 500, size=n), + 'key2': np.random.randint(0, 100, size=n), + 'value1': np.random.randn(n), + 'value2': np.random.randn(n), + 'value3': np.random.randn(n), + 'dates': dates}) + self.draws = Series(np.random.randn(n)) + labels = Series(['foo', 'bar', 'baz', 'qux'] * (n // 4)) + self.cats = labels.astype('category') + + def time_multi_size(self): self.df.groupby(['key1', 'key2']).size() - def time_groupby_dt_size(self): - self.df.groupby(['dates']).size() + def time_dt_timegrouper_size(self): + with warnings.catch_warnings(record=True): + self.df.groupby(TimeGrouper(key='dates', freq='M')).size() - def time_groupby_dt_timegrouper_size(self): - self.df.groupby(TimeGrouper(key='dates', freq='M')).size() + def time_category_size(self): + self.draws.groupby(self.cats).size() -#---------------------------------------------------------------------- -# groupby with a variable value for ngroups +class GroupByMethods(object): -class GroupBySuite(object): - goal_time = 0.2 + param_names = ['dtype', 'method', 'application'] + params = [['int', 'float', 'object', 'datetime'], + ['all', 'any', 'bfill', 'count', 'cumcount', 'cummax', 'cummin', + 'cumprod', 'cumsum', 'describe', 'ffill', 'first', 'head', + 'last', 'mad', 'max', 'min', 'median', 'mean', 'nunique', + 'pct_change', 'prod', 'quantile', 'rank', 'sem', 'shift', + 'size', 'skew', 'std', 'sum', 'tail', 'unique', 'value_counts', + 'var'], + ['direct', 'transformation']] - param_names = ['dtype', 'ngroups'] - params = [['int', 'float'], [100, 10000]] - - def setup(self, dtype, ngroups): - np.random.seed(1234) + def setup(self, dtype, method, application): + if method in method_blacklist.get(dtype, {}): + raise NotImplementedError # skip benchmark + ngroups = 1000 size = ngroups * 2 rng = np.arange(ngroups) values = rng.take(np.random.randint(0, ngroups, size=size)) if dtype == 'int': key = np.random.randint(0, size, size=size) - else: + elif dtype == 'float': key = np.concatenate([np.random.random(ngroups) * 0.1, np.random.random(ngroups) * 10.0]) + elif dtype == 'object': + key = ['foo'] * size + elif dtype == 'datetime': + key = date_range('1/1/2011', periods=size, freq='s') - self.df = DataFrame({'values': values, - 'key': key}) - - def time_all(self, dtype, ngroups): - self.df.groupby('key')['values'].all() - - def time_any(self, dtype, ngroups): - self.df.groupby('key')['values'].any() - - def time_count(self, dtype, ngroups): - self.df.groupby('key')['values'].count() - - def time_cumcount(self, dtype, ngroups): - self.df.groupby('key')['values'].cumcount() - - def time_cummax(self, dtype, ngroups): - self.df.groupby('key')['values'].cummax() - - def time_cummin(self, dtype, ngroups): - self.df.groupby('key')['values'].cummin() - - def time_cumprod(self, dtype, ngroups): - self.df.groupby('key')['values'].cumprod() - - def time_cumsum(self, dtype, ngroups): - self.df.groupby('key')['values'].cumsum() - - def time_describe(self, dtype, ngroups): - self.df.groupby('key')['values'].describe() - - def time_diff(self, dtype, ngroups): - self.df.groupby('key')['values'].diff() - - def time_first(self, dtype, ngroups): - self.df.groupby('key')['values'].first() - - def time_head(self, dtype, ngroups): - self.df.groupby('key')['values'].head() - - def time_last(self, dtype, ngroups): - self.df.groupby('key')['values'].last() - - def time_mad(self, dtype, ngroups): - self.df.groupby('key')['values'].mad() - - def time_max(self, dtype, ngroups): - self.df.groupby('key')['values'].max() - - def time_mean(self, dtype, ngroups): - self.df.groupby('key')['values'].mean() - - def time_median(self, dtype, ngroups): - self.df.groupby('key')['values'].median() - - def time_min(self, dtype, ngroups): - self.df.groupby('key')['values'].min() - - def time_nunique(self, dtype, ngroups): - self.df.groupby('key')['values'].nunique() - - def time_pct_change(self, dtype, ngroups): - self.df.groupby('key')['values'].pct_change() - - def time_prod(self, dtype, ngroups): - self.df.groupby('key')['values'].prod() - - def time_rank(self, dtype, ngroups): - self.df.groupby('key')['values'].rank() - - def time_sem(self, dtype, ngroups): - self.df.groupby('key')['values'].sem() + df = DataFrame({'values': values, 'key': key}) - def time_size(self, dtype, ngroups): - self.df.groupby('key')['values'].size() + if application == 'transform': + if method == 'describe': + raise NotImplementedError - def time_skew(self, dtype, ngroups): - self.df.groupby('key')['values'].skew() + self.as_group_method = lambda: df.groupby( + 'key')['values'].transform(method) + self.as_field_method = lambda: df.groupby( + 'values')['key'].transform(method) + else: + self.as_group_method = getattr(df.groupby('key')['values'], method) + self.as_field_method = getattr(df.groupby('values')['key'], method) - def time_std(self, dtype, ngroups): - self.df.groupby('key')['values'].std() + def time_dtype_as_group(self, dtype, method, application): + self.as_group_method() - def time_sum(self, dtype, ngroups): - self.df.groupby('key')['values'].sum() + def time_dtype_as_field(self, dtype, method, application): + self.as_field_method() - def time_tail(self, dtype, ngroups): - self.df.groupby('key')['values'].tail() - def time_unique(self, dtype, ngroups): - self.df.groupby('key')['values'].unique() +class RankWithTies(object): + # GH 21237 + param_names = ['dtype', 'tie_method'] + params = [['float64', 'float32', 'int64', 'datetime64'], + ['first', 'average', 'dense', 'min', 'max']] - def time_value_counts(self, dtype, ngroups): - self.df.groupby('key')['values'].value_counts() + def setup(self, dtype, tie_method): + N = 10**4 + if dtype == 'datetime64': + data = np.array([Timestamp("2011/01/01")] * N, dtype=dtype) + else: + data = np.array([1] * N, dtype=dtype) + self.df = DataFrame({'values': data, 'key': ['foo'] * N}) - def time_var(self, dtype, ngroups): - self.df.groupby('key')['values'].var() + def time_rank_ties(self, dtype, tie_method): + self.df.groupby('key').rank(method=tie_method) -class groupby_float32(object): +class Float32(object): # GH 13335 - goal_time = 0.2 - def setup(self): tmp1 = (np.random.random(10000) * 0.1).astype(np.float32) tmp2 = (np.random.random(10000) * 10.0).astype(np.float32) @@ -506,27 +386,26 @@ def setup(self): arr = np.repeat(tmp, 10) self.df = DataFrame(dict(a=arr, b=arr)) - def time_groupby_sum(self): + def time_sum(self): self.df.groupby(['a'])['b'].sum() -class groupby_categorical(object): - goal_time = 0.2 +class Categories(object): def setup(self): - N = 100000 + N = 10**5 arr = np.random.random(N) - - self.df = DataFrame(dict( - a=Categorical(np.random.randint(10000, size=N)), - b=arr)) - self.df_ordered = DataFrame(dict( - a=Categorical(np.random.randint(10000, size=N), ordered=True), - b=arr)) - self.df_extra_cat = DataFrame(dict( - a=Categorical(np.random.randint(100, size=N), - categories=np.arange(10000)), - b=arr)) + data = {'a': Categorical(np.random.randint(10000, size=N)), + 'b': arr} + self.df = DataFrame(data) + data = {'a': Categorical(np.random.randint(10000, size=N), + ordered=True), + 'b': arr} + self.df_ordered = DataFrame(data) + data = {'a': Categorical(np.random.randint(100, size=N), + categories=np.arange(10000)), + 'b': arr} + self.df_extra_cat = DataFrame(data) def time_groupby_sort(self): self.df.groupby('a')['b'].count() @@ -547,130 +426,64 @@ def time_groupby_extra_cat_nosort(self): self.df_extra_cat.groupby('a', sort=False)['b'].count() -class groupby_period(object): +class Datelike(object): # GH 14338 - goal_time = 0.2 - - def make_grouper(self, N): - return pd.period_range('1900-01-01', freq='D', periods=N) - - def setup(self): - N = 10000 - self.grouper = self.make_grouper(N) - self.df = pd.DataFrame(np.random.randn(N, 2)) - - def time_groupby_sum(self): + params = ['period_range', 'date_range', 'date_range_tz'] + param_names = ['grouper'] + + def setup(self, grouper): + N = 10**4 + rng_map = {'period_range': period_range, + 'date_range': date_range, + 'date_range_tz': partial(date_range, tz='US/Central')} + self.grouper = rng_map[grouper]('1900-01-01', freq='D', periods=N) + self.df = DataFrame(np.random.randn(10**4, 2)) + + def time_sum(self, grouper): self.df.groupby(self.grouper).sum() -class groupby_datetime(groupby_period): - def make_grouper(self, N): - return pd.date_range('1900-01-01', freq='D', periods=N) - - -class groupby_datetimetz(groupby_period): - def make_grouper(self, N): - return pd.date_range('1900-01-01', freq='D', periods=N, - tz='US/Central') - -#---------------------------------------------------------------------- -# Series.value_counts - -class series_value_counts(object): - goal_time = 0.2 - - def setup(self): - self.s = Series(np.random.randint(0, 1000, size=100000)) - self.s2 = self.s.astype(float) - - self.K = 1000 - self.N = 100000 - self.uniques = tm.makeStringIndex(self.K).values - self.s3 = Series(np.tile(self.uniques, (self.N // self.K))) - - def time_value_counts_int64(self): - self.s.value_counts() - - def time_value_counts_float64(self): - self.s2.value_counts() - - def time_value_counts_strings(self): - self.s.value_counts() - - -#---------------------------------------------------------------------- -# pivot_table - -class groupby_pivot_table(object): - goal_time = 0.2 - - def setup(self): - self.fac1 = np.array(['A', 'B', 'C'], dtype='O') - self.fac2 = np.array(['one', 'two'], dtype='O') - self.ind1 = np.random.randint(0, 3, size=100000) - self.ind2 = np.random.randint(0, 2, size=100000) - self.df = DataFrame({'key1': self.fac1.take(self.ind1), 'key2': self.fac2.take(self.ind2), 'key3': self.fac2.take(self.ind2), 'value1': np.random.randn(100000), 'value2': np.random.randn(100000), 'value3': np.random.randn(100000), }) - - def time_groupby_pivot_table(self): - self.df.pivot_table(index='key1', columns=['key2', 'key3']) - - -#---------------------------------------------------------------------- -# Sum booleans #2692 - -class groupby_sum_booleans(object): - goal_time = 0.2 - +class SumBools(object): + # GH 2692 def setup(self): - self.N = 500 - self.df = DataFrame({'ii': range(self.N), 'bb': [True for x in range(self.N)], }) + N = 500 + self.df = DataFrame({'ii': range(N), + 'bb': [True] * N}) def time_groupby_sum_booleans(self): self.df.groupby('ii').sum() -#---------------------------------------------------------------------- -# multi-indexed group sum #9049 - -class groupby_sum_multiindex(object): - goal_time = 0.2 +class SumMultiLevel(object): + # GH 9049 + timeout = 120.0 def setup(self): - self.N = 50 - self.df = DataFrame({'A': (list(range(self.N)) * 2), 'B': list(range((self.N * 2))), 'C': 1, }).set_index(['A', 'B']) + N = 50 + self.df = DataFrame({'A': list(range(N)) * 2, + 'B': range(N * 2), + 'C': 1}).set_index(['A', 'B']) def time_groupby_sum_multiindex(self): self.df.groupby(level=[0, 1]).sum() -#------------------------------------------------------------------------------- -# Transform testing - class Transform(object): - goal_time = 0.2 def setup(self): n1 = 400 n2 = 250 - - index = MultiIndex( - levels=[np.arange(n1), pd.util.testing.makeStringIndex(n2)], - labels=[[i for i in range(n1) for _ in range(n2)], - (list(range(n2)) * n1)], - names=['lev1', 'lev2']) - - data = DataFrame(np.random.randn(n1 * n2, 3), - index=index, columns=['col1', 'col20', 'col3']) - step = int((n1 * n2 * 0.1)) - for col in range(len(data.columns)): - idx = col - while (idx < len(data)): - data.set_value(data.index[idx], data.columns[col], np.nan) - idx += step + index = MultiIndex(levels=[np.arange(n1), tm.makeStringIndex(n2)], + codes=[np.repeat(range(n1), n2).tolist(), + list(range(n2)) * n1], + names=['lev1', 'lev2']) + arr = np.random.randn(n1 * n2, 3) + arr[::10000, 0] = np.nan + arr[1::10000, 1] = np.nan + arr[2::10000, 2] = np.nan + data = DataFrame(arr, index=index, columns=['col1', 'col20', 'col3']) self.df = data - self.f_fillna = (lambda x: x.fillna(method='pad')) - np.random.seed(2718281) n = 20000 self.df1 = DataFrame(np.random.randint(1, n, (n, 3)), columns=['jim', 'joe', 'jolie']) @@ -682,10 +495,10 @@ def setup(self): self.df4 = self.df3.copy() self.df4['jim'] = self.df4['joe'] - def time_transform_func(self): - self.df.groupby(level='lev2').transform(self.f_fillna) + def time_transform_lambda_max(self): + self.df.groupby(level='lev1').transform(lambda x: max(x)) - def time_transform_ufunc(self): + def time_transform_ufunc_max(self): self.df.groupby(level='lev1').transform(np.max) def time_transform_multi_key1(self): @@ -701,63 +514,30 @@ def time_transform_multi_key4(self): self.df4.groupby(['jim', 'joe'])['jolie'].transform('max') - - -np.random.seed(0) -N = 120000 -N_TRANSITIONS = 1400 -transition_points = np.random.permutation(np.arange(N))[:N_TRANSITIONS] -transition_points.sort() -transitions = np.zeros((N,), dtype=np.bool) -transitions[transition_points] = True -g = transitions.cumsum() -df = DataFrame({'signal': np.random.rand(N), }) - - - - - -class groupby_transform_series(object): - goal_time = 0.2 +class TransformBools(object): def setup(self): - np.random.seed(0) N = 120000 transition_points = np.sort(np.random.choice(np.arange(N), 1400)) - transitions = np.zeros((N,), dtype=np.bool) + transitions = np.zeros(N, dtype=np.bool) transitions[transition_points] = True self.g = transitions.cumsum() self.df = DataFrame({'signal': np.random.rand(N)}) - def time_groupby_transform_series(self): + def time_transform_mean(self): self.df['signal'].groupby(self.g).transform(np.mean) -class groupby_transform_series2(object): - goal_time = 0.2 - +class TransformNaN(object): + # GH 12737 def setup(self): - np.random.seed(0) - self.df = DataFrame({'key': (np.arange(100000) // 3), - 'val': np.random.randn(100000)}) - - self.df_nans = pd.DataFrame({'key': np.repeat(np.arange(1000), 10), - 'B': np.nan, - 'C': np.nan}) - self.df_nans.ix[4::10, 'B':'C'] = 5 + self.df_nans = DataFrame({'key': np.repeat(np.arange(1000), 10), + 'B': np.nan, + 'C': np.nan}) + self.df_nans.loc[4::10, 'B':'C'] = 5 - def time_transform_series2(self): - self.df.groupby('key')['val'].transform(np.mean) - - def time_cumprod(self): - self.df.groupby('key').cumprod() - - def time_cumsum(self): - self.df.groupby('key').cumsum() + def time_first(self): + self.df_nans.groupby('key').transform('first') - def time_shift(self): - self.df.groupby('key').shift() - def time_transform_dataframe(self): - # GH 12737 - self.df_nans.groupby('key').transform('first') +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/hdfstore_bench.py b/asv_bench/benchmarks/hdfstore_bench.py deleted file mode 100644 index 78de5267a2969..0000000000000 --- a/asv_bench/benchmarks/hdfstore_bench.py +++ /dev/null @@ -1,122 +0,0 @@ -from .pandas_vb_common import * -import os - - -class HDF5(object): - goal_time = 0.2 - - def setup(self): - self.index = tm.makeStringIndex(25000) - self.df = DataFrame({'float1': randn(25000), 'float2': randn(25000),}, - index=self.index) - - self.df_mixed = DataFrame( - {'float1': randn(25000), 'float2': randn(25000), - 'string1': (['foo'] * 25000), - 'bool1': ([True] * 25000), - 'int1': np.random.randint(0, 250000, size=25000),}, - index=self.index) - - self.df_wide = DataFrame(np.random.randn(25000, 100)) - - self.df2 = DataFrame({'float1': randn(25000), 'float2': randn(25000)}, - index=date_range('1/1/2000', periods=25000)) - self.df_wide2 = DataFrame(np.random.randn(25000, 100), - index=date_range('1/1/2000', periods=25000)) - - self.df_dc = DataFrame(np.random.randn(10000, 10), - columns=[('C%03d' % i) for i in range(10)]) - - self.f = '__test__.h5' - self.remove(self.f) - - self.store = HDFStore(self.f) - self.store.put('df1', self.df) - self.store.put('df_mixed', self.df_mixed) - - self.store.append('df5', self.df_mixed) - self.store.append('df7', self.df) - - self.store.append('df9', self.df_wide) - - self.store.append('df11', self.df_wide2) - self.store.append('df12', self.df2) - - def teardown(self): - self.store.close() - - def remove(self, f): - try: - os.remove(self.f) - except: - pass - - def time_read_store(self): - self.store.get('df1') - - def time_read_store_mixed(self): - self.store.get('df_mixed') - - def time_write_store(self): - self.store.put('df2', self.df) - - def time_write_store_mixed(self): - self.store.put('df_mixed2', self.df_mixed) - - def time_read_store_table_mixed(self): - self.store.select('df5') - - def time_write_store_table_mixed(self): - self.store.append('df6', self.df_mixed) - - def time_read_store_table(self): - self.store.select('df7') - - def time_write_store_table(self): - self.store.append('df8', self.df) - - def time_read_store_table_wide(self): - self.store.select('df9') - - def time_write_store_table_wide(self): - self.store.append('df10', self.df_wide) - - def time_write_store_table_dc(self): - self.store.append('df15', self.df, data_columns=True) - - def time_query_store_table_wide(self): - self.store.select('df11', [('index', '>', self.df_wide2.index[10000]), - ('index', '<', self.df_wide2.index[15000])]) - - def time_query_store_table(self): - self.store.select('df12', [('index', '>', self.df2.index[10000]), - ('index', '<', self.df2.index[15000])]) - - -class HDF5Panel(object): - goal_time = 0.2 - - def setup(self): - self.f = '__test__.h5' - self.p = Panel(randn(20, 1000, 25), - items=[('Item%03d' % i) for i in range(20)], - major_axis=date_range('1/1/2000', periods=1000), - minor_axis=[('E%03d' % i) for i in range(25)]) - self.remove(self.f) - self.store = HDFStore(self.f) - self.store.append('p1', self.p) - - def teardown(self): - self.store.close() - - def remove(self, f): - try: - os.remove(self.f) - except: - pass - - def time_read_store_table_panel(self): - self.store.select('p1') - - def time_write_store_table_panel(self): - self.store.append('p2', self.p) diff --git a/asv_bench/benchmarks/index_object.py b/asv_bench/benchmarks/index_object.py index 3fb53ce9b3c98..bbe164d4858ab 100644 --- a/asv_bench/benchmarks/index_object.py +++ b/asv_bench/benchmarks/index_object.py @@ -1,201 +1,184 @@ -from .pandas_vb_common import * +import numpy as np +import pandas.util.testing as tm +from pandas import (Series, date_range, DatetimeIndex, Index, RangeIndex, + Float64Index) class SetOperations(object): - goal_time = 0.2 - def setup(self): - self.rng = date_range('1/1/2000', periods=10000, freq='T') - self.rng2 = self.rng[:(-1)] + params = (['datetime', 'date_string', 'int', 'strings'], + ['intersection', 'union', 'symmetric_difference']) + param_names = ['dtype', 'method'] - # object index with datetime values - if (self.rng.dtype == object): - self.idx_rng = self.rng.view(Index) - else: - self.idx_rng = self.rng.asobject - self.idx_rng2 = self.idx_rng[:(-1)] + def setup(self, dtype, method): + N = 10**5 + dates_left = date_range('1/1/2000', periods=N, freq='T') + fmt = '%Y-%m-%d %H:%M:%S' + date_str_left = Index(dates_left.strftime(fmt)) + int_left = Index(np.arange(N)) + str_left = tm.makeStringIndex(N) + data = {'datetime': {'left': dates_left, 'right': dates_left[:-1]}, + 'date_string': {'left': date_str_left, + 'right': date_str_left[:-1]}, + 'int': {'left': int_left, 'right': int_left[:-1]}, + 'strings': {'left': str_left, 'right': str_left[:-1]}} + self.left = data[dtype]['left'] + self.right = data[dtype]['right'] - # other datetime - N = 100000 - A = N - 20000 - B = N + 20000 - self.dtidx1 = DatetimeIndex(range(N)) - self.dtidx2 = DatetimeIndex(range(A, B)) - self.dtidx3 = DatetimeIndex(range(N, B)) - - # integer - self.N = 1000000 - self.options = np.arange(self.N) - self.left = Index( - self.options.take(np.random.permutation(self.N)[:(self.N // 2)])) - self.right = Index( - self.options.take(np.random.permutation(self.N)[:(self.N // 2)])) - - # strings - N = 10000 - strs = tm.rands_array(10, N) - self.leftstr = Index(strs[:N * 2 // 3]) - self.rightstr = Index(strs[N // 3:]) + def time_operation(self, dtype, method): + getattr(self.left, method)(self.right) - def time_datetime_intersection(self): - self.rng.intersection(self.rng2) - def time_datetime_union(self): - self.rng.union(self.rng2) +class SetDisjoint(object): - def time_datetime_difference(self): - self.dtidx1.difference(self.dtidx2) + def setup(self): + N = 10**5 + B = N + 20000 + self.datetime_left = DatetimeIndex(range(N)) + self.datetime_right = DatetimeIndex(range(N, B)) def time_datetime_difference_disjoint(self): - self.dtidx1.difference(self.dtidx3) - - def time_datetime_symmetric_difference(self): - self.dtidx1.symmetric_difference(self.dtidx2) - - def time_index_datetime_intersection(self): - self.idx_rng.intersection(self.idx_rng2) - - def time_index_datetime_union(self): - self.idx_rng.union(self.idx_rng2) - - def time_int64_intersection(self): - self.left.intersection(self.right) - - def time_int64_union(self): - self.left.union(self.right) - - def time_int64_difference(self): - self.left.difference(self.right) - - def time_int64_symmetric_difference(self): - self.left.symmetric_difference(self.right) - - def time_str_difference(self): - self.leftstr.difference(self.rightstr) - - def time_str_symmetric_difference(self): - self.leftstr.symmetric_difference(self.rightstr) + self.datetime_left.difference(self.datetime_right) class Datetime(object): - goal_time = 0.2 def setup(self): - self.dr = pd.date_range('20000101', freq='D', periods=10000) + self.dr = date_range('20000101', freq='D', periods=10000) def time_is_dates_only(self): self.dr._is_dates_only -class Float64(object): - goal_time = 0.2 - - def setup(self): - self.idx = tm.makeFloatIndex(1000000) - self.mask = ((np.arange(self.idx.size) % 3) == 0) - self.series_mask = Series(self.mask) - - self.baseidx = np.arange(1000000.0) +class Ops(object): - def time_boolean_indexer(self): - self.idx[self.mask] + sample_time = 0.2 + params = ['float', 'int'] + param_names = ['dtype'] - def time_boolean_series_indexer(self): - self.idx[self.series_mask] + def setup(self, dtype): + N = 10**6 + indexes = {'int': 'makeIntIndex', 'float': 'makeFloatIndex'} + self.index = getattr(tm, indexes[dtype])(N) - def time_construct(self): - Index(self.baseidx) + def time_add(self, dtype): + self.index + 2 - def time_div(self): - (self.idx / 2) + def time_subtract(self, dtype): + self.index - 2 - def time_get(self): - self.idx[1] + def time_multiply(self, dtype): + self.index * 2 - def time_mul(self): - (self.idx * 2) + def time_divide(self, dtype): + self.index / 2 - def time_slice_indexer_basic(self): - self.idx[:(-1)] - - def time_slice_indexer_even(self): - self.idx[::2] + def time_modulo(self, dtype): + self.index % 2 -class StringIndex(object): - goal_time = 0.2 +class Range(object): def setup(self): - self.idx = tm.makeStringIndex(1000000) - self.mask = ((np.arange(1000000) % 3) == 0) - self.series_mask = Series(self.mask) + self.idx_inc = RangeIndex(start=0, stop=10**7, step=3) + self.idx_dec = RangeIndex(start=10**7, stop=-1, step=-3) - def time_boolean_indexer(self): - self.idx[self.mask] + def time_max(self): + self.idx_inc.max() - def time_boolean_series_indexer(self): - self.idx[self.series_mask] + def time_max_trivial(self): + self.idx_dec.max() - def time_slice_indexer_basic(self): - self.idx[:(-1)] + def time_min(self): + self.idx_dec.min() - def time_slice_indexer_even(self): - self.idx[::2] + def time_min_trivial(self): + self.idx_inc.min() -class Multi1(object): - goal_time = 0.2 +class IndexAppend(object): def setup(self): - (n, k) = (200, 5000) - self.levels = [np.arange(n), tm.makeStringIndex(n).values, (1000 + np.arange(n))] - self.labels = [np.random.choice(n, (k * n)) for lev in self.levels] - self.mi = MultiIndex(levels=self.levels, labels=self.labels) - - self.iterables = [tm.makeStringIndex(10000), range(20)] - - def time_duplicated(self): - self.mi.duplicated() - - def time_from_product(self): - MultiIndex.from_product(self.iterables) + N = 10000 + self.range_idx = RangeIndex(0, 100) + self.int_idx = self.range_idx.astype(int) + self.obj_idx = self.int_idx.astype(str) + self.range_idxs = [] + self.int_idxs = [] + self.object_idxs = [] + for i in range(1, N): + r_idx = RangeIndex(i * 100, (i + 1) * 100) + self.range_idxs.append(r_idx) + i_idx = r_idx.astype(int) + self.int_idxs.append(i_idx) + o_idx = i_idx.astype(str) + self.object_idxs.append(o_idx) + + def time_append_range_list(self): + self.range_idx.append(self.range_idxs) + + def time_append_int_list(self): + self.int_idx.append(self.int_idxs) + + def time_append_obj_list(self): + self.obj_idx.append(self.object_idxs) + + +class Indexing(object): + + params = ['String', 'Float', 'Int'] + param_names = ['dtype'] + + def setup(self, dtype): + N = 10**6 + self.idx = getattr(tm, 'make{}Index'.format(dtype))(N) + self.array_mask = (np.arange(N) % 3) == 0 + self.series_mask = Series(self.array_mask) + self.sorted = self.idx.sort_values() + half = N // 2 + self.non_unique = self.idx[:half].append(self.idx[:half]) + self.non_unique_sorted = (self.sorted[:half].append(self.sorted[:half]) + .sort_values()) + self.key = self.sorted[N // 4] + + def time_boolean_array(self, dtype): + self.idx[self.array_mask] + + def time_boolean_series(self, dtype): + self.idx[self.series_mask] -class Multi2(object): - goal_time = 0.2 + def time_get(self, dtype): + self.idx[1] - def setup(self): - self.n = ((((3 * 5) * 7) * 11) * (1 << 10)) - (low, high) = (((-1) << 12), (1 << 12)) - self.f = (lambda k: np.repeat(np.random.randint(low, high, (self.n // k)), k)) - self.i = np.random.permutation(self.n) - self.mi = MultiIndex.from_arrays([self.f(11), self.f(7), self.f(5), self.f(3), self.f(1)])[self.i] + def time_slice(self, dtype): + self.idx[:-1] - self.a = np.repeat(np.arange(100), 1000) - self.b = np.tile(np.arange(1000), 100) - self.midx2 = MultiIndex.from_arrays([self.a, self.b]) - self.midx2 = self.midx2.take(np.random.permutation(np.arange(100000))) + def time_slice_step(self, dtype): + self.idx[::2] - def time_sortlevel_int64(self): - self.mi.sortlevel() + def time_get_loc(self, dtype): + self.idx.get_loc(self.key) - def time_sortlevel_zero(self): - self.midx2.sortlevel(0) + def time_get_loc_sorted(self, dtype): + self.sorted.get_loc(self.key) - def time_sortlevel_one(self): - self.midx2.sortlevel(1) + def time_get_loc_non_unique(self, dtype): + self.non_unique.get_loc(self.key) + def time_get_loc_non_unique_sorted(self, dtype): + self.non_unique_sorted.get_loc(self.key) -class Multi3(object): - goal_time = 0.2 +class Float64IndexMethod(object): + # GH 13166 def setup(self): - self.level1 = range(1000) - self.level2 = date_range(start='1/1/2012', periods=100) - self.mi = MultiIndex.from_product([self.level1, self.level2]) + N = 100000 + a = np.arange(N) + self.ind = Float64Index(a * 4.8000000418824129e-08) + + def time_get_loc(self): + self.ind.get_loc(0) - def time_datetime_level_values_full(self): - self.mi.copy().values - def time_datetime_level_values_sliced(self): - self.mi[:10].values +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/indexing.py b/asv_bench/benchmarks/indexing.py index d938cc6a6dc4d..57ba9cd80e55c 100644 --- a/asv_bench/benchmarks/indexing.py +++ b/asv_bench/benchmarks/indexing.py @@ -1,237 +1,350 @@ -from .pandas_vb_common import * -try: - import pandas.computation.expressions as expr -except: - expr = None +import warnings +import numpy as np +import pandas.util.testing as tm +from pandas import (Series, DataFrame, Panel, MultiIndex, + Int64Index, UInt64Index, Float64Index, + IntervalIndex, CategoricalIndex, + IndexSlice, concat, date_range) -class Int64Indexing(object): - goal_time = 0.2 - def setup(self): - self.s = Series(np.random.rand(1000000)) - - def time_getitem_scalar(self): - self.s[800000] +class NumericSeriesIndexing(object): - def time_getitem_slice(self): - self.s[:800000] + params = [ + (Int64Index, UInt64Index, Float64Index), + ('unique_monotonic_inc', 'nonunique_monotonic_inc'), + ] + param_names = ['index_dtype', 'index_structure'] - def time_getitem_list_like(self): - self.s[[800000]] + def setup(self, index, index_structure): + N = 10**6 + indices = { + 'unique_monotonic_inc': index(range(N)), + 'nonunique_monotonic_inc': index( + list(range(55)) + [54] + list(range(55, N - 1))), + } + self.data = Series(np.random.rand(N), index=indices[index_structure]) + self.array = np.arange(10000) + self.array_list = self.array.tolist() - def time_getitem_array(self): - self.s[np.arange(10000)] + def time_getitem_scalar(self, index, index_structure): + self.data[800000] - def time_iloc_array(self): - self.s.iloc[np.arange(10000)] + def time_getitem_slice(self, index, index_structure): + self.data[:800000] - def time_iloc_list_like(self): - self.s.iloc[[800000]] + def time_getitem_list_like(self, index, index_structure): + self.data[[800000]] - def time_iloc_scalar(self): - self.s.iloc[800000] + def time_getitem_array(self, index, index_structure): + self.data[self.array] - def time_iloc_slice(self): - self.s.iloc[:800000] + def time_getitem_lists(self, index, index_structure): + self.data[self.array_list] - def time_ix_array(self): - self.s.ix[np.arange(10000)] + def time_iloc_array(self, index, index_structure): + self.data.iloc[self.array] - def time_ix_list_like(self): - self.s.ix[[800000]] + def time_iloc_list_like(self, index, index_structure): + self.data.iloc[[800000]] - def time_ix_scalar(self): - self.s.ix[800000] + def time_iloc_scalar(self, index, index_structure): + self.data.iloc[800000] - def time_ix_slice(self): - self.s.ix[:800000] + def time_iloc_slice(self, index, index_structure): + self.data.iloc[:800000] - def time_loc_array(self): - self.s.loc[np.arange(10000)] + def time_ix_array(self, index, index_structure): + self.data.ix[self.array] - def time_loc_list_like(self): - self.s.loc[[800000]] + def time_ix_list_like(self, index, index_structure): + self.data.ix[[800000]] - def time_loc_scalar(self): - self.s.loc[800000] + def time_ix_scalar(self, index, index_structure): + self.data.ix[800000] - def time_loc_slice(self): - self.s.loc[:800000] + def time_ix_slice(self, index, index_structure): + self.data.ix[:800000] + def time_loc_array(self, index, index_structure): + self.data.loc[self.array] -class StringIndexing(object): - goal_time = 0.2 + def time_loc_list_like(self, index, index_structure): + self.data.loc[[800000]] - def setup(self): - self.index = tm.makeStringIndex(1000000) - self.s = Series(np.random.rand(1000000), index=self.index) - self.lbl = self.s.index[800000] + def time_loc_scalar(self, index, index_structure): + self.data.loc[800000] - def time_getitem_label_slice(self): - self.s[:self.lbl] + def time_loc_slice(self, index, index_structure): + self.data.loc[:800000] - def time_getitem_pos_slice(self): - self.s[:800000] - def time_get_value(self): - self.s.get_value(self.lbl) +class NonNumericSeriesIndexing(object): + params = [ + ('string', 'datetime'), + ('unique_monotonic_inc', 'nonunique_monotonic_inc'), + ] + param_names = ['index_dtype', 'index_structure'] -class DatetimeIndexing(object): - goal_time = 0.2 + def setup(self, index, index_structure): + N = 10**6 + indexes = {'string': tm.makeStringIndex(N), + 'datetime': date_range('1900', periods=N, freq='s')} + index = indexes[index] + if index_structure == 'nonunique_monotonic_inc': + index = index.insert(item=index[2], loc=2)[:-1] + self.s = Series(np.random.rand(N), index=index) + self.lbl = index[80000] - def setup(self): - tm.N = 1000 - self.ts = tm.makeTimeSeries() - self.dt = self.ts.index[500] + def time_getitem_label_slice(self, index, index_structure): + self.s[:self.lbl] - def time_getitem_scalar(self): - self.ts[self.dt] + def time_getitem_pos_slice(self, index, index_structure): + self.s[:80000] + def time_get_value(self, index, index_structure): + with warnings.catch_warnings(record=True): + self.s.get_value(self.lbl) -class DataFrameIndexing(object): - goal_time = 0.2 + def time_getitem_scalar(self, index, index_structure): + self.s[self.lbl] - def setup(self): - self.index = tm.makeStringIndex(1000) - self.columns = tm.makeStringIndex(30) - self.df = DataFrame(np.random.randn(1000, 30), index=self.index, - columns=self.columns) - self.idx = self.index[100] - self.col = self.columns[10] + def time_getitem_list_like(self, index, index_structure): + self.s[[self.lbl]] - self.df2 = DataFrame(np.random.randn(10000, 4), - columns=['A', 'B', 'C', 'D']) - self.indexer = (self.df2['B'] > 0) - self.obj_indexer = self.indexer.astype('O') - # duptes - self.idx_dupe = (np.array(range(30)) * 99) - self.df3 = DataFrame({'A': ([0.1] * 1000), 'B': ([1] * 1000),}) - self.df3 = concat([self.df3, (2 * self.df3), (3 * self.df3)]) +class DataFrameStringIndexing(object): - self.df_big = DataFrame(dict(A=(['foo'] * 1000000))) + def setup(self): + index = tm.makeStringIndex(1000) + columns = tm.makeStringIndex(30) + self.df = DataFrame(np.random.randn(1000, 30), index=index, + columns=columns) + self.idx_scalar = index[100] + self.col_scalar = columns[10] + self.bool_indexer = self.df[self.col_scalar] > 0 + self.bool_obj_indexer = self.bool_indexer.astype(object) def time_get_value(self): - self.df.get_value(self.idx, self.col) + with warnings.catch_warnings(record=True): + self.df.get_value(self.idx_scalar, self.col_scalar) + + def time_ix(self): + self.df.ix[self.idx_scalar, self.col_scalar] - def time_get_value_ix(self): - self.df.ix[(self.idx, self.col)] + def time_loc(self): + self.df.loc[self.idx_scalar, self.col_scalar] def time_getitem_scalar(self): - self.df[self.col][self.idx] + self.df[self.col_scalar][self.idx_scalar] def time_boolean_rows(self): - self.df2[self.indexer] + self.df[self.bool_indexer] def time_boolean_rows_object(self): - self.df2[self.obj_indexer] + self.df[self.bool_obj_indexer] + + +class DataFrameNumericIndexing(object): + + def setup(self): + self.idx_dupe = np.array(range(30)) * 99 + self.df = DataFrame(np.random.randn(10000, 5)) + self.df_dup = concat([self.df, 2 * self.df, 3 * self.df]) + self.bool_indexer = [True] * 5000 + [False] * 5000 def time_iloc_dups(self): - self.df3.iloc[self.idx_dupe] + self.df_dup.iloc[self.idx_dupe] def time_loc_dups(self): - self.df3.loc[self.idx_dupe] + self.df_dup.loc[self.idx_dupe] - def time_iloc_big(self): - self.df_big.iloc[:100, 0] + def time_iloc(self): + self.df.iloc[:100, 0] + def time_loc(self): + self.df.loc[:100, 0] -class IndexingMethods(object): - # GH 13166 - goal_time = 0.2 + def time_bool_indexer(self): + self.df[self.bool_indexer] - def setup(self): - a = np.arange(100000) - self.ind = pd.Float64Index(a * 4.8000000418824129e-08) - self.s = Series(np.random.rand(100000)) - self.ts = Series(np.random.rand(100000), - index=date_range('2011-01-01', freq='S', periods=100000)) - self.indexer = ([True, False, True, True, False] * 20000) +class Take(object): - def time_get_loc_float(self): - self.ind.get_loc(0) + params = ['int', 'datetime'] + param_names = ['index'] - def time_take_dtindex(self): - self.ts.take(self.indexer) + def setup(self, index): + N = 100000 + indexes = {'int': Int64Index(np.arange(N)), + 'datetime': date_range('2011-01-01', freq='S', periods=N)} + index = indexes[index] + self.s = Series(np.random.rand(N), index=index) + self.indexer = [True, False, True, True, False] * 20000 - def time_take_intindex(self): + def time_take(self, index): self.s.take(self.indexer) class MultiIndexing(object): - goal_time = 0.2 def setup(self): - self.mi = MultiIndex.from_tuples([(x, y) for x in range(1000) for y in range(1000)]) - self.s = Series(np.random.randn(1000000), index=self.mi) + mi = MultiIndex.from_product([range(1000), range(1000)]) + self.s = Series(np.random.randn(1000000), index=mi) self.df = DataFrame(self.s) - # slicers - np.random.seed(1234) - self.idx = pd.IndexSlice - self.n = 100000 - self.mdt = pandas.DataFrame() - self.mdt['A'] = np.random.choice(range(10000, 45000, 1000), self.n) - self.mdt['B'] = np.random.choice(range(10, 400), self.n) - self.mdt['C'] = np.random.choice(range(1, 150), self.n) - self.mdt['D'] = np.random.choice(range(10000, 45000), self.n) - self.mdt['x'] = np.random.choice(range(400), self.n) - self.mdt['y'] = np.random.choice(range(25), self.n) - self.test_A = 25000 - self.test_B = 25 - self.test_C = 40 - self.test_D = 35000 - self.eps_A = 5000 - self.eps_B = 5 - self.eps_C = 5 - self.eps_D = 5000 - self.mdt2 = self.mdt.set_index(['A', 'B', 'C', 'D']).sortlevel() - self.miint = MultiIndex.from_product( - [np.arange(1000), - np.arange(1000)], names=['one', 'two']) - - import string - self.mistring = MultiIndex.from_product( - [np.arange(1000), - np.arange(20), list(string.ascii_letters)], - names=['one', 'two', 'three']) - - def time_series_xs_mi_ix(self): + n = 100000 + self.mdt = DataFrame({'A': np.random.choice(range(10000, 45000, 1000), + n), + 'B': np.random.choice(range(10, 400), n), + 'C': np.random.choice(range(1, 150), n), + 'D': np.random.choice(range(10000, 45000), n), + 'x': np.random.choice(range(400), n), + 'y': np.random.choice(range(25), n)}) + self.idx = IndexSlice[20000:30000, 20:30, 35:45, 30000:40000] + self.mdt = self.mdt.set_index(['A', 'B', 'C', 'D']).sort_index() + + def time_series_ix(self): self.s.ix[999] - def time_frame_xs_mi_ix(self): + def time_frame_ix(self): self.df.ix[999] - def time_multiindex_slicers(self): - self.mdt2.loc[self.idx[ - (self.test_A - self.eps_A):(self.test_A + self.eps_A), - (self.test_B - self.eps_B):(self.test_B + self.eps_B), - (self.test_C - self.eps_C):(self.test_C + self.eps_C), - (self.test_D - self.eps_D):(self.test_D + self.eps_D)], :] + def time_index_slice(self): + self.mdt.loc[self.idx, :] + + +class IntervalIndexing(object): + + def setup_cache(self): + idx = IntervalIndex.from_breaks(np.arange(1000001)) + monotonic = Series(np.arange(1000000), index=idx) + return monotonic + + def time_getitem_scalar(self, monotonic): + monotonic[80000] + + def time_loc_scalar(self, monotonic): + monotonic.loc[80000] + + def time_getitem_list(self, monotonic): + monotonic[80000:] + + def time_loc_list(self, monotonic): + monotonic.loc[80000:] + + +class CategoricalIndexIndexing(object): + + params = ['monotonic_incr', 'monotonic_decr', 'non_monotonic'] + param_names = ['index'] + + def setup(self, index): + N = 10**5 + values = list('a' * N + 'b' * N + 'c' * N) + indices = { + 'monotonic_incr': CategoricalIndex(values), + 'monotonic_decr': CategoricalIndex(reversed(values)), + 'non_monotonic': CategoricalIndex(list('abc' * N))} + self.data = indices[index] + + self.int_scalar = 10000 + self.int_list = list(range(10000)) + + self.cat_scalar = 'b' + self.cat_list = ['a', 'c'] - def time_multiindex_get_indexer(self): - self.miint.get_indexer( - np.array([(0, 10), (0, 11), (0, 12), - (0, 13), (0, 14), (0, 15), - (0, 16), (0, 17), (0, 18), - (0, 19)], dtype=object)) + def time_getitem_scalar(self, index): + self.data[self.int_scalar] - def time_multiindex_string_get_loc(self): - self.mistring.get_loc((999, 19, 'Z')) + def time_getitem_slice(self, index): + self.data[:self.int_scalar] - def time_is_monotonic(self): - self.miint.is_monotonic + def time_getitem_list_like(self, index): + self.data[[self.int_scalar]] + + def time_getitem_list(self, index): + self.data[self.int_list] + + def time_getitem_bool_array(self, index): + self.data[self.data == self.cat_scalar] + + def time_get_loc_scalar(self, index): + self.data.get_loc(self.cat_scalar) + + def time_get_indexer_list(self, index): + self.data.get_indexer(self.cat_list) class PanelIndexing(object): - goal_time = 0.2 def setup(self): - self.p = Panel(np.random.randn(100, 100, 100)) - self.inds = range(0, 100, 10) + with warnings.catch_warnings(record=True): + self.p = Panel(np.random.randn(100, 100, 100)) + self.inds = range(0, 100, 10) def time_subset(self): - self.p.ix[(self.inds, self.inds, self.inds)] + with warnings.catch_warnings(record=True): + self.p.ix[(self.inds, self.inds, self.inds)] + + +class MethodLookup(object): + + def setup_cache(self): + s = Series() + return s + + def time_lookup_iloc(self, s): + s.iloc + + def time_lookup_ix(self, s): + s.ix + + def time_lookup_loc(self, s): + s.loc + + +class GetItemSingleColumn(object): + + def setup(self): + self.df_string_col = DataFrame(np.random.randn(3000, 1), columns=['A']) + self.df_int_col = DataFrame(np.random.randn(3000, 1)) + + def time_frame_getitem_single_column_label(self): + self.df_string_col['A'] + + def time_frame_getitem_single_column_int(self): + self.df_int_col[0] + + +class AssignTimeseriesIndex(object): + + def setup(self): + N = 100000 + idx = date_range('1/1/2000', periods=N, freq='H') + self.df = DataFrame(np.random.randn(N, 1), columns=['A'], index=idx) + + def time_frame_assign_timeseries_index(self): + self.df['date'] = self.df.index + + +class InsertColumns(object): + + def setup(self): + self.N = 10**3 + self.df = DataFrame(index=range(self.N)) + + def time_insert(self): + np.random.seed(1234) + for i in range(100): + self.df.insert(0, i, np.random.randn(self.N), + allow_duplicates=True) + + def time_assign_with_setitem(self): + np.random.seed(1234) + for i in range(100): + self.df[i] = np.random.randn(self.N) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/indexing_engines.py b/asv_bench/benchmarks/indexing_engines.py new file mode 100644 index 0000000000000..f3d063ee31bc8 --- /dev/null +++ b/asv_bench/benchmarks/indexing_engines.py @@ -0,0 +1,64 @@ +import numpy as np + +from pandas._libs import index as libindex + + +def _get_numeric_engines(): + engine_names = [ + ('Int64Engine', np.int64), ('Int32Engine', np.int32), + ('Int16Engine', np.int16), ('Int8Engine', np.int8), + ('UInt64Engine', np.uint64), ('UInt32Engine', np.uint32), + ('UInt16engine', np.uint16), ('UInt8Engine', np.uint8), + ('Float64Engine', np.float64), ('Float32Engine', np.float32), + ] + return [(getattr(libindex, engine_name), dtype) + for engine_name, dtype in engine_names + if hasattr(libindex, engine_name)] + + +class NumericEngineIndexing(object): + + params = [_get_numeric_engines(), + ['monotonic_incr', 'monotonic_decr', 'non_monotonic'], + ] + param_names = ['engine_and_dtype', 'index_type'] + + def setup(self, engine_and_dtype, index_type): + engine, dtype = engine_and_dtype + N = 10**5 + values = list([1] * N + [2] * N + [3] * N) + arr = { + 'monotonic_incr': np.array(values, dtype=dtype), + 'monotonic_decr': np.array(list(reversed(values)), + dtype=dtype), + 'non_monotonic': np.array([1, 2, 3] * N, dtype=dtype), + }[index_type] + + self.data = engine(lambda: arr, len(arr)) + # code belows avoids populating the mapping etc. while timing. + self.data.get_loc(2) + + def time_get_loc(self, engine_and_dtype, index_type): + self.data.get_loc(2) + + +class ObjectEngineIndexing(object): + + params = [('monotonic_incr', 'monotonic_decr', 'non_monotonic')] + param_names = ['index_type'] + + def setup(self, index_type): + N = 10**5 + values = list('a' * N + 'b' * N + 'c' * N) + arr = { + 'monotonic_incr': np.array(values, dtype=object), + 'monotonic_decr': np.array(list(reversed(values)), dtype=object), + 'non_monotonic': np.array(list('abc') * N, dtype=object), + }[index_type] + + self.data = libindex.ObjectEngine(lambda: arr, len(arr)) + # code belows avoids populating the mapping etc. while timing. + self.data.get_loc('b') + + def time_get_loc(self, index_type): + self.data.get_loc('b') diff --git a/asv_bench/benchmarks/inference.py b/asv_bench/benchmarks/inference.py index 3635438a7f76b..423bd02b93596 100644 --- a/asv_bench/benchmarks/inference.py +++ b/asv_bench/benchmarks/inference.py @@ -1,77 +1,76 @@ -from .pandas_vb_common import * -import pandas as pd +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, Series, to_numeric +from .pandas_vb_common import numeric_dtypes, lib -class DtypeInfer(object): - goal_time = 0.2 +class NumericInferOps(object): # from GH 7332 + params = numeric_dtypes + param_names = ['dtype'] - def setup(self): - self.N = 500000 - self.df_int64 = DataFrame(dict(A=np.arange(self.N, dtype='int64'), - B=np.arange(self.N, dtype='int64'))) - self.df_int32 = DataFrame(dict(A=np.arange(self.N, dtype='int32'), - B=np.arange(self.N, dtype='int32'))) - self.df_uint32 = DataFrame(dict(A=np.arange(self.N, dtype='uint32'), - B=np.arange(self.N, dtype='uint32'))) - self.df_float64 = DataFrame(dict(A=np.arange(self.N, dtype='float64'), - B=np.arange(self.N, dtype='float64'))) - self.df_float32 = DataFrame(dict(A=np.arange(self.N, dtype='float32'), - B=np.arange(self.N, dtype='float32'))) - self.df_datetime64 = DataFrame(dict(A=pd.to_datetime(np.arange(self.N, dtype='int64'), unit='ms'), - B=pd.to_datetime(np.arange(self.N, dtype='int64'), unit='ms'))) - self.df_timedelta64 = DataFrame(dict(A=(self.df_datetime64['A'] - self.df_datetime64['B']), - B=self.df_datetime64['B'])) + def setup(self, dtype): + N = 5 * 10**5 + self.df = DataFrame({'A': np.arange(N).astype(dtype), + 'B': np.arange(N).astype(dtype)}) - def time_int64(self): - (self.df_int64['A'] + self.df_int64['B']) + def time_add(self, dtype): + self.df['A'] + self.df['B'] - def time_int32(self): - (self.df_int32['A'] + self.df_int32['B']) + def time_subtract(self, dtype): + self.df['A'] - self.df['B'] - def time_uint32(self): - (self.df_uint32['A'] + self.df_uint32['B']) + def time_multiply(self, dtype): + self.df['A'] * self.df['B'] - def time_float64(self): - (self.df_float64['A'] + self.df_float64['B']) + def time_divide(self, dtype): + self.df['A'] / self.df['B'] - def time_float32(self): - (self.df_float32['A'] + self.df_float32['B']) + def time_modulo(self, dtype): + self.df['A'] % self.df['B'] - def time_datetime64(self): - (self.df_datetime64['A'] - self.df_datetime64['B']) - def time_timedelta64_1(self): - (self.df_timedelta64['A'] + self.df_timedelta64['B']) +class DateInferOps(object): + # from GH 7332 + def setup_cache(self): + N = 5 * 10**5 + df = DataFrame({'datetime64': np.arange(N).astype('datetime64[ms]')}) + df['timedelta'] = df['datetime64'] - df['datetime64'] + return df - def time_timedelta64_2(self): - (self.df_timedelta64['A'] + self.df_timedelta64['A']) + def time_subtract_datetimes(self, df): + df['datetime64'] - df['datetime64'] + def time_timedelta_plus_datetime(self, df): + df['timedelta'] + df['datetime64'] -class to_numeric(object): - goal_time = 0.2 + def time_add_timedeltas(self, df): + df['timedelta'] + df['timedelta'] - def setup(self): - self.n = 10000 - self.float = Series(np.random.randn(self.n * 100)) - self.numstr = self.float.astype('str') - self.str = Series(tm.makeStringIndex(self.n)) - def time_from_float(self): - pd.to_numeric(self.float) +class ToNumeric(object): + + params = ['ignore', 'coerce'] + param_names = ['errors'] + + def setup(self, errors): + N = 10000 + self.float = Series(np.random.randn(N)) + self.numstr = self.float.astype('str') + self.str = Series(tm.makeStringIndex(N)) - def time_from_numeric_str(self): - pd.to_numeric(self.numstr) + def time_from_float(self, errors): + to_numeric(self.float, errors=errors) - def time_from_str_ignore(self): - pd.to_numeric(self.str, errors='ignore') + def time_from_numeric_str(self, errors): + to_numeric(self.numstr, errors=errors) - def time_from_str_coerce(self): - pd.to_numeric(self.str, errors='coerce') + def time_from_str(self, errors): + to_numeric(self.str, errors=errors) -class to_numeric_downcast(object): +class ToNumericDowncast(object): param_names = ['dtype', 'downcast'] params = [['string-float', 'string-int', 'string-nint', 'datetime64', @@ -81,37 +80,33 @@ class to_numeric_downcast(object): N = 500000 N2 = int(N / 2) - data_dict = { - 'string-int': (['1'] * N2) + ([2] * N2), - 'string-nint': (['-1'] * N2) + ([2] * N2), - 'datetime64': np.repeat(np.array(['1970-01-01', '1970-01-02'], - dtype='datetime64[D]'), N), - 'string-float': (['1.1'] * N2) + ([2] * N2), - 'int-list': ([1] * N2) + ([2] * N2), - 'int32': np.repeat(np.int32(1), N) - } + data_dict = {'string-int': ['1'] * N2 + [2] * N2, + 'string-nint': ['-1'] * N2 + [2] * N2, + 'datetime64': np.repeat(np.array(['1970-01-01', '1970-01-02'], + dtype='datetime64[D]'), N), + 'string-float': ['1.1'] * N2 + [2] * N2, + 'int-list': [1] * N2 + [2] * N2, + 'int32': np.repeat(np.int32(1), N)} def setup(self, dtype, downcast): self.data = self.data_dict[dtype] def time_downcast(self, dtype, downcast): - pd.to_numeric(self.data, downcast=downcast) + to_numeric(self.data, downcast=downcast) class MaybeConvertNumeric(object): - def setup(self): - n = 1000000 - arr = np.repeat([2**63], n) - arr = arr + np.arange(n).astype('uint64') - arr = np.array([arr[i] if i%2 == 0 else - str(arr[i]) for i in range(n)], - dtype=object) - - arr[-1] = -1 - self.data = arr - self.na_values = set() - - def time_convert(self): - pd.lib.maybe_convert_numeric(self.data, self.na_values, - coerce_numeric=False) + def setup_cache(self): + N = 10**6 + arr = np.repeat([2**63], N) + np.arange(N).astype('uint64') + data = arr.astype(object) + data[1::2] = arr[1::2].astype(str) + data[-1] = -1 + return data + + def time_convert(self, data): + lib.maybe_convert_numeric(data, set(), coerce_numeric=False) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/doc/sphinxext/ipython_sphinxext/__init__.py b/asv_bench/benchmarks/io/__init__.py similarity index 100% rename from doc/sphinxext/ipython_sphinxext/__init__.py rename to asv_bench/benchmarks/io/__init__.py diff --git a/asv_bench/benchmarks/io/csv.py b/asv_bench/benchmarks/io/csv.py new file mode 100644 index 0000000000000..d42a15d61fb0d --- /dev/null +++ b/asv_bench/benchmarks/io/csv.py @@ -0,0 +1,236 @@ +import random +import string + +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, Categorical, date_range, read_csv +from pandas.compat import cStringIO as StringIO + +from ..pandas_vb_common import BaseIO + + +class ToCSV(BaseIO): + + fname = '__test__.csv' + params = ['wide', 'long', 'mixed'] + param_names = ['kind'] + + def setup(self, kind): + wide_frame = DataFrame(np.random.randn(3000, 30)) + long_frame = DataFrame({'A': np.arange(50000), + 'B': np.arange(50000) + 1., + 'C': np.arange(50000) + 2., + 'D': np.arange(50000) + 3.}) + mixed_frame = DataFrame({'float': np.random.randn(5000), + 'int': np.random.randn(5000).astype(int), + 'bool': (np.arange(5000) % 2) == 0, + 'datetime': date_range('2001', + freq='s', + periods=5000), + 'object': ['foo'] * 5000}) + mixed_frame.loc[30:500, 'float'] = np.nan + data = {'wide': wide_frame, + 'long': long_frame, + 'mixed': mixed_frame} + self.df = data[kind] + + def time_frame(self, kind): + self.df.to_csv(self.fname) + + +class ToCSVDatetime(BaseIO): + + fname = '__test__.csv' + + def setup(self): + rng = date_range('1/1/2000', periods=1000) + self.data = DataFrame(rng, index=rng) + + def time_frame_date_formatting(self): + self.data.to_csv(self.fname, date_format='%Y%m%d') + + +class StringIORewind(object): + + def data(self, stringio_object): + stringio_object.seek(0) + return stringio_object + + +class ReadCSVDInferDatetimeFormat(StringIORewind): + + params = ([True, False], ['custom', 'iso8601', 'ymd']) + param_names = ['infer_datetime_format', 'format'] + + def setup(self, infer_datetime_format, format): + rng = date_range('1/1/2000', periods=1000) + formats = {'custom': '%m/%d/%Y %H:%M:%S.%f', + 'iso8601': '%Y-%m-%d %H:%M:%S', + 'ymd': '%Y%m%d'} + dt_format = formats[format] + self.StringIO_input = StringIO('\n'.join( + rng.strftime(dt_format).tolist())) + + def time_read_csv(self, infer_datetime_format, format): + read_csv(self.data(self.StringIO_input), + header=None, names=['foo'], parse_dates=['foo'], + infer_datetime_format=infer_datetime_format) + + +class ReadCSVSkipRows(BaseIO): + + fname = '__test__.csv' + params = [None, 10000] + param_names = ['skiprows'] + + def setup(self, skiprows): + N = 20000 + index = tm.makeStringIndex(N) + df = DataFrame({'float1': np.random.randn(N), + 'float2': np.random.randn(N), + 'string1': ['foo'] * N, + 'bool1': [True] * N, + 'int1': np.random.randint(0, N, size=N)}, + index=index) + df.to_csv(self.fname) + + def time_skipprows(self, skiprows): + read_csv(self.fname, skiprows=skiprows) + + +class ReadUint64Integers(StringIORewind): + + def setup(self): + self.na_values = [2**63 + 500] + arr = np.arange(10000).astype('uint64') + 2**63 + self.data1 = StringIO('\n'.join(arr.astype(str).tolist())) + arr = arr.astype(object) + arr[500] = -1 + self.data2 = StringIO('\n'.join(arr.astype(str).tolist())) + + def time_read_uint64(self): + read_csv(self.data(self.data1), header=None, names=['foo']) + + def time_read_uint64_neg_values(self): + read_csv(self.data(self.data2), header=None, names=['foo']) + + def time_read_uint64_na_values(self): + read_csv(self.data(self.data1), header=None, names=['foo'], + na_values=self.na_values) + + +class ReadCSVThousands(BaseIO): + + fname = '__test__.csv' + params = ([',', '|'], [None, ',']) + param_names = ['sep', 'thousands'] + + def setup(self, sep, thousands): + N = 10000 + K = 8 + data = np.random.randn(N, K) * np.random.randint(100, 10000, (N, K)) + df = DataFrame(data) + if thousands is not None: + fmt = ':{}'.format(thousands) + fmt = '{' + fmt + '}' + df = df.applymap(lambda x: fmt.format(x)) + df.to_csv(self.fname, sep=sep) + + def time_thousands(self, sep, thousands): + read_csv(self.fname, sep=sep, thousands=thousands) + + +class ReadCSVComment(StringIORewind): + + def setup(self): + data = ['A,B,C'] + (['1,2,3 # comment'] * 100000) + self.StringIO_input = StringIO('\n'.join(data)) + + def time_comment(self): + read_csv(self.data(self.StringIO_input), comment='#', + header=None, names=list('abc')) + + +class ReadCSVFloatPrecision(StringIORewind): + + params = ([',', ';'], ['.', '_'], [None, 'high', 'round_trip']) + param_names = ['sep', 'decimal', 'float_precision'] + + def setup(self, sep, decimal, float_precision): + floats = [''.join(random.choice(string.digits) for _ in range(28)) + for _ in range(15)] + rows = sep.join(['0{}'.format(decimal) + '{}'] * 3) + '\n' + data = rows * 5 + data = data.format(*floats) * 200 # 1000 x 3 strings csv + self.StringIO_input = StringIO(data) + + def time_read_csv(self, sep, decimal, float_precision): + read_csv(self.data(self.StringIO_input), sep=sep, header=None, + names=list('abc'), float_precision=float_precision) + + def time_read_csv_python_engine(self, sep, decimal, float_precision): + read_csv(self.data(self.StringIO_input), sep=sep, header=None, + engine='python', float_precision=None, names=list('abc')) + + +class ReadCSVCategorical(BaseIO): + + fname = '__test__.csv' + + def setup(self): + N = 100000 + group1 = ['aaaaaaaa', 'bbbbbbb', 'cccccccc', 'dddddddd', 'eeeeeeee'] + df = DataFrame(np.random.choice(group1, (N, 3)), columns=list('abc')) + df.to_csv(self.fname, index=False) + + def time_convert_post(self): + read_csv(self.fname).apply(Categorical) + + def time_convert_direct(self): + read_csv(self.fname, dtype='category') + + +class ReadCSVParseDates(StringIORewind): + + def setup(self): + data = """{},19:00:00,18:56:00,0.8100,2.8100,7.2000,0.0000,280.0000\n + {},20:00:00,19:56:00,0.0100,2.2100,7.2000,0.0000,260.0000\n + {},21:00:00,20:56:00,-0.5900,2.2100,5.7000,0.0000,280.0000\n + {},21:00:00,21:18:00,-0.9900,2.0100,3.6000,0.0000,270.0000\n + {},22:00:00,21:56:00,-0.5900,1.7100,5.1000,0.0000,290.0000\n + """ + two_cols = ['KORD,19990127'] * 5 + data = data.format(*two_cols) + self.StringIO_input = StringIO(data) + + def time_multiple_date(self): + read_csv(self.data(self.StringIO_input), sep=',', header=None, + names=list(string.digits[:9]), + parse_dates=[[1, 2], [1, 3]]) + + def time_baseline(self): + read_csv(self.data(self.StringIO_input), sep=',', header=None, + parse_dates=[1], + names=list(string.digits[:9])) + + +class ReadCSVMemoryGrowth(BaseIO): + + chunksize = 20 + num_rows = 1000 + fname = "__test__.csv" + + def setup(self): + with open(self.fname, "w") as f: + for i in range(self.num_rows): + f.write("{i}\n".format(i=i)) + + def mem_parser_chunks(self): + # see gh-24805. + result = read_csv(self.fname, chunksize=self.chunksize) + + for _ in result: + pass + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/excel.py b/asv_bench/benchmarks/io/excel.py new file mode 100644 index 0000000000000..1bee864fbcf2d --- /dev/null +++ b/asv_bench/benchmarks/io/excel.py @@ -0,0 +1,36 @@ +import numpy as np +from pandas import DataFrame, date_range, ExcelWriter, read_excel +from pandas.compat import BytesIO +import pandas.util.testing as tm + + +class Excel(object): + + params = ['openpyxl', 'xlsxwriter', 'xlwt'] + param_names = ['engine'] + + def setup(self, engine): + N = 2000 + C = 5 + self.df = DataFrame(np.random.randn(N, C), + columns=['float{}'.format(i) for i in range(C)], + index=date_range('20000101', periods=N, freq='H')) + self.df['object'] = tm.makeStringIndex(N) + self.bio_read = BytesIO() + self.writer_read = ExcelWriter(self.bio_read, engine=engine) + self.df.to_excel(self.writer_read, sheet_name='Sheet1') + self.writer_read.save() + self.bio_read.seek(0) + + def time_read_excel(self, engine): + read_excel(self.bio_read) + + def time_write_excel(self, engine): + bio_write = BytesIO() + bio_write.seek(0) + writer_write = ExcelWriter(bio_write, engine=engine) + self.df.to_excel(writer_write, sheet_name='Sheet1') + writer_write.save() + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/hdf.py b/asv_bench/benchmarks/io/hdf.py new file mode 100644 index 0000000000000..a5dc28eb9508c --- /dev/null +++ b/asv_bench/benchmarks/io/hdf.py @@ -0,0 +1,122 @@ +import numpy as np +from pandas import DataFrame, date_range, HDFStore, read_hdf +import pandas.util.testing as tm + +from ..pandas_vb_common import BaseIO + + +class HDFStoreDataFrame(BaseIO): + + def setup(self): + N = 25000 + index = tm.makeStringIndex(N) + self.df = DataFrame({'float1': np.random.randn(N), + 'float2': np.random.randn(N)}, + index=index) + self.df_mixed = DataFrame({'float1': np.random.randn(N), + 'float2': np.random.randn(N), + 'string1': ['foo'] * N, + 'bool1': [True] * N, + 'int1': np.random.randint(0, N, size=N)}, + index=index) + self.df_wide = DataFrame(np.random.randn(N, 100)) + self.start_wide = self.df_wide.index[10000] + self.stop_wide = self.df_wide.index[15000] + self.df2 = DataFrame({'float1': np.random.randn(N), + 'float2': np.random.randn(N)}, + index=date_range('1/1/2000', periods=N)) + self.start = self.df2.index[10000] + self.stop = self.df2.index[15000] + self.df_wide2 = DataFrame(np.random.randn(N, 100), + index=date_range('1/1/2000', periods=N)) + self.df_dc = DataFrame(np.random.randn(N, 10), + columns=['C%03d' % i for i in range(10)]) + + self.fname = '__test__.h5' + + self.store = HDFStore(self.fname) + self.store.put('fixed', self.df) + self.store.put('fixed_mixed', self.df_mixed) + self.store.append('table', self.df2) + self.store.append('table_mixed', self.df_mixed) + self.store.append('table_wide', self.df_wide) + self.store.append('table_wide2', self.df_wide2) + + def teardown(self): + self.store.close() + self.remove(self.fname) + + def time_read_store(self): + self.store.get('fixed') + + def time_read_store_mixed(self): + self.store.get('fixed_mixed') + + def time_write_store(self): + self.store.put('fixed_write', self.df) + + def time_write_store_mixed(self): + self.store.put('fixed_mixed_write', self.df_mixed) + + def time_read_store_table_mixed(self): + self.store.select('table_mixed') + + def time_write_store_table_mixed(self): + self.store.append('table_mixed_write', self.df_mixed) + + def time_read_store_table(self): + self.store.select('table') + + def time_write_store_table(self): + self.store.append('table_write', self.df) + + def time_read_store_table_wide(self): + self.store.select('table_wide') + + def time_write_store_table_wide(self): + self.store.append('table_wide_write', self.df_wide) + + def time_write_store_table_dc(self): + self.store.append('table_dc_write', self.df_dc, data_columns=True) + + def time_query_store_table_wide(self): + self.store.select('table_wide', where="index > self.start_wide and " + "index < self.stop_wide") + + def time_query_store_table(self): + self.store.select('table', where="index > self.start and " + "index < self.stop") + + def time_store_repr(self): + repr(self.store) + + def time_store_str(self): + str(self.store) + + def time_store_info(self): + self.store.info() + + +class HDF(BaseIO): + + params = ['table', 'fixed'] + param_names = ['format'] + + def setup(self, format): + self.fname = '__test__.h5' + N = 100000 + C = 5 + self.df = DataFrame(np.random.randn(N, C), + columns=['float{}'.format(i) for i in range(C)], + index=date_range('20000101', periods=N, freq='H')) + self.df['object'] = tm.makeStringIndex(N) + self.df.to_hdf(self.fname, 'df', format=format) + + def time_read_hdf(self, format): + read_hdf(self.fname, 'df') + + def time_write_hdf(self, format): + self.df.to_hdf(self.fname, 'df', format=format) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/json.py b/asv_bench/benchmarks/io/json.py new file mode 100644 index 0000000000000..ec2ddc11b7c1d --- /dev/null +++ b/asv_bench/benchmarks/io/json.py @@ -0,0 +1,127 @@ +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, date_range, timedelta_range, concat, read_json + +from ..pandas_vb_common import BaseIO + + +class ReadJSON(BaseIO): + + fname = "__test__.json" + params = (['split', 'index', 'records'], ['int', 'datetime']) + param_names = ['orient', 'index'] + + def setup(self, orient, index): + N = 100000 + indexes = {'int': np.arange(N), + 'datetime': date_range('20000101', periods=N, freq='H')} + df = DataFrame(np.random.randn(N, 5), + columns=['float_{}'.format(i) for i in range(5)], + index=indexes[index]) + df.to_json(self.fname, orient=orient) + + def time_read_json(self, orient, index): + read_json(self.fname, orient=orient) + + +class ReadJSONLines(BaseIO): + + fname = "__test_lines__.json" + params = ['int', 'datetime'] + param_names = ['index'] + + def setup(self, index): + N = 100000 + indexes = {'int': np.arange(N), + 'datetime': date_range('20000101', periods=N, freq='H')} + df = DataFrame(np.random.randn(N, 5), + columns=['float_{}'.format(i) for i in range(5)], + index=indexes[index]) + df.to_json(self.fname, orient='records', lines=True) + + def time_read_json_lines(self, index): + read_json(self.fname, orient='records', lines=True) + + def time_read_json_lines_concat(self, index): + concat(read_json(self.fname, orient='records', lines=True, + chunksize=25000)) + + def peakmem_read_json_lines(self, index): + read_json(self.fname, orient='records', lines=True) + + def peakmem_read_json_lines_concat(self, index): + concat(read_json(self.fname, orient='records', lines=True, + chunksize=25000)) + + +class ToJSON(BaseIO): + + fname = "__test__.json" + params = ['split', 'columns', 'index'] + param_names = ['orient'] + + def setup(self, lines_orient): + N = 10**5 + ncols = 5 + index = date_range('20000101', periods=N, freq='H') + timedeltas = timedelta_range(start=1, periods=N, freq='s') + datetimes = date_range(start=1, periods=N, freq='s') + ints = np.random.randint(100000000, size=N) + floats = np.random.randn(N) + strings = tm.makeStringIndex(N) + self.df = DataFrame(np.random.randn(N, ncols), index=np.arange(N)) + self.df_date_idx = DataFrame(np.random.randn(N, ncols), index=index) + self.df_td_int_ts = DataFrame({'td_1': timedeltas, + 'td_2': timedeltas, + 'int_1': ints, + 'int_2': ints, + 'ts_1': datetimes, + 'ts_2': datetimes}, + index=index) + self.df_int_floats = DataFrame({'int_1': ints, + 'int_2': ints, + 'int_3': ints, + 'float_1': floats, + 'float_2': floats, + 'float_3': floats}, + index=index) + self.df_int_float_str = DataFrame({'int_1': ints, + 'int_2': ints, + 'float_1': floats, + 'float_2': floats, + 'str_1': strings, + 'str_2': strings}, + index=index) + + def time_floats_with_int_index(self, orient): + self.df.to_json(self.fname, orient=orient) + + def time_floats_with_dt_index(self, orient): + self.df_date_idx.to_json(self.fname, orient=orient) + + def time_delta_int_tstamp(self, orient): + self.df_td_int_ts.to_json(self.fname, orient=orient) + + def time_float_int(self, orient): + self.df_int_floats.to_json(self.fname, orient=orient) + + def time_float_int_str(self, orient): + self.df_int_float_str.to_json(self.fname, orient=orient) + + def time_floats_with_int_idex_lines(self, orient): + self.df.to_json(self.fname, orient='records', lines=True) + + def time_floats_with_dt_index_lines(self, orient): + self.df_date_idx.to_json(self.fname, orient='records', lines=True) + + def time_delta_int_tstamp_lines(self, orient): + self.df_td_int_ts.to_json(self.fname, orient='records', lines=True) + + def time_float_int_lines(self, orient): + self.df_int_floats.to_json(self.fname, orient='records', lines=True) + + def time_float_int_str_lines(self, orient): + self.df_int_float_str.to_json(self.fname, orient='records', lines=True) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/msgpack.py b/asv_bench/benchmarks/io/msgpack.py new file mode 100644 index 0000000000000..dc2642d920fd0 --- /dev/null +++ b/asv_bench/benchmarks/io/msgpack.py @@ -0,0 +1,27 @@ +import numpy as np +from pandas import DataFrame, date_range, read_msgpack +import pandas.util.testing as tm + +from ..pandas_vb_common import BaseIO + + +class MSGPack(BaseIO): + + def setup(self): + self.fname = '__test__.msg' + N = 100000 + C = 5 + self.df = DataFrame(np.random.randn(N, C), + columns=['float{}'.format(i) for i in range(C)], + index=date_range('20000101', periods=N, freq='H')) + self.df['object'] = tm.makeStringIndex(N) + self.df.to_msgpack(self.fname) + + def time_read_msgpack(self): + read_msgpack(self.fname) + + def time_write_msgpack(self): + self.df.to_msgpack(self.fname) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/pickle.py b/asv_bench/benchmarks/io/pickle.py new file mode 100644 index 0000000000000..74a58bbb946aa --- /dev/null +++ b/asv_bench/benchmarks/io/pickle.py @@ -0,0 +1,27 @@ +import numpy as np +from pandas import DataFrame, date_range, read_pickle +import pandas.util.testing as tm + +from ..pandas_vb_common import BaseIO + + +class Pickle(BaseIO): + + def setup(self): + self.fname = '__test__.pkl' + N = 100000 + C = 5 + self.df = DataFrame(np.random.randn(N, C), + columns=['float{}'.format(i) for i in range(C)], + index=date_range('20000101', periods=N, freq='H')) + self.df['object'] = tm.makeStringIndex(N) + self.df.to_pickle(self.fname) + + def time_read_pickle(self): + read_pickle(self.fname) + + def time_write_pickle(self): + self.df.to_pickle(self.fname) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/sas.py b/asv_bench/benchmarks/io/sas.py new file mode 100644 index 0000000000000..2783f42cad895 --- /dev/null +++ b/asv_bench/benchmarks/io/sas.py @@ -0,0 +1,20 @@ +import os + +from pandas import read_sas + + +class SAS(object): + + params = ['sas7bdat', 'xport'] + param_names = ['format'] + + def setup(self, format): + # Read files that are located in 'pandas/io/tests/sas/data' + files = {'sas7bdat': 'test1.sas7bdat', 'xport': 'paxraw_d_short.xpt'} + file = files[format] + paths = [os.path.dirname(__file__), '..', '..', '..', 'pandas', + 'tests', 'io', 'sas', 'data', file] + self.f = os.path.join(*paths) + + def time_read_msgpack(self, format): + read_sas(self.f, format=format) diff --git a/asv_bench/benchmarks/io/sql.py b/asv_bench/benchmarks/io/sql.py new file mode 100644 index 0000000000000..075d3bdda5ed9 --- /dev/null +++ b/asv_bench/benchmarks/io/sql.py @@ -0,0 +1,127 @@ +import sqlite3 + +import numpy as np +import pandas.util.testing as tm +from pandas import DataFrame, date_range, read_sql_query, read_sql_table +from sqlalchemy import create_engine + + +class SQL(object): + + params = ['sqlalchemy', 'sqlite'] + param_names = ['connection'] + + def setup(self, connection): + N = 10000 + con = {'sqlalchemy': create_engine('sqlite:///:memory:'), + 'sqlite': sqlite3.connect(':memory:')} + self.table_name = 'test_type' + self.query_all = 'SELECT * FROM {}'.format(self.table_name) + self.con = con[connection] + self.df = DataFrame({'float': np.random.randn(N), + 'float_with_nan': np.random.randn(N), + 'string': ['foo'] * N, + 'bool': [True] * N, + 'int': np.random.randint(0, N, size=N), + 'datetime': date_range('2000-01-01', + periods=N, + freq='s')}, + index=tm.makeStringIndex(N)) + self.df.loc[1000:3000, 'float_with_nan'] = np.nan + self.df['datetime_string'] = self.df['datetime'].astype(str) + self.df.to_sql(self.table_name, self.con, if_exists='replace') + + def time_to_sql_dataframe(self, connection): + self.df.to_sql('test1', self.con, if_exists='replace') + + def time_read_sql_query(self, connection): + read_sql_query(self.query_all, self.con) + + +class WriteSQLDtypes(object): + + params = (['sqlalchemy', 'sqlite'], + ['float', 'float_with_nan', 'string', 'bool', 'int', 'datetime']) + param_names = ['connection', 'dtype'] + + def setup(self, connection, dtype): + N = 10000 + con = {'sqlalchemy': create_engine('sqlite:///:memory:'), + 'sqlite': sqlite3.connect(':memory:')} + self.table_name = 'test_type' + self.query_col = 'SELECT {} FROM {}'.format(dtype, self.table_name) + self.con = con[connection] + self.df = DataFrame({'float': np.random.randn(N), + 'float_with_nan': np.random.randn(N), + 'string': ['foo'] * N, + 'bool': [True] * N, + 'int': np.random.randint(0, N, size=N), + 'datetime': date_range('2000-01-01', + periods=N, + freq='s')}, + index=tm.makeStringIndex(N)) + self.df.loc[1000:3000, 'float_with_nan'] = np.nan + self.df['datetime_string'] = self.df['datetime'].astype(str) + self.df.to_sql(self.table_name, self.con, if_exists='replace') + + def time_to_sql_dataframe_column(self, connection, dtype): + self.df[[dtype]].to_sql('test1', self.con, if_exists='replace') + + def time_read_sql_query_select_column(self, connection, dtype): + read_sql_query(self.query_col, self.con) + + +class ReadSQLTable(object): + + def setup(self): + N = 10000 + self.table_name = 'test' + self.con = create_engine('sqlite:///:memory:') + self.df = DataFrame({'float': np.random.randn(N), + 'float_with_nan': np.random.randn(N), + 'string': ['foo'] * N, + 'bool': [True] * N, + 'int': np.random.randint(0, N, size=N), + 'datetime': date_range('2000-01-01', + periods=N, + freq='s')}, + index=tm.makeStringIndex(N)) + self.df.loc[1000:3000, 'float_with_nan'] = np.nan + self.df['datetime_string'] = self.df['datetime'].astype(str) + self.df.to_sql(self.table_name, self.con, if_exists='replace') + + def time_read_sql_table_all(self): + read_sql_table(self.table_name, self.con) + + def time_read_sql_table_parse_dates(self): + read_sql_table(self.table_name, self.con, columns=['datetime_string'], + parse_dates=['datetime_string']) + + +class ReadSQLTableDtypes(object): + + params = ['float', 'float_with_nan', 'string', 'bool', 'int', 'datetime'] + param_names = ['dtype'] + + def setup(self, dtype): + N = 10000 + self.table_name = 'test' + self.con = create_engine('sqlite:///:memory:') + self.df = DataFrame({'float': np.random.randn(N), + 'float_with_nan': np.random.randn(N), + 'string': ['foo'] * N, + 'bool': [True] * N, + 'int': np.random.randint(0, N, size=N), + 'datetime': date_range('2000-01-01', + periods=N, + freq='s')}, + index=tm.makeStringIndex(N)) + self.df.loc[1000:3000, 'float_with_nan'] = np.nan + self.df['datetime_string'] = self.df['datetime'].astype(str) + self.df.to_sql(self.table_name, self.con, if_exists='replace') + + def time_read_sql_table_column(self, dtype): + read_sql_table(self.table_name, self.con, columns=[dtype]) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io/stata.py b/asv_bench/benchmarks/io/stata.py new file mode 100644 index 0000000000000..a7f854a853f50 --- /dev/null +++ b/asv_bench/benchmarks/io/stata.py @@ -0,0 +1,39 @@ +import numpy as np +from pandas import DataFrame, date_range, read_stata +import pandas.util.testing as tm + +from ..pandas_vb_common import BaseIO + + +class Stata(BaseIO): + + params = ['tc', 'td', 'tm', 'tw', 'th', 'tq', 'ty'] + param_names = ['convert_dates'] + + def setup(self, convert_dates): + self.fname = '__test__.dta' + N = 100000 + C = 5 + self.df = DataFrame(np.random.randn(N, C), + columns=['float{}'.format(i) for i in range(C)], + index=date_range('20000101', periods=N, freq='H')) + self.df['object'] = tm.makeStringIndex(N) + self.df['int8_'] = np.random.randint(np.iinfo(np.int8).min, + np.iinfo(np.int8).max - 27, N) + self.df['int16_'] = np.random.randint(np.iinfo(np.int16).min, + np.iinfo(np.int16).max - 27, N) + self.df['int32_'] = np.random.randint(np.iinfo(np.int32).min, + np.iinfo(np.int32).max - 27, N) + self.df['float32_'] = np.array(np.random.randn(N), + dtype=np.float32) + self.convert_dates = {'index': convert_dates} + self.df.to_stata(self.fname, self.convert_dates) + + def time_read_stata(self, convert_dates): + read_stata(self.fname) + + def time_write_stata(self, convert_dates): + self.df.to_stata(self.fname, self.convert_dates) + + +from ..pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/io_bench.py b/asv_bench/benchmarks/io_bench.py deleted file mode 100644 index 52064d2cdb8a2..0000000000000 --- a/asv_bench/benchmarks/io_bench.py +++ /dev/null @@ -1,194 +0,0 @@ -from .pandas_vb_common import * -from pandas import concat, Timestamp, compat -try: - from StringIO import StringIO -except ImportError: - from io import StringIO -import timeit - - -class frame_to_csv(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(3000, 30)) - - def time_frame_to_csv(self): - self.df.to_csv('__test__.csv') - - -class frame_to_csv2(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame({'A': range(50000), }) - self.df['B'] = (self.df.A + 1.0) - self.df['C'] = (self.df.A + 2.0) - self.df['D'] = (self.df.A + 3.0) - - def time_frame_to_csv2(self): - self.df.to_csv('__test__.csv') - - -class frame_to_csv_date_formatting(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=1000) - self.data = DataFrame(self.rng, index=self.rng) - - def time_frame_to_csv_date_formatting(self): - self.data.to_csv('__test__.csv', date_format='%Y%m%d') - - -class frame_to_csv_mixed(object): - goal_time = 0.2 - - def setup(self): - self.df_float = DataFrame(np.random.randn(5000, 5), dtype='float64', columns=self.create_cols('float')) - self.df_int = DataFrame(np.random.randn(5000, 5), dtype='int64', columns=self.create_cols('int')) - self.df_bool = DataFrame(True, index=self.df_float.index, columns=self.create_cols('bool')) - self.df_object = DataFrame('foo', index=self.df_float.index, columns=self.create_cols('object')) - self.df_dt = DataFrame(Timestamp('20010101'), index=self.df_float.index, columns=self.create_cols('date')) - self.df_float.ix[30:500, 1:3] = np.nan - self.df = concat([self.df_float, self.df_int, self.df_bool, self.df_object, self.df_dt], axis=1) - - def time_frame_to_csv_mixed(self): - self.df.to_csv('__test__.csv') - - def create_cols(self, name): - return [('%s%03d' % (name, i)) for i in range(5)] - - -class read_csv_infer_datetime_format_custom(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=1000) - self.data = '\n'.join(self.rng.map((lambda x: x.strftime('%m/%d/%Y %H:%M:%S.%f')))) - - def time_read_csv_infer_datetime_format_custom(self): - read_csv(StringIO(self.data), header=None, names=['foo'], parse_dates=['foo'], infer_datetime_format=True) - - -class read_csv_infer_datetime_format_iso8601(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=1000) - self.data = '\n'.join(self.rng.map((lambda x: x.strftime('%Y-%m-%d %H:%M:%S')))) - - def time_read_csv_infer_datetime_format_iso8601(self): - read_csv(StringIO(self.data), header=None, names=['foo'], parse_dates=['foo'], infer_datetime_format=True) - - -class read_csv_infer_datetime_format_ymd(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=1000) - self.data = '\n'.join(self.rng.map((lambda x: x.strftime('%Y%m%d')))) - - def time_read_csv_infer_datetime_format_ymd(self): - read_csv(StringIO(self.data), header=None, names=['foo'], parse_dates=['foo'], infer_datetime_format=True) - - -class read_csv_skiprows(object): - goal_time = 0.2 - - def setup(self): - self.index = tm.makeStringIndex(20000) - self.df = DataFrame({'float1': randn(20000), 'float2': randn(20000), 'string1': (['foo'] * 20000), 'bool1': ([True] * 20000), 'int1': np.random.randint(0, 200000, size=20000), }, index=self.index) - self.df.to_csv('__test__.csv') - - def time_read_csv_skiprows(self): - read_csv('__test__.csv', skiprows=10000) - - -class read_csv_standard(object): - goal_time = 0.2 - - def setup(self): - self.index = tm.makeStringIndex(10000) - self.df = DataFrame({'float1': randn(10000), 'float2': randn(10000), 'string1': (['foo'] * 10000), 'bool1': ([True] * 10000), 'int1': np.random.randint(0, 100000, size=10000), }, index=self.index) - self.df.to_csv('__test__.csv') - - def time_read_csv_standard(self): - read_csv('__test__.csv') - - -class read_parse_dates_iso8601(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=1000) - self.data = '\n'.join(self.rng.map((lambda x: x.strftime('%Y-%m-%d %H:%M:%S')))) - - def time_read_parse_dates_iso8601(self): - read_csv(StringIO(self.data), header=None, names=['foo'], parse_dates=['foo']) - - -class read_uint64_integers(object): - goal_time = 0.2 - - def setup(self): - self.na_values = [2**63 + 500] - - self.arr1 = np.arange(10000).astype('uint64') + 2**63 - self.data1 = '\n'.join(map(lambda x: str(x), self.arr1)) - - self.arr2 = self.arr1.copy().astype(object) - self.arr2[500] = -1 - self.data2 = '\n'.join(map(lambda x: str(x), self.arr2)) - - def time_read_uint64(self): - read_csv(StringIO(self.data1), header=None) - - def time_read_uint64_neg_values(self): - read_csv(StringIO(self.data2), header=None) - - def time_read_uint64_na_values(self): - read_csv(StringIO(self.data1), header=None, na_values=self.na_values) - - -class write_csv_standard(object): - goal_time = 0.2 - - def setup(self): - self.index = tm.makeStringIndex(10000) - self.df = DataFrame({'float1': randn(10000), 'float2': randn(10000), 'string1': (['foo'] * 10000), 'bool1': ([True] * 10000), 'int1': np.random.randint(0, 100000, size=10000), }, index=self.index) - - def time_write_csv_standard(self): - self.df.to_csv('__test__.csv') - - -class read_csv_from_s3(object): - # Make sure that we can read part of a file from S3 without - # needing to download the entire thing. Use the timeit.default_timer - # to measure wall time instead of CPU time -- we want to see - # how long it takes to download the data. - timer = timeit.default_timer - params = ([None, "gzip", "bz2"], ["python", "c"]) - param_names = ["compression", "engine"] - - def setup(self, compression, engine): - if compression == "bz2" and engine == "c" and compat.PY2: - # The Python 2 C parser can't read bz2 from open files. - raise NotImplementedError - try: - import s3fs - except ImportError: - # Skip these benchmarks if `boto` is not installed. - raise NotImplementedError - - self.big_fname = "s3://pandas-test/large_random.csv" - - def time_read_nrows(self, compression, engine): - # Read a small number of rows from a huge (100,000 x 50) table. - ext = "" - if compression == "gzip": - ext = ".gz" - elif compression == "bz2": - ext = ".bz2" - pd.read_csv(self.big_fname + ext, nrows=10, - compression=compression, engine=engine) diff --git a/asv_bench/benchmarks/io_sql.py b/asv_bench/benchmarks/io_sql.py deleted file mode 100644 index ec855e5d33525..0000000000000 --- a/asv_bench/benchmarks/io_sql.py +++ /dev/null @@ -1,105 +0,0 @@ -import sqlalchemy -from .pandas_vb_common import * -import sqlite3 -from sqlalchemy import create_engine - - -#------------------------------------------------------------------------------- -# to_sql - -class WriteSQL(object): - goal_time = 0.2 - - def setup(self): - self.engine = create_engine('sqlite:///:memory:') - self.con = sqlite3.connect(':memory:') - self.index = tm.makeStringIndex(10000) - self.df = DataFrame({'float1': randn(10000), 'float2': randn(10000), 'string1': (['foo'] * 10000), 'bool1': ([True] * 10000), 'int1': np.random.randint(0, 100000, size=10000), }, index=self.index) - - def time_fallback(self): - self.df.to_sql('test1', self.con, if_exists='replace') - - def time_sqlalchemy(self): - self.df.to_sql('test1', self.engine, if_exists='replace') - - -#------------------------------------------------------------------------------- -# read_sql - -class ReadSQL(object): - goal_time = 0.2 - - def setup(self): - self.engine = create_engine('sqlite:///:memory:') - self.con = sqlite3.connect(':memory:') - self.index = tm.makeStringIndex(10000) - self.df = DataFrame({'float1': randn(10000), 'float2': randn(10000), 'string1': (['foo'] * 10000), 'bool1': ([True] * 10000), 'int1': np.random.randint(0, 100000, size=10000), }, index=self.index) - self.df.to_sql('test2', self.engine, if_exists='replace') - self.df.to_sql('test2', self.con, if_exists='replace') - - def time_read_query_fallback(self): - read_sql_query('SELECT * FROM test2', self.con) - - def time_read_query_sqlalchemy(self): - read_sql_query('SELECT * FROM test2', self.engine) - - def time_read_table_sqlalchemy(self): - read_sql_table('test2', self.engine) - - -#------------------------------------------------------------------------------- -# type specific write - -class WriteSQLTypes(object): - goal_time = 0.2 - - def setup(self): - self.engine = create_engine('sqlite:///:memory:') - self.con = sqlite3.connect(':memory:') - self.df = DataFrame({'float': randn(10000), 'string': (['foo'] * 10000), 'bool': ([True] * 10000), 'datetime': date_range('2000-01-01', periods=10000, freq='s'), }) - self.df.loc[1000:3000, 'float'] = np.nan - - def time_string_fallback(self): - self.df[['string']].to_sql('test_string', self.con, if_exists='replace') - - def time_string_sqlalchemy(self): - self.df[['string']].to_sql('test_string', self.engine, if_exists='replace') - - def time_float_fallback(self): - self.df[['float']].to_sql('test_float', self.con, if_exists='replace') - - def time_float_sqlalchemy(self): - self.df[['float']].to_sql('test_float', self.engine, if_exists='replace') - - def time_datetime_sqlalchemy(self): - self.df[['datetime']].to_sql('test_datetime', self.engine, if_exists='replace') - - -#------------------------------------------------------------------------------- -# type specific read - -class ReadSQLTypes(object): - goal_time = 0.2 - - def setup(self): - self.engine = create_engine('sqlite:///:memory:') - self.con = sqlite3.connect(':memory:') - self.df = DataFrame({'float': randn(10000), 'datetime': date_range('2000-01-01', periods=10000, freq='s'), }) - self.df['datetime_string'] = self.df['datetime'].map(str) - self.df.to_sql('test_type', self.engine, if_exists='replace') - self.df[['float', 'datetime_string']].to_sql('test_type', self.con, if_exists='replace') - - def time_datetime_read_and_parse_sqlalchemy(self): - read_sql_table('test_type', self.engine, columns=['datetime_string'], parse_dates=['datetime_string']) - - def time_datetime_read_as_native_sqlalchemy(self): - read_sql_table('test_type', self.engine, columns=['datetime']) - - def time_float_read_query_fallback(self): - read_sql_query('SELECT float FROM test_type', self.con) - - def time_float_read_query_sqlalchemy(self): - read_sql_query('SELECT float FROM test_type', self.engine) - - def time_float_read_table_sqlalchemy(self): - read_sql_table('test_type', self.engine, columns=['float']) diff --git a/asv_bench/benchmarks/join_merge.py b/asv_bench/benchmarks/join_merge.py index 776316343e009..6da8287a06d80 100644 --- a/asv_bench/benchmarks/join_merge.py +++ b/asv_bench/benchmarks/join_merge.py @@ -1,4 +1,10 @@ -from .pandas_vb_common import * +import warnings +import string + +import numpy as np +import pandas.util.testing as tm +from pandas import (DataFrame, Series, Panel, MultiIndex, + date_range, concat, merge, merge_asof) try: from pandas import merge_ordered @@ -6,25 +12,18 @@ from pandas import ordered_merge as merge_ordered -# ---------------------------------------------------------------------- -# Append - class Append(object): - goal_time = 0.2 def setup(self): - self.df1 = pd.DataFrame(np.random.randn(10000, 4), - columns=['A', 'B', 'C', 'D']) + self.df1 = DataFrame(np.random.randn(10000, 4), + columns=['A', 'B', 'C', 'D']) self.df2 = self.df1.copy() self.df2.index = np.arange(10000, 20000) self.mdf1 = self.df1.copy() self.mdf1['obj1'] = 'bar' self.mdf1['obj2'] = 'bar' self.mdf1['int1'] = 5 - try: - self.mdf1.consolidate(inplace=True) - except: - pass + self.mdf1 = self.mdf1._consolidate() self.mdf2 = self.mdf1.copy() self.mdf2.index = self.df2.index @@ -35,237 +34,220 @@ def time_append_mixed(self): self.mdf1.append(self.mdf2) -# ---------------------------------------------------------------------- -# Concat - class Concat(object): - goal_time = 0.2 - def setup(self): - self.n = 1000 - self.indices = tm.makeStringIndex(1000) - self.s = Series(self.n, index=self.indices) - self.pieces = [self.s[i:(- i)] for i in range(1, 10)] - self.pieces = (self.pieces * 50) + params = [0, 1] + param_names = ['axis'] - self.df_small = pd.DataFrame(randn(5, 4)) + def setup(self, axis): + N = 1000 + s = Series(N, index=tm.makeStringIndex(N)) + self.series = [s[i:- i] for i in range(1, 10)] * 50 + self.small_frames = [DataFrame(np.random.randn(5, 4))] * 1000 + df = DataFrame({'A': range(N)}, + index=date_range('20130101', periods=N, freq='s')) + self.empty_left = [DataFrame(), df] + self.empty_right = [df, DataFrame()] + self.mixed_ndims = [df, df.head(N // 2)] - # empty - self.df = pd.DataFrame(dict(A=range(10000)), index=date_range('20130101', periods=10000, freq='s')) - self.empty = pd.DataFrame() + def time_concat_series(self, axis): + concat(self.series, axis=axis, sort=False) - def time_concat_series_axis1(self): - concat(self.pieces, axis=1) + def time_concat_small_frames(self, axis): + concat(self.small_frames, axis=axis) - def time_concat_small_frames(self): - concat(([self.df_small] * 1000)) + def time_concat_empty_right(self, axis): + concat(self.empty_right, axis=axis) - def time_concat_empty_frames1(self): - concat([self.df, self.empty]) + def time_concat_empty_left(self, axis): + concat(self.empty_left, axis=axis) - def time_concat_empty_frames2(self): - concat([self.empty, self.df]) + def time_concat_mixed_ndims(self, axis): + concat(self.mixed_ndims, axis=axis) class ConcatPanels(object): - goal_time = 0.2 - def setup(self): - dataset = np.zeros((10000, 200, 2), dtype=np.float32) - self.panels_f = [pd.Panel(np.copy(dataset, order='F')) - for i in range(20)] - self.panels_c = [pd.Panel(np.copy(dataset, order='C')) - for i in range(20)] + params = ([0, 1, 2], [True, False]) + param_names = ['axis', 'ignore_index'] - def time_c_ordered_axis0(self): - concat(self.panels_c, axis=0, ignore_index=True) + def setup(self, axis, ignore_index): + with warnings.catch_warnings(record=True): + panel_c = Panel(np.zeros((10000, 200, 2), + dtype=np.float32, + order='C')) + self.panels_c = [panel_c] * 20 + panel_f = Panel(np.zeros((10000, 200, 2), + dtype=np.float32, + order='F')) + self.panels_f = [panel_f] * 20 - def time_f_ordered_axis0(self): - concat(self.panels_f, axis=0, ignore_index=True) + def time_c_ordered(self, axis, ignore_index): + with warnings.catch_warnings(record=True): + concat(self.panels_c, axis=axis, ignore_index=ignore_index) - def time_c_ordered_axis1(self): - concat(self.panels_c, axis=1, ignore_index=True) + def time_f_ordered(self, axis, ignore_index): + with warnings.catch_warnings(record=True): + concat(self.panels_f, axis=axis, ignore_index=ignore_index) - def time_f_ordered_axis1(self): - concat(self.panels_f, axis=1, ignore_index=True) - def time_c_ordered_axis2(self): - concat(self.panels_c, axis=2, ignore_index=True) +class ConcatDataFrames(object): - def time_f_ordered_axis2(self): - concat(self.panels_f, axis=2, ignore_index=True) + params = ([0, 1], [True, False]) + param_names = ['axis', 'ignore_index'] + def setup(self, axis, ignore_index): + frame_c = DataFrame(np.zeros((10000, 200), + dtype=np.float32, order='C')) + self.frame_c = [frame_c] * 20 + frame_f = DataFrame(np.zeros((10000, 200), + dtype=np.float32, order='F')) + self.frame_f = [frame_f] * 20 -class ConcatFrames(object): - goal_time = 0.2 + def time_c_ordered(self, axis, ignore_index): + concat(self.frame_c, axis=axis, ignore_index=ignore_index) - def setup(self): - dataset = np.zeros((10000, 200), dtype=np.float32) + def time_f_ordered(self, axis, ignore_index): + concat(self.frame_f, axis=axis, ignore_index=ignore_index) - self.frames_f = [pd.DataFrame(np.copy(dataset, order='F')) - for i in range(20)] - self.frames_c = [pd.DataFrame(np.copy(dataset, order='C')) - for i in range(20)] - def time_c_ordered_axis0(self): - concat(self.frames_c, axis=0, ignore_index=True) +class Join(object): - def time_f_ordered_axis0(self): - concat(self.frames_f, axis=0, ignore_index=True) + params = [True, False] + param_names = ['sort'] - def time_c_ordered_axis1(self): - concat(self.frames_c, axis=1, ignore_index=True) + def setup(self, sort): + level1 = tm.makeStringIndex(10).values + level2 = tm.makeStringIndex(1000).values + codes1 = np.arange(10).repeat(1000) + codes2 = np.tile(np.arange(1000), 10) + index2 = MultiIndex(levels=[level1, level2], + codes=[codes1, codes2]) + self.df_multi = DataFrame(np.random.randn(len(index2), 4), + index=index2, + columns=['A', 'B', 'C', 'D']) - def time_f_ordered_axis1(self): - concat(self.frames_f, axis=1, ignore_index=True) + self.key1 = np.tile(level1.take(codes1), 10) + self.key2 = np.tile(level2.take(codes2), 10) + self.df = DataFrame({'data1': np.random.randn(100000), + 'data2': np.random.randn(100000), + 'key1': self.key1, + 'key2': self.key2}) + self.df_key1 = DataFrame(np.random.randn(len(level1), 4), + index=level1, + columns=['A', 'B', 'C', 'D']) + self.df_key2 = DataFrame(np.random.randn(len(level2), 4), + index=level2, + columns=['A', 'B', 'C', 'D']) -# ---------------------------------------------------------------------- -# Joins + shuf = np.arange(100000) + np.random.shuffle(shuf) + self.df_shuf = self.df.reindex(self.df.index[shuf]) -class Join(object): - goal_time = 0.2 + def time_join_dataframe_index_multi(self, sort): + self.df.join(self.df_multi, on=['key1', 'key2'], sort=sort) - def setup(self): - self.level1 = tm.makeStringIndex(10).values - self.level2 = tm.makeStringIndex(1000).values - self.label1 = np.arange(10).repeat(1000) - self.label2 = np.tile(np.arange(1000), 10) - self.key1 = np.tile(self.level1.take(self.label1), 10) - self.key2 = np.tile(self.level2.take(self.label2), 10) - self.shuf = np.arange(100000) - random.shuffle(self.shuf) - try: - self.index2 = MultiIndex(levels=[self.level1, self.level2], - labels=[self.label1, self.label2]) - self.index3 = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], - labels=[np.arange(10).repeat(10000), np.tile(np.arange(100).repeat(100), 10), np.tile(np.tile(np.arange(100), 100), 10)]) - self.df_multi = DataFrame(np.random.randn(len(self.index2), 4), - index=self.index2, - columns=['A', 'B', 'C', 'D']) - except: - pass - self.df = pd.DataFrame({'data1': np.random.randn(100000), - 'data2': np.random.randn(100000), - 'key1': self.key1, - 'key2': self.key2}) - self.df_key1 = pd.DataFrame(np.random.randn(len(self.level1), 4), - index=self.level1, - columns=['A', 'B', 'C', 'D']) - self.df_key2 = pd.DataFrame(np.random.randn(len(self.level2), 4), - index=self.level2, - columns=['A', 'B', 'C', 'D']) - self.df_shuf = self.df.reindex(self.df.index[self.shuf]) - - def time_join_dataframe_index_multi(self): - self.df.join(self.df_multi, on=['key1', 'key2']) - - def time_join_dataframe_index_single_key_bigger(self): - self.df.join(self.df_key2, on='key2') - - def time_join_dataframe_index_single_key_bigger_sort(self): - self.df_shuf.join(self.df_key2, on='key2', sort=True) - - def time_join_dataframe_index_single_key_small(self): - self.df.join(self.df_key1, on='key1') + def time_join_dataframe_index_single_key_bigger(self, sort): + self.df.join(self.df_key2, on='key2', sort=sort) + + def time_join_dataframe_index_single_key_small(self, sort): + self.df.join(self.df_key1, on='key1', sort=sort) + + def time_join_dataframe_index_shuffle_key_bigger_sort(self, sort): + self.df_shuf.join(self.df_key2, on='key2', sort=sort) class JoinIndex(object): - goal_time = 0.2 def setup(self): - np.random.seed(2718281) - self.n = 50000 - self.left = pd.DataFrame(np.random.randint(1, (self.n / 500), (self.n, 2)), columns=['jim', 'joe']) - self.right = pd.DataFrame(np.random.randint(1, (self.n / 500), (self.n, 2)), columns=['jolie', 'jolia']).set_index('jolie') + N = 50000 + self.left = DataFrame(np.random.randint(1, N / 500, (N, 2)), + columns=['jim', 'joe']) + self.right = DataFrame(np.random.randint(1, N / 500, (N, 2)), + columns=['jolie', 'jolia']).set_index('jolie') def time_left_outer_join_index(self): self.left.join(self.right, on='jim') -class join_non_unique_equal(object): +class JoinNonUnique(object): # outer join of non-unique # GH 6329 - - goal_time = 0.2 - def setup(self): - self.date_index = date_range('01-Jan-2013', '23-Jan-2013', freq='T') - self.daily_dates = self.date_index.to_period('D').to_timestamp('S', 'S') - self.fracofday = (self.date_index.view(np.ndarray) - self.daily_dates.view(np.ndarray)) - self.fracofday = (self.fracofday.astype('timedelta64[ns]').astype(np.float64) / 86400000000000.0) - self.fracofday = Series(self.fracofday, self.daily_dates) - self.index = date_range(self.date_index.min().to_period('A').to_timestamp('D', 'S'), self.date_index.max().to_period('A').to_timestamp('D', 'E'), freq='D') - self.temp = Series(1.0, self.index) + date_index = date_range('01-Jan-2013', '23-Jan-2013', freq='T') + daily_dates = date_index.to_period('D').to_timestamp('S', 'S') + self.fracofday = date_index.values - daily_dates.values + self.fracofday = self.fracofday.astype('timedelta64[ns]') + self.fracofday = self.fracofday.astype(np.float64) / 86400000000000.0 + self.fracofday = Series(self.fracofday, daily_dates) + index = date_range(date_index.min(), date_index.max(), freq='D') + self.temp = Series(1.0, index)[self.fracofday.index] def time_join_non_unique_equal(self): - (self.fracofday * self.temp[self.fracofday.index]) + self.fracofday * self.temp -# ---------------------------------------------------------------------- -# Merges - class Merge(object): - goal_time = 0.2 - def setup(self): - self.N = 10000 - self.indices = tm.makeStringIndex(self.N).values - self.indices2 = tm.makeStringIndex(self.N).values - self.key = np.tile(self.indices[:8000], 10) - self.key2 = np.tile(self.indices2[:8000], 10) - self.left = pd.DataFrame({'key': self.key, 'key2': self.key2, - 'value': np.random.randn(80000)}) - self.right = pd.DataFrame({'key': self.indices[2000:], - 'key2': self.indices2[2000:], - 'value2': np.random.randn(8000)}) - - self.df = pd.DataFrame({'key1': np.tile(np.arange(500).repeat(10), 2), - 'key2': np.tile(np.arange(250).repeat(10), 4), - 'value': np.random.randn(10000)}) - self.df2 = pd.DataFrame({'key1': np.arange(500), 'value2': randn(500)}) + params = [True, False] + param_names = ['sort'] + + def setup(self, sort): + N = 10000 + indices = tm.makeStringIndex(N).values + indices2 = tm.makeStringIndex(N).values + key = np.tile(indices[:8000], 10) + key2 = np.tile(indices2[:8000], 10) + self.left = DataFrame({'key': key, 'key2': key2, + 'value': np.random.randn(80000)}) + self.right = DataFrame({'key': indices[2000:], + 'key2': indices2[2000:], + 'value2': np.random.randn(8000)}) + + self.df = DataFrame({'key1': np.tile(np.arange(500).repeat(10), 2), + 'key2': np.tile(np.arange(250).repeat(10), 4), + 'value': np.random.randn(10000)}) + self.df2 = DataFrame({'key1': np.arange(500), + 'value2': np.random.randn(500)}) self.df3 = self.df[:5000] - def time_merge_2intkey_nosort(self): - merge(self.left, self.right, sort=False) + def time_merge_2intkey(self, sort): + merge(self.left, self.right, sort=sort) - def time_merge_2intkey_sort(self): - merge(self.left, self.right, sort=True) + def time_merge_dataframe_integer_2key(self, sort): + merge(self.df, self.df3, sort=sort) - def time_merge_dataframe_integer_2key(self): - merge(self.df, self.df3) + def time_merge_dataframe_integer_key(self, sort): + merge(self.df, self.df2, on='key1', sort=sort) - def time_merge_dataframe_integer_key(self): - merge(self.df, self.df2, on='key1') +class I8Merge(object): -class i8merge(object): - goal_time = 0.2 + params = ['inner', 'outer', 'left', 'right'] + param_names = ['how'] - def setup(self): - (low, high, n) = (((-1) << 10), (1 << 10), (1 << 20)) - self.left = pd.DataFrame(np.random.randint(low, high, (n, 7)), - columns=list('ABCDEFG')) + def setup(self, how): + low, high, n = -1000, 1000, 10**6 + self.left = DataFrame(np.random.randint(low, high, (n, 7)), + columns=list('ABCDEFG')) self.left['left'] = self.left.sum(axis=1) - self.i = np.random.permutation(len(self.left)) - self.right = self.left.iloc[self.i].copy() - self.right.columns = (self.right.columns[:(-1)].tolist() + ['right']) - self.right.index = np.arange(len(self.right)) - self.right['right'] *= (-1) + self.right = self.left.sample(frac=1).rename({'left': 'right'}, axis=1) + self.right = self.right.reset_index(drop=True) + self.right['right'] *= -1 - def time_i8merge(self): - merge(self.left, self.right, how='outer') + def time_i8merge(self, how): + merge(self.left, self.right, how=how) class MergeCategoricals(object): - goal_time = 0.2 def setup(self): - self.left_object = pd.DataFrame( + self.left_object = DataFrame( {'X': np.random.choice(range(0, 10), size=(10000,)), 'Y': np.random.choice(['one', 'two', 'three'], size=(10000,))}) - self.right_object = pd.DataFrame( + self.right_object = DataFrame( {'X': np.random.choice(range(0, 10), size=(10000,)), 'Z': np.random.choice(['jjj', 'kkk', 'sss'], size=(10000,))}) @@ -281,103 +263,91 @@ def time_merge_cat(self): merge(self.left_cat, self.right_cat, on='X') -# ---------------------------------------------------------------------- -# Ordered merge - class MergeOrdered(object): def setup(self): - groups = tm.makeStringIndex(10).values - - self.left = pd.DataFrame({'group': groups.repeat(5000), - 'key' : np.tile(np.arange(0, 10000, 2), 10), - 'lvalue': np.random.randn(50000)}) - - self.right = pd.DataFrame({'key' : np.arange(10000), - 'rvalue' : np.random.randn(10000)}) + self.left = DataFrame({'group': groups.repeat(5000), + 'key': np.tile(np.arange(0, 10000, 2), 10), + 'lvalue': np.random.randn(50000)}) + self.right = DataFrame({'key': np.arange(10000), + 'rvalue': np.random.randn(10000)}) def time_merge_ordered(self): merge_ordered(self.left, self.right, on='key', left_by='group') -# ---------------------------------------------------------------------- -# asof merge - class MergeAsof(object): + params = [['backward', 'forward', 'nearest']] + param_names = ['direction'] - def setup(self): - import string - np.random.seed(0) + def setup(self, direction): one_count = 200000 two_count = 1000000 - self.df1 = pd.DataFrame( + df1 = DataFrame( {'time': np.random.randint(0, one_count / 20, one_count), - 'key': np.random.choice(list(string.uppercase), one_count), + 'key': np.random.choice(list(string.ascii_uppercase), one_count), 'key2': np.random.randint(0, 25, one_count), 'value1': np.random.randn(one_count)}) - self.df2 = pd.DataFrame( + df2 = DataFrame( {'time': np.random.randint(0, two_count / 20, two_count), - 'key': np.random.choice(list(string.uppercase), two_count), + 'key': np.random.choice(list(string.ascii_uppercase), two_count), 'key2': np.random.randint(0, 25, two_count), 'value2': np.random.randn(two_count)}) - self.df1 = self.df1.sort_values('time') - self.df2 = self.df2.sort_values('time') + df1 = df1.sort_values('time') + df2 = df2.sort_values('time') - self.df1['time32'] = np.int32(self.df1.time) - self.df2['time32'] = np.int32(self.df2.time) + df1['time32'] = np.int32(df1.time) + df2['time32'] = np.int32(df2.time) - self.df1a = self.df1[['time', 'value1']] - self.df2a = self.df2[['time', 'value2']] - self.df1b = self.df1[['time', 'key', 'value1']] - self.df2b = self.df2[['time', 'key', 'value2']] - self.df1c = self.df1[['time', 'key2', 'value1']] - self.df2c = self.df2[['time', 'key2', 'value2']] - self.df1d = self.df1[['time32', 'value1']] - self.df2d = self.df2[['time32', 'value2']] - self.df1e = self.df1[['time', 'key', 'key2', 'value1']] - self.df2e = self.df2[['time', 'key', 'key2', 'value2']] + self.df1a = df1[['time', 'value1']] + self.df2a = df2[['time', 'value2']] + self.df1b = df1[['time', 'key', 'value1']] + self.df2b = df2[['time', 'key', 'value2']] + self.df1c = df1[['time', 'key2', 'value1']] + self.df2c = df2[['time', 'key2', 'value2']] + self.df1d = df1[['time32', 'value1']] + self.df2d = df2[['time32', 'value2']] + self.df1e = df1[['time', 'key', 'key2', 'value1']] + self.df2e = df2[['time', 'key', 'key2', 'value2']] - def time_noby(self): - merge_asof(self.df1a, self.df2a, on='time') + def time_on_int(self, direction): + merge_asof(self.df1a, self.df2a, on='time', direction=direction) - def time_by_object(self): - merge_asof(self.df1b, self.df2b, on='time', by='key') + def time_on_int32(self, direction): + merge_asof(self.df1d, self.df2d, on='time32', direction=direction) - def time_by_int(self): - merge_asof(self.df1c, self.df2c, on='time', by='key2') + def time_by_object(self, direction): + merge_asof(self.df1b, self.df2b, on='time', by='key', + direction=direction) - def time_on_int32(self): - merge_asof(self.df1d, self.df2d, on='time32') + def time_by_int(self, direction): + merge_asof(self.df1c, self.df2c, on='time', by='key2', + direction=direction) - def time_multiby(self): - merge_asof(self.df1e, self.df2e, on='time', by=['key', 'key2']) + def time_multiby(self, direction): + merge_asof(self.df1e, self.df2e, on='time', by=['key', 'key2'], + direction=direction) -# ---------------------------------------------------------------------- -# data alignment - class Align(object): - goal_time = 0.2 def setup(self): - self.n = 1000000 - self.sz = 500000 - self.rng = np.arange(0, 10000000000000, 10000000) - self.stamps = (np.datetime64(datetime.now()).view('i8') + self.rng) - self.idx1 = np.sort(self.sample(self.stamps, self.sz)) - self.idx2 = np.sort(self.sample(self.stamps, self.sz)) - self.ts1 = Series(np.random.randn(self.sz), self.idx1) - self.ts2 = Series(np.random.randn(self.sz), self.idx2) - - def sample(self, values, k): - self.sampler = np.random.permutation(len(values)) - return values.take(self.sampler[:k]) + size = 5 * 10**5 + rng = np.arange(0, 10**13, 10**7) + stamps = np.datetime64('now').view('i8') + rng + idx1 = np.sort(np.random.choice(stamps, size, replace=False)) + idx2 = np.sort(np.random.choice(stamps, size, replace=False)) + self.ts1 = Series(np.random.randn(size), idx1) + self.ts2 = Series(np.random.randn(size), idx2) def time_series_align_int64_index(self): - (self.ts1 + self.ts2) + self.ts1 + self.ts2 def time_series_align_left_monotonic(self): self.ts1.align(self.ts2, join='left') + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/multiindex_object.py b/asv_bench/benchmarks/multiindex_object.py new file mode 100644 index 0000000000000..adc6730dcd946 --- /dev/null +++ b/asv_bench/benchmarks/multiindex_object.py @@ -0,0 +1,129 @@ +import string + +import numpy as np +import pandas.util.testing as tm +from pandas import date_range, MultiIndex + + +class GetLoc(object): + + def setup(self): + self.mi_large = MultiIndex.from_product( + [np.arange(1000), np.arange(20), list(string.ascii_letters)], + names=['one', 'two', 'three']) + self.mi_med = MultiIndex.from_product( + [np.arange(1000), np.arange(10), list('A')], + names=['one', 'two', 'three']) + self.mi_small = MultiIndex.from_product( + [np.arange(100), list('A'), list('A')], + names=['one', 'two', 'three']) + + def time_large_get_loc(self): + self.mi_large.get_loc((999, 19, 'Z')) + + def time_large_get_loc_warm(self): + for _ in range(1000): + self.mi_large.get_loc((999, 19, 'Z')) + + def time_med_get_loc(self): + self.mi_med.get_loc((999, 9, 'A')) + + def time_med_get_loc_warm(self): + for _ in range(1000): + self.mi_med.get_loc((999, 9, 'A')) + + def time_string_get_loc(self): + self.mi_small.get_loc((99, 'A', 'A')) + + def time_small_get_loc_warm(self): + for _ in range(1000): + self.mi_small.get_loc((99, 'A', 'A')) + + +class Duplicates(object): + + def setup(self): + size = 65536 + arrays = [np.random.randint(0, 8192, size), + np.random.randint(0, 1024, size)] + mask = np.random.rand(size) < 0.1 + self.mi_unused_levels = MultiIndex.from_arrays(arrays) + self.mi_unused_levels = self.mi_unused_levels[mask] + + def time_remove_unused_levels(self): + self.mi_unused_levels.remove_unused_levels() + + +class Integer(object): + + def setup(self): + self.mi_int = MultiIndex.from_product([np.arange(1000), + np.arange(1000)], + names=['one', 'two']) + self.obj_index = np.array([(0, 10), (0, 11), (0, 12), + (0, 13), (0, 14), (0, 15), + (0, 16), (0, 17), (0, 18), + (0, 19)], dtype=object) + + def time_get_indexer(self): + self.mi_int.get_indexer(self.obj_index) + + def time_is_monotonic(self): + self.mi_int.is_monotonic + + +class Duplicated(object): + + def setup(self): + n, k = 200, 5000 + levels = [np.arange(n), + tm.makeStringIndex(n).values, + 1000 + np.arange(n)] + codes = [np.random.choice(n, (k * n)) for lev in levels] + self.mi = MultiIndex(levels=levels, codes=codes) + + def time_duplicated(self): + self.mi.duplicated() + + +class Sortlevel(object): + + def setup(self): + n = 1182720 + low, high = -4096, 4096 + arrs = [np.repeat(np.random.randint(low, high, (n // k)), k) + for k in [11, 7, 5, 3, 1]] + self.mi_int = MultiIndex.from_arrays(arrs)[np.random.permutation(n)] + + a = np.repeat(np.arange(100), 1000) + b = np.tile(np.arange(1000), 100) + self.mi = MultiIndex.from_arrays([a, b]) + self.mi = self.mi.take(np.random.permutation(np.arange(100000))) + + def time_sortlevel_int64(self): + self.mi_int.sortlevel() + + def time_sortlevel_zero(self): + self.mi.sortlevel(0) + + def time_sortlevel_one(self): + self.mi.sortlevel(1) + + +class Values(object): + + def setup_cache(self): + + level1 = range(1000) + level2 = date_range(start='1/1/2012', periods=100) + mi = MultiIndex.from_product([level1, level2]) + return mi + + def time_datetime_level_values_copy(self, mi): + mi.copy().values + + def time_datetime_level_values_sliced(self, mi): + mi[:10].values + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/offset.py b/asv_bench/benchmarks/offset.py new file mode 100644 index 0000000000000..4570e73cccc71 --- /dev/null +++ b/asv_bench/benchmarks/offset.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +import warnings +from datetime import datetime + +import numpy as np +import pandas as pd +try: + import pandas.tseries.holiday # noqa +except ImportError: + pass + +hcal = pd.tseries.holiday.USFederalHolidayCalendar() +# These offests currently raise a NotImplimentedError with .apply_index() +non_apply = [pd.offsets.Day(), + pd.offsets.BYearEnd(), + pd.offsets.BYearBegin(), + pd.offsets.BQuarterEnd(), + pd.offsets.BQuarterBegin(), + pd.offsets.BMonthEnd(), + pd.offsets.BMonthBegin(), + pd.offsets.CustomBusinessDay(), + pd.offsets.CustomBusinessDay(calendar=hcal), + pd.offsets.CustomBusinessMonthBegin(calendar=hcal), + pd.offsets.CustomBusinessMonthEnd(calendar=hcal), + pd.offsets.CustomBusinessMonthEnd(calendar=hcal)] +other_offsets = [pd.offsets.YearEnd(), pd.offsets.YearBegin(), + pd.offsets.QuarterEnd(), pd.offsets.QuarterBegin(), + pd.offsets.MonthEnd(), pd.offsets.MonthBegin(), + pd.offsets.DateOffset(months=2, days=2), + pd.offsets.BusinessDay(), pd.offsets.SemiMonthEnd(), + pd.offsets.SemiMonthBegin()] +offsets = non_apply + other_offsets + + +class ApplyIndex(object): + + params = other_offsets + param_names = ['offset'] + + def setup(self, offset): + N = 10000 + self.rng = pd.date_range(start='1/1/2000', periods=N, freq='T') + + def time_apply_index(self, offset): + offset.apply_index(self.rng) + + +class OnOffset(object): + + params = offsets + param_names = ['offset'] + + def setup(self, offset): + self.dates = [datetime(2016, m, d) + for m in [10, 11, 12] + for d in [1, 2, 3, 28, 29, 30, 31] + if not (m == 11 and d == 31)] + + def time_on_offset(self, offset): + for date in self.dates: + offset.onOffset(date) + + +class OffsetSeriesArithmetic(object): + + params = offsets + param_names = ['offset'] + + def setup(self, offset): + N = 1000 + rng = pd.date_range(start='1/1/2000', periods=N, freq='T') + self.data = pd.Series(rng) + + def time_add_offset(self, offset): + with warnings.catch_warnings(record=True): + self.data + offset + + +class OffsetDatetimeIndexArithmetic(object): + + params = offsets + param_names = ['offset'] + + def setup(self, offset): + N = 1000 + self.data = pd.date_range(start='1/1/2000', periods=N, freq='T') + + def time_add_offset(self, offset): + with warnings.catch_warnings(record=True): + self.data + offset + + +class OffestDatetimeArithmetic(object): + + params = offsets + param_names = ['offset'] + + def setup(self, offset): + self.date = datetime(2011, 1, 1) + self.dt64 = np.datetime64('2011-01-01 09:00Z') + + def time_apply(self, offset): + offset.apply(self.date) + + def time_apply_np_dt64(self, offset): + offset.apply(self.dt64) + + def time_add(self, offset): + self.date + offset + + def time_add_10(self, offset): + self.date + (10 * offset) + + def time_subtract(self, offset): + self.date - offset + + def time_subtract_10(self, offset): + self.date - (10 * offset) diff --git a/asv_bench/benchmarks/packers.py b/asv_bench/benchmarks/packers.py deleted file mode 100644 index cd43e305ead8f..0000000000000 --- a/asv_bench/benchmarks/packers.py +++ /dev/null @@ -1,316 +0,0 @@ -from .pandas_vb_common import * -from numpy.random import randint -import pandas as pd -from collections import OrderedDict -from pandas.compat import BytesIO -import sqlite3 -import os -from sqlalchemy import create_engine -import numpy as np -from random import randrange - -class _Packers(object): - goal_time = 0.2 - - def _setup(self): - self.f = '__test__.msg' - self.N = 100000 - self.C = 5 - self.index = date_range('20000101', periods=self.N, freq='H') - self.df = DataFrame(dict([('float{0}'.format(i), randn(self.N)) for i in range(self.C)]), index=self.index) - self.df2 = self.df.copy() - self.df2['object'] = [('%08x' % randrange((16 ** 8))) for _ in range(self.N)] - self.remove(self.f) - - def remove(self, f): - try: - os.remove(self.f) - except: - pass - -class Packers(_Packers): - goal_time = 0.2 - - def setup(self): - self._setup() - self.df.to_csv(self.f) - - def time_packers_read_csv(self): - pd.read_csv(self.f) - -class packers_read_excel(_Packers): - goal_time = 0.2 - - def setup(self): - self._setup() - self.bio = BytesIO() - self.writer = pd.io.excel.ExcelWriter(self.bio, engine='xlsxwriter') - self.df[:2000].to_excel(self.writer) - self.writer.save() - - def time_packers_read_excel(self): - self.bio.seek(0) - pd.read_excel(self.bio) - - -class packers_read_hdf_store(_Packers): - goal_time = 0.2 - - def setup(self): - self._setup() - self.df2.to_hdf(self.f, 'df') - - def time_packers_read_hdf_store(self): - pd.read_hdf(self.f, 'df') - - -class packers_read_hdf_table(_Packers): - - def setup(self): - self._setup() - self.df2.to_hdf(self.f, 'df', format='table') - - def time_packers_read_hdf_table(self): - pd.read_hdf(self.f, 'df') - - -class packers_read_json(_Packers): - - def setup(self): - self._setup() - self.df.to_json(self.f, orient='split') - self.df.index = np.arange(self.N) - - def time_packers_read_json(self): - pd.read_json(self.f, orient='split') - - -class packers_read_json_date_index(_Packers): - - def setup(self): - self._setup() - self.remove(self.f) - self.df.to_json(self.f, orient='split') - - def time_packers_read_json_date_index(self): - pd.read_json(self.f, orient='split') - - -class packers_read_pack(_Packers): - - def setup(self): - self._setup() - self.df2.to_msgpack(self.f) - - def time_packers_read_pack(self): - pd.read_msgpack(self.f) - - -class packers_read_pickle(_Packers): - - def setup(self): - self._setup() - self.df2.to_pickle(self.f) - - def time_packers_read_pickle(self): - pd.read_pickle(self.f) - -class packers_read_sql(_Packers): - - def setup(self): - self._setup() - self.engine = create_engine('sqlite:///:memory:') - self.df2.to_sql('table', self.engine, if_exists='replace') - - def time_packers_read_sql(self): - pd.read_sql_table('table', self.engine) - - -class packers_read_stata(_Packers): - - def setup(self): - self._setup() - self.df.to_stata(self.f, {'index': 'tc', }) - - def time_packers_read_stata(self): - pd.read_stata(self.f) - - -class packers_read_stata_with_validation(_Packers): - - def setup(self): - self._setup() - self.df['int8_'] = [randint(np.iinfo(np.int8).min, (np.iinfo(np.int8).max - 27)) for _ in range(self.N)] - self.df['int16_'] = [randint(np.iinfo(np.int16).min, (np.iinfo(np.int16).max - 27)) for _ in range(self.N)] - self.df['int32_'] = [randint(np.iinfo(np.int32).min, (np.iinfo(np.int32).max - 27)) for _ in range(self.N)] - self.df['float32_'] = np.array(randn(self.N), dtype=np.float32) - self.df.to_stata(self.f, {'index': 'tc', }) - - def time_packers_read_stata_with_validation(self): - pd.read_stata(self.f) - - -class packers_read_sas(_Packers): - - def setup(self): - self.f = os.path.join(os.path.dirname(__file__), '..', '..', - 'pandas', 'io', 'tests', 'sas', 'data', - 'test1.sas7bdat') - self.f2 = os.path.join(os.path.dirname(__file__), '..', '..', - 'pandas', 'io', 'tests', 'sas', 'data', - 'paxraw_d_short.xpt') - - def time_read_sas7bdat(self): - pd.read_sas(self.f, format='sas7bdat') - - def time_read_xport(self): - pd.read_sas(self.f, format='xport') - - -class CSV(_Packers): - - def setup(self): - self._setup() - - def time_write_csv(self): - self.df.to_csv(self.f) - - def teardown(self): - self.remove(self.f) - - -class Excel(_Packers): - - def setup(self): - self._setup() - self.bio = BytesIO() - - def time_write_excel_openpyxl(self): - self.bio.seek(0) - self.writer = pd.io.excel.ExcelWriter(self.bio, engine='openpyxl') - self.df[:2000].to_excel(self.writer) - self.writer.save() - - def time_write_excel_xlsxwriter(self): - self.bio.seek(0) - self.writer = pd.io.excel.ExcelWriter(self.bio, engine='xlsxwriter') - self.df[:2000].to_excel(self.writer) - self.writer.save() - - def time_write_excel_xlwt(self): - self.bio.seek(0) - self.writer = pd.io.excel.ExcelWriter(self.bio, engine='xlwt') - self.df[:2000].to_excel(self.writer) - self.writer.save() - - -class HDF(_Packers): - - def setup(self): - self._setup() - - def time_write_hdf_store(self): - self.df2.to_hdf(self.f, 'df') - - def time_write_hdf_table(self): - self.df2.to_hdf(self.f, 'df', table=True) - - def teardown(self): - self.remove(self.f) - -class JSON(_Packers): - - def setup(self): - self._setup() - self.df_date = self.df.copy() - self.df.index = np.arange(self.N) - self.cols = [(lambda i: ('{0}_timedelta'.format(i), [pd.Timedelta(('%d seconds' % randrange(1000000.0))) for _ in range(self.N)])), (lambda i: ('{0}_int'.format(i), randint(100000000.0, size=self.N))), (lambda i: ('{0}_timestamp'.format(i), [pd.Timestamp((1418842918083256000 + randrange(1000000000.0, 1e+18, 200))) for _ in range(self.N)]))] - self.df_mixed = DataFrame(OrderedDict([self.cols[(i % len(self.cols))](i) for i in range(self.C)]), index=self.index) - - self.cols = [(lambda i: ('{0}_float'.format(i), randn(self.N))), (lambda i: ('{0}_int'.format(i), randint(100000000.0, size=self.N)))] - self.df_mixed2 = DataFrame(OrderedDict([self.cols[(i % len(self.cols))](i) for i in range(self.C)]), index=self.index) - - self.cols = [(lambda i: ('{0}_float'.format(i), randn(self.N))), (lambda i: ('{0}_int'.format(i), randint(100000000.0, size=self.N))), (lambda i: ('{0}_str'.format(i), [('%08x' % randrange((16 ** 8))) for _ in range(self.N)]))] - self.df_mixed3 = DataFrame(OrderedDict([self.cols[(i % len(self.cols))](i) for i in range(self.C)]), index=self.index) - - def time_write_json(self): - self.df.to_json(self.f, orient='split') - - def time_write_json_T(self): - self.df.to_json(self.f, orient='columns') - - def time_write_json_date_index(self): - self.df_date.to_json(self.f, orient='split') - - def time_write_json_mixed_delta_int_tstamp(self): - self.df_mixed.to_json(self.f, orient='split') - - def time_write_json_mixed_float_int(self): - self.df_mixed2.to_json(self.f, orient='index') - - def time_write_json_mixed_float_int_T(self): - self.df_mixed2.to_json(self.f, orient='columns') - - def time_write_json_mixed_float_int_str(self): - self.df_mixed3.to_json(self.f, orient='split') - - def time_write_json_lines(self): - self.df.to_json(self.f, orient="records", lines=True) - - def teardown(self): - self.remove(self.f) - - -class MsgPack(_Packers): - - def setup(self): - self._setup() - - def time_write_msgpack(self): - self.df2.to_msgpack(self.f) - - def teardown(self): - self.remove(self.f) - - -class Pickle(_Packers): - - def setup(self): - self._setup() - - def time_write_pickle(self): - self.df2.to_pickle(self.f) - - def teardown(self): - self.remove(self.f) - - -class SQL(_Packers): - - def setup(self): - self._setup() - self.engine = create_engine('sqlite:///:memory:') - - def time_write_sql(self): - self.df2.to_sql('table', self.engine, if_exists='replace') - - -class STATA(_Packers): - - def setup(self): - self._setup() - - self.df3=self.df.copy() - self.df3['int8_'] = [randint(np.iinfo(np.int8).min, (np.iinfo(np.int8).max - 27)) for _ in range(self.N)] - self.df3['int16_'] = [randint(np.iinfo(np.int16).min, (np.iinfo(np.int16).max - 27)) for _ in range(self.N)] - self.df3['int32_'] = [randint(np.iinfo(np.int32).min, (np.iinfo(np.int32).max - 27)) for _ in range(self.N)] - self.df3['float32_'] = np.array(randn(self.N), dtype=np.float32) - - def time_write_stata(self): - self.df.to_stata(self.f, {'index': 'tc', }) - - def time_write_stata_with_validation(self): - self.df3.to_stata(self.f, {'index': 'tc', }) - - def teardown(self): - self.remove(self.f) diff --git a/asv_bench/benchmarks/pandas_vb_common.py b/asv_bench/benchmarks/pandas_vb_common.py index 56ccc94c414fb..d479952cbfbf6 100644 --- a/asv_bench/benchmarks/pandas_vb_common.py +++ b/asv_bench/benchmarks/pandas_vb_common.py @@ -1,37 +1,55 @@ -from pandas import * -import pandas as pd -from datetime import timedelta -from numpy.random import randn -from numpy.random import randint -from numpy.random import permutation -import pandas.util.testing as tm -import random -import numpy as np -import threading +import os from importlib import import_module -try: - from pandas.compat import range -except ImportError: - pass - -np.random.seed(1234) +import numpy as np +import pandas as pd -# try em until it works! -for imp in ['pandas_tseries', 'pandas.lib', 'pandas._libs.lib']: +# Compatibility import for lib +for imp in ['pandas._libs.lib', 'pandas.lib']: try: lib = import_module(imp) break - except: + except (ImportError, TypeError, ValueError): pass +numeric_dtypes = [np.int64, np.int32, np.uint32, np.uint64, np.float32, + np.float64, np.int16, np.int8, np.uint16, np.uint8] +datetime_dtypes = [np.datetime64, np.timedelta64] +string_dtypes = [np.object] try: - Panel = Panel -except Exception: - Panel = WidePanel + extension_dtypes = [pd.Int8Dtype, pd.Int16Dtype, + pd.Int32Dtype, pd.Int64Dtype, + pd.UInt8Dtype, pd.UInt16Dtype, + pd.UInt32Dtype, pd.UInt64Dtype, + pd.CategoricalDtype, + pd.IntervalDtype, + pd.DatetimeTZDtype('ns', 'UTC'), + pd.PeriodDtype('D')] +except AttributeError: + extension_dtypes = [] -# didn't add to namespace until later -try: - from pandas.core.index import MultiIndex -except ImportError: - pass + +def setup(*args, **kwargs): + # This function just needs to be imported into each benchmark file to + # set up the random seed before each function. + # http://asv.readthedocs.io/en/latest/writing_benchmarks.html + np.random.seed(1234) + + +class BaseIO(object): + """ + Base class for IO benchmarks + """ + fname = None + + def remove(self, f): + """Remove created files""" + try: + os.remove(f) + except OSError: + # On Windows, attempting to remove a file that is in use + # causes an exception to be raised + pass + + def teardown(self, *args, **kwargs): + self.remove(self.fname) diff --git a/asv_bench/benchmarks/panel_ctor.py b/asv_bench/benchmarks/panel_ctor.py index faedce6c574ec..627705284481b 100644 --- a/asv_bench/benchmarks/panel_ctor.py +++ b/asv_bench/benchmarks/panel_ctor.py @@ -1,64 +1,55 @@ -from .pandas_vb_common import * +import warnings +from datetime import datetime, timedelta +from pandas import DataFrame, Panel, date_range -class Constructors1(object): - goal_time = 0.2 +class DifferentIndexes(object): def setup(self): self.data_frames = {} - self.start = datetime(1990, 1, 1) - self.end = datetime(2012, 1, 1) + start = datetime(1990, 1, 1) + end = datetime(2012, 1, 1) for x in range(100): - self.end += timedelta(days=1) - self.dr = np.asarray(date_range(self.start, self.end)) - self.df = DataFrame({'a': ([0] * len(self.dr)), 'b': ([1] * len(self.dr)), 'c': ([2] * len(self.dr)), }, index=self.dr) - self.data_frames[x] = self.df + end += timedelta(days=1) + idx = date_range(start, end) + df = DataFrame({'a': 0, 'b': 1, 'c': 2}, index=idx) + self.data_frames[x] = df - def time_panel_from_dict_all_different_indexes(self): - Panel.from_dict(self.data_frames) + def time_from_dict(self): + with warnings.catch_warnings(record=True): + Panel.from_dict(self.data_frames) -class Constructors2(object): - goal_time = 0.2 +class SameIndexes(object): def setup(self): - self.data_frames = {} - for x in range(100): - self.dr = np.asarray(DatetimeIndex(start=datetime(1990, 1, 1), end=datetime(2012, 1, 1), freq=datetools.Day(1))) - self.df = DataFrame({'a': ([0] * len(self.dr)), 'b': ([1] * len(self.dr)), 'c': ([2] * len(self.dr)), }, index=self.dr) - self.data_frames[x] = self.df - - def time_panel_from_dict_equiv_indexes(self): - Panel.from_dict(self.data_frames) - - -class Constructors3(object): - goal_time = 0.2 - - def setup(self): - self.dr = np.asarray(DatetimeIndex(start=datetime(1990, 1, 1), end=datetime(2012, 1, 1), freq=datetools.Day(1))) - self.data_frames = {} - for x in range(100): - self.df = DataFrame({'a': ([0] * len(self.dr)), 'b': ([1] * len(self.dr)), 'c': ([2] * len(self.dr)), }, index=self.dr) - self.data_frames[x] = self.df + idx = date_range(start=datetime(1990, 1, 1), + end=datetime(2012, 1, 1), + freq='D') + df = DataFrame({'a': 0, 'b': 1, 'c': 2}, index=idx) + self.data_frames = dict(enumerate([df] * 100)) - def time_panel_from_dict_same_index(self): - Panel.from_dict(self.data_frames) + def time_from_dict(self): + with warnings.catch_warnings(record=True): + Panel.from_dict(self.data_frames) -class Constructors4(object): - goal_time = 0.2 +class TwoIndexes(object): def setup(self): - self.data_frames = {} - self.start = datetime(1990, 1, 1) - self.end = datetime(2012, 1, 1) - for x in range(100): - if (x == 50): - self.end += timedelta(days=1) - self.dr = np.asarray(date_range(self.start, self.end)) - self.df = DataFrame({'a': ([0] * len(self.dr)), 'b': ([1] * len(self.dr)), 'c': ([2] * len(self.dr)), }, index=self.dr) - self.data_frames[x] = self.df - - def time_panel_from_dict_two_different_indexes(self): - Panel.from_dict(self.data_frames) + start = datetime(1990, 1, 1) + end = datetime(2012, 1, 1) + df1 = DataFrame({'a': 0, 'b': 1, 'c': 2}, + index=date_range(start=start, end=end, freq='D')) + end += timedelta(days=1) + df2 = DataFrame({'a': 0, 'b': 1, 'c': 2}, + index=date_range(start=start, end=end, freq='D')) + dfs = [df1] * 50 + [df2] * 50 + self.data_frames = dict(enumerate(dfs)) + + def time_from_dict(self): + with warnings.catch_warnings(record=True): + Panel.from_dict(self.data_frames) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/panel_methods.py b/asv_bench/benchmarks/panel_methods.py index 6609305502011..a4c12c082236e 100644 --- a/asv_bench/benchmarks/panel_methods.py +++ b/asv_bench/benchmarks/panel_methods.py @@ -1,24 +1,25 @@ -from .pandas_vb_common import * +import warnings + +import numpy as np +from pandas import Panel class PanelMethods(object): - goal_time = 0.2 - def setup(self): - self.index = date_range(start='2000', freq='D', periods=1000) - self.panel = Panel(np.random.randn(100, len(self.index), 1000)) + params = ['items', 'major', 'minor'] + param_names = ['axis'] - def time_pct_change_items(self): - self.panel.pct_change(1, axis='items') + def setup(self, axis): + with warnings.catch_warnings(record=True): + self.panel = Panel(np.random.randn(100, 1000, 100)) - def time_pct_change_major(self): - self.panel.pct_change(1, axis='major') + def time_pct_change(self, axis): + with warnings.catch_warnings(record=True): + self.panel.pct_change(1, axis=axis) - def time_pct_change_minor(self): - self.panel.pct_change(1, axis='minor') + def time_shift(self, axis): + with warnings.catch_warnings(record=True): + self.panel.shift(1, axis=axis) - def time_shift(self): - self.panel.shift(1) - def time_shift_minor(self): - self.panel.shift(1, axis='minor') +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/parser_vb.py b/asv_bench/benchmarks/parser_vb.py deleted file mode 100644 index 32bf7e50d1a89..0000000000000 --- a/asv_bench/benchmarks/parser_vb.py +++ /dev/null @@ -1,121 +0,0 @@ -from .pandas_vb_common import * -import os -from pandas import read_csv -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - - -class read_csv1(object): - goal_time = 0.2 - - def setup(self): - self.N = 10000 - self.K = 8 - self.df = DataFrame((np.random.randn(self.N, self.K) * np.random.randint(100, 10000, (self.N, self.K)))) - self.df.to_csv('test.csv', sep='|') - - self.format = (lambda x: '{:,}'.format(x)) - self.df2 = self.df.applymap(self.format) - self.df2.to_csv('test2.csv', sep='|') - - def time_sep(self): - read_csv('test.csv', sep='|') - - def time_thousands(self): - read_csv('test.csv', sep='|', thousands=',') - - def teardown(self): - os.remove('test.csv') - os.remove('test2.csv') - - -class read_csv2(object): - goal_time = 0.2 - - def setup(self): - self.data = ['A,B,C'] - self.data = (self.data + (['1,2,3 # comment'] * 100000)) - self.data = '\n'.join(self.data) - - def time_comment(self): - read_csv(StringIO(self.data), comment='#') - - -class read_csv3(object): - goal_time = 0.2 - - def setup(self): - self.data = """0.1213700904466425978256438611,0.0525708283766902484401839501,0.4174092731488769913994474336\n -0.4096341697147408700274695547,0.1587830198973579909349496119,0.1292545832485494372576795285\n -0.8323255650024565799327547210,0.9694902427379478160318626578,0.6295047811546814475747169126\n -0.4679375305798131323697930383,0.2963942381834381301075609371,0.5268936082160610157032465394\n -0.6685382761849776311890991564,0.6721207066140679753374342908,0.6519975277021627935170045020\n""" - self.data2 = self.data.replace(',', ';').replace('.', ',') - self.data = (self.data * 200) - self.data2 = (self.data2 * 200) - - def time_default_converter(self): - read_csv(StringIO(self.data), sep=',', header=None, - float_precision=None) - - def time_default_converter_with_decimal(self): - read_csv(StringIO(self.data2), sep=';', header=None, - float_precision=None, decimal=',') - - def time_default_converter_python_engine(self): - read_csv(StringIO(self.data), sep=',', header=None, - float_precision=None, engine='python') - - def time_default_converter_with_decimal_python_engine(self): - read_csv(StringIO(self.data2), sep=';', header=None, - float_precision=None, decimal=',', engine='python') - - def time_precise_converter(self): - read_csv(StringIO(self.data), sep=',', header=None, - float_precision='high') - - def time_roundtrip_converter(self): - read_csv(StringIO(self.data), sep=',', header=None, - float_precision='round_trip') - - -class read_csv_categorical(object): - goal_time = 0.2 - - def setup(self): - N = 100000 - group1 = ['aaaaaaaa', 'bbbbbbb', 'cccccccc', 'dddddddd', 'eeeeeeee'] - df = DataFrame({'a': np.random.choice(group1, N).astype('object'), - 'b': np.random.choice(group1, N).astype('object'), - 'c': np.random.choice(group1, N).astype('object')}) - df.to_csv('strings.csv', index=False) - - def time_convert_post(self): - read_csv('strings.csv').apply(pd.Categorical) - - def time_convert_direct(self): - read_csv('strings.csv', dtype='category') - - def teardown(self): - os.remove('strings.csv') - - -class read_csv_dateparsing(object): - goal_time = 0.2 - - def setup(self): - self.N = 10000 - self.K = 8 - self.data = 'KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000\n KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000\n KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000\n KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000\n KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000\n ' - self.data = (self.data * 200) - self.data2 = 'KORD,19990127 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000\n KORD,19990127 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000\n KORD,19990127 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000\n KORD,19990127 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000\n KORD,19990127 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000\n ' - self.data2 = (self.data2 * 200) - - def time_multiple_date(self): - read_csv(StringIO(self.data), sep=',', header=None, - parse_dates=[[1, 2], [1, 3]]) - - def time_baseline(self): - read_csv(StringIO(self.data2), sep=',', header=None, parse_dates=[1]) diff --git a/asv_bench/benchmarks/period.py b/asv_bench/benchmarks/period.py index f9837191a7bae..6d2c7156a0a3d 100644 --- a/asv_bench/benchmarks/period.py +++ b/asv_bench/benchmarks/period.py @@ -1,59 +1,124 @@ -import pandas as pd -from pandas import Series, Period, PeriodIndex, date_range +from pandas import ( + DataFrame, Period, PeriodIndex, Series, date_range, period_range) +from pandas.tseries.frequencies import to_offset -class Constructor(object): - goal_time = 0.2 +class PeriodProperties(object): - def setup(self): + params = (['M', 'min'], + ['year', 'month', 'day', 'hour', 'minute', 'second', + 'is_leap_year', 'quarter', 'qyear', 'week', 'daysinmonth', + 'dayofweek', 'dayofyear', 'start_time', 'end_time']) + param_names = ['freq', 'attr'] + + def setup(self, freq, attr): + self.per = Period('2012-06-01', freq=freq) + + def time_property(self, freq, attr): + getattr(self.per, attr) + + +class PeriodUnaryMethods(object): + + params = ['M', 'min'] + param_names = ['freq'] + + def setup(self, freq): + self.per = Period('2012-06-01', freq=freq) + + def time_to_timestamp(self, freq): + self.per.to_timestamp() + + def time_now(self, freq): + self.per.now(freq) + + def time_asfreq(self, freq): + self.per.asfreq('A') + + +class PeriodConstructor(object): + params = [['D'], [True, False]] + param_names = ['freq', 'is_offset'] + + def setup(self, freq, is_offset): + if is_offset: + self.freq = to_offset(freq) + else: + self.freq = freq + + def time_period_constructor(self, freq, is_offset): + Period('2012-06-01', freq=freq) + + +class PeriodIndexConstructor(object): + + params = [['D'], [True, False]] + param_names = ['freq', 'is_offset'] + + def setup(self, freq, is_offset): self.rng = date_range('1985', periods=1000) self.rng2 = date_range('1985', periods=1000).to_pydatetime() + self.ints = list(range(2000, 3000)) + self.daily_ints = date_range('1/1/2000', periods=1000, + freq=freq).strftime('%Y%m%d').map(int) + if is_offset: + self.freq = to_offset(freq) + else: + self.freq = freq + + def time_from_date_range(self, freq, is_offset): + PeriodIndex(self.rng, freq=freq) - def time_from_date_range(self): - PeriodIndex(self.rng, freq='D') + def time_from_pydatetime(self, freq, is_offset): + PeriodIndex(self.rng2, freq=freq) - def time_from_pydatetime(self): - PeriodIndex(self.rng2, freq='D') + def time_from_ints(self, freq, is_offset): + PeriodIndex(self.ints, freq=freq) + def time_from_ints_daily(self, freq, is_offset): + PeriodIndex(self.daily_ints, freq=freq) -class DataFrame(object): - goal_time = 0.2 + +class DataFramePeriodColumn(object): def setup(self): - self.rng = pd.period_range(start='1/1/1990', freq='S', periods=20000) - self.df = pd.DataFrame(index=range(len(self.rng))) + self.rng = period_range(start='1/1/1990', freq='S', periods=20000) + self.df = DataFrame(index=range(len(self.rng))) def time_setitem_period_column(self): self.df['col'] = self.rng + def time_set_index(self): + # GH#21582 limited by comparisons of Period objects + self.df['col2'] = self.rng + self.df.set_index('col2', append=True) + class Algorithms(object): - goal_time = 0.2 - def setup(self): + params = ['index', 'series'] + param_names = ['typ'] + + def setup(self, typ): data = [Period('2011-01', freq='M'), Period('2011-02', freq='M'), Period('2011-03', freq='M'), Period('2011-04', freq='M')] - self.s = Series(data * 1000) - self.i = PeriodIndex(data, freq='M') - - def time_drop_duplicates_pseries(self): - self.s.drop_duplicates() - def time_drop_duplicates_pindex(self): - self.i.drop_duplicates() + if typ == 'index': + self.vector = PeriodIndex(data * 1000, freq='M') + elif typ == 'series': + self.vector = Series(data * 1000) - def time_value_counts_pseries(self): - self.s.value_counts() + def time_drop_duplicates(self, typ): + self.vector.drop_duplicates() - def time_value_counts_pindex(self): - self.i.value_counts() + def time_value_counts(self, typ): + self.vector.value_counts() -class period_standard_indexing(object): - goal_time = 0.2 +class Indexing(object): def setup(self): - self.index = PeriodIndex(start='1985', periods=1000, freq='D') + self.index = period_range(start='1985', periods=1000, freq='D') self.series = Series(range(1000), index=self.index) self.period = self.index[500] @@ -70,7 +135,10 @@ def time_series_loc(self): self.series.loc[self.period] def time_align(self): - pd.DataFrame({'a': self.series, 'b': self.series[:500]}) + DataFrame({'a': self.series, 'b': self.series[:500]}) def time_intersection(self): self.index[:750].intersection(self.index[250:]) + + def time_unique(self): + self.index.unique() diff --git a/asv_bench/benchmarks/plotting.py b/asv_bench/benchmarks/plotting.py index 757c3e27dd333..8a67af0bdabd1 100644 --- a/asv_bench/benchmarks/plotting.py +++ b/asv_bench/benchmarks/plotting.py @@ -1,21 +1,69 @@ -from .pandas_vb_common import * +import numpy as np +from pandas import DataFrame, Series, DatetimeIndex, date_range try: - from pandas import date_range + from pandas.plotting import andrews_curves except ImportError: - def date_range(start=None, end=None, periods=None, freq=None): - return DatetimeIndex(start, end, periods=periods, offset=freq) -from pandas.tools.plotting import andrews_curves + from pandas.tools.plotting import andrews_curves +import matplotlib +matplotlib.use('Agg') + + +class SeriesPlotting(object): + params = [['line', 'bar', 'area', 'barh', 'hist', 'kde', 'pie']] + param_names = ['kind'] + + def setup(self, kind): + if kind in ['bar', 'barh', 'pie']: + n = 100 + elif kind in ['kde']: + n = 10000 + else: + n = 1000000 + + self.s = Series(np.random.randn(n)) + if kind in ['area', 'pie']: + self.s = self.s.abs() + + def time_series_plot(self, kind): + self.s.plot(kind=kind) + + +class FramePlotting(object): + params = [['line', 'bar', 'area', 'barh', 'hist', 'kde', 'pie', 'scatter', + 'hexbin']] + param_names = ['kind'] + + def setup(self, kind): + if kind in ['bar', 'barh', 'pie']: + n = 100 + elif kind in ['kde', 'scatter', 'hexbin']: + n = 10000 + else: + n = 1000000 + + self.x = Series(np.random.randn(n)) + self.y = Series(np.random.randn(n)) + if kind in ['area', 'pie']: + self.x = self.x.abs() + self.y = self.y.abs() + self.df = DataFrame({'x': self.x, 'y': self.y}) + + def time_frame_plot(self, kind): + self.df.plot(x='x', y='y', kind=kind) class TimeseriesPlotting(object): - goal_time = 0.2 def setup(self): - import matplotlib - matplotlib.use('Agg') - self.N = 2000 - self.M = 5 - self.df = DataFrame(np.random.randn(self.N, self.M), index=date_range('1/1/1975', periods=self.N)) + N = 2000 + M = 5 + idx = date_range('1/1/1975', periods=N) + self.df = DataFrame(np.random.randn(N, M), index=idx) + + idx_irregular = DatetimeIndex(np.concatenate((idx.values[0:10], + idx.values[12:]))) + self.df2 = DataFrame(np.random.randn(len(idx_irregular), M), + index=idx_irregular) def time_plot_regular(self): self.df.plot() @@ -23,18 +71,23 @@ def time_plot_regular(self): def time_plot_regular_compat(self): self.df.plot(x_compat=True) + def time_plot_irregular(self): + self.df2.plot() + + def time_plot_table(self): + self.df.plot(table=True) + class Misc(object): - goal_time = 0.6 def setup(self): - import matplotlib - matplotlib.use('Agg') - self.N = 500 - self.M = 10 - data_dict = {x: np.random.randn(self.N) for x in range(self.M)} - data_dict["Name"] = ["A"] * self.N - self.df = DataFrame(data_dict) + N = 500 + M = 10 + self.df = DataFrame(np.random.randn(N, M)) + self.df['Name'] = ["A"] * N def time_plot_andrews_curves(self): andrews_curves(self.df, "Name") + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/reindex.py b/asv_bench/benchmarks/reindex.py index 537d275e7c727..3080b34024a33 100644 --- a/asv_bench/benchmarks/reindex.py +++ b/asv_bench/benchmarks/reindex.py @@ -1,98 +1,79 @@ -from .pandas_vb_common import * -from random import shuffle +import numpy as np +import pandas.util.testing as tm +from pandas import (DataFrame, Series, MultiIndex, Index, date_range, + period_range) +from .pandas_vb_common import lib -class Reindexing(object): - goal_time = 0.2 +class Reindex(object): def setup(self): - self.rng = DatetimeIndex(start='1/1/1970', periods=10000, freq='1min') - self.df = DataFrame(np.random.rand(10000, 10), index=self.rng, + rng = date_range(start='1/1/1970', periods=10000, freq='1min') + self.df = DataFrame(np.random.rand(10000, 10), index=rng, columns=range(10)) self.df['foo'] = 'bar' - self.rng2 = Index(self.rng[::2]) - + self.rng_subset = Index(rng[::2]) self.df2 = DataFrame(index=range(10000), data=np.random.rand(10000, 30), columns=range(30)) - - # multi-index N = 5000 K = 200 level1 = tm.makeStringIndex(N).values.repeat(K) level2 = np.tile(tm.makeStringIndex(K).values, N) index = MultiIndex.from_arrays([level1, level2]) - self.s1 = Series(np.random.randn((N * K)), index=index) - self.s2 = self.s1[::2] + self.s = Series(np.random.randn(N * K), index=index) + self.s_subset = self.s[::2] def time_reindex_dates(self): - self.df.reindex(self.rng2) + self.df.reindex(self.rng_subset) def time_reindex_columns(self): self.df2.reindex(columns=self.df.columns[1:5]) def time_reindex_multiindex(self): - self.s1.reindex(self.s2.index) - + self.s.reindex(self.s_subset.index) -#---------------------------------------------------------------------- -# Pad / backfill +class ReindexMethod(object): -class FillMethod(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range('1/1/2000', periods=100000, freq='1min') - self.ts = Series(np.random.randn(len(self.rng)), index=self.rng) - self.ts2 = self.ts[::2] - self.ts3 = self.ts2.reindex(self.ts.index) - self.ts4 = self.ts3.astype('float32') + params = [['pad', 'backfill'], [date_range, period_range]] + param_names = ['method', 'constructor'] - def pad(self, source_series, target_index): - try: - source_series.reindex(target_index, method='pad') - except: - source_series.reindex(target_index, fillMethod='pad') + def setup(self, method, constructor): + N = 100000 + self.idx = constructor('1/1/2000', periods=N, freq='1min') + self.ts = Series(np.random.randn(N), index=self.idx)[::2] - def backfill(self, source_series, target_index): - try: - source_series.reindex(target_index, method='backfill') - except: - source_series.reindex(target_index, fillMethod='backfill') + def time_reindex_method(self, method, constructor): + self.ts.reindex(self.idx, method=method) - def time_backfill_dates(self): - self.backfill(self.ts2, self.ts.index) - def time_pad_daterange(self): - self.pad(self.ts2, self.ts.index) +class Fillna(object): - def time_backfill(self): - self.ts3.fillna(method='backfill') + params = ['pad', 'backfill'] + param_names = ['method'] - def time_backfill_float32(self): - self.ts4.fillna(method='backfill') + def setup(self, method): + N = 100000 + self.idx = date_range('1/1/2000', periods=N, freq='1min') + ts = Series(np.random.randn(N), index=self.idx)[::2] + self.ts_reindexed = ts.reindex(self.idx) + self.ts_float32 = self.ts_reindexed.astype('float32') - def time_pad(self): - self.ts3.fillna(method='pad') + def time_reindexed(self, method): + self.ts_reindexed.fillna(method=method) - def time_pad_float32(self): - self.ts4.fillna(method='pad') - - -#---------------------------------------------------------------------- -# align on level + def time_float_32(self, method): + self.ts_float32.fillna(method=method) class LevelAlign(object): - goal_time = 0.2 def setup(self): self.index = MultiIndex( levels=[np.arange(10), np.arange(100), np.arange(100)], - labels=[np.arange(10).repeat(10000), - np.tile(np.arange(100).repeat(100), 10), - np.tile(np.tile(np.arange(100), 100), 10)]) - random.shuffle(self.index.values) + codes=[np.arange(10).repeat(10000), + np.tile(np.arange(100).repeat(100), 10), + np.tile(np.tile(np.arange(100), 100), 10)]) self.df = DataFrame(np.random.randn(len(self.index), 4), index=self.index) self.df_level = DataFrame(np.random.randn(100, 4), @@ -102,106 +83,82 @@ def time_align_level(self): self.df.align(self.df_level, level=1, copy=False) def time_reindex_level(self): - self.df_level.reindex(self.df.index, level=1) - - -#---------------------------------------------------------------------- -# drop_duplicates + self.df_level.reindex(self.index, level=1) -class Duplicates(object): - goal_time = 0.2 +class DropDuplicates(object): - def setup(self): - self.N = 10000 - self.K = 10 - self.key1 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.key2 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.df = DataFrame({'key1': self.key1, 'key2': self.key2, - 'value': np.random.randn((self.N * self.K)),}) - self.col_array_list = list(self.df.values.T) + params = [True, False] + param_names = ['inplace'] - self.df2 = self.df.copy() - self.df2.ix[:10000, :] = np.nan + def setup(self, inplace): + N = 10000 + K = 10 + key1 = tm.makeStringIndex(N).values.repeat(K) + key2 = tm.makeStringIndex(N).values.repeat(K) + self.df = DataFrame({'key1': key1, 'key2': key2, + 'value': np.random.randn(N * K)}) + self.df_nan = self.df.copy() + self.df_nan.iloc[:10000, :] = np.nan self.s = Series(np.random.randint(0, 1000, size=10000)) - self.s2 = Series(np.tile(tm.makeStringIndex(1000).values, 10)) - - np.random.seed(1234) - self.N = 1000000 - self.K = 10000 - self.key1 = np.random.randint(0, self.K, size=self.N) - self.df_int = DataFrame({'key1': self.key1}) - self.df_bool = DataFrame({i: np.random.randint(0, 2, size=self.K, - dtype=bool) - for i in range(10)}) - - def time_frame_drop_dups(self): - self.df.drop_duplicates(['key1', 'key2']) + self.s_str = Series(np.tile(tm.makeStringIndex(1000).values, 10)) - def time_frame_drop_dups_inplace(self): - self.df.drop_duplicates(['key1', 'key2'], inplace=True) + N = 1000000 + K = 10000 + key1 = np.random.randint(0, K, size=N) + self.df_int = DataFrame({'key1': key1}) + self.df_bool = DataFrame(np.random.randint(0, 2, size=(K, 10), + dtype=bool)) - def time_frame_drop_dups_na(self): - self.df2.drop_duplicates(['key1', 'key2']) + def time_frame_drop_dups(self, inplace): + self.df.drop_duplicates(['key1', 'key2'], inplace=inplace) - def time_frame_drop_dups_na_inplace(self): - self.df2.drop_duplicates(['key1', 'key2'], inplace=True) + def time_frame_drop_dups_na(self, inplace): + self.df_nan.drop_duplicates(['key1', 'key2'], inplace=inplace) - def time_series_drop_dups_int(self): - self.s.drop_duplicates() + def time_series_drop_dups_int(self, inplace): + self.s.drop_duplicates(inplace=inplace) - def time_series_drop_dups_string(self): - self.s2.drop_duplicates() + def time_series_drop_dups_string(self, inplace): + self.s_str.drop_duplicates(inplace=inplace) - def time_frame_drop_dups_int(self): - self.df_int.drop_duplicates() + def time_frame_drop_dups_int(self, inplace): + self.df_int.drop_duplicates(inplace=inplace) - def time_frame_drop_dups_bool(self): - self.df_bool.drop_duplicates() - -#---------------------------------------------------------------------- -# blog "pandas escaped the zoo" + def time_frame_drop_dups_bool(self, inplace): + self.df_bool.drop_duplicates(inplace=inplace) class Align(object): - goal_time = 0.2 - + # blog "pandas escaped the zoo" def setup(self): n = 50000 indices = tm.makeStringIndex(n) subsample_size = 40000 - - def sample(values, k): - sampler = np.arange(len(values)) - shuffle(sampler) - return values.take(sampler[:k]) - - self.x = Series(np.random.randn(50000), indices) + self.x = Series(np.random.randn(n), indices) self.y = Series(np.random.randn(subsample_size), - index=sample(indices, subsample_size)) + index=np.random.choice(indices, subsample_size, + replace=False)) def time_align_series_irregular_string(self): - (self.x + self.y) + self.x + self.y class LibFastZip(object): - goal_time = 0.2 def setup(self): - self.N = 10000 - self.K = 10 - self.key1 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.key2 = tm.makeStringIndex(self.N).values.repeat(self.K) - self.df = DataFrame({'key1': self.key1, 'key2': self.key2, 'value': np.random.randn((self.N * self.K)), }) - self.col_array_list = list(self.df.values.T) - - self.df2 = self.df.copy() - self.df2.ix[:10000, :] = np.nan - self.col_array_list2 = list(self.df2.values.T) + N = 10000 + K = 10 + key1 = tm.makeStringIndex(N).values.repeat(K) + key2 = tm.makeStringIndex(N).values.repeat(K) + col_array = np.vstack([key1, key2, np.random.randn(N * K)]) + col_array2 = col_array.copy() + col_array2[:, :10000] = np.nan + self.col_array_list = list(col_array) def time_lib_fast_zip(self): lib.fast_zip(self.col_array_list) - def time_lib_fast_zip_fillna(self): - lib.fast_zip_fillna(self.col_array_list2) + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/replace.py b/asv_bench/benchmarks/replace.py index 66b8af53801ac..d8efaf99e2c4d 100644 --- a/asv_bench/benchmarks/replace.py +++ b/asv_bench/benchmarks/replace.py @@ -1,72 +1,56 @@ -from .pandas_vb_common import * -from pandas.compat import range -from datetime import timedelta +import numpy as np +import pandas as pd -class replace_fillna(object): - goal_time = 0.2 +class FillNa(object): - def setup(self): - self.N = 1000000 - try: - self.rng = date_range('1/1/2000', periods=self.N, freq='min') - except NameError: - self.rng = DatetimeIndex('1/1/2000', periods=self.N, offset=datetools.Minute()) - self.date_range = DateRange - self.ts = Series(np.random.randn(self.N), index=self.rng) + params = [True, False] + param_names = ['inplace'] - def time_replace_fillna(self): - self.ts.fillna(0.0, inplace=True) + def setup(self, inplace): + N = 10**6 + rng = pd.date_range('1/1/2000', periods=N, freq='min') + data = np.random.randn(N) + data[::2] = np.nan + self.ts = pd.Series(data, index=rng) + def time_fillna(self, inplace): + self.ts.fillna(0.0, inplace=inplace) -class replace_large_dict(object): - goal_time = 0.2 + def time_replace(self, inplace): + self.ts.replace(np.nan, 0.0, inplace=inplace) - def setup(self): - self.n = (10 ** 6) - self.start_value = (10 ** 5) - self.to_rep = dict(((i, (self.start_value + i)) for i in range(self.n))) - self.s = Series(np.random.randint(self.n, size=(10 ** 3))) - def time_replace_large_dict(self): - self.s.replace(self.to_rep, inplace=True) +class ReplaceDict(object): + params = [True, False] + param_names = ['inplace'] -class replace_convert(object): - goal_time = 0.5 + def setup(self, inplace): + N = 10**5 + start_value = 10**5 + self.to_rep = dict(enumerate(np.arange(N) + start_value)) + self.s = pd.Series(np.random.randint(N, size=10**3)) - def setup(self): - self.n = (10 ** 3) - self.to_ts = dict(((i, pd.Timestamp(i)) for i in range(self.n))) - self.to_td = dict(((i, pd.Timedelta(i)) for i in range(self.n))) - self.s = Series(np.random.randint(self.n, size=(10 ** 3))) - self.df = DataFrame({'A': np.random.randint(self.n, size=(10 ** 3)), - 'B': np.random.randint(self.n, size=(10 ** 3))}) + def time_replace_series(self, inplace): + self.s.replace(self.to_rep, inplace=inplace) - def time_replace_series_timestamp(self): - self.s.replace(self.to_ts) - def time_replace_series_timedelta(self): - self.s.replace(self.to_td) +class Convert(object): - def time_replace_frame_timestamp(self): - self.df.replace(self.to_ts) + params = (['DataFrame', 'Series'], ['Timestamp', 'Timedelta']) + param_names = ['constructor', 'replace_data'] - def time_replace_frame_timedelta(self): - self.df.replace(self.to_td) + def setup(self, constructor, replace_data): + N = 10**3 + data = {'Series': pd.Series(np.random.randint(N, size=N)), + 'DataFrame': pd.DataFrame({'A': np.random.randint(N, size=N), + 'B': np.random.randint(N, size=N)})} + self.to_replace = {i: getattr(pd, replace_data) for i in range(N)} + self.data = data[constructor] + def time_replace(self, constructor, replace_data): + self.data.replace(self.to_replace) -class replace_replacena(object): - goal_time = 0.2 - def setup(self): - self.N = 1000000 - try: - self.rng = date_range('1/1/2000', periods=self.N, freq='min') - except NameError: - self.rng = DatetimeIndex('1/1/2000', periods=self.N, offset=datetools.Minute()) - self.date_range = DateRange - self.ts = Series(np.random.randn(self.N), index=self.rng) - - def time_replace_replacena(self): - self.ts.replace(np.nan, 0.0, inplace=True) +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/reshape.py b/asv_bench/benchmarks/reshape.py index b9346c497b9ef..f6ee107ab618e 100644 --- a/asv_bench/benchmarks/reshape.py +++ b/asv_bench/benchmarks/reshape.py @@ -1,13 +1,14 @@ -from .pandas_vb_common import * -from pandas.core.reshape import melt, wide_to_long +import string +from itertools import product +import numpy as np +from pandas import DataFrame, MultiIndex, date_range, melt, wide_to_long +import pandas as pd -class melt_dataframe(object): - goal_time = 0.2 + +class Melt(object): def setup(self): - self.index = MultiIndex.from_arrays([np.arange(100).repeat(100), np.roll(np.tile(np.arange(100), 100), 25)]) - self.df = DataFrame(np.random.randn(10000, 4), index=self.index) self.df = DataFrame(np.random.randn(10000, 3), columns=['A', 'B', 'C']) self.df['id1'] = np.random.randint(0, 10, 10000) self.df['id2'] = np.random.randint(100, 1000, 10000) @@ -16,104 +17,203 @@ def time_melt_dataframe(self): melt(self.df, id_vars=['id1', 'id2']) -class reshape_pivot_time_series(object): - goal_time = 0.2 +class Pivot(object): def setup(self): - self.index = MultiIndex.from_arrays([np.arange(100).repeat(100), np.roll(np.tile(np.arange(100), 100), 25)]) - self.df = DataFrame(np.random.randn(10000, 4), index=self.index) - self.index = date_range('1/1/2000', periods=10000, freq='h') - self.df = DataFrame(randn(10000, 50), index=self.index, columns=range(50)) - self.pdf = self.unpivot(self.df) - self.f = (lambda : self.pdf.pivot('date', 'variable', 'value')) + N = 10000 + index = date_range('1/1/2000', periods=N, freq='h') + data = {'value': np.random.randn(N * 50), + 'variable': np.arange(50).repeat(N), + 'date': np.tile(index.values, 50)} + self.df = DataFrame(data) def time_reshape_pivot_time_series(self): - self.f() - - def unpivot(self, frame): - (N, K) = frame.shape - self.data = {'value': frame.values.ravel('F'), 'variable': np.asarray(frame.columns).repeat(N), 'date': np.tile(np.asarray(frame.index), K), } - return DataFrame(self.data, columns=['date', 'variable', 'value']) + self.df.pivot('date', 'variable', 'value') -class reshape_stack_simple(object): - goal_time = 0.2 +class SimpleReshape(object): def setup(self): - self.index = MultiIndex.from_arrays([np.arange(100).repeat(100), np.roll(np.tile(np.arange(100), 100), 25)]) - self.df = DataFrame(np.random.randn(10000, 4), index=self.index) + arrays = [np.arange(100).repeat(100), + np.roll(np.tile(np.arange(100), 100), 25)] + index = MultiIndex.from_arrays(arrays) + self.df = DataFrame(np.random.randn(10000, 4), index=index) self.udf = self.df.unstack(1) - def time_reshape_stack_simple(self): + def time_stack(self): self.udf.stack() - -class reshape_unstack_simple(object): - goal_time = 0.2 - - def setup(self): - self.index = MultiIndex.from_arrays([np.arange(100).repeat(100), np.roll(np.tile(np.arange(100), 100), 25)]) - self.df = DataFrame(np.random.randn(10000, 4), index=self.index) - - def time_reshape_unstack_simple(self): + def time_unstack(self): self.df.unstack(1) -class reshape_unstack_large_single_dtype(object): - goal_time = 0.2 +class Unstack(object): - def setup(self): + params = ['int', 'category'] + + def setup(self, dtype): m = 100 n = 1000 levels = np.arange(m) - index = pd.MultiIndex.from_product([levels]*2) + index = MultiIndex.from_product([levels] * 2) columns = np.arange(n) - values = np.arange(m*m*n).reshape(m*m, n) - self.df = pd.DataFrame(values, index, columns) + if dtype == 'int': + values = np.arange(m * m * n).reshape(m * m, n) + else: + # the category branch is ~20x slower than int. So we + # cut down the size a bit. Now it's only ~3x slower. + n = 50 + columns = columns[:n] + indices = np.random.randint(0, 52, size=(m * m, n)) + values = np.take(list(string.ascii_letters), indices) + values = [pd.Categorical(v) for v in values.T] + + self.df = DataFrame(values, index, columns) self.df2 = self.df.iloc[:-1] - def time_unstack_full_product(self): + def time_full_product(self, dtype): self.df.unstack() - def time_unstack_with_mask(self): + def time_without_last_row(self, dtype): self.df2.unstack() -class unstack_sparse_keyspace(object): - goal_time = 0.2 +class SparseIndex(object): def setup(self): - self.index = MultiIndex.from_arrays([np.arange(100).repeat(100), np.roll(np.tile(np.arange(100), 100), 25)]) - self.df = DataFrame(np.random.randn(10000, 4), index=self.index) - self.NUM_ROWS = 1000 - for iter in range(10): - self.df = DataFrame({'A': np.random.randint(50, size=self.NUM_ROWS), 'B': np.random.randint(50, size=self.NUM_ROWS), 'C': np.random.randint((-10), 10, size=self.NUM_ROWS), 'D': np.random.randint((-10), 10, size=self.NUM_ROWS), 'E': np.random.randint(10, size=self.NUM_ROWS), 'F': np.random.randn(self.NUM_ROWS), }) - self.idf = self.df.set_index(['A', 'B', 'C', 'D', 'E']) - if (len(self.idf.index.unique()) == self.NUM_ROWS): - break - - def time_unstack_sparse_keyspace(self): - self.idf.unstack() + NUM_ROWS = 1000 + self.df = DataFrame({'A': np.random.randint(50, size=NUM_ROWS), + 'B': np.random.randint(50, size=NUM_ROWS), + 'C': np.random.randint(-10, 10, size=NUM_ROWS), + 'D': np.random.randint(-10, 10, size=NUM_ROWS), + 'E': np.random.randint(10, size=NUM_ROWS), + 'F': np.random.randn(NUM_ROWS)}) + self.df = self.df.set_index(['A', 'B', 'C', 'D', 'E']) + + def time_unstack(self): + self.df.unstack() -class wide_to_long_big(object): - goal_time = 0.2 +class WideToLong(object): def setup(self): - vars = 'ABCD' nyrs = 20 nidvars = 20 N = 5000 - yrvars = [] - for var in vars: - for yr in range(1, nyrs + 1): - yrvars.append(var + str(yr)) - - self.df = pd.DataFrame(np.random.randn(N, nidvars + len(yrvars)), - columns=list(range(nidvars)) + yrvars) - self.vars = vars + self.letters = list('ABCD') + yrvars = [l + str(num) + for l, num in product(self.letters, range(1, nyrs + 1))] + columns = [str(i) for i in range(nidvars)] + yrvars + self.df = DataFrame(np.random.randn(N, nidvars + len(yrvars)), + columns=columns) + self.df['id'] = self.df.index def time_wide_to_long_big(self): - self.df['id'] = self.df.index - wide_to_long(self.df, list(self.vars), i='id', j='year') + wide_to_long(self.df, self.letters, i='id', j='year') + + +class PivotTable(object): + + def setup(self): + N = 100000 + fac1 = np.array(['A', 'B', 'C'], dtype='O') + fac2 = np.array(['one', 'two'], dtype='O') + ind1 = np.random.randint(0, 3, size=N) + ind2 = np.random.randint(0, 2, size=N) + self.df = DataFrame({'key1': fac1.take(ind1), + 'key2': fac2.take(ind2), + 'key3': fac2.take(ind2), + 'value1': np.random.randn(N), + 'value2': np.random.randn(N), + 'value3': np.random.randn(N)}) + + def time_pivot_table(self): + self.df.pivot_table(index='key1', columns=['key2', 'key3']) + + def time_pivot_table_agg(self): + self.df.pivot_table(index='key1', columns=['key2', 'key3'], + aggfunc=['sum', 'mean']) + + def time_pivot_table_margins(self): + self.df.pivot_table(index='key1', columns=['key2', 'key3'], + margins=True) + + +class Crosstab(object): + + def setup(self): + N = 100000 + fac1 = np.array(['A', 'B', 'C'], dtype='O') + fac2 = np.array(['one', 'two'], dtype='O') + self.ind1 = np.random.randint(0, 3, size=N) + self.ind2 = np.random.randint(0, 2, size=N) + self.vec1 = fac1.take(self.ind1) + self.vec2 = fac2.take(self.ind2) + + def time_crosstab(self): + pd.crosstab(self.vec1, self.vec2) + + def time_crosstab_values(self): + pd.crosstab(self.vec1, self.vec2, values=self.ind1, aggfunc='sum') + + def time_crosstab_normalize(self): + pd.crosstab(self.vec1, self.vec2, normalize=True) + + def time_crosstab_normalize_margins(self): + pd.crosstab(self.vec1, self.vec2, normalize=True, margins=True) + + +class GetDummies(object): + def setup(self): + categories = list(string.ascii_letters[:12]) + s = pd.Series(np.random.choice(categories, size=1000000), + dtype=pd.api.types.CategoricalDtype(categories)) + self.s = s + + def time_get_dummies_1d(self): + pd.get_dummies(self.s, sparse=False) + + def time_get_dummies_1d_sparse(self): + pd.get_dummies(self.s, sparse=True) + + +class Cut(object): + params = [[4, 10, 1000]] + param_names = ['bins'] + + def setup(self, bins): + N = 10**5 + self.int_series = pd.Series(np.arange(N).repeat(5)) + self.float_series = pd.Series(np.random.randn(N).repeat(5)) + self.timedelta_series = pd.Series(np.random.randint(N, size=N), + dtype='timedelta64[ns]') + self.datetime_series = pd.Series(np.random.randint(N, size=N), + dtype='datetime64[ns]') + + def time_cut_int(self, bins): + pd.cut(self.int_series, bins) + + def time_cut_float(self, bins): + pd.cut(self.float_series, bins) + + def time_cut_timedelta(self, bins): + pd.cut(self.timedelta_series, bins) + + def time_cut_datetime(self, bins): + pd.cut(self.datetime_series, bins) + + def time_qcut_int(self, bins): + pd.qcut(self.int_series, bins) + + def time_qcut_float(self, bins): + pd.qcut(self.float_series, bins) + + def time_qcut_timedelta(self, bins): + pd.qcut(self.timedelta_series, bins) + + def time_qcut_datetime(self, bins): + pd.qcut(self.datetime_series, bins) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/rolling.py b/asv_bench/benchmarks/rolling.py new file mode 100644 index 0000000000000..659b6591fbd4b --- /dev/null +++ b/asv_bench/benchmarks/rolling.py @@ -0,0 +1,116 @@ +import pandas as pd +import numpy as np + + +class Methods(object): + + sample_time = 0.2 + params = (['DataFrame', 'Series'], + [10, 1000], + ['int', 'float'], + ['median', 'mean', 'max', 'min', 'std', 'count', 'skew', 'kurt', + 'sum']) + param_names = ['contructor', 'window', 'dtype', 'method'] + + def setup(self, constructor, window, dtype, method): + N = 10**5 + arr = (100 * np.random.random(N)).astype(dtype) + self.roll = getattr(pd, constructor)(arr).rolling(window) + + def time_rolling(self, constructor, window, dtype, method): + getattr(self.roll, method)() + + +class ExpandingMethods(object): + + sample_time = 0.2 + params = (['DataFrame', 'Series'], + ['int', 'float'], + ['median', 'mean', 'max', 'min', 'std', 'count', 'skew', 'kurt', + 'sum']) + param_names = ['contructor', 'window', 'dtype', 'method'] + + def setup(self, constructor, dtype, method): + N = 10**5 + arr = (100 * np.random.random(N)).astype(dtype) + self.expanding = getattr(pd, constructor)(arr).expanding() + + def time_expanding(self, constructor, dtype, method): + getattr(self.expanding, method)() + + +class EWMMethods(object): + + sample_time = 0.2 + params = (['DataFrame', 'Series'], + [10, 1000], + ['int', 'float'], + ['mean', 'std']) + param_names = ['contructor', 'window', 'dtype', 'method'] + + def setup(self, constructor, window, dtype, method): + N = 10**5 + arr = (100 * np.random.random(N)).astype(dtype) + self.ewm = getattr(pd, constructor)(arr).ewm(halflife=window) + + def time_ewm(self, constructor, window, dtype, method): + getattr(self.ewm, method)() + + +class VariableWindowMethods(Methods): + sample_time = 0.2 + params = (['DataFrame', 'Series'], + ['50s', '1h', '1d'], + ['int', 'float'], + ['median', 'mean', 'max', 'min', 'std', 'count', 'skew', 'kurt', + 'sum']) + param_names = ['contructor', 'window', 'dtype', 'method'] + + def setup(self, constructor, window, dtype, method): + N = 10**5 + arr = (100 * np.random.random(N)).astype(dtype) + index = pd.date_range('2017-01-01', periods=N, freq='5s') + self.roll = getattr(pd, constructor)(arr, index=index).rolling(window) + + +class Pairwise(object): + + sample_time = 0.2 + params = ([10, 1000, None], + ['corr', 'cov'], + [True, False]) + param_names = ['window', 'method', 'pairwise'] + + def setup(self, window, method, pairwise): + N = 10**4 + arr = np.random.random(N) + self.df = pd.DataFrame(arr) + + def time_pairwise(self, window, method, pairwise): + if window is None: + r = self.df.expanding() + else: + r = self.df.rolling(window=window) + getattr(r, method)(self.df, pairwise=pairwise) + + +class Quantile(object): + sample_time = 0.2 + params = (['DataFrame', 'Series'], + [10, 1000], + ['int', 'float'], + [0, 0.5, 1], + ['linear', 'nearest', 'lower', 'higher', 'midpoint']) + param_names = ['constructor', 'window', 'dtype', 'percentile'] + + def setup(self, constructor, window, dtype, percentile, interpolation): + N = 10 ** 5 + arr = np.random.random(N).astype(dtype) + self.roll = getattr(pd, constructor)(arr).rolling(window) + + def time_quantile(self, constructor, window, dtype, percentile, + interpolation): + self.roll.quantile(percentile, interpolation=interpolation) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/series_methods.py b/asv_bench/benchmarks/series_methods.py index c66654ee1e006..3303483c50e20 100644 --- a/asv_bench/benchmarks/series_methods.py +++ b/asv_bench/benchmarks/series_methods.py @@ -1,122 +1,204 @@ -from .pandas_vb_common import * +from datetime import datetime +import numpy as np +import pandas.util.testing as tm +from pandas import Series, date_range, NaT -class series_constructor_no_data_datetime_index(object): - goal_time = 0.2 - def setup(self): - self.dr = pd.date_range( - start=datetime(2015,10,26), - end=datetime(2016,1,1), - freq='50s' - ) # ~100k long +class SeriesConstructor(object): - def time_series_constructor_no_data_datetime_index(self): - Series(data=None, index=self.dr) + params = [None, 'dict'] + param_names = ['data'] + def setup(self, data): + self.idx = date_range(start=datetime(2015, 10, 26), + end=datetime(2016, 1, 1), + freq='50s') + dict_data = dict(zip(self.idx, range(len(self.idx)))) + self.data = None if data is None else dict_data -class series_constructor_dict_data_datetime_index(object): - goal_time = 0.2 + def time_constructor(self, data): + Series(data=self.data, index=self.idx) - def setup(self): - self.dr = pd.date_range( - start=datetime(2015, 10, 26), - end=datetime(2016, 1, 1), - freq='50s' - ) # ~100k long - self.data = {d: v for d, v in zip(self.dr, range(len(self.dr)))} - def time_series_constructor_no_data_datetime_index(self): - Series(data=self.data, index=self.dr) +class IsIn(object): + params = ['int64', 'uint64', 'object'] + param_names = ['dtype'] -class series_isin_int64(object): - goal_time = 0.2 + def setup(self, dtype): + self.s = Series(np.random.randint(1, 10, 100000)).astype(dtype) + self.values = [1, 2] + + def time_isin(self, dtypes): + self.s.isin(self.values) + + +class IsInFloat64(object): def setup(self): - self.s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') - self.s4 = Series(np.random.randint(1, 100, 10000000)).astype('int64') - self.values = [1, 2] + self.small = Series([1, 2], dtype=np.float64) + self.many_different_values = np.arange(10**6, dtype=np.float64) + self.few_different_values = np.zeros(10**7, dtype=np.float64) + self.only_nans_values = np.full(10**7, np.nan, dtype=np.float64) - def time_series_isin_int64(self): - self.s3.isin(self.values) + def time_isin_many_different(self): + # runtime is dominated by creation of the lookup-table + self.small.isin(self.many_different_values) - def time_series_isin_int64_large(self): - self.s4.isin(self.values) + def time_isin_few_different(self): + # runtime is dominated by creation of the lookup-table + self.small.isin(self.few_different_values) + def time_isin_nan_values(self): + # runtime is dominated by creation of the lookup-table + self.small.isin(self.few_different_values) -class series_isin_object(object): - goal_time = 0.2 + +class IsInForObjects(object): def setup(self): - self.s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') - self.values = [1, 2] - self.s4 = self.s3.astype('object') + self.s_nans = Series(np.full(10**4, np.nan)).astype(np.object) + self.vals_nans = np.full(10**4, np.nan).astype(np.object) + self.s_short = Series(np.arange(2)).astype(np.object) + self.s_long = Series(np.arange(10**5)).astype(np.object) + self.vals_short = np.arange(2).astype(np.object) + self.vals_long = np.arange(10**5).astype(np.object) + # because of nans floats are special: + self.s_long_floats = Series(np.arange(10**5, + dtype=np.float)).astype(np.object) + self.vals_long_floats = np.arange(10**5, + dtype=np.float).astype(np.object) - def time_series_isin_object(self): - self.s4.isin(self.values) + def time_isin_nans(self): + # if nan-objects are different objects, + # this has the potential to trigger O(n^2) running time + self.s_nans.isin(self.vals_nans) + def time_isin_short_series_long_values(self): + # running time dominated by the preprocessing + self.s_short.isin(self.vals_long) -class series_nlargest1(object): - goal_time = 0.2 + def time_isin_long_series_short_values(self): + # running time dominated by look-up + self.s_long.isin(self.vals_short) - def setup(self): - self.s1 = Series(np.random.randn(10000)) - self.s2 = Series(np.random.randint(1, 10, 10000)) - self.s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') - self.values = [1, 2] - self.s4 = self.s3.astype('object') + def time_isin_long_series_long_values(self): + # no dominating part + self.s_long.isin(self.vals_long) - def time_series_nlargest1(self): - self.s1.nlargest(3, keep='last') - self.s1.nlargest(3, keep='first') + def time_isin_long_series_long_values_floats(self): + # no dominating part + self.s_long_floats.isin(self.vals_long_floats) -class series_nlargest2(object): - goal_time = 0.2 +class NSort(object): - def setup(self): - self.s1 = Series(np.random.randn(10000)) - self.s2 = Series(np.random.randint(1, 10, 10000)) - self.s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') - self.values = [1, 2] - self.s4 = self.s3.astype('object') + params = ['first', 'last', 'all'] + param_names = ['keep'] - def time_series_nlargest2(self): - self.s2.nlargest(3, keep='last') - self.s2.nlargest(3, keep='first') + def setup(self, keep): + self.s = Series(np.random.randint(1, 10, 100000)) + def time_nlargest(self, keep): + self.s.nlargest(3, keep=keep) -class series_nsmallest2(object): - goal_time = 0.2 + def time_nsmallest(self, keep): + self.s.nsmallest(3, keep=keep) - def setup(self): - self.s1 = Series(np.random.randn(10000)) - self.s2 = Series(np.random.randint(1, 10, 10000)) - self.s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') - self.values = [1, 2] - self.s4 = self.s3.astype('object') - def time_series_nsmallest2(self): - self.s2.nsmallest(3, keep='last') - self.s2.nsmallest(3, keep='first') +class Dropna(object): + + params = ['int', 'datetime'] + param_names = ['dtype'] + + def setup(self, dtype): + N = 10**6 + data = {'int': np.random.randint(1, 10, N), + 'datetime': date_range('2000-01-01', freq='S', periods=N)} + self.s = Series(data[dtype]) + if dtype == 'datetime': + self.s[np.random.randint(1, N, 100)] = NaT + + def time_dropna(self, dtype): + self.s.dropna() + +class SearchSorted(object): -class series_dropna_int64(object): goal_time = 0.2 + params = ['int8', 'int16', 'int32', 'int64', + 'uint8', 'uint16', 'uint32', 'uint64', + 'float16', 'float32', 'float64', + 'str'] + param_names = ['dtype'] + + def setup(self, dtype): + N = 10**5 + data = np.array([1] * N + [2] * N + [3] * N).astype(dtype) + self.s = Series(data) + + def time_searchsorted(self, dtype): + key = '2' if dtype == 'str' else 2 + self.s.searchsorted(key) + + +class Map(object): + + params = ['dict', 'Series'] + param_names = 'mapper' + + def setup(self, mapper): + map_size = 1000 + map_data = Series(map_size - np.arange(map_size)) + self.map_data = map_data if mapper == 'Series' else map_data.to_dict() + self.s = Series(np.random.randint(0, map_size, 10000)) + + def time_map(self, mapper): + self.s.map(self.map_data) + + +class Clip(object): + params = [50, 1000, 10**5] + param_names = ['n'] + + def setup(self, n): + self.s = Series(np.random.randn(n)) + + def time_clip(self, n): + self.s.clip(0, 1) + + +class ValueCounts(object): + + params = ['int', 'uint', 'float', 'object'] + param_names = ['dtype'] + + def setup(self, dtype): + self.s = Series(np.random.randint(0, 1000, size=100000)).astype(dtype) + + def time_value_counts(self, dtype): + self.s.value_counts() + + +class Dir(object): def setup(self): - self.s = Series(np.random.randint(1, 10, 1000000)) + self.s = Series(index=tm.makeStringIndex(10000)) - def time_series_dropna_int64(self): - self.s.dropna() + def time_dir_strings(self): + dir(self.s) -class series_dropna_datetime(object): - goal_time = 0.2 +class SeriesGetattr(object): + # https://github.com/pandas-dev/pandas/issues/19764 def setup(self): - self.s = Series(pd.date_range('2000-01-01', freq='S', periods=1000000)) - self.s[np.random.randint(1, 1000000, 100)] = pd.NaT + self.s = Series(1, + index=date_range("2012-01-01", freq='s', + periods=int(1e6))) - def time_series_dropna_datetime(self): - self.s.dropna() + def time_series_datetimeindex_repr(self): + getattr(self.s, 'a', None) + + +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/sparse.py b/asv_bench/benchmarks/sparse.py index 717fe7218ceda..64f87c1670170 100644 --- a/asv_bench/benchmarks/sparse.py +++ b/asv_bench/benchmarks/sparse.py @@ -1,142 +1,152 @@ -from .pandas_vb_common import * -import pandas.sparse.series +import itertools + +import numpy as np import scipy.sparse -from pandas.core.sparse import SparseSeries, SparseDataFrame -from pandas.core.sparse import SparseDataFrame +from pandas import (SparseSeries, SparseDataFrame, SparseArray, Series, + date_range, MultiIndex) + +def make_array(size, dense_proportion, fill_value, dtype): + dense_size = int(size * dense_proportion) + arr = np.full(size, fill_value, dtype) + indexer = np.random.choice(np.arange(size), dense_size, replace=False) + arr[indexer] = np.random.choice(np.arange(100, dtype=dtype), dense_size) + return arr -class sparse_series_to_frame(object): - goal_time = 0.2 + +class SparseSeriesToFrame(object): def setup(self): - self.K = 50 - self.N = 50000 - self.rng = np.asarray(date_range('1/1/2000', periods=self.N, freq='T')) + K = 50 + N = 50001 + rng = date_range('1/1/2000', periods=N, freq='T') self.series = {} - for i in range(1, (self.K + 1)): - self.data = np.random.randn(self.N)[:(- i)] - self.this_rng = self.rng[:(- i)] - self.data[100:] = np.nan - self.series[i] = SparseSeries(self.data, index=self.this_rng) + for i in range(1, K): + data = np.random.randn(N)[:-i] + idx = rng[:-i] + data[100:] = np.nan + self.series[i] = SparseSeries(data, index=idx) - def time_sparse_series_to_frame(self): + def time_series_to_frame(self): SparseDataFrame(self.series) -class sparse_frame_constructor(object): - goal_time = 0.2 +class SparseArrayConstructor(object): - def time_sparse_frame_constructor(self): - SparseDataFrame(columns=np.arange(100), index=np.arange(1000)) + params = ([0.1, 0.01], [0, np.nan], + [np.int64, np.float64, np.object]) + param_names = ['dense_proportion', 'fill_value', 'dtype'] + def setup(self, dense_proportion, fill_value, dtype): + N = 10**6 + self.array = make_array(N, dense_proportion, fill_value, dtype) -class sparse_series_from_coo(object): - goal_time = 0.2 + def time_sparse_array(self, dense_proportion, fill_value, dtype): + SparseArray(self.array, fill_value=fill_value, dtype=dtype) - def setup(self): - self.A = scipy.sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])), shape=(100, 100)) - def time_sparse_series_from_coo(self): - self.ss = pandas.sparse.series.SparseSeries.from_coo(self.A) +class SparseDataFrameConstructor(object): + def setup(self): + N = 1000 + self.arr = np.arange(N) + self.sparse = scipy.sparse.rand(N, N, 0.005) + self.dict = dict(zip(range(N), itertools.repeat([0]))) -class sparse_series_to_coo(object): - goal_time = 0.2 + def time_constructor(self): + SparseDataFrame(columns=self.arr, index=self.arr) - def setup(self): - self.s = pd.Series(([np.nan] * 10000)) - self.s[0] = 3.0 - self.s[100] = (-1.0) - self.s[999] = 12.1 - self.s.index = pd.MultiIndex.from_product((range(10), range(10), range(10), range(10))) - self.ss = self.s.to_sparse() + def time_from_scipy(self): + SparseDataFrame(self.sparse) - def time_sparse_series_to_coo(self): - self.ss.to_coo(row_levels=[0, 1], column_levels=[2, 3], sort_labels=True) + def time_from_dict(self): + SparseDataFrame(self.dict) -class sparse_arithmetic_int(object): - goal_time = 0.2 +class FromCoo(object): def setup(self): - np.random.seed(1) - self.a_10percent = self.make_sparse_array(length=1000000, dense_size=100000, fill_value=np.nan) - self.b_10percent = self.make_sparse_array(length=1000000, dense_size=100000, fill_value=np.nan) + self.matrix = scipy.sparse.coo_matrix(([3.0, 1.0, 2.0], + ([1, 0, 0], [0, 2, 3])), + shape=(100, 100)) + + def time_sparse_series_from_coo(self): + SparseSeries.from_coo(self.matrix) - self.a_10percent_zero = self.make_sparse_array(length=1000000, dense_size=100000, fill_value=0) - self.b_10percent_zero = self.make_sparse_array(length=1000000, dense_size=100000, fill_value=0) - self.a_1percent = self.make_sparse_array(length=1000000, dense_size=10000, fill_value=np.nan) - self.b_1percent = self.make_sparse_array(length=1000000, dense_size=10000, fill_value=np.nan) +class ToCoo(object): - def make_sparse_array(self, length, dense_size, fill_value): - arr = np.array([fill_value] * length, dtype=np.float64) - indexer = np.unique(np.random.randint(0, length, dense_size)) - arr[indexer] = np.random.randint(0, 100, len(indexer)) - return pd.SparseArray(arr, fill_value=fill_value) + def setup(self): + s = Series([np.nan] * 10000) + s[0] = 3.0 + s[100] = -1.0 + s[999] = 12.1 + s.index = MultiIndex.from_product([range(10)] * 4) + self.ss = s.to_sparse() - def time_sparse_make_union(self): - self.a_10percent.sp_index.make_union(self.b_10percent.sp_index) + def time_sparse_series_to_coo(self): + self.ss.to_coo(row_levels=[0, 1], + column_levels=[2, 3], + sort_labels=True) - def time_sparse_intersect(self): - self.a_10percent.sp_index.intersect(self.b_10percent.sp_index) - def time_sparse_addition_10percent(self): - self.a_10percent + self.b_10percent +class Arithmetic(object): - def time_sparse_addition_10percent_zero(self): - self.a_10percent_zero + self.b_10percent_zero + params = ([0.1, 0.01], [0, np.nan]) + param_names = ['dense_proportion', 'fill_value'] - def time_sparse_addition_1percent(self): - self.a_1percent + self.b_1percent + def setup(self, dense_proportion, fill_value): + N = 10**6 + arr1 = make_array(N, dense_proportion, fill_value, np.int64) + self.array1 = SparseArray(arr1, fill_value=fill_value) + arr2 = make_array(N, dense_proportion, fill_value, np.int64) + self.array2 = SparseArray(arr2, fill_value=fill_value) - def time_sparse_division_10percent(self): - self.a_10percent / self.b_10percent + def time_make_union(self, dense_proportion, fill_value): + self.array1.sp_index.make_union(self.array2.sp_index) - def time_sparse_division_10percent_zero(self): - self.a_10percent_zero / self.b_10percent_zero + def time_intersect(self, dense_proportion, fill_value): + self.array1.sp_index.intersect(self.array2.sp_index) - def time_sparse_division_1percent(self): - self.a_1percent / self.b_1percent + def time_add(self, dense_proportion, fill_value): + self.array1 + self.array2 + def time_divide(self, dense_proportion, fill_value): + self.array1 / self.array2 -class sparse_arithmetic_block(object): - goal_time = 0.2 +class ArithmeticBlock(object): - def setup(self): - np.random.seed(1) - self.a = self.make_sparse_array(length=1000000, num_blocks=1000, - block_size=10, fill_value=np.nan) - self.b = self.make_sparse_array(length=1000000, num_blocks=1000, - block_size=10, fill_value=np.nan) + params = [np.nan, 0] + param_names = ['fill_value'] - self.a_zero = self.make_sparse_array(length=1000000, num_blocks=1000, - block_size=10, fill_value=0) - self.b_zero = self.make_sparse_array(length=1000000, num_blocks=1000, - block_size=10, fill_value=np.nan) + def setup(self, fill_value): + N = 10**6 + self.arr1 = self.make_block_array(length=N, num_blocks=1000, + block_size=10, fill_value=fill_value) + self.arr2 = self.make_block_array(length=N, num_blocks=1000, + block_size=10, fill_value=fill_value) - def make_sparse_array(self, length, num_blocks, block_size, fill_value): - a = np.array([fill_value] * length) - for block in range(num_blocks): - i = np.random.randint(0, length) - a[i:i + block_size] = np.random.randint(0, 100, len(a[i:i + block_size])) - return pd.SparseArray(a, fill_value=fill_value) + def make_block_array(self, length, num_blocks, block_size, fill_value): + arr = np.full(length, fill_value) + indicies = np.random.choice(np.arange(0, length, block_size), + num_blocks, + replace=False) + for ind in indicies: + arr[ind:ind + block_size] = np.random.randint(0, 100, block_size) + return SparseArray(arr, fill_value=fill_value) - def time_sparse_make_union(self): - self.a.sp_index.make_union(self.b.sp_index) + def time_make_union(self, fill_value): + self.arr1.sp_index.make_union(self.arr2.sp_index) - def time_sparse_intersect(self): - self.a.sp_index.intersect(self.b.sp_index) + def time_intersect(self, fill_value): + self.arr2.sp_index.intersect(self.arr2.sp_index) - def time_sparse_addition(self): - self.a + self.b + def time_addition(self, fill_value): + self.arr1 + self.arr2 - def time_sparse_addition_zero(self): - self.a_zero + self.b_zero + def time_division(self, fill_value): + self.arr1 / self.arr2 - def time_sparse_division(self): - self.a / self.b - def time_sparse_division_zero(self): - self.a_zero / self.b_zero +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/stat_ops.py b/asv_bench/benchmarks/stat_ops.py index 12fbb2478c2a5..7fdc713f076ed 100644 --- a/asv_bench/benchmarks/stat_ops.py +++ b/asv_bench/benchmarks/stat_ops.py @@ -1,261 +1,144 @@ -from .pandas_vb_common import * +import numpy as np +import pandas as pd -class stat_ops_frame_mean_float_axis_0(object): - goal_time = 0.2 +ops = ['mean', 'sum', 'median', 'std', 'skew', 'kurt', 'mad', 'prod', 'sem', + 'var'] - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) - def time_stat_ops_frame_mean_float_axis_0(self): - self.df.mean() +class FrameOps(object): + params = [ops, ['float', 'int'], [0, 1], [True, False]] + param_names = ['op', 'dtype', 'axis', 'use_bottleneck'] -class stat_ops_frame_mean_float_axis_1(object): - goal_time = 0.2 + def setup(self, op, dtype, axis, use_bottleneck): + df = pd.DataFrame(np.random.randn(100000, 4)).astype(dtype) + try: + pd.options.compute.use_bottleneck = use_bottleneck + except TypeError: + from pandas.core import nanops + nanops._USE_BOTTLENECK = use_bottleneck + self.df_func = getattr(df, op) - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) + def time_op(self, op, dtype, axis, use_bottleneck): + self.df_func(axis=axis) - def time_stat_ops_frame_mean_float_axis_1(self): - self.df.mean(1) +class FrameMultiIndexOps(object): -class stat_ops_frame_mean_int_axis_0(object): - goal_time = 0.2 + params = ([0, 1, [0, 1]], ops) + param_names = ['level', 'op'] - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) + def setup(self, level, op): + levels = [np.arange(10), np.arange(100), np.arange(100)] + codes = [np.arange(10).repeat(10000), + np.tile(np.arange(100).repeat(100), 10), + np.tile(np.tile(np.arange(100), 100), 10)] + index = pd.MultiIndex(levels=levels, codes=codes) + df = pd.DataFrame(np.random.randn(len(index), 4), index=index) + self.df_func = getattr(df, op) - def time_stat_ops_frame_mean_int_axis_0(self): - self.dfi.mean() + def time_op(self, level, op): + self.df_func(level=level) -class stat_ops_frame_mean_int_axis_1(object): - goal_time = 0.2 +class SeriesOps(object): - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) + params = [ops, ['float', 'int'], [True, False]] + param_names = ['op', 'dtype', 'use_bottleneck'] - def time_stat_ops_frame_mean_int_axis_1(self): - self.dfi.mean(1) + def setup(self, op, dtype, use_bottleneck): + s = pd.Series(np.random.randn(100000)).astype(dtype) + try: + pd.options.compute.use_bottleneck = use_bottleneck + except TypeError: + from pandas.core import nanops + nanops._USE_BOTTLENECK = use_bottleneck + self.s_func = getattr(s, op) + def time_op(self, op, dtype, use_bottleneck): + self.s_func() -class stat_ops_frame_sum_float_axis_0(object): - goal_time = 0.2 - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) +class SeriesMultiIndexOps(object): - def time_stat_ops_frame_sum_float_axis_0(self): - self.df.sum() + params = ([0, 1, [0, 1]], ops) + param_names = ['level', 'op'] + def setup(self, level, op): + levels = [np.arange(10), np.arange(100), np.arange(100)] + codes = [np.arange(10).repeat(10000), + np.tile(np.arange(100).repeat(100), 10), + np.tile(np.tile(np.arange(100), 100), 10)] + index = pd.MultiIndex(levels=levels, codes=codes) + s = pd.Series(np.random.randn(len(index)), index=index) + self.s_func = getattr(s, op) -class stat_ops_frame_sum_float_axis_1(object): - goal_time = 0.2 + def time_op(self, level, op): + self.s_func(level=level) - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) - def time_stat_ops_frame_sum_float_axis_1(self): - self.df.sum(1) +class Rank(object): + params = [['DataFrame', 'Series'], [True, False]] + param_names = ['constructor', 'pct'] -class stat_ops_frame_sum_int_axis_0(object): - goal_time = 0.2 + def setup(self, constructor, pct): + values = np.random.randn(10**5) + self.data = getattr(pd, constructor)(values) - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) + def time_rank(self, constructor, pct): + self.data.rank(pct=pct) - def time_stat_ops_frame_sum_int_axis_0(self): - self.dfi.sum() + def time_average_old(self, constructor, pct): + self.data.rank(pct=pct) / len(self.data) -class stat_ops_frame_sum_int_axis_1(object): - goal_time = 0.2 +class Correlation(object): - def setup(self): - self.df = DataFrame(np.random.randn(100000, 4)) - self.dfi = DataFrame(np.random.randint(1000, size=self.df.shape)) + params = [['spearman', 'kendall', 'pearson'], [True, False]] + param_names = ['method', 'use_bottleneck'] - def time_stat_ops_frame_sum_int_axis_1(self): - self.dfi.sum(1) + def setup(self, method, use_bottleneck): + try: + pd.options.compute.use_bottleneck = use_bottleneck + except TypeError: + from pandas.core import nanops + nanops._USE_BOTTLENECK = use_bottleneck + self.df = pd.DataFrame(np.random.randn(1000, 30)) + self.df2 = pd.DataFrame(np.random.randn(1000, 30)) + self.s = pd.Series(np.random.randn(1000)) + self.s2 = pd.Series(np.random.randn(1000)) + def time_corr(self, method, use_bottleneck): + self.df.corr(method=method) -class stat_ops_level_frame_sum(object): - goal_time = 0.2 + def time_corr_series(self, method, use_bottleneck): + self.s.corr(self.s2, method=method) - def setup(self): - self.index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], labels=[np.arange(10).repeat(10000), np.tile(np.arange(100).repeat(100), 10), np.tile(np.tile(np.arange(100), 100), 10)]) - random.shuffle(self.index.values) - self.df = DataFrame(np.random.randn(len(self.index), 4), index=self.index) - self.df_level = DataFrame(np.random.randn(100, 4), index=self.index.levels[1]) + def time_corrwith_cols(self, method, use_bottleneck): + self.df.corrwith(self.df2, method=method) - def time_stat_ops_level_frame_sum(self): - self.df.sum(level=1) + def time_corrwith_rows(self, method, use_bottleneck): + self.df.corrwith(self.df2, axis=1, method=method) -class stat_ops_level_frame_sum_multiple(object): - goal_time = 0.2 +class Covariance(object): - def setup(self): - self.index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], labels=[np.arange(10).repeat(10000), np.tile(np.arange(100).repeat(100), 10), np.tile(np.tile(np.arange(100), 100), 10)]) - random.shuffle(self.index.values) - self.df = DataFrame(np.random.randn(len(self.index), 4), index=self.index) - self.df_level = DataFrame(np.random.randn(100, 4), index=self.index.levels[1]) + params = [[True, False]] + param_names = ['use_bottleneck'] - def time_stat_ops_level_frame_sum_multiple(self): - self.df.sum(level=[0, 1]) + def setup(self, use_bottleneck): + try: + pd.options.compute.use_bottleneck = use_bottleneck + except TypeError: + from pandas.core import nanops + nanops._USE_BOTTLENECK = use_bottleneck + self.s = pd.Series(np.random.randn(100000)) + self.s2 = pd.Series(np.random.randn(100000)) + def time_cov_series(self, use_bottleneck): + self.s.cov(self.s2) -class stat_ops_level_series_sum(object): - goal_time = 0.2 - def setup(self): - self.index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], labels=[np.arange(10).repeat(10000), np.tile(np.arange(100).repeat(100), 10), np.tile(np.tile(np.arange(100), 100), 10)]) - random.shuffle(self.index.values) - self.df = DataFrame(np.random.randn(len(self.index), 4), index=self.index) - self.df_level = DataFrame(np.random.randn(100, 4), index=self.index.levels[1]) - - def time_stat_ops_level_series_sum(self): - self.df[1].sum(level=1) - - -class stat_ops_level_series_sum_multiple(object): - goal_time = 0.2 - - def setup(self): - self.index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], labels=[np.arange(10).repeat(10000), np.tile(np.arange(100).repeat(100), 10), np.tile(np.tile(np.arange(100), 100), 10)]) - random.shuffle(self.index.values) - self.df = DataFrame(np.random.randn(len(self.index), 4), index=self.index) - self.df_level = DataFrame(np.random.randn(100, 4), index=self.index.levels[1]) - - def time_stat_ops_level_series_sum_multiple(self): - self.df[1].sum(level=[0, 1]) - - -class stat_ops_series_std(object): - goal_time = 0.2 - - def setup(self): - self.s = Series(np.random.randn(100000), index=np.arange(100000)) - self.s[::2] = np.nan - - def time_stat_ops_series_std(self): - self.s.std() - - -class stats_corr_spearman(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(1000, 30)) - - def time_stats_corr_spearman(self): - self.df.corr(method='spearman') - - -class stats_rank2d_axis0_average(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(5000, 50)) - - def time_stats_rank2d_axis0_average(self): - self.df.rank() - - -class stats_rank2d_axis1_average(object): - goal_time = 0.2 - - def setup(self): - self.df = DataFrame(np.random.randn(5000, 50)) - - def time_stats_rank2d_axis1_average(self): - self.df.rank(1) - - -class stats_rank_average(object): - goal_time = 0.2 - - def setup(self): - self.values = np.concatenate([np.arange(100000), np.random.randn(100000), np.arange(100000)]) - self.s = Series(self.values) - - def time_stats_rank_average(self): - self.s.rank() - - -class stats_rank_average_int(object): - goal_time = 0.2 - - def setup(self): - self.values = np.random.randint(0, 100000, size=200000) - self.s = Series(self.values) - - def time_stats_rank_average_int(self): - self.s.rank() - - -class stats_rank_pct_average(object): - goal_time = 0.2 - - def setup(self): - self.values = np.concatenate([np.arange(100000), np.random.randn(100000), np.arange(100000)]) - self.s = Series(self.values) - - def time_stats_rank_pct_average(self): - self.s.rank(pct=True) - - -class stats_rank_pct_average_old(object): - goal_time = 0.2 - - def setup(self): - self.values = np.concatenate([np.arange(100000), np.random.randn(100000), np.arange(100000)]) - self.s = Series(self.values) - - def time_stats_rank_pct_average_old(self): - (self.s.rank() / len(self.s)) - - -class stats_rolling_mean(object): - goal_time = 0.2 - - def setup(self): - self.arr = np.random.randn(100000) - self.win = 100 - - def time_rolling_mean(self): - rolling_mean(self.arr, self.win) - - def time_rolling_median(self): - rolling_median(self.arr, self.win) - - def time_rolling_min(self): - rolling_min(self.arr, self.win) - - def time_rolling_max(self): - rolling_max(self.arr, self.win) - - def time_rolling_sum(self): - rolling_sum(self.arr, self.win) - - def time_rolling_std(self): - rolling_std(self.arr, self.win) - - def time_rolling_var(self): - rolling_var(self.arr, self.win) - - def time_rolling_skew(self): - rolling_skew(self.arr, self.win) - - def time_rolling_kurt(self): - rolling_kurt(self.arr, self.win) +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/strings.py b/asv_bench/benchmarks/strings.py index c1600d4e07f58..b5b2c955f0133 100644 --- a/asv_bench/benchmarks/strings.py +++ b/asv_bench/benchmarks/strings.py @@ -1,107 +1,188 @@ -from .pandas_vb_common import * -import string -import itertools as IT -import pandas.util.testing as testing +import warnings +import numpy as np +from pandas import Series, DataFrame +import pandas.util.testing as tm -class StringMethods(object): - goal_time = 0.2 - def make_series(self, letters, strlen, size): - return Series([str(x) for x in np.fromiter(IT.cycle(letters), count=(size * strlen), dtype='|S1').view('|S{}'.format(strlen))]) +class Methods(object): def setup(self): - self.many = self.make_series(('matchthis' + string.ascii_uppercase), strlen=19, size=10000) - self.few = self.make_series(('matchthis' + (string.ascii_uppercase * 42)), strlen=19, size=10000) - self.s = self.make_series(string.ascii_uppercase, strlen=10, size=10000).str.join('|') - - def time_cat(self): - self.many.str.cat(sep=',') + self.s = Series(tm.makeStringIndex(10**5)) def time_center(self): - self.many.str.center(100) - - def time_contains_few(self): - self.few.str.contains('matchthis') - - def time_contains_few_noregex(self): - self.few.str.contains('matchthis', regex=False) - - def time_contains_many(self): - self.many.str.contains('matchthis') - - def time_contains_many_noregex(self): - self.many.str.contains('matchthis', regex=False) + self.s.str.center(100) def time_count(self): - self.many.str.count('matchthis') + self.s.str.count('A') def time_endswith(self): - self.many.str.endswith('matchthis') + self.s.str.endswith('A') def time_extract(self): - self.many.str.extract('(\\w*)matchthis(\\w*)') + with warnings.catch_warnings(record=True): + self.s.str.extract('(\\w*)A(\\w*)') def time_findall(self): - self.many.str.findall('[A-Z]+') + self.s.str.findall('[A-Z]+') - def time_get(self): - self.many.str.get(0) + def time_find(self): + self.s.str.find('[A-Z]+') - def time_join_split(self): - self.many.str.join('--').str.split('--') + def time_rfind(self): + self.s.str.rfind('[A-Z]+') - def time_join_split_expand(self): - self.many.str.join('--').str.split('--', expand=True) + def time_get(self): + self.s.str.get(0) def time_len(self): - self.many.str.len() + self.s.str.len() + + def time_join(self): + self.s.str.join(' ') def time_match(self): - self.many.str.match('mat..this') + self.s.str.match('A') + + def time_normalize(self): + self.s.str.normalize('NFC') def time_pad(self): - self.many.str.pad(100, side='both') + self.s.str.pad(100, side='both') - def time_repeat(self): - self.many.str.repeat(list(IT.islice(IT.cycle(range(1, 4)), len(self.many)))) + def time_partition(self): + self.s.str.partition('A') + + def time_rpartition(self): + self.s.str.rpartition('A') def time_replace(self): - self.many.str.replace('(matchthis)', '\x01\x01') + self.s.str.replace('A', '\x01\x01') + + def time_translate(self): + self.s.str.translate({'A': '\x01\x01'}) def time_slice(self): - self.many.str.slice(5, 15, 2) + self.s.str.slice(5, 15, 2) def time_startswith(self): - self.many.str.startswith('matchthis') + self.s.str.startswith('A') def time_strip(self): - self.many.str.strip('matchthis') + self.s.str.strip('A') def time_rstrip(self): - self.many.str.rstrip('matchthis') + self.s.str.rstrip('A') def time_lstrip(self): - self.many.str.lstrip('matchthis') + self.s.str.lstrip('A') def time_title(self): - self.many.str.title() + self.s.str.title() def time_upper(self): - self.many.str.upper() + self.s.str.upper() def time_lower(self): - self.many.str.lower() + self.s.str.lower() + + def time_wrap(self): + self.s.str.wrap(10) + + def time_zfill(self): + self.s.str.zfill(10) + + +class Repeat(object): + + params = ['int', 'array'] + param_names = ['repeats'] + + def setup(self, repeats): + N = 10**5 + self.s = Series(tm.makeStringIndex(N)) + repeat = {'int': 1, 'array': np.random.randint(1, 3, N)} + self.values = repeat[repeats] + + def time_repeat(self, repeats): + self.s.str.repeat(self.values) + + +class Cat(object): + + params = ([0, 3], [None, ','], [None, '-'], [0.0, 0.001, 0.15]) + param_names = ['other_cols', 'sep', 'na_rep', 'na_frac'] + + def setup(self, other_cols, sep, na_rep, na_frac): + N = 10 ** 5 + mask_gen = lambda: np.random.choice([True, False], N, + p=[1 - na_frac, na_frac]) + self.s = Series(tm.makeStringIndex(N)).where(mask_gen()) + if other_cols == 0: + # str.cat self-concatenates only for others=None + self.others = None + else: + self.others = DataFrame({i: tm.makeStringIndex(N).where(mask_gen()) + for i in range(other_cols)}) + + def time_cat(self, other_cols, sep, na_rep, na_frac): + # before the concatenation (one caller + other_cols columns), the total + # expected fraction of rows containing any NaN is: + # reduce(lambda t, _: t + (1 - t) * na_frac, range(other_cols + 1), 0) + # for other_cols=3 and na_frac=0.15, this works out to ~48% + self.s.str.cat(others=self.others, sep=sep, na_rep=na_rep) + + +class Contains(object): + + params = [True, False] + param_names = ['regex'] + + def setup(self, regex): + self.s = Series(tm.makeStringIndex(10**5)) + + def time_contains(self, regex): + self.s.str.contains('A', regex=regex) + + +class Split(object): + + params = [True, False] + param_names = ['expand'] + + def setup(self, expand): + self.s = Series(tm.makeStringIndex(10**5)).str.join('--') + + def time_split(self, expand): + self.s.str.split('--', expand=expand) + + def time_rsplit(self, expand): + self.s.str.rsplit('--', expand=expand) + + +class Dummies(object): + + def setup(self): + self.s = Series(tm.makeStringIndex(10**5)).str.join('|') def time_get_dummies(self): self.s.str.get_dummies('|') -class StringEncode(object): - goal_time = 0.2 +class Encode(object): def setup(self): - self.ser = Series(testing.makeUnicodeIndex()) + self.ser = Series(tm.makeUnicodeIndex()) def time_encode_decode(self): self.ser.str.encode('utf-8').str.decode('utf-8') + + +class Slice(object): + + def setup(self): + self.s = Series(['abcdefg', np.nan] * 500000) + + def time_vector_slice(self): + # GH 2602 + self.s.str[:5] diff --git a/asv_bench/benchmarks/timedelta.py b/asv_bench/benchmarks/timedelta.py index c112d1ef72eb8..0cfbbd536bc8b 100644 --- a/asv_bench/benchmarks/timedelta.py +++ b/asv_bench/benchmarks/timedelta.py @@ -1,42 +1,153 @@ -from .pandas_vb_common import * -from pandas import to_timedelta, Timestamp +import datetime +import numpy as np -class ToTimedelta(object): - goal_time = 0.2 +from pandas import ( + DataFrame, Series, Timedelta, Timestamp, timedelta_range, to_timedelta) - def setup(self): - self.arr = np.random.randint(0, 1000, size=10000) - self.arr2 = ['{0} days'.format(i) for i in self.arr] - self.arr3 = np.random.randint(0, 60, size=10000) - self.arr3 = ['00:00:{0:02d}'.format(i) for i in self.arr3] +class TimedeltaConstructor(object): + + def time_from_int(self): + Timedelta(123456789) + + def time_from_unit(self): + Timedelta(1, unit='d') + + def time_from_components(self): + Timedelta(days=1, hours=2, minutes=3, seconds=4, milliseconds=5, + microseconds=6, nanoseconds=7) + + def time_from_datetime_timedelta(self): + Timedelta(datetime.timedelta(days=1, seconds=1)) + + def time_from_np_timedelta(self): + Timedelta(np.timedelta64(1, 'ms')) + + def time_from_string(self): + Timedelta('1 days') + + def time_from_iso_format(self): + Timedelta('P4DT12H30M5S') + + def time_from_missing(self): + Timedelta('nat') + - self.arr4 = list(self.arr2) - self.arr4[-1] = 'apple' +class ToTimedelta(object): + + def setup(self): + self.ints = np.random.randint(0, 60, size=10000) + self.str_days = [] + self.str_seconds = [] + for i in self.ints: + self.str_days.append('{0} days'.format(i)) + self.str_seconds.append('00:00:{0:02d}'.format(i)) def time_convert_int(self): - to_timedelta(self.arr, unit='s') + to_timedelta(self.ints, unit='s') - def time_convert_string(self): - to_timedelta(self.arr2) + def time_convert_string_days(self): + to_timedelta(self.str_days) def time_convert_string_seconds(self): - to_timedelta(self.arr3) + to_timedelta(self.str_seconds) + + +class ToTimedeltaErrors(object): + + params = ['coerce', 'ignore'] + param_names = ['errors'] - def time_convert_coerce(self): - to_timedelta(self.arr4, errors='coerce') + def setup(self, errors): + ints = np.random.randint(0, 60, size=10000) + self.arr = ['{0} days'.format(i) for i in ints] + self.arr[-1] = 'apple' - def time_convert_ignore(self): - to_timedelta(self.arr4, errors='ignore') + def time_convert(self, errors): + to_timedelta(self.arr, errors=errors) -class Ops(object): - goal_time = 0.2 +class TimedeltaOps(object): def setup(self): self.td = to_timedelta(np.arange(1000000)) self.ts = Timestamp('2000') - def test_add_td_ts(self): + def time_add_td_ts(self): self.td + self.ts + + +class TimedeltaProperties(object): + + def setup_cache(self): + td = Timedelta(days=365, minutes=35, seconds=25, milliseconds=35) + return td + + def time_timedelta_days(self, td): + td.days + + def time_timedelta_seconds(self, td): + td.seconds + + def time_timedelta_microseconds(self, td): + td.microseconds + + def time_timedelta_nanoseconds(self, td): + td.nanoseconds + + +class DatetimeAccessor(object): + + def setup_cache(self): + N = 100000 + series = Series(timedelta_range('1 days', periods=N, freq='h')) + return series + + def time_dt_accessor(self, series): + series.dt + + def time_timedelta_days(self, series): + series.dt.days + + def time_timedelta_seconds(self, series): + series.dt.seconds + + def time_timedelta_microseconds(self, series): + series.dt.microseconds + + def time_timedelta_nanoseconds(self, series): + series.dt.nanoseconds + + +class TimedeltaIndexing(object): + + def setup(self): + self.index = timedelta_range(start='1985', periods=1000, freq='D') + self.index2 = timedelta_range(start='1986', periods=1000, freq='D') + self.series = Series(range(1000), index=self.index) + self.timedelta = self.index[500] + + def time_get_loc(self): + self.index.get_loc(self.timedelta) + + def time_shape(self): + self.index.shape + + def time_shallow_copy(self): + self.index._shallow_copy() + + def time_series_loc(self): + self.series.loc[self.timedelta] + + def time_align(self): + DataFrame({'a': self.series, 'b': self.series[:500]}) + + def time_intersection(self): + self.index.intersection(self.index2) + + def time_union(self): + self.index.union(self.index2) + + def time_unique(self): + self.index.unique() diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index 6e9ef4b10273c..6efd720d1acdd 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -1,349 +1,313 @@ -from pandas.tseries.converter import DatetimeConverter -from .pandas_vb_common import * -import pandas as pd from datetime import timedelta -import datetime as dt + +import dateutil +import numpy as np +from pandas import to_datetime, date_range, Series, DataFrame, period_range +from pandas.tseries.frequencies import infer_freq try: - import pandas.tseries.holiday + from pandas.plotting._converter import DatetimeConverter except ImportError: - pass -from pandas.tseries.frequencies import infer_freq -import numpy as np - -if hasattr(Series, 'convert'): - Series.resample = Series.convert + from pandas.tseries.converter import DatetimeConverter class DatetimeIndex(object): - goal_time = 0.2 - def setup(self): - self.N = 100000 - self.rng = date_range(start='1/1/2000', periods=self.N, freq='T') - self.delta_offset = pd.offsets.Day() - self.fast_offset = pd.offsets.DateOffset(months=2, days=2) - self.slow_offset = pd.offsets.BusinessDay() + params = ['dst', 'repeated', 'tz_aware', 'tz_local', 'tz_naive'] + param_names = ['index_type'] - self.rng2 = date_range(start='1/1/2000 9:30', periods=10000, freq='S', tz='US/Eastern') + def setup(self, index_type): + N = 100000 + dtidxes = {'dst': date_range(start='10/29/2000 1:00:00', + end='10/29/2000 1:59:59', freq='S'), + 'repeated': date_range(start='2000', + periods=N / 10, + freq='s').repeat(10), + 'tz_aware': date_range(start='2000', + periods=N, + freq='s', + tz='US/Eastern'), + 'tz_local': date_range(start='2000', + periods=N, + freq='s', + tz=dateutil.tz.tzlocal()), + 'tz_naive': date_range(start='2000', + periods=N, + freq='s')} + self.index = dtidxes[index_type] - self.index_repeated = date_range(start='1/1/2000', periods=1000, freq='T').repeat(10) + def time_add_timedelta(self, index_type): + self.index + timedelta(minutes=2) - self.rng3 = date_range(start='1/1/2000', periods=1000, freq='H') - self.df = DataFrame(np.random.randn(len(self.rng3), 2), self.rng3) + def time_normalize(self, index_type): + self.index.normalize() - self.rng4 = date_range(start='1/1/2000', periods=1000, freq='H', tz='US/Eastern') - self.df2 = DataFrame(np.random.randn(len(self.rng4), 2), index=self.rng4) + def time_unique(self, index_type): + self.index.unique() - N = 100000 - self.dti = pd.date_range('2011-01-01', freq='H', periods=N).repeat(5) - self.dti_tz = pd.date_range('2011-01-01', freq='H', periods=N, - tz='Asia/Tokyo').repeat(5) + def time_to_time(self, index_type): + self.index.time + + def time_get(self, index_type): + self.index[0] - self.rng5 = date_range(start='1/1/2000', end='3/1/2000', tz='US/Eastern') + def time_timeseries_is_month_start(self, index_type): + self.index.is_month_start - self.dst_rng = date_range(start='10/29/2000 1:00:00', end='10/29/2000 1:59:59', freq='S') - self.index = date_range(start='10/29/2000', end='10/29/2000 00:59:59', freq='S') - self.index = self.index.append(self.dst_rng) - self.index = self.index.append(self.dst_rng) - self.index = self.index.append(date_range(start='10/29/2000 2:00:00', end='10/29/2000 3:00:00', freq='S')) + def time_to_date(self, index_type): + self.index.date - self.N = 10000 - self.rng6 = date_range(start='1/1/1', periods=self.N, freq='B') + def time_to_pydatetime(self, index_type): + self.index.to_pydatetime() - self.rng7 = date_range(start='1/1/1700', freq='D', periods=100000) - self.a = self.rng7[:50000].append(self.rng7[50002:]) - def time_add_timedelta(self): - (self.rng + timedelta(minutes=2)) +class TzLocalize(object): - def time_add_offset_delta(self): - (self.rng + self.delta_offset) + params = [None, 'US/Eastern', 'UTC', dateutil.tz.tzutc()] + param_names = 'tz' - def time_add_offset_fast(self): - (self.rng + self.fast_offset) + def setup(self, tz): + dst_rng = date_range(start='10/29/2000 1:00:00', + end='10/29/2000 1:59:59', freq='S') + self.index = date_range(start='10/29/2000', + end='10/29/2000 00:59:59', freq='S') + self.index = self.index.append(dst_rng) + self.index = self.index.append(dst_rng) + self.index = self.index.append(date_range(start='10/29/2000 2:00:00', + end='10/29/2000 3:00:00', + freq='S')) - def time_add_offset_slow(self): - (self.rng + self.slow_offset) + def time_infer_dst(self, tz): + self.index.tz_localize(tz, ambiguous='infer') - def time_normalize(self): - self.rng2.normalize() - def time_unique(self): - self.index_repeated.unique() +class ResetIndex(object): - def time_reset_index(self): + params = [None, 'US/Eastern'] + param_names = 'tz' + + def setup(self, tz): + idx = date_range(start='1/1/2000', periods=1000, freq='H', tz=tz) + self.df = DataFrame(np.random.randn(1000, 2), index=idx) + + def time_reest_datetimeindex(self, tz): self.df.reset_index() - def time_reset_index_tz(self): - self.df2.reset_index() - def time_dti_factorize(self): +class Factorize(object): + + params = [None, 'Asia/Tokyo'] + param_names = 'tz' + + def setup(self, tz): + N = 100000 + self.dti = date_range('2011-01-01', freq='H', periods=N, tz=tz) + self.dti = self.dti.repeat(5) + + def time_factorize(self, tz): self.dti.factorize() - def time_dti_tz_factorize(self): - self.dti_tz.factorize() - def time_timestamp_tzinfo_cons(self): - self.rng5[0] +class InferFreq(object): - def time_infer_dst(self): - self.index.tz_localize('US/Eastern', infer_dst=True) + params = [None, 'D', 'B'] + param_names = ['freq'] - def time_timeseries_is_month_start(self): - self.rng6.is_month_start + def setup(self, freq): + if freq is None: + self.idx = date_range(start='1/1/1700', freq='D', periods=10000) + self.idx.freq = None + else: + self.idx = date_range(start='1/1/1700', freq=freq, periods=10000) - def time_infer_freq(self): - infer_freq(self.a) + def time_infer_freq(self, freq): + infer_freq(self.idx) class TimeDatetimeConverter(object): - goal_time = 0.2 def setup(self): - self.N = 100000 - self.rng = date_range(start='1/1/2000', periods=self.N, freq='T') + N = 100000 + self.rng = date_range(start='1/1/2000', periods=N, freq='T') def time_convert(self): DatetimeConverter.convert(self.rng, None, None) class Iteration(object): - goal_time = 0.2 - - def setup(self): - self.N = 1000000 - self.M = 10000 - self.idx1 = date_range(start='20140101', freq='T', periods=self.N) - self.idx2 = period_range(start='20140101', freq='T', periods=self.N) - - def iter_n(self, iterable, n=None): - self.i = 0 - for _ in iterable: - self.i += 1 - if ((n is not None) and (self.i > n)): - break - def time_iter_datetimeindex(self): - self.iter_n(self.idx1) + params = [date_range, period_range] + param_names = ['time_index'] - def time_iter_datetimeindex_preexit(self): - self.iter_n(self.idx1, self.M) + def setup(self, time_index): + N = 10**6 + self.idx = time_index(start='20140101', freq='T', periods=N) + self.exit = 10000 - def time_iter_periodindex(self): - self.iter_n(self.idx2) - - def time_iter_periodindex_preexit(self): - self.iter_n(self.idx2, self.M) + def time_iter(self, time_index): + for _ in self.idx: + pass + def time_iter_preexit(self, time_index): + for i, _ in enumerate(self.idx): + if i > self.exit: + break -#---------------------------------------------------------------------- -# Resampling class ResampleDataFrame(object): - goal_time = 0.2 - - def setup(self): - self.rng = date_range(start='20130101', periods=100000, freq='50L') - self.df = DataFrame(np.random.randn(100000, 2), index=self.rng) - - def time_max_numpy(self): - self.df.resample('1s', how=np.max) - def time_max_string(self): - self.df.resample('1s', how='max') + params = ['max', 'mean', 'min'] + param_names = ['method'] - def time_mean_numpy(self): - self.df.resample('1s', how=np.mean) + def setup(self, method): + rng = date_range(start='20130101', periods=100000, freq='50L') + df = DataFrame(np.random.randn(100000, 2), index=rng) + self.resample = getattr(df.resample('1s'), method) - def time_mean_string(self): - self.df.resample('1s', how='mean') - - def time_min_numpy(self): - self.df.resample('1s', how=np.min) - - def time_min_string(self): - self.df.resample('1s', how='min') + def time_method(self, method): + self.resample() class ResampleSeries(object): - goal_time = 0.2 - - def setup(self): - self.rng1 = period_range(start='1/1/2000', end='1/1/2001', freq='T') - self.ts1 = Series(np.random.randn(len(self.rng1)), index=self.rng1) - - self.rng2 = date_range(start='1/1/2000', end='1/1/2001', freq='T') - self.ts2 = Series(np.random.randn(len(self.rng2)), index=self.rng2) - self.rng3 = date_range(start='2000-01-01 00:00:00', end='2000-01-01 10:00:00', freq='555000U') - self.int_ts = Series(5, self.rng3, dtype='int64') - self.dt_ts = self.int_ts.astype('datetime64[ns]') + params = (['period', 'datetime'], ['5min', '1D'], ['mean', 'ohlc']) + param_names = ['index', 'freq', 'method'] - def time_period_downsample_mean(self): - self.ts1.resample('D', how='mean') + def setup(self, index, freq, method): + indexes = {'period': period_range(start='1/1/2000', + end='1/1/2001', + freq='T'), + 'datetime': date_range(start='1/1/2000', + end='1/1/2001', + freq='T')} + idx = indexes[index] + ts = Series(np.random.randn(len(idx)), index=idx) + self.resample = getattr(ts.resample(freq), method) - def time_timestamp_downsample_mean(self): - self.ts2.resample('D', how='mean') + def time_resample(self, index, freq, method): + self.resample() - def time_resample_datetime64(self): - # GH 7754 - self.dt_ts.resample('1S', how='last') - def time_1min_5min_mean(self): - self.ts2[:10000].resample('5min', how='mean') +class ResampleDatetetime64(object): + # GH 7754 + def setup(self): + rng3 = date_range(start='2000-01-01 00:00:00', + end='2000-01-01 10:00:00', freq='555000U') + self.dt_ts = Series(5, rng3, dtype='datetime64[ns]') - def time_1min_5min_ohlc(self): - self.ts2[:10000].resample('5min', how='ohlc') + def time_resample(self): + self.dt_ts.resample('1S').last() class AsOf(object): - goal_time = 0.2 - def setup(self): - self.N = 10000 - self.rng = date_range(start='1/1/1990', periods=self.N, freq='53s') - self.ts = Series(np.random.randn(self.N), index=self.rng) - self.dates = date_range(start='1/1/1990', periods=(self.N * 10), freq='5s') + params = ['DataFrame', 'Series'] + param_names = ['constructor'] + + def setup(self, constructor): + N = 10000 + M = 10 + rng = date_range(start='1/1/1990', periods=N, freq='53s') + data = {'DataFrame': DataFrame(np.random.randn(N, M)), + 'Series': Series(np.random.randn(N))} + self.ts = data[constructor] + self.ts.index = rng self.ts2 = self.ts.copy() - self.ts2[250:5000] = np.nan + self.ts2.iloc[250:5000] = np.nan self.ts3 = self.ts.copy() - self.ts3[-5000:] = np.nan + self.ts3.iloc[-5000:] = np.nan + self.dates = date_range(start='1/1/1990', periods=N * 10, freq='5s') + self.date = self.dates[0] + self.date_last = self.dates[-1] + self.date_early = self.date - timedelta(10) # test speed of pre-computing NAs. - def time_asof(self): + def time_asof(self, constructor): self.ts.asof(self.dates) # should be roughly the same as above. - def time_asof_nan(self): + def time_asof_nan(self, constructor): self.ts2.asof(self.dates) # test speed of the code path for a scalar index # without *while* loop - def time_asof_single(self): - self.ts.asof(self.dates[0]) + def time_asof_single(self, constructor): + self.ts.asof(self.date) # test speed of the code path for a scalar index # before the start. should be the same as above. - def time_asof_single_early(self): - self.ts.asof(self.dates[0] - dt.timedelta(10)) + def time_asof_single_early(self, constructor): + self.ts.asof(self.date_early) # test the speed of the code path for a scalar index # with a long *while* loop. should still be much # faster than pre-computing all the NAs. - def time_asof_nan_single(self): - self.ts3.asof(self.dates[-1]) + def time_asof_nan_single(self, constructor): + self.ts3.asof(self.date_last) -class AsOfDataFrame(object): - goal_time = 0.2 +class SortIndex(object): - def setup(self): - self.N = 10000 - self.M = 100 - self.rng = date_range(start='1/1/1990', periods=self.N, freq='53s') - self.dates = date_range(start='1/1/1990', periods=(self.N * 10), freq='5s') - self.ts = DataFrame(np.random.randn(self.N, self.M), index=self.rng) - self.ts2 = self.ts.copy() - self.ts2.iloc[250:5000] = np.nan - self.ts3 = self.ts.copy() - self.ts3.iloc[-5000:] = np.nan + params = [True, False] + param_names = ['monotonic'] - # test speed of pre-computing NAs. - def time_asof(self): - self.ts.asof(self.dates) - - # should be roughly the same as above. - def time_asof_nan(self): - self.ts2.asof(self.dates) + def setup(self, monotonic): + N = 10**5 + idx = date_range(start='1/1/2000', periods=N, freq='s') + self.s = Series(np.random.randn(N), index=idx) + if not monotonic: + self.s = self.s.sample(frac=1) - # test speed of the code path for a scalar index - # with pre-computing all NAs. - def time_asof_single(self): - self.ts.asof(self.dates[0]) + def time_sort_index(self, monotonic): + self.s.sort_index() - # should be roughly the same as above. - def time_asof_nan_single(self): - self.ts3.asof(self.dates[-1]) + def time_get_slice(self, monotonic): + self.s[:10000] - # test speed of the code path for a scalar index - # before the start. should be without the cost of - # pre-computing all the NAs. - def time_asof_single_early(self): - self.ts.asof(self.dates[0] - dt.timedelta(10)) - -class TimeSeries(object): - goal_time = 0.2 +class IrregularOps(object): def setup(self): - self.N = 100000 - self.rng = date_range(start='1/1/2000', periods=self.N, freq='s') - self.rng = self.rng.take(np.random.permutation(self.N)) - self.ts = Series(np.random.randn(self.N), index=self.rng) - - self.rng2 = date_range(start='1/1/2000', periods=self.N, freq='T') - self.ts2 = Series(np.random.randn(self.N), index=self.rng2) + N = 10**5 + idx = date_range(start='1/1/2000', periods=N, freq='s') + s = Series(np.random.randn(N), index=idx) + self.left = s.sample(frac=1) + self.right = s.sample(frac=1) - self.lindex = np.random.permutation(self.N)[:(self.N // 2)] - self.rindex = np.random.permutation(self.N)[:(self.N // 2)] - self.left = Series(self.ts2.values.take(self.lindex), index=self.ts2.index.take(self.lindex)) - self.right = Series(self.ts2.values.take(self.rindex), index=self.ts2.index.take(self.rindex)) + def time_add(self): + self.left + self.right - self.rng3 = date_range(start='1/1/2000', periods=1500000, freq='S') - self.ts3 = Series(1, index=self.rng3) - def time_sort_index(self): - self.ts.sort_index() +class Lookup(object): - def time_timeseries_slice_minutely(self): - self.ts2[:10000] - - def time_add_irregular(self): - (self.left + self.right) + def setup(self): + N = 1500000 + rng = date_range(start='1/1/2000', periods=N, freq='S') + self.ts = Series(1, index=rng) + self.lookup_val = rng[N // 2] - def time_large_lookup_value(self): - self.ts3[self.ts3.index[(len(self.ts3) // 2)]] - self.ts3.index._cleanup() + def time_lookup_and_cleanup(self): + self.ts[self.lookup_val] + self.ts.index._cleanup() -class SeriesArithmetic(object): - goal_time = 0.2 +class ToDatetimeYYYYMMDD(object): def setup(self): - self.N = 100000 - self.s = Series(date_range(start='20140101', freq='T', periods=self.N)) - self.delta_offset = pd.offsets.Day() - self.fast_offset = pd.offsets.DateOffset(months=2, days=2) - self.slow_offset = pd.offsets.BusinessDay() - - def time_add_offset_delta(self): - (self.s + self.delta_offset) + rng = date_range(start='1/1/2000', periods=10000, freq='D') + self.stringsD = Series(rng.strftime('%Y%m%d')) - def time_add_offset_fast(self): - (self.s + self.fast_offset) - - def time_add_offset_slow(self): - (self.s + self.slow_offset) + def time_format_YYYYMMDD(self): + to_datetime(self.stringsD, format='%Y%m%d') -class ToDatetime(object): - goal_time = 0.2 +class ToDatetimeISO8601(object): def setup(self): - self.rng = date_range(start='1/1/2000', periods=10000, freq='D') - self.stringsD = Series((((self.rng.year * 10000) + (self.rng.month * 100)) + self.rng.day), dtype=np.int64).apply(str) - - self.rng = date_range(start='1/1/2000', periods=20000, freq='H') - self.strings = [x.strftime('%Y-%m-%d %H:%M:%S') for x in self.rng] - self.strings_nosep = [x.strftime('%Y%m%d %H:%M:%S') for x in self.rng] + rng = date_range(start='1/1/2000', periods=20000, freq='H') + self.strings = rng.strftime('%Y-%m-%d %H:%M:%S').tolist() + self.strings_nosep = rng.strftime('%Y%m%d %H:%M:%S').tolist() self.strings_tz_space = [x.strftime('%Y-%m-%d %H:%M:%S') + ' -0800' - for x in self.rng] - - self.s = Series((['19MAY11', '19MAY11:00:00:00'] * 100000)) - self.s2 = self.s.str.replace(':\\S+$', '') - - def time_format_YYYYMMDD(self): - to_datetime(self.stringsD, format='%Y%m%d') + for x in rng] def time_iso8601(self): to_datetime(self.strings) @@ -360,138 +324,105 @@ def time_iso8601_format_no_sep(self): def time_iso8601_tz_spaceformat(self): to_datetime(self.strings_tz_space) - def time_format_exact(self): - to_datetime(self.s2, format='%d%b%y') - - def time_format_no_exact(self): - to_datetime(self.s, format='%d%b%y', exact=False) - -class Offsets(object): - goal_time = 0.2 +class ToDatetimeNONISO8601(object): def setup(self): - self.date = dt.datetime(2011, 1, 1) - self.dt64 = np.datetime64('2011-01-01 09:00Z') - self.hcal = pd.tseries.holiday.USFederalHolidayCalendar() - self.day = pd.offsets.Day() - self.year = pd.offsets.YearBegin() - self.cday = pd.offsets.CustomBusinessDay() - self.cmb = pd.offsets.CustomBusinessMonthBegin(calendar=self.hcal) - self.cme = pd.offsets.CustomBusinessMonthEnd(calendar=self.hcal) - self.cdayh = pd.offsets.CustomBusinessDay(calendar=self.hcal) - - def time_timeseries_day_apply(self): - self.day.apply(self.date) - - def time_timeseries_day_incr(self): - (self.date + self.day) - - def time_timeseries_year_apply(self): - self.year.apply(self.date) + N = 10000 + half = int(N / 2) + ts_string_1 = 'March 1, 2018 12:00:00+0400' + ts_string_2 = 'March 1, 2018 12:00:00+0500' + self.same_offset = [ts_string_1] * N + self.diff_offset = [ts_string_1] * half + [ts_string_2] * half - def time_timeseries_year_incr(self): - (self.date + self.year) + def time_same_offset(self): + to_datetime(self.same_offset) - # custom business offsets + def time_different_offset(self): + to_datetime(self.diff_offset) - def time_custom_bday_decr(self): - (self.date - self.cday) - def time_custom_bday_incr(self): - (self.date + self.cday) +class ToDatetimeFormatQuarters(object): - def time_custom_bday_apply(self): - self.cday.apply(self.date) - - def time_custom_bday_apply_dt64(self): - self.cday.apply(self.dt64) - - def time_custom_bday_cal_incr(self): - self.date + 1 * self.cdayh + def setup(self): + self.s = Series(['2Q2005', '2Q05', '2005Q1', '05Q1'] * 10000) - def time_custom_bday_cal_decr(self): - self.date - 1 * self.cdayh + def time_infer_quarter(self): + to_datetime(self.s) - def time_custom_bday_cal_incr_n(self): - self.date + 10 * self.cdayh - def time_custom_bday_cal_incr_neg_n(self): - self.date - 10 * self.cdayh +class ToDatetimeFormat(object): - # Increment custom business month + def setup(self): + self.s = Series(['19MAY11', '19MAY11:00:00:00'] * 100000) + self.s2 = self.s.str.replace(':\\S+$', '') - def time_custom_bmonthend_incr(self): - (self.date + self.cme) + def time_exact(self): + to_datetime(self.s2, format='%d%b%y') - def time_custom_bmonthend_incr_n(self): - (self.date + (10 * self.cme)) + def time_no_exact(self): + to_datetime(self.s, format='%d%b%y', exact=False) - def time_custom_bmonthend_decr_n(self): - (self.date - (10 * self.cme)) - def time_custom_bmonthbegin_decr_n(self): - (self.date - (10 * self.cmb)) +class ToDatetimeCache(object): - def time_custom_bmonthbegin_incr_n(self): - (self.date + (10 * self.cmb)) + params = [True, False] + param_names = ['cache'] + def setup(self, cache): + N = 10000 + self.unique_numeric_seconds = list(range(N)) + self.dup_numeric_seconds = [1000] * N + self.dup_string_dates = ['2000-02-11'] * N + self.dup_string_with_tz = ['2000-02-11 15:00:00-0800'] * N -class SemiMonthOffset(object): - goal_time = 0.2 + def time_unique_seconds_and_unit(self, cache): + to_datetime(self.unique_numeric_seconds, unit='s', cache=cache) - def setup(self): - self.N = 100000 - self.rng = date_range(start='1/1/2000', periods=self.N, freq='T') - # date is not on an offset which will be slowest case - self.date = dt.datetime(2011, 1, 2) - self.semi_month_end = pd.offsets.SemiMonthEnd() - self.semi_month_begin = pd.offsets.SemiMonthBegin() + def time_dup_seconds_and_unit(self, cache): + to_datetime(self.dup_numeric_seconds, unit='s', cache=cache) - def time_end_apply(self): - self.semi_month_end.apply(self.date) + def time_dup_string_dates(self, cache): + to_datetime(self.dup_string_dates, cache=cache) - def time_end_incr(self): - self.date + self.semi_month_end + def time_dup_string_dates_and_format(self, cache): + to_datetime(self.dup_string_dates, format='%Y-%m-%d', cache=cache) - def time_end_incr_n(self): - self.date + 10 * self.semi_month_end + def time_dup_string_tzoffset_dates(self, cache): + to_datetime(self.dup_string_with_tz, cache=cache) - def time_end_decr(self): - self.date - self.semi_month_end - def time_end_decr_n(self): - self.date - 10 * self.semi_month_end +class DatetimeAccessor(object): - def time_end_apply_index(self): - self.semi_month_end.apply_index(self.rng) + params = [None, 'US/Eastern', 'UTC', dateutil.tz.tzutc()] + param_names = 'tz' - def time_end_incr_rng(self): - self.rng + self.semi_month_end + def setup(self, tz): + N = 100000 + self.series = Series( + date_range(start='1/1/2000', periods=N, freq='T', tz=tz) + ) - def time_end_decr_rng(self): - self.rng - self.semi_month_end + def time_dt_accessor(self, tz): + self.series.dt - def time_begin_apply(self): - self.semi_month_begin.apply(self.date) + def time_dt_accessor_normalize(self, tz): + self.series.dt.normalize() - def time_begin_incr(self): - self.date + self.semi_month_begin + def time_dt_accessor_month_name(self, tz): + self.series.dt.month_name() - def time_begin_incr_n(self): - self.date + 10 * self.semi_month_begin + def time_dt_accessor_day_name(self, tz): + self.series.dt.day_name() - def time_begin_decr(self): - self.date - self.semi_month_begin + def time_dt_accessor_time(self, tz): + self.series.dt.time - def time_begin_decr_n(self): - self.date - 10 * self.semi_month_begin + def time_dt_accessor_date(self, tz): + self.series.dt.date - def time_begin_apply_index(self): - self.semi_month_begin.apply_index(self.rng) + def time_dt_accessor_year(self, tz): + self.series.dt.year - def time_begin_incr_rng(self): - self.rng + self.semi_month_begin - def time_begin_decr_rng(self): - self.rng - self.semi_month_begin +from .pandas_vb_common import setup # noqa: F401 diff --git a/asv_bench/benchmarks/timestamp.py b/asv_bench/benchmarks/timestamp.py new file mode 100644 index 0000000000000..b45ae22650e17 --- /dev/null +++ b/asv_bench/benchmarks/timestamp.py @@ -0,0 +1,140 @@ +import datetime + +import dateutil +import pytz + +from pandas import Timestamp + + +class TimestampConstruction(object): + + def time_parse_iso8601_no_tz(self): + Timestamp('2017-08-25 08:16:14') + + def time_parse_iso8601_tz(self): + Timestamp('2017-08-25 08:16:14-0500') + + def time_parse_dateutil(self): + Timestamp('2017/08/25 08:16:14 AM') + + def time_parse_today(self): + Timestamp('today') + + def time_parse_now(self): + Timestamp('now') + + def time_fromordinal(self): + Timestamp.fromordinal(730120) + + def time_fromtimestamp(self): + Timestamp.fromtimestamp(1515448538) + + +class TimestampProperties(object): + _tzs = [None, pytz.timezone('Europe/Amsterdam'), pytz.UTC, + dateutil.tz.tzutc()] + _freqs = [None, 'B'] + params = [_tzs, _freqs] + param_names = ['tz', 'freq'] + + def setup(self, tz, freq): + self.ts = Timestamp('2017-08-25 08:16:14', tzinfo=tz, freq=freq) + + def time_tz(self, tz, freq): + self.ts.tz + + def time_dayofweek(self, tz, freq): + self.ts.dayofweek + + def time_weekday_name(self, tz, freq): + self.ts.day_name + + def time_dayofyear(self, tz, freq): + self.ts.dayofyear + + def time_week(self, tz, freq): + self.ts.week + + def time_quarter(self, tz, freq): + self.ts.quarter + + def time_days_in_month(self, tz, freq): + self.ts.days_in_month + + def time_freqstr(self, tz, freq): + self.ts.freqstr + + def time_is_month_start(self, tz, freq): + self.ts.is_month_start + + def time_is_month_end(self, tz, freq): + self.ts.is_month_end + + def time_is_quarter_start(self, tz, freq): + self.ts.is_quarter_start + + def time_is_quarter_end(self, tz, freq): + self.ts.is_quarter_end + + def time_is_year_start(self, tz, freq): + self.ts.is_year_start + + def time_is_year_end(self, tz, freq): + self.ts.is_year_end + + def time_is_leap_year(self, tz, freq): + self.ts.is_leap_year + + def time_microsecond(self, tz, freq): + self.ts.microsecond + + def time_month_name(self, tz, freq): + self.ts.month_name() + + +class TimestampOps(object): + params = [None, 'US/Eastern', pytz.UTC, + dateutil.tz.tzutc()] + param_names = ['tz'] + + def setup(self, tz): + self.ts = Timestamp('2017-08-25 08:16:14', tz=tz) + + def time_replace_tz(self, tz): + self.ts.replace(tzinfo=pytz.timezone('US/Eastern')) + + def time_replace_None(self, tz): + self.ts.replace(tzinfo=None) + + def time_to_pydatetime(self, tz): + self.ts.to_pydatetime() + + def time_normalize(self, tz): + self.ts.normalize() + + def time_tz_convert(self, tz): + if self.ts.tz is not None: + self.ts.tz_convert(tz) + + def time_tz_localize(self, tz): + if self.ts.tz is None: + self.ts.tz_localize(tz) + + def time_to_julian_date(self, tz): + self.ts.to_julian_date() + + def time_floor(self, tz): + self.ts.floor('5T') + + def time_ceil(self, tz): + self.ts.ceil('5T') + + +class TimestampAcrossDst(object): + def setup(self): + dt = datetime.datetime(2016, 3, 27, 1) + self.tzinfo = pytz.timezone('CET').localize(dt, is_dst=False).tzinfo + self.ts2 = Timestamp(dt) + + def time_replace_across_dst(self): + self.ts2.replace(tzinfo=self.tzinfo) diff --git a/asv_bench/vbench_to_asv.py b/asv_bench/vbench_to_asv.py deleted file mode 100644 index c3041ec2b1ba1..0000000000000 --- a/asv_bench/vbench_to_asv.py +++ /dev/null @@ -1,163 +0,0 @@ -import ast -import vbench -import os -import sys -import astor -import glob - - -def vbench_to_asv_source(bench, kinds=None): - tab = ' ' * 4 - if kinds is None: - kinds = ['time'] - - output = 'class {}(object):\n'.format(bench.name) - output += tab + 'goal_time = 0.2\n\n' - - if bench.setup: - indented_setup = [tab * 2 + '{}\n'.format(x) for x in bench.setup.splitlines()] - output += tab + 'def setup(self):\n' + ''.join(indented_setup) + '\n' - - for kind in kinds: - output += tab + 'def {}_{}(self):\n'.format(kind, bench.name) - for line in bench.code.splitlines(): - output += tab * 2 + line + '\n' - output += '\n\n' - - if bench.cleanup: - output += tab + 'def teardown(self):\n' + tab * 2 + bench.cleanup - - output += '\n\n' - return output - - -class AssignToSelf(ast.NodeTransformer): - def __init__(self): - super(AssignToSelf, self).__init__() - self.transforms = {} - self.imports = [] - - self.in_class_define = False - self.in_setup = False - - def visit_ClassDef(self, node): - self.transforms = {} - self.in_class_define = True - - functions_to_promote = [] - setup_func = None - - for class_func in ast.iter_child_nodes(node): - if isinstance(class_func, ast.FunctionDef): - if class_func.name == 'setup': - setup_func = class_func - for anon_func in ast.iter_child_nodes(class_func): - if isinstance(anon_func, ast.FunctionDef): - functions_to_promote.append(anon_func) - - if setup_func: - for func in functions_to_promote: - setup_func.body.remove(func) - func.args.args.insert(0, ast.Name(id='self', ctx=ast.Load())) - node.body.append(func) - self.transforms[func.name] = 'self.' + func.name - - ast.fix_missing_locations(node) - - self.generic_visit(node) - - return node - - def visit_TryExcept(self, node): - if any([isinstance(x, (ast.Import, ast.ImportFrom)) for x in node.body]): - self.imports.append(node) - else: - self.generic_visit(node) - return node - - def visit_Assign(self, node): - for target in node.targets: - if isinstance(target, ast.Name) and not isinstance(target.ctx, ast.Param) and not self.in_class_define: - self.transforms[target.id] = 'self.' + target.id - self.generic_visit(node) - - return node - - def visit_Name(self, node): - new_node = node - if node.id in self.transforms: - if not isinstance(node.ctx, ast.Param): - new_node = ast.Attribute(value=ast.Name(id='self', ctx=node.ctx), attr=node.id, ctx=node.ctx) - - self.generic_visit(node) - - return ast.copy_location(new_node, node) - - def visit_Import(self, node): - self.imports.append(node) - - def visit_ImportFrom(self, node): - self.imports.append(node) - - def visit_FunctionDef(self, node): - """Delete functions that are empty due to imports being moved""" - self.in_class_define = False - - self.generic_visit(node) - - if node.body: - return node - - -def translate_module(target_module): - g_vars = {} - l_vars = {} - exec('import ' + target_module) in g_vars - - print target_module - module = eval(target_module, g_vars) - - benchmarks = [] - for obj_str in dir(module): - obj = getattr(module, obj_str) - if isinstance(obj, vbench.benchmark.Benchmark): - benchmarks.append(obj) - - if not benchmarks: - return - - rewritten_output = '' - for bench in benchmarks: - rewritten_output += vbench_to_asv_source(bench) - - with open('rewrite.py', 'w') as f: - f.write(rewritten_output) - - ast_module = ast.parse(rewritten_output) - - transformer = AssignToSelf() - transformed_module = transformer.visit(ast_module) - - unique_imports = {astor.to_source(node): node for node in transformer.imports} - - transformed_module.body = unique_imports.values() + transformed_module.body - - transformed_source = astor.to_source(transformed_module) - - with open('benchmarks/{}.py'.format(target_module), 'w') as f: - f.write(transformed_source) - - -if __name__ == '__main__': - cwd = os.getcwd() - new_dir = os.path.join(os.path.dirname(__file__), '../vb_suite') - sys.path.insert(0, new_dir) - - for module in glob.glob(os.path.join(new_dir, '*.py')): - mod = os.path.basename(module) - if mod in ['make.py', 'measure_memory_consumption.py', 'perf_HEAD.py', 'run_suite.py', 'test_perf.py', 'generate_rst_files.py', 'test.py', 'suite.py']: - continue - print - print mod - - translate_module(mod.replace('.py', '')) diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000000..f0567d76659b6 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,119 @@ +# Adapted from https://github.com/numba/numba/blob/master/azure-pipelines.yml +jobs: +# Mac and Linux use the same template +- template: ci/azure/posix.yml + parameters: + name: macOS + vmImage: xcode9-macos10.13 +- template: ci/azure/posix.yml + parameters: + name: Linux + vmImage: ubuntu-16.04 + +# Windows Python 2.7 needs VC 9.0 installed, handled in the template +- template: ci/azure/windows.yml + parameters: + name: Windows + vmImage: vs2017-win2016 + +- job: 'Checks_and_doc' + pool: + vmImage: ubuntu-16.04 + timeoutInMinutes: 90 + steps: + - script: | + # XXX next command should avoid redefining the path in every step, but + # made the process crash as it couldn't find deactivate + #echo '##vso[task.prependpath]$HOME/miniconda3/bin' + echo '##vso[task.setvariable variable=CONDA_ENV]pandas-dev' + echo '##vso[task.setvariable variable=ENV_FILE]environment.yml' + echo '##vso[task.setvariable variable=AZURE]true' + displayName: 'Setting environment variables' + + # Do not require a conda environment + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + ci/code_checks.sh patterns + displayName: 'Looking for unwanted patterns' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + sudo apt-get install -y libc6-dev-i386 + ci/incremental/install_miniconda.sh + ci/incremental/setup_conda_environment.sh + displayName: 'Set up environment' + condition: true + + # Do not require pandas + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/code_checks.sh lint + displayName: 'Linting' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/code_checks.sh dependencies + displayName: 'Dependencies consistency' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/incremental/build.sh + displayName: 'Build' + condition: true + + # Require pandas + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/code_checks.sh code + displayName: 'Checks on imported code' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/code_checks.sh doctests + displayName: 'Running doctests' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/code_checks.sh docstrings + displayName: 'Docstring validation' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + pytest --capture=no --strict scripts + displayName: 'Testing docstring validaton script' + condition: true + + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + git remote add upstream https://github.com/pandas-dev/pandas.git + git fetch upstream + if git diff upstream/master --name-only | grep -q "^asv_bench/"; then + cd asv_bench + asv machine --yes + ASV_OUTPUT="$(asv dev)" + if [[ $(echo "$ASV_OUTPUT" | grep "failed") ]]; then + echo "##vso[task.logissue type=error]Benchmarks run with errors" + echo "$ASV_OUTPUT" + exit 1 + else + echo "Benchmarks run without errors" + fi + else + echo "Benchmarks did not run, no changes detected" + fi + displayName: 'Running benchmarks' + condition: true diff --git a/bench/alignment.py b/bench/alignment.py deleted file mode 100644 index bc3134f597ee0..0000000000000 --- a/bench/alignment.py +++ /dev/null @@ -1,22 +0,0 @@ -# Setup -from pandas.compat import range, lrange -import numpy as np -import pandas -import la -N = 1000 -K = 50 -arr1 = np.random.randn(N, K) -arr2 = np.random.randn(N, K) -idx1 = lrange(N) -idx2 = lrange(K) - -# pandas -dma1 = pandas.DataFrame(arr1, idx1, idx2) -dma2 = pandas.DataFrame(arr2, idx1[::-1], idx2[::-1]) - -# larry -lar1 = la.larry(arr1, [idx1, idx2]) -lar2 = la.larry(arr2, [idx1[::-1], idx2[::-1]]) - -for i in range(100): - result = lar1 + lar2 diff --git a/bench/bench_dense_to_sparse.py b/bench/bench_dense_to_sparse.py deleted file mode 100644 index e1dcd3456e88d..0000000000000 --- a/bench/bench_dense_to_sparse.py +++ /dev/null @@ -1,14 +0,0 @@ -from pandas import * - -K = 100 -N = 100000 -rng = DatetimeIndex('1/1/2000', periods=N, offset=datetools.Minute()) - -rng2 = np.asarray(rng).astype('M8[us]').astype('i8') - -series = {} -for i in range(1, K + 1): - data = np.random.randn(N)[:-i] - this_rng = rng2[:-i] - data[100:] = np.nan - series[i] = SparseSeries(data, index=this_rng) diff --git a/bench/bench_get_put_value.py b/bench/bench_get_put_value.py deleted file mode 100644 index 427e0b1b10a22..0000000000000 --- a/bench/bench_get_put_value.py +++ /dev/null @@ -1,56 +0,0 @@ -from pandas import * -from pandas.util.testing import rands -from pandas.compat import range - -N = 1000 -K = 50 - - -def _random_index(howmany): - return Index([rands(10) for _ in range(howmany)]) - -df = DataFrame(np.random.randn(N, K), index=_random_index(N), - columns=_random_index(K)) - - -def get1(): - for col in df.columns: - for row in df.index: - _ = df[col][row] - - -def get2(): - for col in df.columns: - for row in df.index: - _ = df.get_value(row, col) - - -def put1(): - for col in df.columns: - for row in df.index: - df[col][row] = 0 - - -def put2(): - for col in df.columns: - for row in df.index: - df.set_value(row, col, 0) - - -def resize1(): - buf = DataFrame() - for col in df.columns: - for row in df.index: - buf = buf.set_value(row, col, 5.) - return buf - - -def resize2(): - from collections import defaultdict - - buf = defaultdict(dict) - for col in df.columns: - for row in df.index: - buf[col][row] = 5. - - return DataFrame(buf) diff --git a/bench/bench_groupby.py b/bench/bench_groupby.py deleted file mode 100644 index d7a2853e1e7b2..0000000000000 --- a/bench/bench_groupby.py +++ /dev/null @@ -1,66 +0,0 @@ -from pandas import * -from pandas.util.testing import rands -from pandas.compat import range - -import string -import random - -k = 20000 -n = 10 - -foo = np.tile(np.array([rands(10) for _ in range(k)], dtype='O'), n) -foo2 = list(foo) -random.shuffle(foo) -random.shuffle(foo2) - -df = DataFrame({'A': foo, - 'B': foo2, - 'C': np.random.randn(n * k)}) - -import pandas._sandbox as sbx - - -def f(): - table = sbx.StringHashTable(len(df)) - ret = table.factorize(df['A']) - return ret - - -def g(): - table = sbx.PyObjectHashTable(len(df)) - ret = table.factorize(df['A']) - return ret - -ret = f() - -""" -import pandas._tseries as lib - -f = np.std - - -grouped = df.groupby(['A', 'B']) - -label_list = [ping.labels for ping in grouped.groupings] -shape = [len(ping.ids) for ping in grouped.groupings] - -from pandas.core.groupby import get_group_index - - -group_index = get_group_index(label_list, shape, - sort=True, xnull=True).astype('i4') - -ngroups = np.prod(shape) - -indexer = lib.groupsort_indexer(group_index, ngroups) - -values = df['C'].values.take(indexer) -group_index = group_index.take(indexer) - -f = lambda x: x.std(ddof=1) - -grouper = lib.Grouper(df['C'], np.ndarray.std, group_index, ngroups) -result = grouper.get_result() - -expected = grouped.std() -""" diff --git a/bench/bench_join_panel.py b/bench/bench_join_panel.py deleted file mode 100644 index f3c3f8ba15f70..0000000000000 --- a/bench/bench_join_panel.py +++ /dev/null @@ -1,85 +0,0 @@ -# reasonably efficient - - -def create_panels_append(cls, panels): - """ return an append list of panels """ - panels = [a for a in panels if a is not None] - # corner cases - if len(panels) == 0: - return None - elif len(panels) == 1: - return panels[0] - elif len(panels) == 2 and panels[0] == panels[1]: - return panels[0] - # import pdb; pdb.set_trace() - # create a joint index for the axis - - def joint_index_for_axis(panels, axis): - s = set() - for p in panels: - s.update(list(getattr(p, axis))) - return sorted(list(s)) - - def reindex_on_axis(panels, axis, axis_reindex): - new_axis = joint_index_for_axis(panels, axis) - new_panels = [p.reindex(**{axis_reindex: new_axis, - 'copy': False}) for p in panels] - return new_panels, new_axis - # create the joint major index, dont' reindex the sub-panels - we are - # appending - major = joint_index_for_axis(panels, 'major_axis') - # reindex on minor axis - panels, minor = reindex_on_axis(panels, 'minor_axis', 'minor') - # reindex on items - panels, items = reindex_on_axis(panels, 'items', 'items') - # concatenate values - try: - values = np.concatenate([p.values for p in panels], axis=1) - except Exception as detail: - raise Exception("cannot append values that dont' match dimensions! -> [%s] %s" - % (','.join(["%s" % p for p in panels]), str(detail))) - # pm('append - create_panel') - p = Panel(values, items=items, major_axis=major, - minor_axis=minor) - # pm('append - done') - return p - - -# does the job but inefficient (better to handle like you read a table in -# pytables...e.g create a LongPanel then convert to Wide) -def create_panels_join(cls, panels): - """ given an array of panels's, create a single panel """ - panels = [a for a in panels if a is not None] - # corner cases - if len(panels) == 0: - return None - elif len(panels) == 1: - return panels[0] - elif len(panels) == 2 and panels[0] == panels[1]: - return panels[0] - d = dict() - minor, major, items = set(), set(), set() - for panel in panels: - items.update(panel.items) - major.update(panel.major_axis) - minor.update(panel.minor_axis) - values = panel.values - for item, item_index in panel.items.indexMap.items(): - for minor_i, minor_index in panel.minor_axis.indexMap.items(): - for major_i, major_index in panel.major_axis.indexMap.items(): - try: - d[(minor_i, major_i, item)] = values[item_index, major_index, minor_index] - except: - pass - # stack the values - minor = sorted(list(minor)) - major = sorted(list(major)) - items = sorted(list(items)) - # create the 3d stack (items x columns x indicies) - data = np.dstack([np.asarray([np.asarray([d.get((minor_i, major_i, item), np.nan) - for item in items]) - for major_i in major]).transpose() - for minor_i in minor]) - # construct the panel - return Panel(data, items, major, minor) -add_class_method(Panel, create_panels_join, 'join_many') diff --git a/bench/bench_khash_dict.py b/bench/bench_khash_dict.py deleted file mode 100644 index 054fc36131b65..0000000000000 --- a/bench/bench_khash_dict.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Some comparisons of khash.h to Python dict -""" -from __future__ import print_function - -import numpy as np -import os - -from vbench.api import Benchmark -from pandas.util.testing import rands -from pandas.compat import range -import pandas._tseries as lib -import pandas._sandbox as sbx -import time - -import psutil - -pid = os.getpid() -proc = psutil.Process(pid) - - -def object_test_data(n): - pass - - -def string_test_data(n): - return np.array([rands(10) for _ in range(n)], dtype='O') - - -def int_test_data(n): - return np.arange(n, dtype='i8') - -N = 1000000 - -#---------------------------------------------------------------------- -# Benchmark 1: map_locations - - -def map_locations_python_object(): - arr = string_test_data(N) - return _timeit(lambda: lib.map_indices_object(arr)) - - -def map_locations_khash_object(): - arr = string_test_data(N) - - def f(): - table = sbx.PyObjectHashTable(len(arr)) - table.map_locations(arr) - return _timeit(f) - - -def _timeit(f, iterations=10): - start = time.time() - for _ in range(iterations): - foo = f() - elapsed = time.time() - start - return elapsed - -#---------------------------------------------------------------------- -# Benchmark 2: lookup_locations - - -def lookup_python(values): - table = lib.map_indices_object(values) - return _timeit(lambda: lib.merge_indexer_object(values, table)) - - -def lookup_khash(values): - table = sbx.PyObjectHashTable(len(values)) - table.map_locations(values) - locs = table.lookup_locations(values) - # elapsed = _timeit(lambda: table.lookup_locations2(values)) - return table - - -def leak(values): - for _ in range(100): - print(proc.get_memory_info()) - table = lookup_khash(values) - # table.destroy() - -arr = string_test_data(N) - -#---------------------------------------------------------------------- -# Benchmark 3: unique - -#---------------------------------------------------------------------- -# Benchmark 4: factorize diff --git a/bench/bench_merge.R b/bench/bench_merge.R deleted file mode 100644 index 3ed4618494857..0000000000000 --- a/bench/bench_merge.R +++ /dev/null @@ -1,161 +0,0 @@ -library(plyr) -library(data.table) -N <- 10000 -indices = rep(NA, N) -indices2 = rep(NA, N) -for (i in 1:N) { - indices[i] <- paste(sample(letters, 10), collapse="") - indices2[i] <- paste(sample(letters, 10), collapse="") -} -left <- data.frame(key=rep(indices[1:8000], 10), - key2=rep(indices2[1:8000], 10), - value=rnorm(80000)) -right <- data.frame(key=indices[2001:10000], - key2=indices2[2001:10000], - value2=rnorm(8000)) - -right2 <- data.frame(key=rep(right$key, 2), - key2=rep(right$key2, 2), - value2=rnorm(16000)) - -left.dt <- data.table(left, key=c("key", "key2")) -right.dt <- data.table(right, key=c("key", "key2")) -right2.dt <- data.table(right2, key=c("key", "key2")) - -# left.dt2 <- data.table(left) -# right.dt2 <- data.table(right) - -## left <- data.frame(key=rep(indices[1:1000], 10), -## key2=rep(indices2[1:1000], 10), -## value=rnorm(100000)) -## right <- data.frame(key=indices[1:1000], -## key2=indices2[1:1000], -## value2=rnorm(10000)) - -timeit <- function(func, niter=10) { - timing = rep(NA, niter) - for (i in 1:niter) { - gc() - timing[i] <- system.time(func())[3] - } - mean(timing) -} - -left.join <- function(sort=FALSE) { - result <- base::merge(left, right, all.x=TRUE, sort=sort) -} - -right.join <- function(sort=FALSE) { - result <- base::merge(left, right, all.y=TRUE, sort=sort) -} - -outer.join <- function(sort=FALSE) { - result <- base::merge(left, right, all=TRUE, sort=sort) -} - -inner.join <- function(sort=FALSE) { - result <- base::merge(left, right, all=FALSE, sort=sort) -} - -left.join.dt <- function(sort=FALSE) { - result <- right.dt[left.dt] -} - -right.join.dt <- function(sort=FALSE) { - result <- left.dt[right.dt] -} - -outer.join.dt <- function(sort=FALSE) { - result <- merge(left.dt, right.dt, all=TRUE, sort=sort) -} - -inner.join.dt <- function(sort=FALSE) { - result <- merge(left.dt, right.dt, all=FALSE, sort=sort) -} - -plyr.join <- function(type) { - result <- plyr::join(left, right, by=c("key", "key2"), - type=type, match="first") -} - -sort.options <- c(FALSE, TRUE) - -# many-to-one - -results <- matrix(nrow=4, ncol=3) -colnames(results) <- c("base::merge", "plyr", "data.table") -rownames(results) <- c("inner", "outer", "left", "right") - -base.functions <- c(inner.join, outer.join, left.join, right.join) -plyr.functions <- c(function() plyr.join("inner"), - function() plyr.join("full"), - function() plyr.join("left"), - function() plyr.join("right")) -dt.functions <- c(inner.join.dt, outer.join.dt, left.join.dt, right.join.dt) -for (i in 1:4) { - base.func <- base.functions[[i]] - plyr.func <- plyr.functions[[i]] - dt.func <- dt.functions[[i]] - results[i, 1] <- timeit(base.func) - results[i, 2] <- timeit(plyr.func) - results[i, 3] <- timeit(dt.func) -} - - -# many-to-many - -left.join <- function(sort=FALSE) { - result <- base::merge(left, right2, all.x=TRUE, sort=sort) -} - -right.join <- function(sort=FALSE) { - result <- base::merge(left, right2, all.y=TRUE, sort=sort) -} - -outer.join <- function(sort=FALSE) { - result <- base::merge(left, right2, all=TRUE, sort=sort) -} - -inner.join <- function(sort=FALSE) { - result <- base::merge(left, right2, all=FALSE, sort=sort) -} - -left.join.dt <- function(sort=FALSE) { - result <- right2.dt[left.dt] -} - -right.join.dt <- function(sort=FALSE) { - result <- left.dt[right2.dt] -} - -outer.join.dt <- function(sort=FALSE) { - result <- merge(left.dt, right2.dt, all=TRUE, sort=sort) -} - -inner.join.dt <- function(sort=FALSE) { - result <- merge(left.dt, right2.dt, all=FALSE, sort=sort) -} - -sort.options <- c(FALSE, TRUE) - -# many-to-one - -results <- matrix(nrow=4, ncol=3) -colnames(results) <- c("base::merge", "plyr", "data.table") -rownames(results) <- c("inner", "outer", "left", "right") - -base.functions <- c(inner.join, outer.join, left.join, right.join) -plyr.functions <- c(function() plyr.join("inner"), - function() plyr.join("full"), - function() plyr.join("left"), - function() plyr.join("right")) -dt.functions <- c(inner.join.dt, outer.join.dt, left.join.dt, right.join.dt) -for (i in 1:4) { - base.func <- base.functions[[i]] - plyr.func <- plyr.functions[[i]] - dt.func <- dt.functions[[i]] - results[i, 1] <- timeit(base.func) - results[i, 2] <- timeit(plyr.func) - results[i, 3] <- timeit(dt.func) -} - diff --git a/bench/bench_merge.py b/bench/bench_merge.py deleted file mode 100644 index 330dba7b9af69..0000000000000 --- a/bench/bench_merge.py +++ /dev/null @@ -1,105 +0,0 @@ -import random -import gc -import time -from pandas import * -from pandas.compat import range, lrange, StringIO -from pandas.util.testing import rands - -N = 10000 -ngroups = 10 - - -def get_test_data(ngroups=100, n=N): - unique_groups = lrange(ngroups) - arr = np.asarray(np.tile(unique_groups, n / ngroups), dtype=object) - - if len(arr) < n: - arr = np.asarray(list(arr) + unique_groups[:n - len(arr)], - dtype=object) - - random.shuffle(arr) - return arr - -# aggregate multiple columns -# df = DataFrame({'key1' : get_test_data(ngroups=ngroups), -# 'key2' : get_test_data(ngroups=ngroups), -# 'data1' : np.random.randn(N), -# 'data2' : np.random.randn(N)}) - -# df2 = DataFrame({'key1' : get_test_data(ngroups=ngroups, n=N//10), -# 'key2' : get_test_data(ngroups=ngroups//2, n=N//10), -# 'value' : np.random.randn(N // 10)}) -# result = merge.merge(df, df2, on='key2') - -N = 10000 - -indices = np.array([rands(10) for _ in range(N)], dtype='O') -indices2 = np.array([rands(10) for _ in range(N)], dtype='O') -key = np.tile(indices[:8000], 10) -key2 = np.tile(indices2[:8000], 10) - -left = DataFrame({'key': key, 'key2': key2, - 'value': np.random.randn(80000)}) -right = DataFrame({'key': indices[2000:], 'key2': indices2[2000:], - 'value2': np.random.randn(8000)}) - -right2 = right.append(right, ignore_index=True) - - -join_methods = ['inner', 'outer', 'left', 'right'] -results = DataFrame(index=join_methods, columns=[False, True]) -niter = 10 -for sort in [False, True]: - for join_method in join_methods: - f = lambda: merge(left, right, how=join_method, sort=sort) - gc.disable() - start = time.time() - for _ in range(niter): - f() - elapsed = (time.time() - start) / niter - gc.enable() - results[sort][join_method] = elapsed -# results.columns = ['pandas'] -results.columns = ['dont_sort', 'sort'] - - -# R results -# many to one -r_results = read_table(StringIO(""" base::merge plyr data.table -inner 0.2475 0.1183 0.1100 -outer 0.4213 0.1916 0.2090 -left 0.2998 0.1188 0.0572 -right 0.3102 0.0536 0.0376 -"""), sep='\s+') - -presults = results[['dont_sort']].rename(columns={'dont_sort': 'pandas'}) -all_results = presults.join(r_results) - -all_results = all_results.div(all_results['pandas'], axis=0) - -all_results = all_results.ix[:, ['pandas', 'data.table', 'plyr', - 'base::merge']] - -sort_results = DataFrame.from_items([('pandas', results['sort']), - ('R', r_results['base::merge'])]) -sort_results['Ratio'] = sort_results['R'] / sort_results['pandas'] - - -nosort_results = DataFrame.from_items([('pandas', results['dont_sort']), - ('R', r_results['base::merge'])]) -nosort_results['Ratio'] = nosort_results['R'] / nosort_results['pandas'] - -# many to many - -# many to one -r_results = read_table(StringIO("""base::merge plyr data.table -inner 0.4610 0.1276 0.1269 -outer 0.9195 0.1881 0.2725 -left 0.6559 0.1257 0.0678 -right 0.6425 0.0522 0.0428 -"""), sep='\s+') - -all_results = presults.join(r_results) -all_results = all_results.div(all_results['pandas'], axis=0) -all_results = all_results.ix[:, ['pandas', 'data.table', 'plyr', - 'base::merge']] diff --git a/bench/bench_merge_sqlite.py b/bench/bench_merge_sqlite.py deleted file mode 100644 index 3ad4b810119c3..0000000000000 --- a/bench/bench_merge_sqlite.py +++ /dev/null @@ -1,87 +0,0 @@ -import numpy as np -from collections import defaultdict -import gc -import time -from pandas import DataFrame -from pandas.util.testing import rands -from pandas.compat import range, zip -import random - -N = 10000 - -indices = np.array([rands(10) for _ in range(N)], dtype='O') -indices2 = np.array([rands(10) for _ in range(N)], dtype='O') -key = np.tile(indices[:8000], 10) -key2 = np.tile(indices2[:8000], 10) - -left = DataFrame({'key': key, 'key2': key2, - 'value': np.random.randn(80000)}) -right = DataFrame({'key': indices[2000:], 'key2': indices2[2000:], - 'value2': np.random.randn(8000)}) - -# right2 = right.append(right, ignore_index=True) -# right = right2 - -# random.shuffle(key2) -# indices2 = indices.copy() -# random.shuffle(indices2) - -# Prepare Database -import sqlite3 -create_sql_indexes = True - -conn = sqlite3.connect(':memory:') -conn.execute( - 'create table left( key varchar(10), key2 varchar(10), value int);') -conn.execute( - 'create table right( key varchar(10), key2 varchar(10), value2 int);') -conn.executemany('insert into left values (?, ?, ?)', - zip(key, key2, left['value'])) -conn.executemany('insert into right values (?, ?, ?)', - zip(right['key'], right['key2'], right['value2'])) - -# Create Indices -if create_sql_indexes: - conn.execute('create index left_ix on left(key, key2)') - conn.execute('create index right_ix on right(key, key2)') - - -join_methods = ['inner', 'left outer', 'left'] # others not supported -sql_results = DataFrame(index=join_methods, columns=[False]) -niter = 5 -for sort in [False]: - for join_method in join_methods: - sql = """CREATE TABLE test as select * - from left - %s join right - on left.key=right.key - and left.key2 = right.key2;""" % join_method - sql = """select * - from left - %s join right - on left.key=right.key - and left.key2 = right.key2;""" % join_method - - if sort: - sql = '%s order by key, key2' % sql - f = lambda: list(conn.execute(sql)) # list fetches results - g = lambda: conn.execute(sql) # list fetches results - gc.disable() - start = time.time() - # for _ in range(niter): - g() - elapsed = (time.time() - start) / niter - gc.enable() - - cur = conn.execute("DROP TABLE test") - conn.commit() - - sql_results[sort][join_method] = elapsed - sql_results.columns = ['sqlite3'] # ['dont_sort', 'sort'] - sql_results.index = ['inner', 'outer', 'left'] - - sql = """select * - from left - inner join right - on left.key=right.key - and left.key2 = right.key2;""" diff --git a/bench/bench_pivot.R b/bench/bench_pivot.R deleted file mode 100644 index 06dc6a105bc43..0000000000000 --- a/bench/bench_pivot.R +++ /dev/null @@ -1,27 +0,0 @@ -library(reshape2) - - -n <- 100000 -a.size <- 5 -b.size <- 5 - -data <- data.frame(a=sample(letters[1:a.size], n, replace=T), - b=sample(letters[1:b.size], n, replace=T), - c=rnorm(n), - d=rnorm(n)) - -timings <- numeric() - -# acast(melt(data, id=c("a", "b")), a ~ b, mean) -# acast(melt(data, id=c("a", "b")), a + b ~ variable, mean) - -for (i in 1:10) { - gc() - tim <- system.time(acast(melt(data, id=c("a", "b")), a ~ b, mean, - subset=.(variable=="c"))) - timings[i] = tim[3] -} - -mean(timings) - -acast(melt(data, id=c("a", "b")), a ~ b, mean, subset=.(variable="c")) diff --git a/bench/bench_pivot.py b/bench/bench_pivot.py deleted file mode 100644 index 007bd0aaebc2f..0000000000000 --- a/bench/bench_pivot.py +++ /dev/null @@ -1,16 +0,0 @@ -from pandas import * -import string - - -n = 100000 -asize = 5 -bsize = 5 - -letters = np.asarray(list(string.letters), dtype=object) - -data = DataFrame(dict(foo=letters[:asize][np.random.randint(0, asize, n)], - bar=letters[:bsize][np.random.randint(0, bsize, n)], - baz=np.random.randn(n), - qux=np.random.randn(n))) - -table = pivot_table(data, xby=['foo', 'bar']) diff --git a/bench/bench_take_indexing.py b/bench/bench_take_indexing.py deleted file mode 100644 index 5fb584bcfe45f..0000000000000 --- a/bench/bench_take_indexing.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import print_function -import numpy as np - -from pandas import * -import pandas._tseries as lib - -from pandas import DataFrame -import timeit -from pandas.compat import zip - -setup = """ -from pandas import Series -import pandas._tseries as lib -import random -import numpy as np - -import random -n = %d -k = %d -arr = np.random.randn(n, k) -indexer = np.arange(n, dtype=np.int32) -indexer = indexer[::-1] -""" - -sizes = [100, 1000, 10000, 100000] -iters = [1000, 1000, 100, 1] - -fancy_2d = [] -take_2d = [] -cython_2d = [] - -n = 1000 - - -def _timeit(stmt, size, k=5, iters=1000): - timer = timeit.Timer(stmt=stmt, setup=setup % (sz, k)) - return timer.timeit(n) / n - -for sz, its in zip(sizes, iters): - print(sz) - fancy_2d.append(_timeit('arr[indexer]', sz, iters=its)) - take_2d.append(_timeit('arr.take(indexer, axis=0)', sz, iters=its)) - cython_2d.append(_timeit('lib.take_axis0(arr, indexer)', sz, iters=its)) - -df = DataFrame({'fancy': fancy_2d, - 'take': take_2d, - 'cython': cython_2d}) - -print(df) - -from pandas.rpy.common import r -r('mat <- matrix(rnorm(50000), nrow=10000, ncol=5)') -r('set.seed(12345') -r('indexer <- sample(1:10000)') -r('mat[indexer,]') diff --git a/bench/bench_unique.py b/bench/bench_unique.py deleted file mode 100644 index 87bd2f2df586c..0000000000000 --- a/bench/bench_unique.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import print_function -from pandas import * -from pandas.util.testing import rands -from pandas.compat import range, zip -import pandas._tseries as lib -import numpy as np -import matplotlib.pyplot as plt - -N = 50000 -K = 10000 - -groups = np.array([rands(10) for _ in range(K)], dtype='O') -groups2 = np.array([rands(10) for _ in range(K)], dtype='O') - -labels = np.tile(groups, N // K) -labels2 = np.tile(groups2, N // K) -data = np.random.randn(N) - - -def timeit(f, niter): - import gc - import time - gc.disable() - start = time.time() - for _ in range(niter): - f() - elapsed = (time.time() - start) / niter - gc.enable() - return elapsed - - -def algo1(): - unique_labels = np.unique(labels) - result = np.empty(len(unique_labels)) - for i, label in enumerate(unique_labels): - result[i] = data[labels == label].sum() - - -def algo2(): - unique_labels = np.unique(labels) - indices = lib.groupby_indices(labels) - result = np.empty(len(unique_labels)) - - for i, label in enumerate(unique_labels): - result[i] = data.take(indices[label]).sum() - - -def algo3_nosort(): - rizer = lib.DictFactorizer() - labs, counts = rizer.factorize(labels, sort=False) - k = len(rizer.uniques) - out = np.empty(k) - lib.group_add(out, counts, data, labs) - - -def algo3_sort(): - rizer = lib.DictFactorizer() - labs, counts = rizer.factorize(labels, sort=True) - k = len(rizer.uniques) - out = np.empty(k) - lib.group_add(out, counts, data, labs) - -import numpy as np -import random - - -# dict to hold results -counts = {} - -# a hack to generate random key, value pairs. -# 5k keys, 100k values -x = np.tile(np.arange(5000, dtype='O'), 20) -random.shuffle(x) -xarr = x -x = [int(y) for y in x] -data = np.random.uniform(0, 1, 100000) - - -def f(): - # groupby sum - for k, v in zip(x, data): - try: - counts[k] += v - except KeyError: - counts[k] = v - - -def f2(): - rizer = lib.DictFactorizer() - labs, counts = rizer.factorize(xarr, sort=False) - k = len(rizer.uniques) - out = np.empty(k) - lib.group_add(out, counts, data, labs) - - -def algo4(): - rizer = lib.DictFactorizer() - labs1, _ = rizer.factorize(labels, sort=False) - k1 = len(rizer.uniques) - - rizer = lib.DictFactorizer() - labs2, _ = rizer.factorize(labels2, sort=False) - k2 = len(rizer.uniques) - - group_id = labs1 * k2 + labs2 - max_group = k1 * k2 - - if max_group > 1e6: - rizer = lib.Int64Factorizer(len(group_id)) - group_id, _ = rizer.factorize(group_id.astype('i8'), sort=True) - max_group = len(rizer.uniques) - - out = np.empty(max_group) - counts = np.zeros(max_group, dtype='i4') - lib.group_add(out, counts, data, group_id) - -# cumtime percall filename:lineno(function) -# 0.592 0.592 :1() - # 0.584 0.006 groupby_ex.py:37(algo3_nosort) - # 0.535 0.005 {method 'factorize' of DictFactorizer' objects} - # 0.047 0.000 {pandas._tseries.group_add} - # 0.002 0.000 numeric.py:65(zeros_like) - # 0.001 0.000 {method 'fill' of 'numpy.ndarray' objects} - # 0.000 0.000 {numpy.core.multiarray.empty_like} - # 0.000 0.000 {numpy.core.multiarray.empty} - -# UNIQUE timings - -# N = 10000000 -# K = 500000 - -# groups = np.array([rands(10) for _ in range(K)], dtype='O') - -# labels = np.tile(groups, N // K) -data = np.random.randn(N) - -data = np.random.randn(N) - -Ks = [100, 1000, 5000, 10000, 25000, 50000, 100000] - -# Ks = [500000, 1000000, 2500000, 5000000, 10000000] - -import psutil -import os -import gc - -pid = os.getpid() -proc = psutil.Process(pid) - - -def dict_unique(values, expected_K, sort=False, memory=False): - if memory: - gc.collect() - before_mem = proc.get_memory_info().rss - - rizer = lib.DictFactorizer() - result = rizer.unique_int64(values) - - if memory: - result = proc.get_memory_info().rss - before_mem - return result - - if sort: - result.sort() - assert(len(result) == expected_K) - return result - - -def khash_unique(values, expected_K, size_hint=False, sort=False, - memory=False): - if memory: - gc.collect() - before_mem = proc.get_memory_info().rss - - if size_hint: - rizer = lib.Factorizer(len(values)) - else: - rizer = lib.Factorizer(100) - - result = [] - result = rizer.unique(values) - - if memory: - result = proc.get_memory_info().rss - before_mem - return result - - if sort: - result.sort() - assert(len(result) == expected_K) - - -def khash_unique_str(values, expected_K, size_hint=False, sort=False, - memory=False): - if memory: - gc.collect() - before_mem = proc.get_memory_info().rss - - if size_hint: - rizer = lib.StringHashTable(len(values)) - else: - rizer = lib.StringHashTable(100) - - result = [] - result = rizer.unique(values) - - if memory: - result = proc.get_memory_info().rss - before_mem - return result - - if sort: - result.sort() - assert(len(result) == expected_K) - - -def khash_unique_int64(values, expected_K, size_hint=False, sort=False): - if size_hint: - rizer = lib.Int64HashTable(len(values)) - else: - rizer = lib.Int64HashTable(100) - - result = [] - result = rizer.unique(values) - - if sort: - result.sort() - assert(len(result) == expected_K) - - -def hash_bench(): - numpy = [] - dict_based = [] - dict_based_sort = [] - khash_hint = [] - khash_nohint = [] - for K in Ks: - print(K) - # groups = np.array([rands(10) for _ in range(K)]) - # labels = np.tile(groups, N // K).astype('O') - - groups = np.random.randint(0, long(100000000000), size=K) - labels = np.tile(groups, N // K) - dict_based.append(timeit(lambda: dict_unique(labels, K), 20)) - khash_nohint.append(timeit(lambda: khash_unique_int64(labels, K), 20)) - khash_hint.append(timeit(lambda: khash_unique_int64(labels, K, - size_hint=True), 20)) - - # memory, hard to get - # dict_based.append(np.mean([dict_unique(labels, K, memory=True) - # for _ in range(10)])) - # khash_nohint.append(np.mean([khash_unique(labels, K, memory=True) - # for _ in range(10)])) - # khash_hint.append(np.mean([khash_unique(labels, K, size_hint=True, memory=True) - # for _ in range(10)])) - - # dict_based_sort.append(timeit(lambda: dict_unique(labels, K, - # sort=True), 10)) - # numpy.append(timeit(lambda: np.unique(labels), 10)) - - # unique_timings = DataFrame({'numpy.unique' : numpy, - # 'dict, no sort' : dict_based, - # 'dict, sort' : dict_based_sort}, - # columns=['dict, no sort', - # 'dict, sort', 'numpy.unique'], - # index=Ks) - - unique_timings = DataFrame({'dict': dict_based, - 'khash, preallocate': khash_hint, - 'khash': khash_nohint}, - columns=['khash, preallocate', 'khash', 'dict'], - index=Ks) - - unique_timings.plot(kind='bar', legend=False) - plt.legend(loc='best') - plt.title('Unique on 100,000 values, int64') - plt.xlabel('Number of unique labels') - plt.ylabel('Mean execution time') - - plt.show() diff --git a/bench/bench_with_subset.R b/bench/bench_with_subset.R deleted file mode 100644 index 69d0f7a9eec63..0000000000000 --- a/bench/bench_with_subset.R +++ /dev/null @@ -1,53 +0,0 @@ -library(microbenchmark) -library(data.table) - - -data.frame.subset.bench <- function (n=1e7, times=30) { - df <- data.frame(a=rnorm(n), b=rnorm(n), c=rnorm(n)) - print(microbenchmark(subset(df, a <= b & b <= (c ^ 2 + b ^ 2 - a) & b > c), - times=times)) -} - - -# data.table allows something very similar to query with an expression -# but we have chained comparisons AND we're faster BOO YAH! -data.table.subset.expression.bench <- function (n=1e7, times=30) { - dt <- data.table(a=rnorm(n), b=rnorm(n), c=rnorm(n)) - print(microbenchmark(dt[, a <= b & b <= (c ^ 2 + b ^ 2 - a) & b > c], - times=times)) -} - - -# compare against subset with data.table for good measure -data.table.subset.bench <- function (n=1e7, times=30) { - dt <- data.table(a=rnorm(n), b=rnorm(n), c=rnorm(n)) - print(microbenchmark(subset(dt, a <= b & b <= (c ^ 2 + b ^ 2 - a) & b > c), - times=times)) -} - - -data.frame.with.bench <- function (n=1e7, times=30) { - df <- data.frame(a=rnorm(n), b=rnorm(n), c=rnorm(n)) - - print(microbenchmark(with(df, a + b * (c ^ 2 + b ^ 2 - a) / (a * c) ^ 3), - times=times)) -} - - -data.table.with.bench <- function (n=1e7, times=30) { - dt <- data.table(a=rnorm(n), b=rnorm(n), c=rnorm(n)) - print(microbenchmark(with(dt, a + b * (c ^ 2 + b ^ 2 - a) / (a * c) ^ 3), - times=times)) -} - - -bench <- function () { - data.frame.subset.bench() - data.table.subset.expression.bench() - data.table.subset.bench() - data.frame.with.bench() - data.table.with.bench() -} - - -bench() diff --git a/bench/bench_with_subset.py b/bench/bench_with_subset.py deleted file mode 100644 index 017401df3f7f3..0000000000000 --- a/bench/bench_with_subset.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python - -""" -Microbenchmarks for comparison with R's "with" and "subset" functions -""" - -from __future__ import print_function -import numpy as np -from numpy import array -from timeit import repeat as timeit -from pandas.compat import range, zip -from pandas import DataFrame - - -setup_common = """from pandas import DataFrame -from numpy.random import randn -df = DataFrame(randn(%d, 3), columns=list('abc')) -%s""" - - -setup_with = "s = 'a + b * (c ** 2 + b ** 2 - a) / (a * c) ** 3'" - - -def bench_with(n, times=10, repeat=3, engine='numexpr'): - return np.array(timeit('df.eval(s, engine=%r)' % engine, - setup=setup_common % (n, setup_with), - repeat=repeat, number=times)) / times - - -setup_subset = "s = 'a <= b <= c ** 2 + b ** 2 - a and b > c'" - - -def bench_subset(n, times=10, repeat=3, engine='numexpr'): - return np.array(timeit('df.query(s, engine=%r)' % engine, - setup=setup_common % (n, setup_subset), - repeat=repeat, number=times)) / times - - -def bench(mn=1, mx=7, num=100, engines=('python', 'numexpr'), verbose=False): - r = np.logspace(mn, mx, num=num).round().astype(int) - - ev = DataFrame(np.empty((num, len(engines))), columns=engines) - qu = ev.copy(deep=True) - - ev['size'] = qu['size'] = r - - for engine in engines: - for i, n in enumerate(r): - if verbose: - print('engine: %r, i == %d' % (engine, i)) - ev.loc[i, engine] = bench_with(n, times=1, repeat=1, engine=engine) - qu.loc[i, engine] = bench_subset(n, times=1, repeat=1, - engine=engine) - - return ev, qu - - -def plot_perf(df, engines, title, filename=None): - from matplotlib.pyplot import figure, rc - - try: - from mpltools import style - except ImportError: - pass - else: - style.use('ggplot') - - rc('text', usetex=True) - - fig = figure(figsize=(4, 3), dpi=100) - ax = fig.add_subplot(111) - - for engine in engines: - ax.plot(df.size, df[engine], label=engine, lw=2) - - ax.set_xlabel('Number of Rows') - ax.set_ylabel('Time (s)') - ax.set_title(title) - ax.legend(loc='best') - ax.tick_params(top=False, right=False) - - fig.tight_layout() - - if filename is not None: - fig.savefig(filename) - - -if __name__ == '__main__': - import os - import pandas as pd - - pandas_dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) - static_path = os.path.join(pandas_dir, 'doc', 'source', '_static') - - join = lambda p: os.path.join(static_path, p) - - fn = join('eval-query-perf-data.h5') - - engines = 'python', 'numexpr' - - if not os.path.exists(fn): - ev, qu = bench(verbose=True) - ev.to_hdf(fn, 'eval') - qu.to_hdf(fn, 'query') - else: - ev = pd.read_hdf(fn, 'eval') - qu = pd.read_hdf(fn, 'query') - - plot_perf(ev, engines, 'DataFrame.eval()', filename=join('eval-perf.png')) - plot_perf(qu, engines, 'DataFrame.query()', - filename=join('query-perf.png')) - - plot_perf(ev[ev.size <= 50000], engines, 'DataFrame.eval()', - filename=join('eval-perf-small.png')) - plot_perf(qu[qu.size <= 500000], engines, 'DataFrame.query()', - filename=join('query-perf-small.png')) diff --git a/bench/better_unique.py b/bench/better_unique.py deleted file mode 100644 index e03a4f433ce66..0000000000000 --- a/bench/better_unique.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import print_function -from pandas import DataFrame -from pandas.compat import range, zip -import timeit - -setup = """ -from pandas import Series -import pandas._tseries as _tseries -from pandas.compat import range -import random -import numpy as np - -def better_unique(values): - uniques = _tseries.fast_unique(values) - id_map = _tseries.map_indices_buf(uniques) - labels = _tseries.get_unique_labels(values, id_map) - return uniques, labels - -tot = 100000 - -def get_test_data(ngroups=100, n=tot): - unique_groups = range(ngroups) - random.shuffle(unique_groups) - arr = np.asarray(np.tile(unique_groups, n / ngroups), dtype=object) - - if len(arr) < n: - arr = np.asarray(list(arr) + unique_groups[:n - len(arr)], - dtype=object) - - return arr - -arr = get_test_data(ngroups=%d) -""" - -group_sizes = [10, 100, 1000, 10000, - 20000, 30000, 40000, - 50000, 60000, 70000, - 80000, 90000, 100000] - -numbers = [100, 100, 50] + [10] * 10 - -numpy = [] -wes = [] - -for sz, n in zip(group_sizes, numbers): - # wes_timer = timeit.Timer(stmt='better_unique(arr)', - # setup=setup % sz) - wes_timer = timeit.Timer(stmt='_tseries.fast_unique(arr)', - setup=setup % sz) - - numpy_timer = timeit.Timer(stmt='np.unique(arr)', - setup=setup % sz) - - print(n) - numpy_result = numpy_timer.timeit(number=n) / n - wes_result = wes_timer.timeit(number=n) / n - - print('Groups: %d, NumPy: %s, Wes: %s' % (sz, numpy_result, wes_result)) - - wes.append(wes_result) - numpy.append(numpy_result) - -result = DataFrame({'wes': wes, 'numpy': numpy}, index=group_sizes) - - -def make_plot(numpy, wes): - pass - -# def get_test_data(ngroups=100, n=100000): -# unique_groups = range(ngroups) -# random.shuffle(unique_groups) -# arr = np.asarray(np.tile(unique_groups, n / ngroups), dtype=object) - -# if len(arr) < n: -# arr = np.asarray(list(arr) + unique_groups[:n - len(arr)], -# dtype=object) - -# return arr - -# arr = get_test_data(ngroups=1000) diff --git a/bench/duplicated.R b/bench/duplicated.R deleted file mode 100644 index eb2376df2932a..0000000000000 --- a/bench/duplicated.R +++ /dev/null @@ -1,22 +0,0 @@ -N <- 100000 - -k1 = rep(NA, N) -k2 = rep(NA, N) -for (i in 1:N){ - k1[i] <- paste(sample(letters, 1), collapse="") - k2[i] <- paste(sample(letters, 1), collapse="") -} -df <- data.frame(a=k1, b=k2, c=rep(1:100, N / 100)) -df2 <- data.frame(a=k1, b=k2) - -timings <- numeric() -timings2 <- numeric() -for (i in 1:50) { - gc() - timings[i] = system.time(deduped <- df[!duplicated(df),])[3] - gc() - timings2[i] = system.time(deduped <- df[!duplicated(df[,c("a", "b")]),])[3] -} - -mean(timings) -mean(timings2) diff --git a/bench/io_roundtrip.py b/bench/io_roundtrip.py deleted file mode 100644 index d87da0ec6321a..0000000000000 --- a/bench/io_roundtrip.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import print_function -import time -import os -import numpy as np - -import la -import pandas -from pandas.compat import range -from pandas import datetools, DatetimeIndex - - -def timeit(f, iterations): - start = time.clock() - - for i in range(iterations): - f() - - return time.clock() - start - - -def rountrip_archive(N, K=50, iterations=10): - # Create data - arr = np.random.randn(N, K) - # lar = la.larry(arr) - dma = pandas.DataFrame(arr, - DatetimeIndex('1/1/2000', periods=N, - offset=datetools.Minute())) - dma[201] = 'bar' - - # filenames - filename_numpy = '/Users/wesm/tmp/numpy.npz' - filename_larry = '/Users/wesm/tmp/archive.hdf5' - filename_pandas = '/Users/wesm/tmp/pandas_tmp' - - # Delete old files - try: - os.unlink(filename_numpy) - except: - pass - try: - os.unlink(filename_larry) - except: - pass - - try: - os.unlink(filename_pandas) - except: - pass - - # Time a round trip save and load - # numpy_f = lambda: numpy_roundtrip(filename_numpy, arr, arr) - # numpy_time = timeit(numpy_f, iterations) / iterations - - # larry_f = lambda: larry_roundtrip(filename_larry, lar, lar) - # larry_time = timeit(larry_f, iterations) / iterations - - pandas_f = lambda: pandas_roundtrip(filename_pandas, dma, dma) - pandas_time = timeit(pandas_f, iterations) / iterations - print('pandas (HDF5) %7.4f seconds' % pandas_time) - - pickle_f = lambda: pandas_roundtrip(filename_pandas, dma, dma) - pickle_time = timeit(pickle_f, iterations) / iterations - print('pandas (pickle) %7.4f seconds' % pickle_time) - - # print('Numpy (npz) %7.4f seconds' % numpy_time) - # print('larry (HDF5) %7.4f seconds' % larry_time) - - # Delete old files - try: - os.unlink(filename_numpy) - except: - pass - try: - os.unlink(filename_larry) - except: - pass - - try: - os.unlink(filename_pandas) - except: - pass - - -def numpy_roundtrip(filename, arr1, arr2): - np.savez(filename, arr1=arr1, arr2=arr2) - npz = np.load(filename) - arr1 = npz['arr1'] - arr2 = npz['arr2'] - - -def larry_roundtrip(filename, lar1, lar2): - io = la.IO(filename) - io['lar1'] = lar1 - io['lar2'] = lar2 - lar1 = io['lar1'] - lar2 = io['lar2'] - - -def pandas_roundtrip(filename, dma1, dma2): - # What's the best way to code this? - from pandas.io.pytables import HDFStore - store = HDFStore(filename) - store['dma1'] = dma1 - store['dma2'] = dma2 - dma1 = store['dma1'] - dma2 = store['dma2'] - - -def pandas_roundtrip_pickle(filename, dma1, dma2): - dma1.save(filename) - dma1 = pandas.DataFrame.load(filename) - dma2.save(filename) - dma2 = pandas.DataFrame.load(filename) - -if __name__ == '__main__': - rountrip_archive(10000, K=200) diff --git a/bench/serialize.py b/bench/serialize.py deleted file mode 100644 index b0edd6a5752d2..0000000000000 --- a/bench/serialize.py +++ /dev/null @@ -1,89 +0,0 @@ -from __future__ import print_function -from pandas.compat import range, lrange -import time -import os -import numpy as np - -import la -import pandas - - -def timeit(f, iterations): - start = time.clock() - - for i in range(iterations): - f() - - return time.clock() - start - - -def roundtrip_archive(N, iterations=10): - - # Create data - arr = np.random.randn(N, N) - lar = la.larry(arr) - dma = pandas.DataFrame(arr, lrange(N), lrange(N)) - - # filenames - filename_numpy = '/Users/wesm/tmp/numpy.npz' - filename_larry = '/Users/wesm/tmp/archive.hdf5' - filename_pandas = '/Users/wesm/tmp/pandas_tmp' - - # Delete old files - try: - os.unlink(filename_numpy) - except: - pass - try: - os.unlink(filename_larry) - except: - pass - try: - os.unlink(filename_pandas) - except: - pass - - # Time a round trip save and load - numpy_f = lambda: numpy_roundtrip(filename_numpy, arr, arr) - numpy_time = timeit(numpy_f, iterations) / iterations - - larry_f = lambda: larry_roundtrip(filename_larry, lar, lar) - larry_time = timeit(larry_f, iterations) / iterations - - pandas_f = lambda: pandas_roundtrip(filename_pandas, dma, dma) - pandas_time = timeit(pandas_f, iterations) / iterations - - print('Numpy (npz) %7.4f seconds' % numpy_time) - print('larry (HDF5) %7.4f seconds' % larry_time) - print('pandas (HDF5) %7.4f seconds' % pandas_time) - - -def numpy_roundtrip(filename, arr1, arr2): - np.savez(filename, arr1=arr1, arr2=arr2) - npz = np.load(filename) - arr1 = npz['arr1'] - arr2 = npz['arr2'] - - -def larry_roundtrip(filename, lar1, lar2): - io = la.IO(filename) - io['lar1'] = lar1 - io['lar2'] = lar2 - lar1 = io['lar1'] - lar2 = io['lar2'] - - -def pandas_roundtrip(filename, dma1, dma2): - from pandas.io.pytables import HDFStore - store = HDFStore(filename) - store['dma1'] = dma1 - store['dma2'] = dma2 - dma1 = store['dma1'] - dma2 = store['dma2'] - - -def pandas_roundtrip_pickle(filename, dma1, dma2): - dma1.save(filename) - dma1 = pandas.DataFrame.load(filename) - dma2.save(filename) - dma2 = pandas.DataFrame.load(filename) diff --git a/bench/test.py b/bench/test.py deleted file mode 100644 index 2339deab313a1..0000000000000 --- a/bench/test.py +++ /dev/null @@ -1,70 +0,0 @@ -import numpy as np -import itertools -import collections -import scipy.ndimage as ndi -from pandas.compat import zip, range - -N = 10000 - -lat = np.random.randint(0, 360, N) -lon = np.random.randint(0, 360, N) -data = np.random.randn(N) - - -def groupby1(lat, lon, data): - indexer = np.lexsort((lon, lat)) - lat = lat.take(indexer) - lon = lon.take(indexer) - sorted_data = data.take(indexer) - - keys = 1000. * lat + lon - unique_keys = np.unique(keys) - bounds = keys.searchsorted(unique_keys) - - result = group_agg(sorted_data, bounds, lambda x: x.mean()) - - decoder = keys.searchsorted(unique_keys) - - return dict(zip(zip(lat.take(decoder), lon.take(decoder)), result)) - - -def group_mean(lat, lon, data): - indexer = np.lexsort((lon, lat)) - lat = lat.take(indexer) - lon = lon.take(indexer) - sorted_data = data.take(indexer) - - keys = 1000 * lat + lon - unique_keys = np.unique(keys) - - result = ndi.mean(sorted_data, labels=keys, index=unique_keys) - decoder = keys.searchsorted(unique_keys) - - return dict(zip(zip(lat.take(decoder), lon.take(decoder)), result)) - - -def group_mean_naive(lat, lon, data): - grouped = collections.defaultdict(list) - for lt, ln, da in zip(lat, lon, data): - grouped[(lt, ln)].append(da) - - averaged = dict((ltln, np.mean(da)) for ltln, da in grouped.items()) - - return averaged - - -def group_agg(values, bounds, f): - N = len(values) - result = np.empty(len(bounds), dtype=float) - for i, left_bound in enumerate(bounds): - if i == len(bounds) - 1: - right_bound = N - else: - right_bound = bounds[i + 1] - - result[i] = f(values[left_bound: right_bound]) - - return result - -# for i in range(10): -# groupby1(lat, lon, data) diff --git a/bench/zoo_bench.R b/bench/zoo_bench.R deleted file mode 100644 index 294d55f51a9ab..0000000000000 --- a/bench/zoo_bench.R +++ /dev/null @@ -1,71 +0,0 @@ -library(zoo) -library(xts) -library(fts) -library(tseries) -library(its) -library(xtable) - -## indices = rep(NA, 100000) -## for (i in 1:100000) -## indices[i] <- paste(sample(letters, 10), collapse="") - - - -## x <- zoo(rnorm(100000), indices) -## y <- zoo(rnorm(90000), indices[sample(1:100000, 90000)]) - -## indices <- as.POSIXct(1:100000) - -indices <- as.POSIXct(Sys.Date()) + seq(1, 100000000, 100) - -sz <- 500000 - -## x <- xts(rnorm(sz), sample(indices, sz)) -## y <- xts(rnorm(sz), sample(indices, sz)) - -zoo.bench <- function(){ - x <- zoo(rnorm(sz), sample(indices, sz)) - y <- zoo(rnorm(sz), sample(indices, sz)) - timeit(function() {x + y}) -} - -xts.bench <- function(){ - x <- xts(rnorm(sz), sample(indices, sz)) - y <- xts(rnorm(sz), sample(indices, sz)) - timeit(function() {x + y}) -} - -fts.bench <- function(){ - x <- fts(rnorm(sz), sort(sample(indices, sz))) - y <- fts(rnorm(sz), sort(sample(indices, sz)) - timeit(function() {x + y}) -} - -its.bench <- function(){ - x <- its(rnorm(sz), sort(sample(indices, sz))) - y <- its(rnorm(sz), sort(sample(indices, sz))) - timeit(function() {x + y}) -} - -irts.bench <- function(){ - x <- irts(sort(sample(indices, sz)), rnorm(sz)) - y <- irts(sort(sample(indices, sz)), rnorm(sz)) - timeit(function() {x + y}) -} - -timeit <- function(f){ - timings <- numeric() - for (i in 1:10) { - gc() - timings[i] = system.time(f())[3] - } - mean(timings) -} - -bench <- function(){ - results <- c(xts.bench(), fts.bench(), its.bench(), zoo.bench()) - names <- c("xts", "fts", "its", "zoo") - data.frame(results, names) -} - -result <- bench() diff --git a/bench/zoo_bench.py b/bench/zoo_bench.py deleted file mode 100644 index 74cb1952a5a2a..0000000000000 --- a/bench/zoo_bench.py +++ /dev/null @@ -1,36 +0,0 @@ -from pandas import * -from pandas.util.testing import rands - -n = 1000000 -# indices = Index([rands(10) for _ in xrange(n)]) - - -def sample(values, k): - sampler = np.random.permutation(len(values)) - return values.take(sampler[:k]) -sz = 500000 -rng = np.arange(0, 10000000000000, 10000000) -stamps = np.datetime64(datetime.now()).view('i8') + rng -idx1 = np.sort(sample(stamps, sz)) -idx2 = np.sort(sample(stamps, sz)) -ts1 = Series(np.random.randn(sz), idx1) -ts2 = Series(np.random.randn(sz), idx2) - - -# subsample_size = 90000 - -# x = Series(np.random.randn(100000), indices) -# y = Series(np.random.randn(subsample_size), -# index=sample(indices, subsample_size)) - - -# lx = larry(np.random.randn(100000), [list(indices)]) -# ly = larry(np.random.randn(subsample_size), [list(y.index)]) - -# Benchmark 1: Two 1-million length time series (int64-based index) with -# randomly chosen timestamps - -# Benchmark 2: Join two 5-variate time series DataFrames (outer and inner join) - -# df1 = DataFrame(np.random.randn(1000000, 5), idx1, columns=range(5)) -# df2 = DataFrame(np.random.randn(1000000, 5), idx2, columns=range(5, 10)) diff --git a/ci/README.txt b/ci/README.txt deleted file mode 100644 index bb71dc25d6093..0000000000000 --- a/ci/README.txt +++ /dev/null @@ -1,17 +0,0 @@ -Travis is a ci service that's well-integrated with GitHub. -The following types of breakage should be detected -by Travis builds: - -1) Failing tests on any supported version of Python. -2) Pandas should install and the tests should run if no optional deps are installed. -That also means tests which rely on optional deps need to raise SkipTest() -if the dep is missing. -3) unicode related fails when running under exotic locales. - -We tried running the vbench suite for a while, but with varying load -on Travis machines, that wasn't useful. - -Travis currently (4/2013) has a 5-job concurrency limit. Exceeding it -basically doubles the total runtime for a commit through travis, and -since dep+pandas installation is already quite long, this should become -a hard limit on concurrent travis runs. diff --git a/ci/azure/posix.yml b/ci/azure/posix.yml new file mode 100644 index 0000000000000..b9e0cd0b9258c --- /dev/null +++ b/ci/azure/posix.yml @@ -0,0 +1,100 @@ +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + matrix: + ${{ if eq(parameters.name, 'macOS') }}: + py35_np_120: + ENV_FILE: ci/deps/azure-macos-35.yaml + CONDA_PY: "35" + PATTERN: "not slow and not network" + + ${{ if eq(parameters.name, 'Linux') }}: + py27_np_120: + ENV_FILE: ci/deps/azure-27-compat.yaml + CONDA_PY: "27" + PATTERN: "not slow and not network" + + py27_locale_slow_old_np: + ENV_FILE: ci/deps/azure-27-locale.yaml + CONDA_PY: "27" + PATTERN: "slow" + LOCALE_OVERRIDE: "zh_CN.UTF-8" + EXTRA_APT: "language-pack-zh-hans" + + py36_locale_slow: + ENV_FILE: ci/deps/azure-36-locale_slow.yaml + CONDA_PY: "36" + PATTERN: "not slow and not network" + LOCALE_OVERRIDE: "it_IT.UTF-8" + + py37_locale: + ENV_FILE: ci/deps/azure-37-locale.yaml + CONDA_PY: "37" + PATTERN: "not slow and not network" + LOCALE_OVERRIDE: "zh_CN.UTF-8" + + py37_np_dev: + ENV_FILE: ci/deps/azure-37-numpydev.yaml + CONDA_PY: "37" + PATTERN: "not slow and not network" + TEST_ARGS: "-W error" + PANDAS_TESTING_MODE: "deprecate" + EXTRA_APT: "xsel" + + steps: + - script: | + if [ "$(uname)" == "Linux" ]; then sudo apt-get install -y libc6-dev-i386 $EXTRA_APT; fi + echo "Installing Miniconda" + ci/incremental/install_miniconda.sh + export PATH=$HOME/miniconda3/bin:$PATH + echo "Setting up Conda environment" + ci/incremental/setup_conda_environment.sh + displayName: 'Before Install' + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/incremental/build.sh + displayName: 'Build' + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev + ci/run_tests.sh + displayName: 'Test' + - script: | + export PATH=$HOME/miniconda3/bin:$PATH + source activate pandas-dev && pushd /tmp && python -c "import pandas; pandas.show_versions();" && popd + - task: PublishTestResults@2 + inputs: + testResultsFiles: 'test-data-*.xml' + testRunTitle: ${{ format('{0}-$(CONDA_PY)', parameters.name) }} + - powershell: | + $junitXml = "test-data-single.xml" + $(Get-Content $junitXml | Out-String) -match 'failures="(.*?)"' + if ($matches[1] -eq 0) + { + Write-Host "No test failures in test-data-single" + } + else + { + # note that this will produce $LASTEXITCODE=1 + Write-Error "$($matches[1]) tests failed" + } + + $junitXmlMulti = "test-data-multiple.xml" + $(Get-Content $junitXmlMulti | Out-String) -match 'failures="(.*?)"' + if ($matches[1] -eq 0) + { + Write-Host "No test failures in test-data-multi" + } + else + { + # note that this will produce $LASTEXITCODE=1 + Write-Error "$($matches[1]) tests failed" + } + displayName: Check for test failures diff --git a/ci/azure/windows.yml b/ci/azure/windows.yml new file mode 100644 index 0000000000000..cece002024936 --- /dev/null +++ b/ci/azure/windows.yml @@ -0,0 +1,59 @@ +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + matrix: + py36_np14: + ENV_FILE: ci/deps/azure-windows-36.yaml + CONDA_PY: "36" + + py27_np121: + ENV_FILE: ci/deps/azure-windows-27.yaml + CONDA_PY: "27" + + steps: + - task: CondaEnvironment@1 + inputs: + updateConda: no + packageSpecs: '' + + - powershell: | + $wc = New-Object net.webclient + $wc.Downloadfile("https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi", "VCForPython27.msi") + Start-Process "VCForPython27.msi" /qn -Wait + displayName: 'Install VC 9.0 only for Python 2.7' + condition: eq(variables.CONDA_PY, '27') + + - script: | + ci\\incremental\\setup_conda_environment.cmd + displayName: 'Before Install' + - script: | + call activate pandas-dev + ci\\incremental\\build.cmd + displayName: 'Build' + - script: | + call activate pandas-dev + pytest -m "not slow and not network" --junitxml=test-data.xml pandas -n 2 -r sxX --strict --durations=10 %* + displayName: 'Test' + - task: PublishTestResults@2 + inputs: + testResultsFiles: 'test-data.xml' + testRunTitle: 'Windows-$(CONDA_PY)' + - powershell: | + $junitXml = "test-data.xml" + $(Get-Content $junitXml | Out-String) -match 'failures="(.*?)"' + if ($matches[1] -eq 0) + { + Write-Host "No test failures in test-data" + } + else + { + # note that this will produce $LASTEXITCODE=1 + Write-Error "$($matches[1]) tests failed" + } + displayName: Check for test failures diff --git a/ci/before_install_travis.sh b/ci/before_install_travis.sh deleted file mode 100755 index f90427f97d3b7..0000000000000 --- a/ci/before_install_travis.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# If envars.sh determined we're running in an authorized fork -# and the user opted in to the network cache,and that cached versions -# are available on the cache server, download and deploy the cached -# files to the local filesystem - -echo "inside $0" - -# overview -if [ "${TRAVIS_OS_NAME}" == "linux" ]; then - sh -e /etc/init.d/xvfb start -fi - -true # never fail because bad things happened here diff --git a/ci/before_script_travis.sh b/ci/before_script_travis.sh new file mode 100755 index 0000000000000..0b3939b1906a2 --- /dev/null +++ b/ci/before_script_travis.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "inside $0" + +if [ "${TRAVIS_OS_NAME}" == "linux" ]; then + sh -e /etc/init.d/xvfb start + sleep 3 +fi + +# Never fail because bad things happened here. +true diff --git a/ci/build_docs.sh b/ci/build_docs.sh index 1356d097025c9..bf22f0764144c 100755 --- a/ci/build_docs.sh +++ b/ci/build_docs.sh @@ -1,31 +1,19 @@ #!/bin/bash +set -e + if [ "${TRAVIS_OS_NAME}" != "linux" ]; then echo "not doing build_docs on non-linux" exit 0 fi -cd "$TRAVIS_BUILD_DIR" +cd "$TRAVIS_BUILD_DIR"/doc echo "inside $0" -git show --pretty="format:" --name-only HEAD~5.. --first-parent | grep -P "rst|txt|doc" - -if [ "$?" != "0" ]; then - echo "Skipping doc build, none were modified" - # nope, skip docs build - exit 0 -fi - - if [ "$DOC" ]; then echo "Will build docs" - source activate pandas - - mv "$TRAVIS_BUILD_DIR"/doc /tmp - cd /tmp/doc - echo ############################### echo # Log file for the doc build # echo ############################### @@ -37,24 +25,32 @@ if [ "$DOC" ]; then echo # Create and send docs # echo ######################## - cd /tmp/doc/build/html - git config --global user.email "pandas-docs-bot@localhost.foo" - git config --global user.name "pandas-docs-bot" - git config --global credential.helper cache - - # create the repo - git init - touch README - git add README - git commit -m "Initial commit" --allow-empty - git branch gh-pages - git checkout gh-pages - touch .nojekyll - git add --all . - git commit -m "Version" --allow-empty - git remote remove origin - git remote add origin "https://${PANDAS_GH_TOKEN}@github.com/pandas-docs/pandas-docs-travis.git" - git push origin gh-pages -f + echo "Only uploading docs when TRAVIS_PULL_REQUEST is 'false'" + echo "TRAVIS_PULL_REQUEST: ${TRAVIS_PULL_REQUEST}" + + if [ "${TRAVIS_PULL_REQUEST}" == "false" ]; then + cd build/html + git config --global user.email "pandas-docs-bot@localhost.foo" + git config --global user.name "pandas-docs-bot" + + # create the repo + git init + + touch README + git add README + git commit -m "Initial commit" --allow-empty + git branch gh-pages + git checkout gh-pages + touch .nojekyll + git add --all . + git commit -m "Version" --allow-empty + + git remote add origin "https://${PANDAS_GH_TOKEN}@github.com/pandas-dev/pandas-docs-travis.git" + git fetch origin + git remote -v + + git push origin gh-pages -f + fi fi exit 0 diff --git a/ci/code_checks.sh b/ci/code_checks.sh new file mode 100755 index 0000000000000..c4840f1e836c4 --- /dev/null +++ b/ci/code_checks.sh @@ -0,0 +1,259 @@ +#!/bin/bash +# +# Run checks related to code quality. +# +# This script is intended for both the CI and to check locally that code standards are +# respected. We are currently linting (PEP-8 and similar), looking for patterns of +# common mistakes (sphinx directives with missing blank lines, old style classes, +# unwanted imports...), we run doctests here (currently some files only), and we +# validate formatting error in docstrings. +# +# Usage: +# $ ./ci/code_checks.sh # run all checks +# $ ./ci/code_checks.sh lint # run linting only +# $ ./ci/code_checks.sh patterns # check for patterns that should not exist +# $ ./ci/code_checks.sh code # checks on imported code +# $ ./ci/code_checks.sh doctests # run doctests +# $ ./ci/code_checks.sh docstrings # validate docstring errors +# $ ./ci/code_checks.sh dependencies # check that dependencies are consistent + +[[ -z "$1" || "$1" == "lint" || "$1" == "patterns" || "$1" == "code" || "$1" == "doctests" || "$1" == "docstrings" || "$1" == "dependencies" ]] || \ + { echo "Unknown command $1. Usage: $0 [lint|patterns|code|doctests|docstrings|dependencies]"; exit 9999; } + +BASE_DIR="$(dirname $0)/.." +RET=0 +CHECK=$1 + +function invgrep { + # grep with inverse exist status and formatting for azure-pipelines + # + # This function works exactly as grep, but with opposite exit status: + # - 0 (success) when no patterns are found + # - 1 (fail) when the patterns are found + # + # This is useful for the CI, as we want to fail if one of the patterns + # that we want to avoid is found by grep. + if [[ "$AZURE" == "true" ]]; then + set -o pipefail + grep -n "$@" | awk -F ":" '{print "##vso[task.logissue type=error;sourcepath=" $1 ";linenumber=" $2 ";] Found unwanted pattern: " $3}' + else + grep "$@" + fi + return $((! $?)) +} + +if [[ "$AZURE" == "true" ]]; then + FLAKE8_FORMAT="##vso[task.logissue type=error;sourcepath=%(path)s;linenumber=%(row)s;columnnumber=%(col)s;code=%(code)s;]%(text)s" +else + FLAKE8_FORMAT="default" +fi + +### LINTING ### +if [[ -z "$CHECK" || "$CHECK" == "lint" ]]; then + + # `setup.cfg` contains the list of error codes that are being ignored in flake8 + + echo "flake8 --version" + flake8 --version + + # pandas/_libs/src is C code, so no need to search there. + MSG='Linting .py code' ; echo $MSG + flake8 --format="$FLAKE8_FORMAT" . + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Linting .pyx code' ; echo $MSG + flake8 --format="$FLAKE8_FORMAT" pandas --filename=*.pyx --select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126,E265,E305,E301,E127,E261,E271,E129,W291,E222,E241,E123,F403,C400,C401,C402,C403,C404,C405,C406,C407,C408,C409,C410,C411 + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Linting .pxd and .pxi.in' ; echo $MSG + flake8 --format="$FLAKE8_FORMAT" pandas/_libs --filename=*.pxi.in,*.pxd --select=E501,E302,E203,E111,E114,E221,E303,E231,E126,F403 + RET=$(($RET + $?)) ; echo $MSG "DONE" + + echo "flake8-rst --version" + flake8-rst --version + + MSG='Linting code-blocks in .rst documentation' ; echo $MSG + flake8-rst doc/source --filename=*.rst --format="$FLAKE8_FORMAT" + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # Check that cython casting is of the form `obj` as opposed to ` obj`; + # it doesn't make a difference, but we want to be internally consistent. + # Note: this grep pattern is (intended to be) equivalent to the python + # regex r'(?])> ' + MSG='Linting .pyx code for spacing conventions in casting' ; echo $MSG + invgrep -r -E --include '*.pyx' --include '*.pxi.in' '[a-zA-Z0-9*]> ' pandas/_libs + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # readability/casting: Warnings about C casting instead of C++ casting + # runtime/int: Warnings about using C number types instead of C++ ones + # build/include_subdir: Warnings about prefacing included header files with directory + + # We don't lint all C files because we don't want to lint any that are built + # from Cython files nor do we want to lint C files that we didn't modify for + # this particular codebase (e.g. src/headers, src/klib, src/msgpack). However, + # we can lint all header files since they aren't "generated" like C files are. + MSG='Linting .c and .h' ; echo $MSG + cpplint --quiet --extensions=c,h --headers=h --recursive --filter=-readability/casting,-runtime/int,-build/include_subdir pandas/_libs/src/*.h pandas/_libs/src/parser pandas/_libs/ujson pandas/_libs/tslibs/src/datetime pandas/io/msgpack pandas/_libs/*.cpp pandas/util + RET=$(($RET + $?)) ; echo $MSG "DONE" + + echo "isort --version-number" + isort --version-number + + # Imports - Check formatting using isort see setup.cfg for settings + MSG='Check import format using isort ' ; echo $MSG + isort --recursive --check-only pandas asv_bench + RET=$(($RET + $?)) ; echo $MSG "DONE" + +fi + +### PATTERNS ### +if [[ -z "$CHECK" || "$CHECK" == "patterns" ]]; then + + # Check for imports from pandas.core.common instead of `import pandas.core.common as com` + MSG='Check for non-standard imports' ; echo $MSG + invgrep -R --include="*.py*" -E "from pandas.core.common import " pandas + # invgrep -R --include="*.py*" -E "from numpy import nan " pandas # GH#24822 not yet implemented since the offending imports have not all been removed + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for pytest warns' ; echo $MSG + invgrep -r -E --include '*.py' 'pytest\.warns' pandas/tests/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # Check for the following code in testing: `np.testing` and `np.array_equal` + MSG='Check for invalid testing' ; echo $MSG + invgrep -r -E --include '*.py' --exclude testing.py '(numpy|np)(\.testing|\.array_equal)' pandas/tests/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # Check for the following code in the extension array base tests: `tm.assert_frame_equal` and `tm.assert_series_equal` + MSG='Check for invalid EA testing' ; echo $MSG + invgrep -r -E --include '*.py' --exclude base.py 'tm.assert_(series|frame)_equal' pandas/tests/extension/base + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for deprecated messages without sphinx directive' ; echo $MSG + invgrep -R --include="*.py" --include="*.pyx" -E "(DEPRECATED|DEPRECATE|Deprecated)(:|,|\.)" pandas + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for old-style classes' ; echo $MSG + invgrep -R --include="*.py" -E "class\s\S*[^)]:" pandas scripts + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for backticks incorrectly rendering because of missing spaces' ; echo $MSG + invgrep -R --include="*.rst" -E "[a-zA-Z0-9]\`\`?[a-zA-Z0-9]" doc/source/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for incorrect sphinx directives' ; echo $MSG + invgrep -R --include="*.py" --include="*.pyx" --include="*.rst" -E "\.\. (autosummary|contents|currentmodule|deprecated|function|image|important|include|ipython|literalinclude|math|module|note|raw|seealso|toctree|versionadded|versionchanged|warning):[^:]" ./pandas ./doc/source + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check that the deprecated `assert_raises_regex` is not used (`pytest.raises(match=pattern)` should be used instead)' ; echo $MSG + invgrep -R --exclude=*.pyc --exclude=testing.py --exclude=test_util.py assert_raises_regex pandas + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # Check for the following code in testing: `unittest.mock`, `mock.Mock()` or `mock.patch` + MSG='Check that unittest.mock is not used (pytest builtin monkeypatch fixture should be used instead)' ; echo $MSG + invgrep -r -E --include '*.py' '(unittest(\.| import )mock|mock\.Mock\(\)|mock\.patch)' pandas/tests/ + RET=$(($RET + $?)) ; echo $MSG "DONE" + + # Check that we use pytest.raises only as a context manager + # + # For any flake8-compliant code, the only way this regex gets + # matched is if there is no "with" statement preceding "pytest.raises" + MSG='Check for pytest.raises as context manager (a line starting with `pytest.raises` is invalid, needs a `with` to precede it)' ; echo $MSG + MSG='TODO: This check is currently skipped because so many files fail this. Please enable when all are corrected (xref gh-24332)' ; echo $MSG + # invgrep -R --include '*.py' -E '[[:space:]] pytest.raises' pandas/tests + # RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for wrong space after code-block directive and before colon (".. code-block ::" instead of ".. code-block::")' ; echo $MSG + invgrep -R --include="*.rst" ".. code-block ::" doc/source + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check for wrong space after ipython directive and before colon (".. ipython ::" instead of ".. ipython::")' ; echo $MSG + invgrep -R --include="*.rst" ".. ipython ::" doc/source + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Check that no file in the repo contains tailing whitespaces' ; echo $MSG + set -o pipefail + if [[ "$AZURE" == "true" ]]; then + # we exclude all c/cpp files as the c/cpp files of pandas code base are tested when Linting .c and .h files + ! grep -n '--exclude=*.'{svg,c,cpp,html} -RI "\s$" * | awk -F ":" '{print "##vso[task.logissue type=error;sourcepath=" $1 ";linenumber=" $2 ";] Tailing whitespaces found: " $3}' + else + ! grep -n '--exclude=*.'{svg,c,cpp,html} -RI "\s$" * | awk -F ":" '{print $1 ":" $2 ":Tailing whitespaces found: " $3}' + fi + RET=$(($RET + $?)) ; echo $MSG "DONE" +fi + +### CODE ### +if [[ -z "$CHECK" || "$CHECK" == "code" ]]; then + + MSG='Check import. No warnings, and blacklist some optional dependencies' ; echo $MSG + python -W error -c " +import sys +import pandas + +blacklist = {'bs4', 'gcsfs', 'html5lib', 'ipython', 'jinja2' 'hypothesis', + 'lxml', 'numexpr', 'openpyxl', 'py', 'pytest', 's3fs', 'scipy', + 'tables', 'xlrd', 'xlsxwriter', 'xlwt'} +mods = blacklist & set(m.split('.')[0] for m in sys.modules) +if mods: + sys.stderr.write('err: pandas should not import: {}\n'.format(', '.join(mods))) + sys.exit(len(mods)) + " + RET=$(($RET + $?)) ; echo $MSG "DONE" + +fi + +### DOCTESTS ### +if [[ -z "$CHECK" || "$CHECK" == "doctests" ]]; then + + MSG='Doctests frame.py' ; echo $MSG + pytest -q --doctest-modules pandas/core/frame.py \ + -k" -itertuples -join -reindex -reindex_axis -round" + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Doctests series.py' ; echo $MSG + pytest -q --doctest-modules pandas/core/series.py \ + -k"-nonzero -reindex -searchsorted -to_dict" + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Doctests generic.py' ; echo $MSG + pytest -q --doctest-modules pandas/core/generic.py \ + -k"-_set_axis_name -_xs -describe -droplevel -groupby -interpolate -pct_change -pipe -reindex -reindex_axis -to_json -transpose -values -xs -to_clipboard" + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Doctests top-level reshaping functions' ; echo $MSG + pytest -q --doctest-modules \ + pandas/core/reshape/concat.py \ + pandas/core/reshape/pivot.py \ + pandas/core/reshape/reshape.py \ + pandas/core/reshape/tile.py \ + -k"-crosstab -pivot_table -cut" + RET=$(($RET + $?)) ; echo $MSG "DONE" + + MSG='Doctests interval classes' ; echo $MSG + pytest --doctest-modules -v \ + pandas/core/indexes/interval.py \ + pandas/core/arrays/interval.py \ + -k"-from_arrays -from_breaks -from_intervals -from_tuples -get_loc -set_closed -to_tuples -interval_range" + RET=$(($RET + $?)) ; echo $MSG "DONE" + +fi + +### DOCSTRINGS ### +if [[ -z "$CHECK" || "$CHECK" == "docstrings" ]]; then + + MSG='Validate docstrings (GL06, GL07, GL09, SS04, SS05, PR03, PR04, PR05, PR10, EX04, RT04, RT05, SA05)' ; echo $MSG + $BASE_DIR/scripts/validate_docstrings.py --format=azure --errors=GL06,GL07,GL09,SS04,SS05,PR03,PR04,PR05,PR10,EX04,RT04,RT05,SA05 + RET=$(($RET + $?)) ; echo $MSG "DONE" + +fi + +### DEPENDENCIES ### +if [[ -z "$CHECK" || "$CHECK" == "dependencies" ]]; then + + MSG='Check that requirements-dev.txt has been generated from environment.yml' ; echo $MSG + $BASE_DIR/scripts/generate_pip_deps_from_conda.py --compare --azure + RET=$(($RET + $?)) ; echo $MSG "DONE" + +fi + +exit $RET diff --git a/ci/deps/azure-27-compat.yaml b/ci/deps/azure-27-compat.yaml new file mode 100644 index 0000000000000..a7784f17d1956 --- /dev/null +++ b/ci/deps/azure-27-compat.yaml @@ -0,0 +1,28 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - bottleneck=1.2.0 + - cython=0.28.2 + - jinja2=2.8 + - numexpr=2.6.1 + - numpy=1.12.0 + - openpyxl=2.5.5 + - pytables=3.4.2 + - python-dateutil=2.5.0 + - python=2.7* + - pytz=2013b + - scipy=0.18.1 + - xlrd=1.0.0 + - xlsxwriter=0.5.2 + - xlwt=0.7.5 + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - isort + - pip: + - html5lib==1.0b2 + - beautifulsoup4==4.2.1 + - hypothesis>=3.58.0 diff --git a/ci/deps/azure-27-locale.yaml b/ci/deps/azure-27-locale.yaml new file mode 100644 index 0000000000000..8636a63d02fed --- /dev/null +++ b/ci/deps/azure-27-locale.yaml @@ -0,0 +1,30 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - bottleneck=1.2.0 + - cython=0.28.2 + - lxml + - matplotlib=2.0.0 + - numpy=1.12.0 + - openpyxl=2.4.0 + - python-dateutil + - python-blosc + - python=2.7 + - pytz + - pytz=2013b + - scipy + - sqlalchemy=0.8.1 + - xlrd=1.0.0 + - xlsxwriter=0.5.2 + - xlwt=0.7.5 + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - hypothesis>=3.58.0 + - isort + - pip: + - html5lib==1.0b2 + - beautifulsoup4==4.2.1 diff --git a/ci/deps/azure-36-locale_slow.yaml b/ci/deps/azure-36-locale_slow.yaml new file mode 100644 index 0000000000000..3f788e5ddcf39 --- /dev/null +++ b/ci/deps/azure-36-locale_slow.yaml @@ -0,0 +1,35 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - cython>=0.28.2 + - gcsfs + - html5lib + - ipython + - jinja2 + - lxml + - matplotlib + - nomkl + - numexpr + - numpy + - openpyxl + - pytables + - python-dateutil + - python=3.6* + - pytz + - s3fs + - scipy + - xarray + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - moto + - isort + - pip: + - hypothesis>=3.58.0 diff --git a/ci/deps/azure-37-locale.yaml b/ci/deps/azure-37-locale.yaml new file mode 100644 index 0000000000000..9d598cddce91a --- /dev/null +++ b/ci/deps/azure-37-locale.yaml @@ -0,0 +1,34 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - cython>=0.28.2 + - html5lib + - ipython + - jinja2 + - lxml + - matplotlib + - nomkl + - numexpr + - numpy + - openpyxl + - pytables + - python-dateutil + - python=3.7* + - pytz + - s3fs + - scipy + - xarray + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - isort + - pip: + - hypothesis>=3.58.0 + - moto # latest moto in conda-forge fails with 3.7, move to conda dependencies when this is fixed diff --git a/ci/deps/azure-37-numpydev.yaml b/ci/deps/azure-37-numpydev.yaml new file mode 100644 index 0000000000000..e58c1f599279c --- /dev/null +++ b/ci/deps/azure-37-numpydev.yaml @@ -0,0 +1,19 @@ +name: pandas-dev +channels: + - defaults +dependencies: + - python=3.7* + - pytz + - Cython>=0.28.2 + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - hypothesis>=3.58.0 + - isort + - pip: + - "git+git://github.com/dateutil/dateutil.git" + - "-f https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" + - "--pre" + - "numpy" + - "scipy" diff --git a/ci/deps/azure-macos-35.yaml b/ci/deps/azure-macos-35.yaml new file mode 100644 index 0000000000000..2326e8092cc85 --- /dev/null +++ b/ci/deps/azure-macos-35.yaml @@ -0,0 +1,31 @@ +name: pandas-dev +channels: + - defaults +dependencies: + - beautifulsoup4 + - bottleneck + - cython>=0.28.2 + - html5lib + - jinja2 + - lxml + - matplotlib=2.2.0 + - nomkl + - numexpr + - numpy=1.12.0 + - openpyxl=2.5.5 + - pyarrow + - pytables + - python=3.5* + - pytz + - xarray + - xlrd + - xlsxwriter + - xlwt + - isort + - pip: + - python-dateutil==2.5.3 + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - hypothesis>=3.58.0 diff --git a/ci/deps/azure-windows-27.yaml b/ci/deps/azure-windows-27.yaml new file mode 100644 index 0000000000000..f40efdfca3cbd --- /dev/null +++ b/ci/deps/azure-windows-27.yaml @@ -0,0 +1,33 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - bottleneck + - dateutil + - gcsfs + - html5lib + - jinja2=2.8 + - lxml + - matplotlib=2.0.1 + - numexpr + - numpy=1.12* + - openpyxl + - pytables + - python=2.7.* + - pytz + - s3fs + - scipy + - sqlalchemy + - xlrd + - xlsxwriter + - xlwt + # universal + - cython>=0.28.2 + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - moto + - hypothesis>=3.58.0 + - isort diff --git a/ci/deps/azure-windows-36.yaml b/ci/deps/azure-windows-36.yaml new file mode 100644 index 0000000000000..8517d340f2ba8 --- /dev/null +++ b/ci/deps/azure-windows-36.yaml @@ -0,0 +1,30 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - blosc + - bottleneck + - boost-cpp<1.67 + - fastparquet>=0.2.1 + - matplotlib + - numexpr + - numpy=1.14* + - openpyxl + - parquet-cpp + - pyarrow + - pytables + - python-dateutil + - python=3.6.6 + - pytz + - scipy + - xlrd + - xlsxwriter + - xlwt + # universal + - cython>=0.28.2 + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - hypothesis>=3.58.0 + - isort diff --git a/ci/deps/travis-27.yaml b/ci/deps/travis-27.yaml new file mode 100644 index 0000000000000..a910af36a6b10 --- /dev/null +++ b/ci/deps/travis-27.yaml @@ -0,0 +1,51 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - bottleneck + - cython=0.28.2 + - fastparquet>=0.2.1 + - gcsfs + - html5lib + - ipython + - jemalloc=4.5.0.post + - jinja2=2.8 + - lxml + - matplotlib=2.2.2 + - mock + - nomkl + - numexpr + - numpy=1.13* + - openpyxl=2.4.0 + - patsy + - psycopg2 + - py + - pyarrow=0.9.0 + - PyCrypto + - pymysql=0.6.3 + - pytables + - blosc=1.14.3 + - python-blosc + - python-dateutil=2.5.0 + - python=2.7* + - pytz=2013b + - s3fs + - scipy + - sqlalchemy=0.9.6 + - xarray=0.9.6 + - xlrd=1.0.0 + - xlsxwriter=0.5.2 + - xlwt=0.7.5 + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - moto==1.3.4 + - hypothesis>=3.58.0 + - isort + - pip: + - backports.lzma + - pandas-gbq + - pathlib diff --git a/ci/deps/travis-36-doc.yaml b/ci/deps/travis-36-doc.yaml new file mode 100644 index 0000000000000..6f33bc58a8b21 --- /dev/null +++ b/ci/deps/travis-36-doc.yaml @@ -0,0 +1,46 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - bottleneck + - cython>=0.28.2 + - fastparquet>=0.2.1 + - gitpython + - html5lib + - hypothesis>=3.58.0 + - ipykernel + - ipython + - ipywidgets + - lxml + - matplotlib + - nbconvert + - nbformat + - nbsphinx + - notebook + - numexpr + - numpy=1.13* + - numpydoc + - openpyxl + - pandoc + - pyarrow + - pyqt + - pytables + - python-dateutil + - python-snappy + - python=3.6* + - pytz + - scipy + - seaborn + - sphinx + - sqlalchemy + - statsmodels + - xarray + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - isort diff --git a/ci/deps/travis-36-locale.yaml b/ci/deps/travis-36-locale.yaml new file mode 100644 index 0000000000000..34b289e6c0c2f --- /dev/null +++ b/ci/deps/travis-36-locale.yaml @@ -0,0 +1,37 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - cython>=0.28.2 + - html5lib + - ipython + - jinja2 + - lxml + - matplotlib + - nomkl + - numexpr + - numpy + - openpyxl + - psycopg2 + - pymysql + - pytables + - python-dateutil + - python=3.6* + - pytz + - s3fs + - scipy + - sqlalchemy + - xarray + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - moto + - isort + - pip: + - hypothesis>=3.58.0 diff --git a/ci/deps/travis-36-slow.yaml b/ci/deps/travis-36-slow.yaml new file mode 100644 index 0000000000000..46875d59411d9 --- /dev/null +++ b/ci/deps/travis-36-slow.yaml @@ -0,0 +1,33 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - cython>=0.28.2 + - html5lib + - lxml + - matplotlib + - numexpr + - numpy + - openpyxl + - patsy + - psycopg2 + - pymysql + - pytables + - python-dateutil + - python=3.6* + - pytz + - s3fs + - scipy + - sqlalchemy + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - moto + - hypothesis>=3.58.0 + - isort diff --git a/ci/deps/travis-36.yaml b/ci/deps/travis-36.yaml new file mode 100644 index 0000000000000..06fc0d76a3d16 --- /dev/null +++ b/ci/deps/travis-36.yaml @@ -0,0 +1,47 @@ +name: pandas-dev +channels: + - defaults + - conda-forge +dependencies: + - beautifulsoup4 + - botocore>=1.11 + - cython>=0.28.2 + - dask + - fastparquet>=0.2.1 + - gcsfs + - geopandas + - html5lib + - matplotlib + - nomkl + - numexpr + - numpy + - openpyxl + - psycopg2 + - pyarrow=0.9.0 + - pymysql + - pytables + - python-snappy + - python=3.6.6 + - pytz + - s3fs + - scikit-learn + - scipy + - sqlalchemy + - statsmodels + - xarray + - xlrd + - xlsxwriter + - xlwt + # universal + - pytest>=4.0.2 + - pytest-xdist + - pytest-cov + - pytest-mock + - hypothesis>=3.58.0 + - isort + - pip: + - brotlipy + - coverage + - moto + - pandas-datareader + - python-dateutil diff --git a/ci/deps/travis-37.yaml b/ci/deps/travis-37.yaml new file mode 100644 index 0000000000000..f71d29fe13378 --- /dev/null +++ b/ci/deps/travis-37.yaml @@ -0,0 +1,22 @@ +name: pandas-dev +channels: + - defaults + - conda-forge + - c3i_test +dependencies: + - python=3.7 + - botocore>=1.11 + - cython>=0.28.2 + - numpy + - python-dateutil + - nomkl + - pyarrow + - pytz + - pytest>=4.0.2 + - pytest-xdist + - pytest-mock + - hypothesis>=3.58.0 + - s3fs + - isort + - pip: + - moto diff --git a/ci/incremental/build.cmd b/ci/incremental/build.cmd new file mode 100644 index 0000000000000..2cce38c03f406 --- /dev/null +++ b/ci/incremental/build.cmd @@ -0,0 +1,9 @@ +@rem https://github.com/numba/numba/blob/master/buildscripts/incremental/build.cmd + +@rem Build numba extensions without silencing compile errors +python setup.py build_ext -q --inplace + +@rem Install pandas locally +python -m pip install -e . + +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/ci/incremental/build.sh b/ci/incremental/build.sh new file mode 100755 index 0000000000000..05648037935a3 --- /dev/null +++ b/ci/incremental/build.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Make sure any error below is reported as such +set -v -e + +echo "[building extensions]" +python setup.py build_ext -q --inplace +python -m pip install -e . + +echo +echo "[show environment]" +conda list + +echo +echo "[done]" +exit 0 diff --git a/ci/incremental/install_miniconda.sh b/ci/incremental/install_miniconda.sh new file mode 100755 index 0000000000000..a47dfdb324b34 --- /dev/null +++ b/ci/incremental/install_miniconda.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -v -e + +# Install Miniconda +unamestr=`uname` +if [[ "$unamestr" == 'Linux' ]]; then + if [[ "$BITS32" == "yes" ]]; then + wget -q https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86.sh -O miniconda.sh + else + wget -q https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh + fi +elif [[ "$unamestr" == 'Darwin' ]]; then + wget -q https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O miniconda.sh +else + echo Error +fi +chmod +x miniconda.sh +./miniconda.sh -b diff --git a/ci/incremental/setup_conda_environment.cmd b/ci/incremental/setup_conda_environment.cmd new file mode 100644 index 0000000000000..c104d78591384 --- /dev/null +++ b/ci/incremental/setup_conda_environment.cmd @@ -0,0 +1,21 @@ +@rem https://github.com/numba/numba/blob/master/buildscripts/incremental/setup_conda_environment.cmd +@rem The cmd /C hack circumvents a regression where conda installs a conda.bat +@rem script in non-root environments. +set CONDA_INSTALL=cmd /C conda install -q -y +set PIP_INSTALL=pip install -q + +@echo on + +@rem Deactivate any environment +call deactivate +@rem Display root environment (for debugging) +conda list +@rem Clean up any left-over from a previous build +conda remove --all -q -y -n pandas-dev +@rem Scipy, CFFI, jinja2 and IPython are optional dependencies, but exercised in the test suite +conda env create --file=ci\deps\azure-windows-%CONDA_PY%.yaml + +call activate pandas-dev +conda list + +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/ci/incremental/setup_conda_environment.sh b/ci/incremental/setup_conda_environment.sh new file mode 100755 index 0000000000000..f174c17a614d8 --- /dev/null +++ b/ci/incremental/setup_conda_environment.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +set -v -e + +CONDA_INSTALL="conda install -q -y" +PIP_INSTALL="pip install -q" + + +# Deactivate any environment +source deactivate +# Display root environment (for debugging) +conda list +# Clean up any left-over from a previous build +# (note workaround for https://github.com/conda/conda/issues/2679: +# `conda env remove` issue) +conda remove --all -q -y -n pandas-dev + +echo +echo "[create env]" +time conda env create -q --file="${ENV_FILE}" || exit 1 + +set +v +source activate pandas-dev +set -v + +# remove any installed pandas package +# w/o removing anything else +echo +echo "[removing installed pandas]" +conda remove pandas -y --force || true +pip uninstall -y pandas || true + +echo +echo "[no installed pandas]" +conda list pandas + +if [ -n "$LOCALE_OVERRIDE" ]; then + sudo locale-gen "$LOCALE_OVERRIDE" +fi + +# # Install the compiler toolchain +# if [[ $(uname) == Linux ]]; then +# if [[ "$CONDA_SUBDIR" == "linux-32" || "$BITS32" == "yes" ]] ; then +# $CONDA_INSTALL gcc_linux-32 gxx_linux-32 +# else +# $CONDA_INSTALL gcc_linux-64 gxx_linux-64 +# fi +# elif [[ $(uname) == Darwin ]]; then +# $CONDA_INSTALL clang_osx-64 clangxx_osx-64 +# # Install llvm-openmp and intel-openmp on OSX too +# $CONDA_INSTALL llvm-openmp intel-openmp +# fi diff --git a/ci/install.ps1 b/ci/install.ps1 deleted file mode 100644 index 64ec7f81884cd..0000000000000 --- a/ci/install.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -# Sample script to install Miniconda under Windows -# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner, Robert McGibbon -# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ - -$MINICONDA_URL = "http://repo.continuum.io/miniconda/" - - -function DownloadMiniconda ($python_version, $platform_suffix) { - $webclient = New-Object System.Net.WebClient - $filename = "Miniconda3-latest-Windows-" + $platform_suffix + ".exe" - $url = $MINICONDA_URL + $filename - - $basedir = $pwd.Path + "\" - $filepath = $basedir + $filename - if (Test-Path $filename) { - Write-Host "Reusing" $filepath - return $filepath - } - - # Download and retry up to 3 times in case of network transient errors. - Write-Host "Downloading" $filename "from" $url - $retry_attempts = 2 - for($i=0; $i -lt $retry_attempts; $i++){ - try { - $webclient.DownloadFile($url, $filepath) - break - } - Catch [Exception]{ - Start-Sleep 1 - } - } - if (Test-Path $filepath) { - Write-Host "File saved at" $filepath - } else { - # Retry once to get the error message if any at the last try - $webclient.DownloadFile($url, $filepath) - } - return $filepath -} - - -function InstallMiniconda ($python_version, $architecture, $python_home) { - Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home - if (Test-Path $python_home) { - Write-Host $python_home "already exists, skipping." - return $false - } - if ($architecture -match "32") { - $platform_suffix = "x86" - } else { - $platform_suffix = "x86_64" - } - - $filepath = DownloadMiniconda $python_version $platform_suffix - Write-Host "Installing" $filepath "to" $python_home - $install_log = $python_home + ".log" - $args = "/S /D=$python_home" - Write-Host $filepath $args - Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru - if (Test-Path $python_home) { - Write-Host "Python $python_version ($architecture) installation complete" - } else { - Write-Host "Failed to install Python in $python_home" - Get-Content -Path $install_log - Exit 1 - } -} - - -function InstallCondaPackages ($python_home, $spec) { - $conda_path = $python_home + "\Scripts\conda.exe" - $args = "install --yes " + $spec - Write-Host ("conda " + $args) - Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru -} - -function UpdateConda ($python_home) { - $conda_path = $python_home + "\Scripts\conda.exe" - Write-Host "Updating conda..." - $args = "update --yes conda" - Write-Host $conda_path $args - Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru -} - - -function main () { - InstallMiniconda "3.5" $env:PYTHON_ARCH $env:CONDA_ROOT - UpdateConda $env:CONDA_ROOT - InstallCondaPackages $env:CONDA_ROOT "conda-build jinja2 anaconda-client" -} - -main diff --git a/ci/install_circle.sh b/ci/install_circle.sh deleted file mode 100755 index 00e14b10ebbd6..0000000000000 --- a/ci/install_circle.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash - -home_dir=$(pwd) -echo "[home_dir: $home_dir]" - -echo "[ls -ltr]" -ls -ltr - -echo "[Using clean Miniconda install]" -rm -rf "$MINICONDA_DIR" - -# install miniconda -wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -q -O miniconda.sh || exit 1 -bash miniconda.sh -b -p "$MINICONDA_DIR" || exit 1 - -export PATH="$MINICONDA_DIR/bin:$PATH" - -echo "[update conda]" -conda config --set ssl_verify false || exit 1 -conda config --set always_yes true --set changeps1 false || exit 1 -conda update -q conda - -# add the pandas channel to take priority -# to add extra packages -echo "[add channels]" -conda config --add channels pandas || exit 1 -conda config --remove channels defaults || exit 1 -conda config --add channels defaults || exit 1 - -# Useful for debugging any issues with conda -conda info -a || exit 1 - -# support env variables passed -export ENVS_FILE=".envs" - -# make sure that the .envs file exists. it is ok if it is empty -touch $ENVS_FILE - -# assume all command line arguments are environmental variables -for var in "$@" -do - echo "export $var" >> $ENVS_FILE -done - -echo "[environmental variable file]" -cat $ENVS_FILE -source $ENVS_FILE - -export REQ_BUILD=ci/requirements-${JOB}.build -export REQ_RUN=ci/requirements-${JOB}.run -export REQ_PIP=ci/requirements-${JOB}.pip - -# edit the locale override if needed -if [ -n "$LOCALE_OVERRIDE" ]; then - echo "[Adding locale to the first line of pandas/__init__.py]" - rm -f pandas/__init__.pyc - sedc="3iimport locale\nlocale.setlocale(locale.LC_ALL, '$LOCALE_OVERRIDE')\n" - sed -i "$sedc" pandas/__init__.py - echo "[head -4 pandas/__init__.py]" - head -4 pandas/__init__.py - echo -fi - -# create envbuild deps -echo "[create env: ${REQ_BUILD}]" -time conda create -n pandas -q --file=${REQ_BUILD} || exit 1 -time conda install -n pandas pytest || exit 1 - -source activate pandas - -# build but don't install -echo "[build em]" -time python setup.py build_ext --inplace || exit 1 - -# we may have run installations -echo "[conda installs: ${REQ_RUN}]" -if [ -e ${REQ_RUN} ]; then - time conda install -q --file=${REQ_RUN} || exit 1 -fi - -# we may have additional pip installs -echo "[pip installs: ${REQ_PIP}]" -if [ -e ${REQ_PIP} ]; then - pip install -r $REQ_PIP -fi diff --git a/ci/install_db_circle.sh b/ci/install_db_circle.sh deleted file mode 100755 index a00f74f009f54..0000000000000 --- a/ci/install_db_circle.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "installing dbs" -mysql -e 'create database pandas_nosetest;' -psql -c 'create database pandas_nosetest;' -U postgres - -echo "done" -exit 0 diff --git a/ci/install_travis.sh b/ci/install_travis.sh index f71df979c9df0..d1a940f119228 100755 --- a/ci/install_travis.sh +++ b/ci/install_travis.sh @@ -34,9 +34,9 @@ fi # install miniconda if [ "${TRAVIS_OS_NAME}" == "osx" ]; then - time wget http://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O miniconda.sh || exit 1 + time wget http://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -q -O miniconda.sh || exit 1 else - time wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh || exit 1 + time wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -q -O miniconda.sh || exit 1 fi time bash miniconda.sh -b -p "$MINICONDA_DIR" || exit 1 @@ -47,22 +47,9 @@ which conda echo echo "[update conda]" conda config --set ssl_verify false || exit 1 -conda config --set always_yes true --set changeps1 false || exit 1 +conda config --set quiet true --set always_yes true --set changeps1 false || exit 1 conda update -q conda -echo -echo "[add channels]" -# add the pandas channel to take priority -# to add extra packages -conda config --add channels pandas || exit 1 -conda config --remove channels defaults || exit 1 -conda config --add channels defaults || exit 1 - -if [ "$CONDA_FORGE" ]; then - # add conda-forge channel as priority - conda config --add channels conda-forge || exit 1 -fi - # Useful for debugging any issues with conda conda info -a || exit 1 @@ -93,89 +80,29 @@ echo echo "[create env]" # create our environment -REQ="ci/requirements-${JOB}.build" -time conda create -n pandas --file=${REQ} || exit 1 +time conda env create -q --file="${ENV_FILE}" || exit 1 -source activate pandas +source activate pandas-dev -# may have addtl installation instructions for this build +# remove any installed pandas package +# w/o removing anything else echo -echo "[build addtl installs]" -REQ="ci/requirements-${JOB}.build.sh" -if [ -e ${REQ} ]; then - time bash $REQ || exit 1 -fi - -time conda install -n pandas pytest -time pip install pytest-xdist - -if [ "$LINT" ]; then - conda install flake8 - pip install cpplint -fi - -if [ "$COVERAGE" ]; then - pip install coverage pytest-cov -fi - -echo -if [ "$BUILD_TEST" ]; then - - # build & install testing - echo ["Starting installation test."] - python setup.py clean - python setup.py build_ext --inplace - python setup.py sdist --formats=gztar - conda uninstall cython - pip install dist/*tar.gz || exit 1 - -else - - # build but don't install - echo "[build em]" - time python setup.py build_ext --inplace || exit 1 - -fi +echo "[removing installed pandas]" +conda remove pandas -y --force +pip uninstall -y pandas -# we may have run installations echo -echo "[conda installs]" -REQ="ci/requirements-${JOB}.run" -if [ -e ${REQ} ]; then - time conda install -n pandas --file=${REQ} || exit 1 -fi +echo "[no installed pandas]" +conda list pandas +pip list --format columns |grep pandas -# we may have additional pip installs -echo -echo "[pip installs]" -REQ="ci/requirements-${JOB}.pip" -if [ -e ${REQ} ]; then - pip install -r $REQ -fi +# build and install +echo "[running setup.py develop]" +python setup.py develop || exit 1 -# may have addtl installation instructions for this build echo -echo "[addtl installs]" -REQ="ci/requirements-${JOB}.sh" -if [ -e ${REQ} ]; then - time bash $REQ || exit 1 -fi - -# finish install if we are not doing a build-testk -if [ -z "$BUILD_TEST" ]; then - - # remove any installed pandas package - # w/o removing anything else - echo - echo "[removing installed pandas]" - conda remove pandas --force - - # install our pandas - echo - echo "[running setup.py develop]" - python setup.py develop || exit 1 - -fi +echo "[show environment]" +conda list echo echo "[done]" diff --git a/ci/lint.sh b/ci/lint.sh deleted file mode 100755 index ed3af2568811c..0000000000000 --- a/ci/lint.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -echo "inside $0" - -source activate pandas - -RET=0 - -if [ "$LINT" ]; then - - # pandas/_libs/src is C code, so no need to search there. - echo "Linting *.py" - flake8 pandas --filename=*.py --exclude pandas/_libs/src - if [ $? -ne "0" ]; then - RET=1 - fi - echo "Linting *.py DONE" - - echo "Linting *.pyx" - flake8 pandas --filename=*.pyx --select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126 - if [ $? -ne "0" ]; then - RET=1 - fi - echo "Linting *.pyx DONE" - - echo "Linting *.pxi.in" - for path in 'src' - do - echo "linting -> pandas/$path" - flake8 pandas/$path --filename=*.pxi.in --select=E501,E302,E203,E111,E114,E221,E303,E231,E126 - if [ $? -ne "0" ]; then - RET=1 - fi - - done - echo "Linting *.pxi.in DONE" - - # readability/casting: Warnings about C casting instead of C++ casting - # runtime/int: Warnings about using C number types instead of C++ ones - # build/include_subdir: Warnings about prefacing included header files with directory - - # We don't lint all C files because we don't want to lint any that are built - # from Cython files nor do we want to lint C files that we didn't modify for - # this particular codebase (e.g. src/headers, src/klib, src/msgpack). However, - # we can lint all header files since they aren't "generated" like C files are. - echo "Linting *.c and *.h" - for path in '*.h' 'period_helper.c' 'datetime' 'parser' 'ujson' - do - echo "linting -> pandas/_libs/src/$path" - cpplint --quiet --extensions=c,h --headers=h --filter=-readability/casting,-runtime/int,-build/include_subdir --recursive pandas/_libs/src/$path - if [ $? -ne "0" ]; then - RET=1 - fi - done - echo "Linting *.c and *.h DONE" - - echo "Check for invalid testing" - grep -r -E --include '*.py' --exclude testing.py '(numpy|np)\.testing' pandas - if [ $? = "0" ]; then - RET=1 - fi - echo "Check for invalid testing DONE" - -else - echo "NOT Linting" -fi - -exit $RET diff --git a/ci/print_skipped.py b/ci/print_skipped.py index dd2180f6eeb19..67bc7b556cd43 100755 --- a/ci/print_skipped.py +++ b/ci/print_skipped.py @@ -10,7 +10,7 @@ def parse_results(filename): root = tree.getroot() skipped = [] - current_class = old_class = '' + current_class = '' i = 1 assert i - 1 == len(skipped) for el in root.findall('testcase'): @@ -24,7 +24,9 @@ def parse_results(filename): out = '' if old_class != current_class: ndigits = int(math.log(i, 10) + 1) - out += ('-' * (len(name + msg) + 4 + ndigits) + '\n') # 4 for : + space + # + space + + # 4 for : + space + # + space + out += ('-' * (len(name + msg) + 4 + ndigits) + '\n') out += '#{i} {name}: {msg}'.format(i=i, name=name, msg=msg) skipped.append(out) i += 1 diff --git a/ci/print_versions.py b/ci/print_versions.py deleted file mode 100755 index 8be795174d76d..0000000000000 --- a/ci/print_versions.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - - -def show_versions(as_json=False): - import imp - import os - fn = __file__ - this_dir = os.path.dirname(fn) - pandas_dir = os.path.abspath(os.path.join(this_dir, "..")) - sv_path = os.path.join(pandas_dir, 'pandas', 'util') - mod = imp.load_module( - 'pvmod', *imp.find_module('print_versions', [sv_path])) - return mod.show_versions(as_json) - - -if __name__ == '__main__': - # optparse is 2.6-safe - from optparse import OptionParser - parser = OptionParser() - parser.add_option("-j", "--json", metavar="FILE", nargs=1, - help="Save output as JSON into file, pass in '-' to output to stdout") - - (options, args) = parser.parse_args() - - if options.json == "-": - options.json = True - - show_versions(as_json=options.json) diff --git a/ci/requirements-2.7.build b/ci/requirements-2.7.build deleted file mode 100644 index 415df13179fcf..0000000000000 --- a/ci/requirements-2.7.build +++ /dev/null @@ -1,6 +0,0 @@ -python=2.7* -python-dateutil=2.4.1 -pytz=2013b -nomkl -numpy -cython=0.23 diff --git a/ci/requirements-2.7.pip b/ci/requirements-2.7.pip deleted file mode 100644 index eb796368e7820..0000000000000 --- a/ci/requirements-2.7.pip +++ /dev/null @@ -1,8 +0,0 @@ -blosc -pandas-gbq -pathlib -backports.lzma -py -PyCrypto -mock -ipython diff --git a/ci/requirements-2.7.run b/ci/requirements-2.7.run deleted file mode 100644 index 62e31e4ae24e3..0000000000000 --- a/ci/requirements-2.7.run +++ /dev/null @@ -1,22 +0,0 @@ -python-dateutil=2.4.1 -pytz=2013b -numpy -xlwt=0.7.5 -numexpr -pytables -matplotlib -openpyxl=1.6.2 -xlrd=0.9.2 -sqlalchemy=0.9.6 -lxml=3.2.1 -scipy -xlsxwriter=0.4.6 -s3fs -bottleneck -psycopg2=2.5.2 -patsy -pymysql=0.6.3 -html5lib=1.0b2 -beautiful-soup=4.2.1 -jinja2=2.8 -xarray=0.8.0 diff --git a/ci/requirements-2.7.sh b/ci/requirements-2.7.sh deleted file mode 100644 index 64d470e5c6e0e..0000000000000 --- a/ci/requirements-2.7.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "install 27" - -conda install -n pandas -c conda-forge feather-format diff --git a/ci/requirements-2.7_BUILD_TEST.build b/ci/requirements-2.7_BUILD_TEST.build deleted file mode 100644 index aadec00cb7ebf..0000000000000 --- a/ci/requirements-2.7_BUILD_TEST.build +++ /dev/null @@ -1,6 +0,0 @@ -python=2.7* -dateutil -pytz -nomkl -numpy -cython diff --git a/ci/requirements-2.7_COMPAT.build b/ci/requirements-2.7_COMPAT.build deleted file mode 100644 index 0e1ccf9eac9bf..0000000000000 --- a/ci/requirements-2.7_COMPAT.build +++ /dev/null @@ -1,5 +0,0 @@ -python=2.7* -numpy=1.7.1 -cython=0.23 -dateutil=1.5 -pytz=2013b diff --git a/ci/requirements-2.7_COMPAT.pip b/ci/requirements-2.7_COMPAT.pip deleted file mode 100644 index 9533a630d06a4..0000000000000 --- a/ci/requirements-2.7_COMPAT.pip +++ /dev/null @@ -1,2 +0,0 @@ -openpyxl -argparse diff --git a/ci/requirements-2.7_COMPAT.run b/ci/requirements-2.7_COMPAT.run deleted file mode 100644 index d27b6a72c2d15..0000000000000 --- a/ci/requirements-2.7_COMPAT.run +++ /dev/null @@ -1,16 +0,0 @@ -numpy=1.7.1 -dateutil=1.5 -pytz=2013b -scipy=0.11.0 -xlwt=0.7.5 -xlrd=0.9.2 -bottleneck=0.8.0 -numexpr=2.2.2 -pytables=3.0.0 -html5lib=1.0b2 -beautiful-soup=4.2.0 -psycopg2=2.5.1 -pymysql=0.6.0 -sqlalchemy=0.7.8 -xlsxwriter=0.4.6 -jinja2=2.8 diff --git a/ci/requirements-2.7_LOCALE.build b/ci/requirements-2.7_LOCALE.build deleted file mode 100644 index 4a37ce8fbe161..0000000000000 --- a/ci/requirements-2.7_LOCALE.build +++ /dev/null @@ -1,5 +0,0 @@ -python=2.7* -python-dateutil -pytz=2013b -numpy=1.8.2 -cython=0.23 diff --git a/ci/requirements-2.7_LOCALE.pip b/ci/requirements-2.7_LOCALE.pip deleted file mode 100644 index cf8e6b8b3d3a6..0000000000000 --- a/ci/requirements-2.7_LOCALE.pip +++ /dev/null @@ -1 +0,0 @@ -blosc diff --git a/ci/requirements-2.7_LOCALE.run b/ci/requirements-2.7_LOCALE.run deleted file mode 100644 index 5d7cc31b7d55e..0000000000000 --- a/ci/requirements-2.7_LOCALE.run +++ /dev/null @@ -1,14 +0,0 @@ -python-dateutil -pytz=2013b -numpy=1.8.2 -xlwt=0.7.5 -openpyxl=1.6.2 -xlsxwriter=0.4.6 -xlrd=0.9.2 -bottleneck=0.8.0 -matplotlib=1.3.1 -sqlalchemy=0.8.1 -html5lib=1.0b2 -lxml=3.2.1 -scipy -beautiful-soup=4.2.1 diff --git a/ci/requirements-2.7_SLOW.build b/ci/requirements-2.7_SLOW.build deleted file mode 100644 index 0f4a2c6792e6b..0000000000000 --- a/ci/requirements-2.7_SLOW.build +++ /dev/null @@ -1,5 +0,0 @@ -python=2.7* -python-dateutil -pytz -numpy=1.8.2 -cython diff --git a/ci/requirements-2.7_SLOW.run b/ci/requirements-2.7_SLOW.run deleted file mode 100644 index c2d2a14285ad6..0000000000000 --- a/ci/requirements-2.7_SLOW.run +++ /dev/null @@ -1,20 +0,0 @@ -python-dateutil -pytz -numpy=1.8.2 -matplotlib=1.3.1 -scipy -patsy -xlwt -openpyxl -xlsxwriter -xlrd -numexpr -pytables -sqlalchemy -lxml -s3fs -bottleneck -psycopg2 -pymysql -html5lib -beautiful-soup diff --git a/ci/requirements-2.7_WIN.run b/ci/requirements-2.7_WIN.run deleted file mode 100644 index f953682f52d45..0000000000000 --- a/ci/requirements-2.7_WIN.run +++ /dev/null @@ -1,18 +0,0 @@ -dateutil -pytz -numpy=1.10* -xlwt -numexpr -pytables==3.2.2 -matplotlib -openpyxl -xlrd -sqlalchemy -lxml=3.2.1 -scipy -xlsxwriter -s3fs -bottleneck -html5lib -beautiful-soup -jinja2=2.8 diff --git a/ci/requirements-3.4.build b/ci/requirements-3.4.build deleted file mode 100644 index e8a957f70d40e..0000000000000 --- a/ci/requirements-3.4.build +++ /dev/null @@ -1,4 +0,0 @@ -python=3.4* -numpy=1.8.1 -cython=0.24.1 -libgfortran=1.0 diff --git a/ci/requirements-3.4.pip b/ci/requirements-3.4.pip deleted file mode 100644 index 4e5fe52d56cf1..0000000000000 --- a/ci/requirements-3.4.pip +++ /dev/null @@ -1,2 +0,0 @@ -python-dateutil==2.2 -blosc diff --git a/ci/requirements-3.4.run b/ci/requirements-3.4.run deleted file mode 100644 index 3e12adae7dd9f..0000000000000 --- a/ci/requirements-3.4.run +++ /dev/null @@ -1,18 +0,0 @@ -pytz=2015.7 -numpy=1.8.1 -openpyxl -xlsxwriter -xlrd -xlwt -html5lib -patsy -beautiful-soup -scipy -numexpr -pytables -lxml -sqlalchemy -bottleneck -pymysql=0.6.3 -psycopg2 -jinja2=2.8 diff --git a/ci/requirements-3.4_SLOW.build b/ci/requirements-3.4_SLOW.build deleted file mode 100644 index 88212053af472..0000000000000 --- a/ci/requirements-3.4_SLOW.build +++ /dev/null @@ -1,6 +0,0 @@ -python=3.4* -python-dateutil -pytz -nomkl -numpy=1.10* -cython diff --git a/ci/requirements-3.4_SLOW.run b/ci/requirements-3.4_SLOW.run deleted file mode 100644 index 90156f62c6e71..0000000000000 --- a/ci/requirements-3.4_SLOW.run +++ /dev/null @@ -1,20 +0,0 @@ -python-dateutil -pytz -numpy=1.10* -openpyxl -xlsxwriter -xlrd -xlwt -html5lib -patsy -beautiful-soup -scipy -numexpr=2.4.6 -pytables -matplotlib -lxml -sqlalchemy -bottleneck -pymysql -psycopg2 -jinja2=2.8 diff --git a/ci/requirements-3.4_SLOW.sh b/ci/requirements-3.4_SLOW.sh deleted file mode 100644 index 24f1e042ed69e..0000000000000 --- a/ci/requirements-3.4_SLOW.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "install 34_slow" - -conda install -n pandas -c conda-forge matplotlib diff --git a/ci/requirements-3.5.build b/ci/requirements-3.5.build deleted file mode 100644 index 76227e106e1fd..0000000000000 --- a/ci/requirements-3.5.build +++ /dev/null @@ -1,6 +0,0 @@ -python=3.5* -python-dateutil -pytz -nomkl -numpy=1.11.3 -cython diff --git a/ci/requirements-3.5.pip b/ci/requirements-3.5.pip deleted file mode 100644 index 6e4f7b65f9728..0000000000000 --- a/ci/requirements-3.5.pip +++ /dev/null @@ -1,2 +0,0 @@ -xarray==0.9.1 -pandas-gbq diff --git a/ci/requirements-3.5.run b/ci/requirements-3.5.run deleted file mode 100644 index 43e6814ed6c8e..0000000000000 --- a/ci/requirements-3.5.run +++ /dev/null @@ -1,21 +0,0 @@ -python-dateutil -pytz -numpy=1.11.3 -openpyxl -xlsxwriter -xlrd -xlwt -scipy -numexpr -pytables -html5lib -lxml -matplotlib -jinja2 -bottleneck -sqlalchemy -pymysql -psycopg2 -s3fs -beautifulsoup4 -ipython diff --git a/ci/requirements-3.5.sh b/ci/requirements-3.5.sh deleted file mode 100644 index d0f0b81802dc6..0000000000000 --- a/ci/requirements-3.5.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "install 35" - -conda install -n pandas -c conda-forge feather-format diff --git a/ci/requirements-3.5_ASCII.build b/ci/requirements-3.5_ASCII.build deleted file mode 100644 index f7befe3b31865..0000000000000 --- a/ci/requirements-3.5_ASCII.build +++ /dev/null @@ -1,6 +0,0 @@ -python=3.5* -python-dateutil -pytz -nomkl -numpy -cython diff --git a/ci/requirements-3.5_ASCII.run b/ci/requirements-3.5_ASCII.run deleted file mode 100644 index b9d543f557d06..0000000000000 --- a/ci/requirements-3.5_ASCII.run +++ /dev/null @@ -1,3 +0,0 @@ -python-dateutil -pytz -numpy diff --git a/ci/requirements-3.5_DOC.build b/ci/requirements-3.5_DOC.build deleted file mode 100644 index 73aeb3192242f..0000000000000 --- a/ci/requirements-3.5_DOC.build +++ /dev/null @@ -1,5 +0,0 @@ -python=3.5* -python-dateutil -pytz -numpy -cython diff --git a/ci/requirements-3.5_DOC.run b/ci/requirements-3.5_DOC.run deleted file mode 100644 index 644a16f51f4b6..0000000000000 --- a/ci/requirements-3.5_DOC.run +++ /dev/null @@ -1,21 +0,0 @@ -ipython -ipykernel -sphinx -nbconvert -nbformat -notebook -matplotlib -scipy -lxml -beautifulsoup4 -html5lib -pytables -openpyxl=1.8.5 -xlrd -xlwt -xlsxwriter -sqlalchemy -numexpr -bottleneck -statsmodels -pyqt=4.11.4 diff --git a/ci/requirements-3.5_DOC.sh b/ci/requirements-3.5_DOC.sh deleted file mode 100644 index 1a5d4643edcf2..0000000000000 --- a/ci/requirements-3.5_DOC.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "[install DOC_BUILD deps]" - -pip install pandas-gbq - -conda install -n pandas -c conda-forge feather-format - -conda install -n pandas -c r r rpy2 --yes diff --git a/ci/requirements-3.5_OSX.build b/ci/requirements-3.5_OSX.build deleted file mode 100644 index f5bc01b67a20a..0000000000000 --- a/ci/requirements-3.5_OSX.build +++ /dev/null @@ -1,4 +0,0 @@ -python=3.5* -nomkl -numpy=1.10.4 -cython diff --git a/ci/requirements-3.5_OSX.pip b/ci/requirements-3.5_OSX.pip deleted file mode 100644 index d1fc1fe24a079..0000000000000 --- a/ci/requirements-3.5_OSX.pip +++ /dev/null @@ -1 +0,0 @@ -python-dateutil==2.5.3 diff --git a/ci/requirements-3.5_OSX.run b/ci/requirements-3.5_OSX.run deleted file mode 100644 index 1d83474d10f2f..0000000000000 --- a/ci/requirements-3.5_OSX.run +++ /dev/null @@ -1,16 +0,0 @@ -pytz -numpy=1.10.4 -openpyxl -xlsxwriter -xlrd -xlwt -numexpr -pytables -html5lib -lxml -matplotlib -jinja2 -bottleneck -xarray -s3fs -beautifulsoup4 diff --git a/ci/requirements-3.5_OSX.sh b/ci/requirements-3.5_OSX.sh deleted file mode 100644 index cfbd2882a8a2d..0000000000000 --- a/ci/requirements-3.5_OSX.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "install 35_OSX" - -conda install -n pandas -c conda-forge feather-format diff --git a/ci/requirements-3.6.build b/ci/requirements-3.6.build deleted file mode 100644 index 1c4b46aea3865..0000000000000 --- a/ci/requirements-3.6.build +++ /dev/null @@ -1,6 +0,0 @@ -python=3.6* -python-dateutil -pytz -nomkl -numpy -cython diff --git a/ci/requirements-3.6.run b/ci/requirements-3.6.run deleted file mode 100644 index 41c9680ce1b7e..0000000000000 --- a/ci/requirements-3.6.run +++ /dev/null @@ -1,22 +0,0 @@ -python-dateutil -pytz -numpy -scipy -openpyxl -xlsxwriter -xlrd -xlwt -numexpr -pytables -matplotlib -lxml -html5lib -jinja2 -sqlalchemy -pymysql -feather-format -# psycopg2 (not avail on defaults ATM) -beautifulsoup4 -s3fs -xarray -ipython diff --git a/ci/requirements-3.6_NUMPY_DEV.build b/ci/requirements-3.6_NUMPY_DEV.build deleted file mode 100644 index 738366867a217..0000000000000 --- a/ci/requirements-3.6_NUMPY_DEV.build +++ /dev/null @@ -1,4 +0,0 @@ -python=3.6* -python-dateutil -pytz -cython diff --git a/ci/requirements-3.6_NUMPY_DEV.build.sh b/ci/requirements-3.6_NUMPY_DEV.build.sh deleted file mode 100644 index 4af1307f26a18..0000000000000 --- a/ci/requirements-3.6_NUMPY_DEV.build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -source activate pandas - -echo "install numpy master wheel" - -# remove the system installed numpy -pip uninstall numpy -y - -# install numpy wheel from master -PRE_WHEELS="https://7933911d6844c6c53a7d-47bd50c35cd79bd838daf386af554a83.ssl.cf2.rackcdn.com" -pip install --pre --upgrade --timeout=60 -f $PRE_WHEELS numpy scipy - -true diff --git a/ci/requirements-3.6_NUMPY_DEV.run b/ci/requirements-3.6_NUMPY_DEV.run deleted file mode 100644 index 0aa987baefb1d..0000000000000 --- a/ci/requirements-3.6_NUMPY_DEV.run +++ /dev/null @@ -1,2 +0,0 @@ -python-dateutil -pytz diff --git a/ci/requirements-3.6_WIN.run b/ci/requirements-3.6_WIN.run deleted file mode 100644 index 840d2867e9297..0000000000000 --- a/ci/requirements-3.6_WIN.run +++ /dev/null @@ -1,13 +0,0 @@ -python-dateutil -pytz -numpy=1.12* -openpyxl -xlsxwriter -xlrd -xlwt -scipy -feather-format -numexpr -pytables -matplotlib -blosc diff --git a/ci/requirements_all.txt b/ci/requirements_all.txt deleted file mode 100644 index 4ff80a478f247..0000000000000 --- a/ci/requirements_all.txt +++ /dev/null @@ -1,26 +0,0 @@ -pytest -pytest-cov -pytest-xdist -flake8 -sphinx -ipython -python-dateutil -pytz -openpyxl -xlsxwriter -xlrd -xlwt -html5lib -patsy -beautiful-soup -numpy -cython -scipy -numexpr -pytables -matplotlib -lxml -sqlalchemy -bottleneck -pymysql -Jinja2 diff --git a/ci/requirements_dev.txt b/ci/requirements_dev.txt deleted file mode 100644 index 1e051802ec9f8..0000000000000 --- a/ci/requirements_dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -python-dateutil -pytz -numpy -cython -pytest -pytest-cov -flake8 diff --git a/ci/run_build_docs.sh b/ci/run_build_docs.sh deleted file mode 100755 index 2909b9619552e..0000000000000 --- a/ci/run_build_docs.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -echo "inside $0" - -"$TRAVIS_BUILD_DIR"/ci/build_docs.sh 2>&1 - -# wait until subprocesses finish (build_docs.sh) -wait - -exit 0 diff --git a/ci/run_circle.sh b/ci/run_circle.sh deleted file mode 100755 index 0e46d28ab6fc4..0000000000000 --- a/ci/run_circle.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -echo "[running tests]" -export PATH="$MINICONDA_DIR/bin:$PATH" - -source activate pandas - -echo "pytest --junitxml=$CIRCLE_TEST_REPORTS/reports/junit.xml $@ pandas" -pytest --junitxml=$CIRCLE_TEST_REPORTS/reports/junit.xml $@ pandas diff --git a/ci/run_tests.sh b/ci/run_tests.sh new file mode 100755 index 0000000000000..ee46da9f52eab --- /dev/null +++ b/ci/run_tests.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -e + +if [ "$DOC" ]; then + echo "We are not running pytest as this is a doc-build" + exit 0 +fi + +# Workaround for pytest-xdist flaky collection order +# https://github.com/pytest-dev/pytest/issues/920 +# https://github.com/pytest-dev/pytest/issues/1075 +export PYTHONHASHSEED=$(python -c 'import random; print(random.randint(1, 4294967295))') + +if [ -n "$LOCALE_OVERRIDE" ]; then + export LC_ALL="$LOCALE_OVERRIDE" + export LANG="$LOCALE_OVERRIDE" + PANDAS_LOCALE=`python -c 'import pandas; pandas.get_option("display.encoding")'` + if [[ "$LOCALE_OVERIDE" != "$PANDAS_LOCALE" ]]; then + echo "pandas could not detect the locale. System locale: $LOCALE_OVERRIDE, pandas detected: $PANDAS_LOCALE" + # TODO Not really aborting the tests until https://github.com/pandas-dev/pandas/issues/23923 is fixed + # exit 1 + fi +fi +if [[ "not network" == *"$PATTERN"* ]]; then + export http_proxy=http://1.2.3.4 https_proxy=http://1.2.3.4; +fi + + +if [ -n "$PATTERN" ]; then + PATTERN=" and $PATTERN" +fi + +for TYPE in single multiple +do + if [ "$COVERAGE" ]; then + COVERAGE_FNAME="/tmp/coc-$TYPE.xml" + COVERAGE="-s --cov=pandas --cov-report=xml:$COVERAGE_FNAME" + fi + + TYPE_PATTERN=$TYPE + NUM_JOBS=1 + if [[ "$TYPE_PATTERN" == "multiple" ]]; then + TYPE_PATTERN="not single" + NUM_JOBS=2 + fi + + PYTEST_CMD="pytest -m \"$TYPE_PATTERN$PATTERN\" -n $NUM_JOBS -s --strict --durations=10 --junitxml=test-data-$TYPE.xml $TEST_ARGS $COVERAGE pandas" + echo $PYTEST_CMD + # if no tests are found (the case of "single and slow"), pytest exits with code 5, and would make the script fail, if not for the below code + sh -c "$PYTEST_CMD; ret=\$?; [ \$ret = 5 ] && exit 0 || exit \$ret" + + if [[ "$COVERAGE" && $? == 0 ]]; then + echo "uploading coverage for $TYPE tests" + echo "bash <(curl -s https://codecov.io/bash) -Z -c -F $TYPE -f $COVERAGE_FNAME" + bash <(curl -s https://codecov.io/bash) -Z -c -F $TYPE -f $COVERAGE_FNAME + fi +done diff --git a/ci/script_multi.sh b/ci/script_multi.sh deleted file mode 100755 index 88ecaf344a410..0000000000000 --- a/ci/script_multi.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -echo "[script multi]" - -source activate pandas - -if [ -n "$LOCALE_OVERRIDE" ]; then - export LC_ALL="$LOCALE_OVERRIDE"; - echo "Setting LC_ALL to $LOCALE_OVERRIDE" - - pycmd='import pandas; print("pandas detected console encoding: %s" % pandas.get_option("display.encoding"))' - python -c "$pycmd" -fi - -# Workaround for pytest-xdist flaky collection order -# https://github.com/pytest-dev/pytest/issues/920 -# https://github.com/pytest-dev/pytest/issues/1075 -export PYTHONHASHSEED=$(python -c 'import random; print(random.randint(1, 4294967295))') -echo PYTHONHASHSEED=$PYTHONHASHSEED - -if [ "$BUILD_TEST" ]; then - cd /tmp - python -c "import pandas; pandas.test(['-n 2'])" -elif [ "$DOC" ]; then - echo "We are not running pytest as this is a doc-build" -elif [ "$COVERAGE" ]; then - echo pytest -s -n 2 -m "not single" --cov=pandas --cov-report xml:/tmp/cov-multiple.xml --junitxml=/tmp/multiple.xml $TEST_ARGS pandas - pytest -s -n 2 -m "not single" --cov=pandas --cov-report xml:/tmp/cov-multiple.xml --junitxml=/tmp/multiple.xml $TEST_ARGS pandas -else - echo pytest -n 2 -m "not single" --junitxml=/tmp/multiple.xml $TEST_ARGS pandas - pytest -n 2 -m "not single" --junitxml=/tmp/multiple.xml $TEST_ARGS pandas # TODO: doctest -fi - -RET="$?" - -exit "$RET" diff --git a/ci/script_single.sh b/ci/script_single.sh deleted file mode 100755 index db637679f0e0f..0000000000000 --- a/ci/script_single.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -echo "[script_single]" - -source activate pandas - -if [ -n "$LOCALE_OVERRIDE" ]; then - export LC_ALL="$LOCALE_OVERRIDE"; - echo "Setting LC_ALL to $LOCALE_OVERRIDE" - - pycmd='import pandas; print("pandas detected console encoding: %s" % pandas.get_option("display.encoding"))' - python -c "$pycmd" -fi - -if [ "$BUILD_TEST" ]; then - echo "We are not running pytest as this is a build test." -elif [ "$DOC" ]; then - echo "We are not running pytest as this is a doc-build" -elif [ "$COVERAGE" ]; then - echo pytest -s -m "single" --cov=pandas --cov-report xml:/tmp/cov-single.xml --junitxml=/tmp/single.xml $TEST_ARGS pandas - pytest -s -m "single" --cov=pandas --cov-report xml:/tmp/cov-single.xml --junitxml=/tmp/single.xml $TEST_ARGS pandas -else - echo pytest -m "single" --junitxml=/tmp/single.xml $TEST_ARGS pandas - pytest -m "single" --junitxml=/tmp/single.xml $TEST_ARGS pandas # TODO: doctest -fi - -RET="$?" - -exit "$RET" diff --git a/ci/show_circle.sh b/ci/show_circle.sh deleted file mode 100755 index bfaa65c1d84f2..0000000000000 --- a/ci/show_circle.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -echo "[installed versions]" - -export PATH="$MINICONDA_DIR/bin:$PATH" -source activate pandas - -python -c "import pandas; pandas.show_versions();" diff --git a/ci/upload_coverage.sh b/ci/upload_coverage.sh deleted file mode 100755 index a7ef2fa908079..0000000000000 --- a/ci/upload_coverage.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -if [ -z "$COVERAGE" ]; then - echo "coverage is not selected for this build" - exit 0 -fi - -source activate pandas - -echo "uploading coverage" -bash <(curl -s https://codecov.io/bash) -Z -c -F single -f /tmp/cov-single.xml -bash <(curl -s https://codecov.io/bash) -Z -c -F multiple -f /tmp/cov-multiple.xml diff --git a/circle.yml b/circle.yml deleted file mode 100644 index fa2da0680f388..0000000000000 --- a/circle.yml +++ /dev/null @@ -1,38 +0,0 @@ -machine: - environment: - # these are globally set - MINICONDA_DIR: /home/ubuntu/miniconda3 - - -database: - override: - - ./ci/install_db_circle.sh - - -checkout: - post: - # since circleci does a shallow fetch - # we need to populate our tags - - git fetch --depth=1000 - - -dependencies: - override: - - > - case $CIRCLE_NODE_INDEX in - 0) - sudo apt-get install language-pack-it && ./ci/install_circle.sh JOB="2.7_COMPAT" LOCALE_OVERRIDE="it_IT.UTF-8" ;; - 1) - sudo apt-get install language-pack-zh-hans && ./ci/install_circle.sh JOB="3.4_SLOW" LOCALE_OVERRIDE="zh_CN.UTF-8" ;; - 2) - sudo apt-get install language-pack-zh-hans && ./ci/install_circle.sh JOB="3.4" LOCALE_OVERRIDE="zh_CN.UTF-8" ;; - 3) - ./ci/install_circle.sh JOB="3.5_ASCII" LOCALE_OVERRIDE="C" ;; - esac - - ./ci/show_circle.sh - - -test: - override: - - case $CIRCLE_NODE_INDEX in 0) ./ci/run_circle.sh --skip-slow --skip-network ;; 1) ./ci/run_circle.sh --only-slow --skip-network ;; 2) ./ci/run_circle.sh --skip-slow --skip-network ;; 3) ./ci/run_circle.sh --skip-slow --skip-network ;; esac: - parallel: true diff --git a/codecov.yml b/codecov.yml index b4552563deeaa..512bc2e82a736 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,7 +5,9 @@ coverage: status: project: default: + enabled: no target: '82' patch: default: + enabled: no target: '50' diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 2aee11772896f..f92090fecccf3 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,9 +1,9 @@ package: name: pandas - version: {{ GIT_DESCRIBE_TAG|replace("v","") }} + version: {{ environ.get('GIT_DESCRIBE_TAG','').replace('v', '', 1) }} build: - number: {{ GIT_DESCRIBE_NUMBER|int }} + number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} {% if GIT_DESCRIBE_NUMBER|int == 0 %}string: np{{ CONDA_NPY }}py{{ CONDA_PY }}_0 {% else %}string: np{{ CONDA_NPY }}py{{ CONDA_PY }}_{{ GIT_BUILD_STR }}{% endif %} @@ -12,22 +12,28 @@ source: requirements: build: + - {{ compiler('c') }} + - {{ compiler('cxx') }} + host: - python + - pip - cython - - numpy x.x - - setuptools + - numpy + - setuptools >=3.3 + - python-dateutil >=2.5.0 - pytz - - python-dateutil - run: - - python - - numpy x.x - - python-dateutil + - python {{ python }} + - {{ pin_compatible('numpy') }} + - python-dateutil >=2.5.0 - pytz test: - imports: - - pandas + requires: + - pytest + commands: + - python -c "import pandas; pandas.test()" + about: home: http://pandas.pydata.org diff --git a/doc/README.rst b/doc/README.rst index a3733846d9ed1..5423e7419d03b 100644 --- a/doc/README.rst +++ b/doc/README.rst @@ -1,169 +1 @@ -.. _contributing.docs: - -Contributing to the documentation -================================= - -If you're not the developer type, contributing to the documentation is still -of huge value. You don't even have to be an expert on -*pandas* to do so! Something as simple as rewriting small passages for clarity -as you reference the docs is a simple but effective way to contribute. The -next person to read that passage will be in your debt! - -Actually, there are sections of the docs that are worse off by being written -by experts. If something in the docs doesn't make sense to you, updating the -relevant section after you figure it out is a simple way to ensure it will -help the next person. - -.. contents:: Table of contents: - :local: - - -About the pandas documentation ------------------------------- - -The documentation is written in **reStructuredText**, which is almost like writing -in plain English, and built using `Sphinx `__. The -Sphinx Documentation has an excellent `introduction to reST -`__. Review the Sphinx docs to perform more -complex changes to the documentation as well. - -Some other important things to know about the docs: - -- The pandas documentation consists of two parts: the docstrings in the code - itself and the docs in this folder ``pandas/doc/``. - - The docstrings provide a clear explanation of the usage of the individual - functions, while the documentation in this folder consists of tutorial-like - overviews per topic together with some other information (what's new, - installation, etc). - -- The docstrings follow the **Numpy Docstring Standard** which is used widely - in the Scientific Python community. This standard specifies the format of - the different sections of the docstring. See `this document - `_ - for a detailed explanation, or look at some of the existing functions to - extend it in a similar manner. - -- The tutorials make heavy use of the `ipython directive - `_ sphinx extension. - This directive lets you put code in the documentation which will be run - during the doc build. For example: - - :: - - .. ipython:: python - - x = 2 - x**3 - - will be rendered as - - :: - - In [1]: x = 2 - - In [2]: x**3 - Out[2]: 8 - - This means that almost all code examples in the docs are always run (and the - output saved) during the doc build. This way, they will always be up to date, - but it makes the doc building a bit more complex. - - -How to build the pandas documentation -------------------------------------- - -Requirements -^^^^^^^^^^^^ - -To build the pandas docs there are some extra requirements: you will need to -have ``sphinx`` and ``ipython`` installed. `numpydoc -`_ is used to parse the docstrings that -follow the Numpy Docstring Standard (see above), but you don't need to install -this because a local copy of ``numpydoc`` is included in the pandas source -code. - -Furthermore, it is recommended to have all `optional dependencies -`_ -installed. This is not needed, but be aware that you will see some error -messages. Because all the code in the documentation is executed during the doc -build, the examples using this optional dependencies will generate errors. -Run ``pd.show_versions()`` to get an overview of the installed version of all -dependencies. - -.. warning:: - - Sphinx version >= 1.2.2 or the older 1.1.3 is required. - -Building pandas -^^^^^^^^^^^^^^^ - -For a step-by-step overview on how to set up your environment, to work with -the pandas code and git, see `the developer pages -`_. -When you start to work on some docs, be sure to update your code to the latest -development version ('master'):: - - git fetch upstream - git rebase upstream/master - -Often it will be necessary to rebuild the C extension after updating:: - - python setup.py build_ext --inplace - -Building the documentation -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -So how do you build the docs? Navigate to your local folder -``pandas/doc/`` directory in the console and run:: - - python make.py html - -And then you can find the html output in the folder ``pandas/doc/build/html/``. - -The first time it will take quite a while, because it has to run all the code -examples in the documentation and build all generated docstring pages. -In subsequent evocations, sphinx will try to only build the pages that have -been modified. - -If you want to do a full clean build, do:: - - python make.py clean - python make.py build - - -Starting with 0.13.1 you can tell ``make.py`` to compile only a single section -of the docs, greatly reducing the turn-around time for checking your changes. -You will be prompted to delete `.rst` files that aren't required, since the -last committed version can always be restored from git. - -:: - - #omit autosummary and API section - python make.py clean - python make.py --no-api - - # compile the docs with only a single - # section, that which is in indexing.rst - python make.py clean - python make.py --single indexing - -For comparison, a full doc build may take 10 minutes. a ``-no-api`` build -may take 3 minutes and a single section may take 15 seconds. - -Where to start? ---------------- - -There are a number of issues listed under `Docs -`_ -and `Good as first PR -`_ -where you could start out. - -Or maybe you have an idea of your own, by using pandas, looking for something -in the documentation and thinking 'this can be improved', let's do something -about that! - -Feel free to ask questions on `mailing list -`_ or submit an -issue on Github. +See `contributing.rst `_ in this repo. diff --git a/doc/_templates/api_redirect.html b/doc/_templates/api_redirect.html index 24bdd8363830f..c04a8b58ce544 100644 --- a/doc/_templates/api_redirect.html +++ b/doc/_templates/api_redirect.html @@ -1,15 +1,10 @@ -{% set pgn = pagename.split('.') -%} -{% if pgn[-2][0].isupper() -%} - {% set redirect = ["pandas", pgn[-2], pgn[-1], 'html']|join('.') -%} -{% else -%} - {% set redirect = ["pandas", pgn[-1], 'html']|join('.') -%} -{% endif -%} +{% set redirect = redirects[pagename.split("/")[-1]] %} - + This API page has moved -

This API page has moved here.

+

This API page has moved here.

- \ No newline at end of file + diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet.pdf b/doc/cheatsheet/Pandas_Cheat_Sheet.pdf index d504926d225809d21b3f900c7956a2f9cea421cc..48da05d053b96a1235566aa45c14e8cbe6b2c08c 100644 GIT binary patch literal 345905 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�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�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 p?Ejz5<*4T6Azl(dvcw*u1f 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

a$by+1dIOS^{SKJ2puvN$GEJ%y&=@dw;su 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%0Ak3Tk`rL$EzD%zMt+}{3>bu;h@ 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�H zPkGH-eOtg!ThSF>!=zHBpM zfBZ^}#V&7__RmU4hm5+v$+@$^B{6tvoo%zM)|q{xKh32=g-3Nctn4J_r5ert3TngT z+Mj(5X|(@z7o^QISDeIzs!*HKe_E6TM*Tnko27Oj{WF`)+4m&W)mi@SN2OuaEs_Ma ze?qpZ9;9}sf4kZd4z->OA6O(-)MhVy$ir+!`orQd?w`Nm#W~bO!i(-n1gQ#9J4ukL zWOF56oS?=Ko{JYRQ{!qbr0#PlY;FhcKi!bJIZ`vElZRJdJIJB-pQHchu>YsJJH&d` zkUsrS<(aep*=O_De=Ar>y+(|O745)r)cuez3j4e3MN_*T^)h6*?cc6f^bF~G9HZgL z+4{G0QO^i-_Wz!k4b}Q@|Ll7xtnAA1L;rWbEPRRdw5X`>FI}#id<&mBY<~U9(I{f_ zKfOEkrArX22G{;YExe7|i+`>M)R*Li_@F9s8Df!eP^HoQVDyD}T!1(BO@8%)pvl`l zGi7%F*{2NqF1><*j?TA}M#^(_JcR`^0=JvdK?M!*mX)nsA8u*F3%Z zBj@17QcIoPq9&~A0j)VgZKcO`=&VdU(jP8lZzXNwDxEkdjm>Y&#%cCz+P>7daw znrPy?IcM)xB%;bV>9kT>T>fQNLq=}qcYW?t`_TyR_kQX^Ju+-(qbfdggwRjoO$L0o z+t-Q-#Zr2-<8QQm4h`Mhx;sg}61?4VaWpk!*ds~3vBDxh|MQPP;U^LZI08TQZ~ys+ zA@Tq78^7)UmSM02bQu}B1*87^zv76J*uTm!1fnD!T}Jr(7#IQxBZ-P5lPG_e5iwX) z91%+)qT&d69IA|j`8y8`k%F!ZNs@xxYW`Pl7!n4HF2j;h#~|T={ShdrJV+!GYJX%& z3~GO53=wty$XGIZTO8%@*8@XF=4Jk!2bqXNy+&jb>b1m>DL7P}DUt-#d8Q!iy!?F( z3YLO;jVO3Y)N4*55YgL`FsOVfWE?7A3I+dnzF0{~;@|mVB{Af`^TkTyaOiCbn7?zw zN|I3ZL&gI5zw^K%O%ins437ABov}zGN5_%yf9H$EP_U>nEQW;24NE|^KOBjK^y&X@ zyErnD^7sC5WHKHVMaTIhrz!OMFumA635XeZSQDr#PF$iP=5gkXu{oM|by%QzT z*CyngqOMoS{?KJOpbQU`5r8rxP(}jE$UqrzJtYBoAblTIXQXYQ%YZycKpsfjKyM4= zK?3q10eK+TUDW=_Kpx0-2ptFHfwWU}9FPYY$b$^zK?d?bu3_k70C^zSCR7}9y+@Y; zc~F2nkiLc97RUoJ3LOXJfn3MYap*iSNFV$=4kL-q1A|nU5U0^)KpuD?59FQ!y)BRj;w37M0OWzZb&8Gy@*n_t5P&=g zKpx1w9{LzS9>`m<=r|w`z;g`XIR@|?gWQLqj)B}4qsxFih(I1hAP*vt2NB2v@EjQ( zpz8v7PQ6z~)fw=ddas9$1M&bo#{iyV0M9Xi=NQ0q>isjSE`aA40MBuN z=QzM~9N;+)@EiwtjsrZ$0iNRk&yi1K(B}{E90z!g13bq8p5p+|ae(JIV0?}PJV!n) zLf0Aa9QgzZhy(fu;5iQP90z!g13bq8p5p+|ae(JIV0=#f+!WQu0nhP(=Xk(#Jm5JV z@Ei|#jt4x)1D;bqOZ~gfc))W!;5i=f91nPo2Rz3Ep5p<}@qp)ez;is{IUevF4|t9T zJjVl`;{ng{faiF?b3EWV9`GCwc#a1=#{-_@0nhP(=Xk(#Jm5JV@Ei|#jt4x)1D@jn z&+&lgc))W!;5i=f91nPo2Rz3Ep5p<}@qp)ez;is{IUevF4|t9TJjVl`;{ng{faiF? zbL5kDbUz2i=Xk(#Jm5JV@Ei|#jt4x)1D@jn&+&lgc))W!;5i=f91nPo2Rz3Ep5p<} z@qp)ez;is{IUevF4|t9TJjVl`;{ng{faiF?b3EWV9`GCwc#a1=#{-_@f$=#W@Ei|# zjt4x)1D@jn&+&lgc))W!;5i=f91nO-06ZrEo)ZAi34rGWz;goNIRWsT0C-LSJSPC2 z69CT%fae6ja{}Nw0q~sq9NFLZ8`S5e&}HcB6#?*^0C-LSJSPC269CT%fae6ja{}Nw z0q~pvcuoL3Cjg!k0M7}4=LEoW0^m6T@SOU@ChFV+o)ZAi34rGWz;goNIRWsT0C-M) z_7YV;z;goNIRWsT0C-LSJSPC269CT%fae6jbL7!jbRK}`1i*6w;5h;CoB()E06a$? zq(#>S@SOU59qQZzo)ZAi34rGWz;goNIRWsT0CO-WL6OP@h{z$Dv;jBH%d@@SF&E zP6RwB0-h5A&yfe=(e(p7Cjy=m0ndqm=R{z9P6RwB0-h5A&xwHNM8I<*;5iZSoCtVM z1Ux4Ko)ZDjk%79 z69LbOfagTOb0Xk5b&kp3$0Y%tlK{_2!1$a5cuoSw=On;$65u%r@En;qhORT+kx0MAK)=On;$65u%r@SFsA zP69k90iKfp&&h!2$b@IqIUoa`lL61kfaheub28vL8StD8cuod9Cj*|70nf>R=VV}f zP6j+D1D+!j!O`am@SF^Ij!e!)Zwq)%20SMNo|6I3$$;l%z;iO-IT`RAndA=C8OQ_h zoH}3p@4iI_JSPL5lL61kfahdjd`<>DCj*|70nf>R=VZWhGT=EG@SF^IP6j+D1D=xs z&&h!2)H&p+^9*>7EJOgrq3?Iefaheub28vLGO-*u2Kw_TGT=EG@SF^IP6j+D1D+#` zETHQGjL*q{=VZWhGT=EG@En=wk3KHoIT`Sr40uikJSPL5lL61k!1$aDc#bSS0n{1D z1Mr*-cuod9Cj*`%3m~A63wTZjJSPL5lL61k!1$aDcuod9Cj*`%i*lgr40uikJSPL5 zlL61kfaheua|+-&1@N2#cuoO4rvRQK3yz@jr2w8&0M99a=M=zm3g9^f@SFm8P60fp z0G?9-&nbZC$YLt!d;!lXfaeszb7Uz*^!@KtNDS+qHHBwOL3hY6p^`k}A86u@%|;5h~GoC1u`DS+n`z;g=VIkGqqy3WA(oC0`G0X(Mwo>KtNDS+n` zz;g=VIR)^X0(edVJf{GjQvlB?faesza|+-&1@N2#cuoO4rvRQ)0MC&{gwU@WFg~XM zo>KtNDS+q5f>7vV0G?9-&nbZC6u@%|;5h~GoC0`G0X(Mwo+FDZq4NMdrvRQ)0M99a z=M=zm3fgmIIT_UZ?MNBQ52Orr-;R`_@<7T^ejsHi&yg}zKS#G|ZpgqS(qCH2-fOduU9Ek(k720zo4ro_s&yhHwU7G|Zpgl*H&I0m4dyd2bd7wQ<;($ERo+EKU z9%#>zI3N$S=NL(}=SUgQKhT~daX|k-dyd2b{R8be5(o4TwC6}1&_B?gBg>Wnd7wQ< z;($ERo+EKU9%#>zI3N$S=SUnn4+O`*?=3J0K0q0|T_G1hAP(KGkj@Ikq1zSGh=4eB zyFwB{#{r&W0M9Xi=NQ0q4B$DkC@yeZAP>NE4B$Bi@SM7C?O$Usq{_hS0eFr9JjVc@ zQ`hN59|On(@EikpjsZN!0G?w2&oO}K7{GH3+H>mCqJQNYTb$;Q9mqG9BC5Ed=-i24I88xQqSDqHAg-0*62cfdYX60RbTa!M0n4ZmUJ>2LrAO4FubtX&HwxPzWiY z4#CFf%~Kg9_O3CYf^>c`Rzf1D_PTHK&tmiJEbHobry&fFJVCIm;dO%a zez|BJYgWTFLZ4q&u<6dDkGkdH+07~Q%??2+wMWnI(_@e5&(0-cq2Cv)2&@I8&`WVy zq#X@ht{qNlcLNO5rqO$Ak%eAKA;OBR5*o#vzgf0N>5pv|qXYzBvV(=}BNOTn{d+w6%KZ)*5p_dwRRuL{bMrCG`9CB6? zE;5BpwUruDE>HS&q)fpzGRwfQK96`Z$=F3saTNab3nNyv-CGtNbKY%_n1~~I4DraY z%&b829Yef70!BS31S>e)K!u6d9T*6RC{lb50zR-|wZe!pxB>l46sad}Ihq6nlagud zGaaa#FAE4ZB=#vMMHUoT48GlGnvgjtK?Qd_4O6VGsm+LkZOBw1vpL-X31UpK?STnWjF|7iNWJPEcBA=X5TZ3}&U<$hZoe>3+xI77kCuA`LLX zjxJ0M%iT$0*okk6ygO?l2(W^;>H*g}bI)yy;Ia|%S4J@$^2n?6xv<|DsD>yXX*V20 zSl$=zVm>^9ttdiXd({?ARUvqt>3)QF$I!FnH*Ad}HIk&qPjHSF*5QP6*>hO9x{wv5 zP5(1SKEgN=2O3*w2pViANC5{<5(xC8{^Apsz}-SX45G!9qU1({`$alDV@+&at1j#i zBcBAp83iqj89|ni0UNA} zMe70ml{!pXGm7%k=VvYcNh~4k=oij0aZYv7eDvU9%C_NNUf~F?t9@WO4j>S-VS4dV zOGFoq&IAu%zeRFU`Ig(KsfUr*C=s5GRu zU;)93KyoKD36cxRvQWLVcPZ)-y}ya?R`|@+r~dWcF|0eh(c}>b#cd1eC5A2;Ja;JD zU>$$^$o=QXg3`Qo3e;|JYxD*85Z!VkyUOH2%q2Shjq$CY!yovax zKH>701lEt7y7lLJy|I4#dZiSJ$fv{Fw<9pxWp|)#mHRL{)(Z*}2y{5|a;cB&Wb4HYU?RD#RZzqosPe=4h3gkPWj(`N@zqTzx6M8=KzH$d zNoI9&E4e$-em}Sp==x-TpPu4nI}3I6EWzF*U3;W3{pt3ORiN#1>Rg8+-~g@yJG=A> zyAU$Q$H5L#Pl4H=V7946Nd^e_>?)#+goqouFMhP0&`G1)E!L+K+dG0;qj(eBzL)yC z+}C>YlRFu`Nqz9F(Rz8H(#qEbnC~EuR0w#FB@SJBq&#|;7i@pv2QUB9l`Lw(Iwb2g zO*yz<&*^y+$9aVzYV>8$kc%NhaeaXU?_vO!tr8)e46ks9?*-mO@uz{NZ`$W- zRj?NCYbmDzN+6E*i(T1!9|ErMO6Y3^N4u-IFR|t?;gwUwHPYKUfa!#S%gzU=94Mbm z1T%2-){wh#*&)v^;O?beu^h6?F7EXCq%T$?GM_AaDJDRbUEQ13KxFWnJ*DB!*VKma z2s?QZ<@2D$(!)_*KRu5qC4AAW|v<_aUelFCoboL z=jEs~zu6I6j7ldYP;ZNlR)yS&4xOneq+6HK+(Gll#gQ*(Wh0?P4;L@;K;`SrnYwux zW*ssn7_-;=7f&g4rti*)P1T$KyxaxHvxmiE{;sWg~iTQlu%!Y zR}llZ$LuJrH;0v4RJ>A~l3$DW0j}{)&t}m{s(W-izL@Vm*B(jUzP&$9zDKl%QOKye z;WY)flMa)afX+#R$n|bGVpueKrQ0_&q?A8SG^YU?W4RbW+@57sOB!CIfdMrBoFA9n ze$IMqpgm3_^Qf?TM*-b0-cYKYOvBF+%VTMpBAMr{g=p|BPIx+FiAiC^A?v}%wfUzqayC$T*=nA`smx+`HmVW2dF`|~h< z4rMIS`-4=2QiB30=dRXAbqnsW^nO}+H66-@}psEIrq`M3tvgY~bHH-dGEo{7GL&7gJa3O5xonj>sALW1wgF&yL1Y8yTobfRLsd!GtIpEm^agXNy-cb3 ztfK0M)}=k-O{K}a2Q{m7$vD-GBPf22x2S<*|6SdW!Ccd{>6P;rfyb0Z zmV|oh@_0SvZit`Ap}a%Qtacft;vWxun$CBJafIxM5^dw-fXVtXa+1dDZVu+7NLu9+ zeKtLW`1-(a&(g9fvLY~}bfXlMXMI2hIG+2N!|;6_u0FyP7(06%11?0{(6oHz7M5-r zLA=;|a7HlgR&ymSmJPR~R~2(At)1?F8use=Z4S-zfxS;Xw_;T}Hf_&6P} zj+gQ*t#YF;cz(05X_m70_?`#qIQmLlT}KcU?Id-;T4l6<(_;tCRBOTl?==1nXxTyc zI2t7%n(tF^Fg7}-Y9DcOFA_Ee&Uy0;IoNP7Dn0N*1c*BcRL(8Yv;~00_}#T1shCEIG7)2j>=VbNT794QK&?j%)NM)B>EH9^rwY<#sXz#FK{&UR%D|N0 zDk(vjuk35VE~>S}WoJ@{BwEXdBqC)-^OB0dbE%i3bA`W9z`HJH>;=^TA5)#$mmscw z`a3}wC!aN|Phc#jFhU)bIw-#iTJ+F_$=dN?4Ag=c7oa)_NfK7BNAmb)sfL^45Egfm z643%I^a+X(^EUayp;9-vkzF$vz50rr%&XWyAb(tH|zxTF6 zx@Uy+Scz9awxN@1(#dwOCI&J7+7b#@-}A#$li|~4Rgy%}hAkj4nvgTd+?E6>jr^fY ztw?WjdyLx4^5^p{C+~z~I~{&vDu*&q%c7|N-4>8SB{$Z?!! z7cPcd=5!rl>49as8n1&_jE9Pm5?8o2uDNI6Lv;X3o+AQ>C7aAlSLSxl^~e|SH#YnSV_`=XalagsUT(&!}zqsNPTt^q)EEaEs*1i zC4C_X_|NJnZkj2NY^L+w=V?9FEDzrqW1E!#~?roCJb1chnkERr=fZ^!nwPbqd@?1U5oCDx^Vx zm#x)93CdnE(?%`&^qATO-=i{1?6Y_5;7a1rwry443+-1ChRT;IDfm2UaeI6E zARpB8JT{<3I{kZ*qXP7AYCrOSikg;r5TsO~QY-HgdMuo|Pl&A>i%!kQh8sdT3{ z6t5;4c3ARuYT*6xx2o@lhXf`dK6uMzXka6`;&oprrmCchZw51$TW6q|T{D-He~Dab z9U`kO?RAbD4x^>aFpO{4OjMC2gSAsP!H_IHt4k;wLr@y7tt&U?8MBAkZB0UBOC^JU zmQ}#1-DzBH*C$irxkk>@FJx|`r`l}}$)Is~ciw@Vy`A`dcAfPEJQ=jN-*K^9alYl%s9FrQXy}Uf{6F zqq(;wXg0%HH1%jj`&BFEEJ_StO$ed_@ zf!+JDknqC%3jA;6niBl}H-bLpD8?=*!nUwj2w`_OjBAaJLZ-FFGYC|=LwFed8Y71v zXk4-g1h^=p;FTlHAZM9<HJNQ3=JG*%2uWX~9 zgYfrnY1>=CVEXxM1Aa&{7|`b&5KJFRsZ zQoUQkfXzsIw@jdB+2-ROWc*TwzX#|TyuL2THCPH(cRR=)qOU#WcHnUm|K6zz*~P&)cMj|CvCWA!1-MQp3+qSPQFm#ZGBx~_16K*42}{Ff5{ z?+RBd?blbHJ^ot@U&#hIA4sIhDw#$baVeEHq=gBV1&8P4n&Jh5@4sNAMykAD5$}e% zo3MV%n`Tg$u^ZhViEJ>C<9*GW9*x_Q;*h#2RG7J5wsE1&A$F~o+mu2GCdqqMLp%9EDopF!HIB>1z(e9T&9L1u~HLk*(5CnF|$c8>(-z z2%UPX37tw)vaZ^rx2G&JhiWkOiU^f2(3;Oj{L0dbFS)CJ1+~3{-dT~|4qS1dUHm!- zEe`&3PkF4pwOxm5FtJ^h20)}YnqHzmqorM{ctBLa?aRNw(*)DUAN@U#-QjFfFyl}YZ&WM!dC>a{ z@eV%N`FgtN{oO7~zW5o+%KpQGeW-u$!Ra^W?2%KgSR!{ZKSzJZ7kqZ-9+)48!PucL-RbBzvF4m@+j+%nK7^nPoQHElVvZScSAE!4mE7v(m; zQCj1Ip#UndVZZ8XDkXaD5$JQZY_f9#)M-HAEfRVPKtV@QY7jt2LIRvH!Uu$= zIbqcQrY%$v{}k&8^XYkkOAmJzwai?jv}CAFxuN7rI+pb>nXk@oP+z$k#zuV2am0kI zYZ&}|Ku_S?>KW!t8Ks1mdep(%+Ip}d28bDseFo%$zWp+24V9>64V~{YQi|*}_k=%&!(`Epon|{(4^! zEg4Y#KK-UfJ2LD{G)U#v)wl?97yU!huY(6o!xD)&&oWJ8$c(3zsLpN35@PsBXu|Al z3XBp2wV5q=`1fe?)Biv|oI3S-x-n+G{)wML#M3T3b=heY5_lO;;`$KE60hatzPd~h z51&&MsVvG|D>F;U4r)ZNn>Xu^*+b)2S^`Tm874|;RR(m03W$p7Dg`)Y@=2f5|J*&}NKNvvm1IV4wZ)Tt zWtE6t{uYBcSU|1s=rg;l7vD(%ddei$G>MAX|MKlREq!Hq3K~?AGGYpt>caDNYi;Hk zb)U(yqX(bypqp8$O83e#kl2^uz+i4a4{>Wq*^IE__}1apoe@Vv<_1U3r?@B0OI3T^ zlN|iK1@@}Tfa$}}CE#(lT7Wt0eKlClvS%jdOo`_iPfz8xFI!6RP!KK3`)sh`RR#^$ z&h4@u3ajw5lpK=s8qoxZe}Mhw1jpGQKj{28qTJ$|;XhRn)JP^n*`yoCK27O~LT$0G zcWH!lC&gO%IP2Wax-OU3th&6=zu_zR8!vm$8xJKV) zVg^wy{pEtoQ@Z#OfQ)~=JK3Q%pb&e*tB*gm_xHDk<>6AM*n@Tjb45M55m@U>PQPs0 zFRa4!sP30NCMyE;XFaIUqlm;b;kNX@!nlP`*rg_d;U#cKPf$xs!>6}z!S^NEP<8)s zH{{xpsZ%sZ%}D18V6sj1aoMY&6?NZhH>M^12G{-%Pb+I| zwK2mg0jrUp=|6jFav{xRMU_FO|J{Hvb%LmUBLVz`H?y*G#+oYkwmkJgA&Gc5O-2rNs~gv z74m`M+7W^sPvu83Ig83{r#xzWeS~W#`f0ao1J4#bGrt;vZJc~-BW<=ovAi~Cpb|G? zUepzhi;YN1E(#J)CaCry+xt16Y7$s*`POPa8;q@h_JcgHjZVS|&5I9^$K1sig}qu` zXJk-tN+{$MV)ZvKD>r|7N@V-t=V0w^^&9P?Gffi4Kzjw$odP?j}R~jVC&1@Lr|$yJ9-yv!LX{-!#{0-TRy7Gi)DeUY<}@IdqomBq!B6cu1hq&0ry8@lNfQMkh#zDK^e^21CFDo> z2lc~Dc(d|~utWR=WXs_(xof}sHOu+<&2Q*gygzh*=u+0#JzV#aGmGWUbx{R^l-6BUX|qI7G8Ir4ao_Qg3Mc>h+b-Svug;qoj2y?Mp<>a z9EROQ8tGiN=QZ70kyIrYot*+mDmXq<>gKyAd1Z?o^{Yx2^Q`L6sV2^*nlNusHqA9V zax*cJf6u2Wb{e3fo1wx$ln8h`5c@)kOphCeo!}Y!EDx8ph3df_Nortj2v;mi`MEN@ z60$!Naet(D;z-u~blH~IusXdLYj+lBGHojYo7~!3Yi+rqhZeN!#25#DZwrXnZ}KX9(}BsMXK-St9G7Ki zREIMkYsg`-ggwZPB}k^y&U{LGDHfkD$x>f-U9XZaeCMJ_Q)J5l;4#@YX{h!y(_n47 z51ieW?xt2!^TqBV7Dt%V#gOPe5I*-Z){Ej(`F*ev*#$0Hq4CB9ct_1&tR0mY*VL5$0_7Dpn>zNs(gn?_z^PJZ@z zZ_A3(zDmfZ3kLdTG1ho!#RFV$hVl+OI+0>bySnCt2g$B%HH78bTNjgKLLIsCGb^`Ik zWTU9?kGUP-T_~v;CYXp&uqjuh3w*Ii!SX92=1CSR&p$N04wOeT>o^^fiUT_>FPMrF z7UHz!7D0?I0uP+Xg(9c0rMD}sNC~9s9avslQ{tG0T&~LY%KF_5P%rRW+9*Z9zA+AV zHsFAsM|>Uux;R#$xW)BwEYoEAy4Z>(3RR7SqO<@u%<`}kaz{)4q%2%`_B0ryOM%Bb zaiy$e+#MxEuBX9g;%gu_Q-FYU`oM0<2j88l=KeTtdI6c@HJ<#fs+2$M zoJxnf&flvwC$XFO-r?S6EW046;bbp@#x#(d=YVx?N&=_ z6Fm?<5i7gCUUW1f-9`9n9lEOHVinn9)wM93W%F|@w{S^=8@HVwH$A(hjw{hB+eM8x zh;w9ytkq1a7^I1shH!vNi628kJZSIh9vM90lE_j=e|c6|p)9o;Wed>+UTDt<{dj$) z%oE2%(DG}BrU4U%F zL0bTtmGO38t911@o6yi}M8NL!=m-!6th{^u&wq4Qz^}$)(IfD&0fVN8 zJWGIisz1^STPF7s%{)HFW|oS$xz-1!0jl{QtL?L|U->vh zn+P4ZIWHg&Os@>(00eT3qbVw3z3;x^FA5o=$XWRLD{gkL-IG}gtb!dS=b3~hm#&`QRf1`zLhYLzHP5d0a*edI_)E*@1r_*#qgw_ z_KI>~sSC#*qlcFE_74{hThcCJQG-2Himtg1#*(*H%n%xxRG+^KkY6gtUYykLXDGAs9odFjUZAtzrDVIi8W0FdeReq=e^0 z!65_h_@^b)VpS1mn-Alya=(mu`J+382@4#@GBK=rSm%=rH(Z}QqHogheW}yqP^{za z5440AF%1OY1K_=FiLB4KVy&*K$~{8it0qC<65Yy)HsG3hEVWvADu8|}vz`=EwvV_D zMTFK>{d{`g_OW5?8P;E#5*X|-pX@4r@yjGaZ|M}Xcg(V()}+DwIS%`0?}f_>?bbzb zTyq=e%ghv~0FnTaP!v>s!GZlNR6VIb@Hd{TenjJQNiOUtI7sY+3d86_bR!=2)+@%|#H?dGD?c_yG!WJo zMzR$~f|ohc{C1Tn3MtPc4f7XFXe+QV2QrGh2%Fk(OptZVbktg6FwE(wL zwRgUtX)rq9d2bbEW5)bTa~d#3|NjaB0SZFqSV68rH6ZFwe?Nk5EN9NnoTexmaLW)_ z{|}&`3JZXFUFL#M-6%qa*dD7F_xTrj0vHOh)-<#gSX5s%}Cz%({6DQdCRNaBVC!sN&y* zd6g$3iu{ljuzB;duwyswZ4Dy z1Zuqq<|!~@m1{o9iq}hWj$`|1-n3P&1r zIyt$-2saJwPv&8I-V%1s-XH_HNpVYWGQ0}Nz*Scttox2GA7@|5FNqNCoAFIBgz!%= zcxGgZ5Sf`3_b{73YnP4#R=fHm$RmhRj5jmu+Y3=N|KGAc{AaqyEMyo#d^P5Hgj2=iW=!fHo#U{6UVBVHM!|X26h+ogI7YD69 zxlAw{Wy(_KXI=A^1&Ai-hYnQDj;O0UIV>LVUJ)o*`85y(P=pOxmuISx*!vhS4nYlw zWjqkf55Ir=sVwOH4l&_t2R|ku=Kk=+qb4Lko|Wzai;dFb2jd6ncYB5y`n(ZW;x?A` zR4M=<#-1x*ztl)jymB0t5!XJmHQIG;7F5+ILyisSj0T!ROg~_@dOH*4|KMo7jdMO2 z%$Wo?2mL^hCE?LeoegcG*tw`94gQDgFUs8N+#uay_wR>de1aG$aiC=&Wzj1M`1f+J ztnJ9ElXhxOG*?;s9IdZBDRDv((9-}+-Y3!RM$H#wDotc*Gx;tU2Unns^=CyvkF=it zA;<&F2X_?JW3pj>WbP<1>?+WizpUcKLB+z>6QNbDYYf3{L2WxkXzt#ue;XwDPvb`Z z*8d5GNJvWZ?U$SFPs|xOgayAJf9W@`r*3Lu)bjj7Ak=S$`Ud?Cfc{o32Zvmv6L^aI z_baRel`C`n*tS=?1~}g3t_9MxG_>xsz55WetqeTgt6b5v(2}9l*9~C79PxJ^cw#bt zm~^kXe43Z(=td@!o@fkRt=T{?Pc33S{uGr;>g4I7^H(EXO2G zdX|%9B5%qzRgaHLf%K)!f^UJ(A=t=hmRPvuJWL9P9l)Wx1T&TYe1^^^DO&Jh4)MQw9)4l^4GzRbQosozkPN6N2F0J_o?Qjc3K;? zRX>}GNfWlvNx*?Tiz6xag(wwS=7(_&Qa zF_yp#30w<4OqSn@3>s0ltY^rLc;{~4+TjGyfS5$%E0HTd{FZ%N+2Z;?3;W} z8vLdL@psFMdg)I2)8OPQ6~Jfbh7wR%d>46t-WaDR5tePx`aO?f$21b;sL$ADlAbwQ z<>V9A_VYC)Z+>{iu_cqk9nV|(rSq(518YY7NH_>rbZYigS#QH*KGna=MF#v8p@!mjt!UJ`)}HaS2bI z8Fh0me+5vXUeZbtiSr4lCegLb$W=a&%{y1pc4Yi*mS5=@c50b#hrr&Me?1_k(0ZxF~RK0)wD8^8oj7%z%* zcZ1(CBS|+i@xi)yk&gO`e+LvN4uZP=9YpPwmw9LI9@gtsV!WhB z@Z!3z!no2a(Uf(;v06!!rr5EF!P4?w74XZUtAY03QQNUZ~UIbpJ2Bm;;4?S za}~6?j>%#E$2%KY+ZT*s5BBat);p11(sXErwBIcjeS zISzo;eug^3InUx6Pm@n9|IDs1%jwN`Vd(ZXsaMzQr=702NJkMHs%B@;$;8tiHxSfP z`9`VGlt2_Vs8FTc2NPXB>^+gKH4izg^sqHJ<&)CUvccQfn(Oe%6F)b}7H;%c<<-xs z7kO&B;eEi)Dzt4tdokzb%v{W`wLkCV@wzrfX@yoGPS?ctaU2PhXyeLo91Tuy%KQjLJ3P&O_Q&HX936eH`ebz_THFG2`m(hcyl&c2_&#aa zK=Co%^dNin_#6qiZ_I1>J+pak>N&)-jxT7AVpIc~2&B=90%%}lV$ESUcpTFq{tSZo z^4l-yI1Rp+4IbxgT6=TG<0s*_bro9CHPfPO^fq?=WnK;f#FHgL_y;zOTDCXb7(It}Y8@!qnHP$Z^F-=gUG2t}@s5M6BDjsMsmO zH!=a6nB(wm37Rz`1nHVBn9_86J4++?t!l_rKOVT^3R{e)+iJ3j&K=N*OcAt@cw%+D z6Sx|CJ`VZVt(!6l*OJPeW{H24ef~9D?feaz`hMCf(NgV)Y1UR?C7Mh5$k|T4Jt`KQ z-@4HBRu%A-UvRVruml^#N*ZD2F^@HD94dj1p#LfFc2=n8EF84a^x-~?DCkS-G_4zk zz_9_Z7yHRa7G2{6C4_JR{T5DdJ+`@`AK40)8eivjy&gIly zk}Na${jVZ$onR7Z{b-TXlkSy(#7ur}GV?^Bg!{)+dI=Xv3Sf9E2DC>j~@`_C+oH=j*KP$54u6~k7;W+L{QWCE*cnq%Kf-e8udLEWEt!ENRK3A$L znV=M_s({Xd{DY*Tq{B44>~B#fr{^&G0(Ga{?egK)^SF#cj>7VOx*%2MYy~Kg`OOOY z2jm|lIR_sShdqAug^tp4qPHtKtAWIhFkPQ~mbuq!x8irWG&UGQ6kcL?OU%-Xx$7f<%$*CE+;+c$zjA(Iof|f_oF^L-yO}b_uJDZH4N~=|9P{2M^-vkSXLmeX zSYUq#19?;Ny#6?n^wtLg3s^XNw2xC@tNbD5f!iMCuL2x60gSTD#v5bS zu0~{vr|v4GY2)Xz+z$l#n#jEt*5!2pT7>Q4@>F<^( zkyn;;V6|n+dF-_RM_~&x4eTsrE;p7VRI@^XwN!N~Y^G(-g@51!{Ha)~Gg`;@*U{5!pV5uFtFP~%O_!Mj@ z_zb)tyxvmXF!w!7e=p?;03Jwi+P;x* zG6-RMs{mo(bV??UPYYUPO7Eq=vm6|Ef)rFE9jrGZOK?M%I(DU{h760y+`klIKzx#w zA8sJLE*3>2>!iT0l^#Vw>$K8c)GdOvwdCTUoAyhm7CdTE1vQlZlsN+=*!$a;c2JS4 z$2M@4W?aSc`KfOtsv}|;15Oeu!)agkCl+J2A@IDZJKG3uJcTe{mAE;CJ{@domYQWX zg4@)kym8Eu&AGwO%&O*#5cY&BeN23QT=*AC7a`8RNpjJik2%RZJo})neL2fhS&DWYKQek1-G$R0KqF|eZSR;(40~D z+4zrNUm~%eeYd`C!cWk`6UV>~ko(}&^VTH11jfhNGF@JyO9xVTnIx2NK7N z8@1VfD*?DD=R`)#R(ST%y0_L;t@+wkqQ~mOWqy94aR^WDY)9s{T&tcH&EyGq89IcP zNGrF~KGAI%!#JXF1xlrE-sko5^JZUBZ}QYf5lr-5TZNKF4S(LJ64+eoxC@AVwL)?e z?|r%LoekNTV8=U(R#P2L@_!K*m)OQpU7KX%f8%75rXf)!-Lf#)JpdLw&t#bo)`rI< z&NFF!Mly>_4!D^QoHK<`U%419MPCKxDb8r&&I}*#6xz?f1B1*K1N}WrSKv6p-c{)TCB7j| zguit0r{t7pu)-P#C%c_0#r-12Vn1e8V&=TwmuvdE#cip+=&e*XlKitVKUW}aE9cWW zUN}Q7;N;@zA~HqQv9(Z~p`AC&aB{tW%#flOF#Udj0tn<~ds;k*BzEWcW=r56h-K1_ z&pvNe#NUma1YY|-k=Fa-rb0mMBxxWFGze;cr@Vu0aQxEra7gw0o?y-^!8kgcC&dFmY{Y*%cswlyKTGKbu- z>n4Qi3&>w6HqbjQz#yGJS`Mx?CboS@M8)PPK4IzNEew4sP9D0K z8{Uo59Ht5H)b>n60PEwYI>V3ifAV2trd;?s3xE(5`x}@mcomP`#X!09X9xw7vp6?2 z7%;0?w_?PX^7f9B8{w30Wq)>Rfg8TO_fZ zUX`fxnpVG-wy5peO0A$WjhRtvqLb%6YEBN zu1R*D^f2|az5c9zrjY$6JLX+C>9@=PApScd`62!ib$gL3)E(A^NeCj*#1}6_icw@t z#M`000O5yVGsG{lb6k4RVsEY6Vu>G6#q@nL>+ujhVhg%QHqW)Ue@gBJ@#(ET+I2jU zUmsRIWNk<5ep|aPo9g)IQu7;@3ZV8gAJdVWpru-9z>)384HKNn8N|`Ri4b=St@GDc zQ>y)49!K!1i>+1Uxac0l6~^FYs#bqo9X(Sal1x?wEK1aI^kHs5R3hcrKlxhn+*d6< zp66KA*rqM3SXypUE<-C-O!0NKob(oL|GHjRs4x)!w)Zh@p!Ur>L+WsK7~u4IdYFC@ znwH@_pmYqA&h^!BW0L-*hNs|Mc%1{Cn9T*8kMzMly>iA$N*9GM-dStOj@h%MB5(Z) zbG$9ZHV(tJ&f&^0plUOkO@uPD91hSGNO~;BV0pnB<=>Ho7s!V?#_$y_U6?`UMfFY5 z-}5Me2ogzQtPTjU)FXL_2B-}a9#(u6#!hq89I_3$N4>w^&8t29%-?0cU0u62t*c%Uc8vPb@ca}rImesYEL`u*wW%7fQww#F(J~GFt+m8F1hlt889^Cke=_J9 z-_v4IhuNTuanv85%h$`(p-iR>^Gp=WA8p2~{1LcMP@#UYnuihJR;%Cme^K?$LB91) z_~6*KZF}a9ZS#(8+w-|&+qQM@*tTukHg~@7?yq*YQq`4oPEMWkN0KK`ce?GmFYu74 zL8E}~L%J=C3XA45vL_>v+?EJj z@ti}f0)F3MDSamk5s29WWM#oU!NkLoei~6#aO;wvjpiuI!*y;HFSjk1;xO! zhrBtrq0Uy&TG9&ubj%&b@xO0*)`|;Eyomz`(Sd~pfdz5;%u`hAb%J~b3**Q`zu9O| z{j#StK4Dk2fmi#4p=%P8TS3Civ?TM=Gn+x0KIY#sBBWwyl26R&Q(fcAo~P5c)YY2y*+oSQWTfZTwtj`GQ+zaAxo5-wI6PX_7OiTy!c z!=vZ{u4;Tw!%8}(I-81GnbPqJB3r~03lva}HXm#RWU5Q9S=8pE58 zCWVSZk`IOqsr2s|TI>zqT+d;vb^;!3!II%Y}JBhh;*@+ml1#ISGgajBVKaXg=F`fD42Qtm9i zgMH34(3ymGGyHt(`D(xXSEU&C7?feABBO#1)B)wvgk+{RCA#i0`Bt(;mX0;aQJ6Gv z`sj(=HX$l~p5*E4#s=ZU65VI52nqX~%SN`+@tLyPIIU-S&Pj$CZZUPc-KdQPhY_KV zxG3EQEZ9}3IqI-u6r@-CCYcq$C($XI68GwdIWx>13dFE8c83kbqd3GfZ-nkP?PpOI zlu?4wpI-l;;y$@!()NwZvDTFHWly2GeBvmM{V#L`{BxVjG5BwKXSKyQN2lW_5i}?{ zLEZmvBg=a5>xbYv) zLu(rj9c|GnijD6hrd25-A!>nt_66O!XS^L~>m`o}r?c#J)5rfEDrcf1a7!1ZyRtc} z78vHao$K*wLS)x`Rh+1IiryuLbFLj{SU<8Wp@>`6LJH}89ZtM{;61$kn-H*_YVKFh z7wH1vyFzTn*{5GSZoUagP=oJ9I8(l2u*Q_vh%ou$g6|5eUgbYM+HNRaM|I$CVh&q0 z#$cZjGKh?y*NwQY0UUFZ233@rJpb(_v|~apF5lTmRHlHB5)Siey3F6~iBKi#_^rIb z_^Dk_T%u5v-y=O(U!Xk`jF-R_;L~TIv}ghN`Kh8y_0CR|*ql#s76r?;EycA+VY`fB$o?(3tI5IZIOWW~67f zGs{rkZ_7|-?FDeA^UdV%uAOSrz_J>aYh{=T$i^a$b0-co6#ir$*HnA3&wu6Sn4ID=ZR;#(x3U0FDl zYxL_d;*k@)RYg^_uEY~ZRg~A6F6~#DyWg)Z&$k{bHUzJ807?tLnk6gk zz3-Ijt;;FzZCm+Q<=@t8&C4KT+z{GivN#d`F{$qGS5P-xXm3# zSy!-r$VFt?>~Fr93x1m`c|ii>FeC@cKRm!2(DaU!RlrU&En*w#0T4xDfTtGFr@#Mf zZ6jZ*bt9ps!uZ_98`4v|fEfL0i(alEJfI)u47rU`$gmJI%RW#Logibq$~ua3H0iT? z`WnN2DDkOMSbX1ItLC659>s~)cWFLnMyfV1gYmkER3)Wb-s6LO8OLpxLh8L>t6ldR z486D--T);{+J3#`jo%P2fR9oJ#fF@^W@n(G0OUdO3{WJe^uK8p#&m+)O>Ugdu z#d~3WsX5=oX&cJdIw6m4`OdM9@T)^GPP2GoqRBWhie3v21LOlwkn;yUcplQ0>r@!< z-zJQj2ME7S_#-C-MdI4>h{4FSsOP8BJmE;s6fBWN!Q6ax);#%9_t;NLD1_T=Q0%Z1 zu5I&YCF^Ehzb%sv0JJGIE;)(oPW!cw!|9!b7?c|rng&GeXoeIRm?2R#=wq4YPk@X3 zeKG^0`Y-kHpa?7|5}i!C3@omvptOM7f`Ml+TJVyD8q+KQG5vs&(f*;chl&2wzd+X%2{G{V;*7X*RaDq^H>_k@G{c$!0CJ^xw_!!N;;?3ZaWgf_ zGKu}zAt{uEp41y;oRu)rlA6$^B>U>|XjOvMT-qodbiUvgT+%L{DmDTcqtG470*eqm z>OdPYBrHr{9ub`KC{Z$8#aUj3-L|?ROxyeM&as^1quVTW*?%z&_L-Q^LB8piORST4Z!W@@%S~w@KmcE-cgG$Q!1J-X(*& zavIqV`LMll3v7Px%ocG8J5lEj6%Vtkz9!AL*haNVJ*T;PW_l`jMNk^!!Yq#U&T*C` zs`#x);tKtLRaqkck1DT)FycM`f5JUJ$^+oAy4WKdM*x4E z^PD6J0pS-4-*F;aik6wDEmMkC0X6z8t>fHVZhvq|^&!H9%0D;r$?c7WI?!SDMK9Q@ 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%0Dx0`!YGcn7%;;H&54tHxl$S@jLI18_@uGf_y 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 b?4EYEYKrhs5Afq9{PPLk+XiCbWr6+=B_crV delta 64234 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⪼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 z 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 diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf b/doc/cheatsheet/Pandas_Cheat_Sheet_JA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cf1e40e627f336e4fd38c246824e238a154770dc GIT binary patch literal 420632 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

+ The page has been moved to {title} +

+ + + ''' + with open(REDIRECTS_FILE) as mapping_fd: + reader = csv.reader(mapping_fd) + for row in reader: + if not row or row[0].strip().startswith('#'): + continue + + path = os.path.join(BUILD_PATH, + 'html', + *row[0].split('/')) + '.html' + + try: + title = self._get_page_title(row[1]) + except Exception: + # the file can be an ipynb and not an rst, or docutils + # may not be able to read the rst because it has some + # sphinx specific stuff + title = 'this page' + + if os.path.exists(path): + raise RuntimeError(( + 'Redirection would overwrite an existing file: ' + '{}').format(path)) + + with open(path, 'w') as moved_page_fd: + moved_page_fd.write( + html.format(url='{}.html'.format(row[1]), + title=title)) + + def html(self): + """ + Build HTML documentation. + """ + ret_code = self._sphinx_build('html') + zip_fname = os.path.join(BUILD_PATH, 'html', 'pandas.zip') + if os.path.exists(zip_fname): + os.remove(zip_fname) + + if self.single_doc_html is not None: + self._open_browser(self.single_doc_html) + else: + self._add_redirects() + return ret_code + + def latex(self, force=False): + """ + Build PDF documentation. + """ + if sys.platform == 'win32': + sys.stderr.write('latex build has not been tested on windows\n') + else: + ret_code = self._sphinx_build('latex') + os.chdir(os.path.join(BUILD_PATH, 'latex')) + if force: + for i in range(3): + self._run_os('pdflatex', + '-interaction=nonstopmode', + 'pandas.tex') + raise SystemExit('You should check the file ' + '"build/latex/pandas.pdf" for problems.') + else: + self._run_os('make') + return ret_code + + def latex_forced(self): + """ + Build PDF documentation with retries to find missing references. + """ + return self.latex(force=True) + + @staticmethod + def clean(): + """ + Clean documentation generated files. + """ + shutil.rmtree(BUILD_PATH, ignore_errors=True) + shutil.rmtree(os.path.join(SOURCE_PATH, 'reference', 'api'), + ignore_errors=True) + + def zip_html(self): + """ + Compress HTML documentation into a zip file. + """ + zip_fname = os.path.join(BUILD_PATH, 'html', 'pandas.zip') + if os.path.exists(zip_fname): + os.remove(zip_fname) + dirname = os.path.join(BUILD_PATH, 'html') + fnames = os.listdir(dirname) + os.chdir(dirname) + self._run_os('zip', + zip_fname, + '-r', + '-q', + *fnames) -import argparse -argparser = argparse.ArgumentParser(description="pandas documentation builder", - epilog="Targets : %s" % funcd.keys()) - -argparser.add_argument('--no-api', - default=False, - help='Ommit api and autosummary', - action='store_true') -argparser.add_argument('--single', - metavar='FILENAME', - type=str, - default=False, - help='filename of section to compile, e.g. "indexing"') -argparser.add_argument('--user', - type=str, - default=False, - help='Username to connect to the pydata server') def main(): - args, unknown = argparser.parse_known_args() - sys.argv = [sys.argv[0]] + unknown - if args.single: - args.single = os.path.basename(args.single).split(".rst")[0] - - if 'clean' in unknown: - args.single=False - - generate_index(api=not args.no_api and not args.single, single=args.single) - - if len(sys.argv) > 2: - ftype = sys.argv[1] - ver = sys.argv[2] - - if ftype == 'build_previous': - build_prev(ver, user=args.user) - if ftype == 'upload_previous': - upload_prev(ver, user=args.user) - elif len(sys.argv) == 2: - for arg in sys.argv[1:]: - func = funcd.get(arg) - if func is None: - raise SystemExit('Do not know how to handle %s; valid args are %s' % ( - arg, list(funcd.keys()))) - if args.user: - func(user=args.user) - else: - func() - else: - small_docs = False - all() -# os.chdir(current_dir) + cmds = [method for method in dir(DocBuilder) if not method.startswith('_')] + + argparser = argparse.ArgumentParser( + description='pandas documentation builder', + epilog='Commands: {}'.format(','.join(cmds))) + argparser.add_argument('command', + nargs='?', + default='html', + help='command to run: {}'.format(', '.join(cmds))) + argparser.add_argument('--num-jobs', + type=int, + default=0, + help='number of jobs used by sphinx-build') + argparser.add_argument('--no-api', + default=False, + help='omit api and autosummary', + action='store_true') + argparser.add_argument('--single', + metavar='FILENAME', + type=str, + default=None, + help=('filename (relative to the "source" folder)' + ' of section or method name to compile, e.g. ' + '"development/contributing.rst",' + ' "ecosystem.rst", "pandas.DataFrame.join"')) + argparser.add_argument('--python-path', + type=str, + default=os.path.dirname(DOC_PATH), + help='path') + argparser.add_argument('-v', action='count', dest='verbosity', default=0, + help=('increase verbosity (can be repeated), ' + 'passed to the sphinx build command')) + argparser.add_argument('--warnings-are-errors', '-W', + action='store_true', + help='fail if warnings are raised') + args = argparser.parse_args() + + if args.command not in cmds: + raise ValueError('Unknown command {}. Available options: {}'.format( + args.command, ', '.join(cmds))) + + # Below we update both os.environ and sys.path. The former is used by + # external libraries (namely Sphinx) to compile this module and resolve + # the import of `python_path` correctly. The latter is used to resolve + # the import within the module, injecting it into the global namespace + os.environ['PYTHONPATH'] = args.python_path + sys.path.insert(0, args.python_path) + globals()['pandas'] = importlib.import_module('pandas') + + # Set the matplotlib backend to the non-interactive Agg backend for all + # child processes. + os.environ['MPLBACKEND'] = 'module://matplotlib.backends.backend_agg' + + builder = DocBuilder(args.num_jobs, not args.no_api, args.single, + args.verbosity, args.warnings_are_errors) + return getattr(builder, args.command)() + if __name__ == '__main__': - import sys sys.exit(main()) diff --git a/doc/plots/stats/moment_plots.py b/doc/plots/stats/moment_plots.py deleted file mode 100644 index 9e3a902592c6b..0000000000000 --- a/doc/plots/stats/moment_plots.py +++ /dev/null @@ -1,30 +0,0 @@ -import numpy as np - -import matplotlib.pyplot as plt -import pandas.util.testing as t -import pandas.stats.moments as m - - -def test_series(n=1000): - t.N = n - s = t.makeTimeSeries() - return s - - -def plot_timeseries(*args, **kwds): - n = len(args) - - fig, axes = plt.subplots(n, 1, figsize=kwds.get('size', (10, 5)), - sharex=True) - titles = kwds.get('titles', None) - - for k in range(1, n + 1): - ax = axes[k - 1] - ts = args[k - 1] - ax.plot(ts.index, ts.values) - - if titles: - ax.set_title(titles[k - 1]) - - fig.autofmt_xdate() - fig.subplots_adjust(bottom=0.10, top=0.95) diff --git a/doc/plots/stats/moments_ewma.py b/doc/plots/stats/moments_ewma.py deleted file mode 100644 index 3e521ed60bb8f..0000000000000 --- a/doc/plots/stats/moments_ewma.py +++ /dev/null @@ -1,15 +0,0 @@ -import matplotlib.pyplot as plt -import pandas.util.testing as t -import pandas.stats.moments as m - -t.N = 200 -s = t.makeTimeSeries().cumsum() - -plt.figure(figsize=(10, 5)) -plt.plot(s.index, s.values) -plt.plot(s.index, m.ewma(s, 20, min_periods=1).values) -f = plt.gcf() -f.autofmt_xdate() - -plt.show() -plt.close('all') diff --git a/doc/plots/stats/moments_ewmvol.py b/doc/plots/stats/moments_ewmvol.py deleted file mode 100644 index 093f62868fc4e..0000000000000 --- a/doc/plots/stats/moments_ewmvol.py +++ /dev/null @@ -1,23 +0,0 @@ -import matplotlib.pyplot as plt -import pandas.util.testing as t -import pandas.stats.moments as m - -t.N = 500 -ts = t.makeTimeSeries() -ts[::100] = 20 - -s = ts.cumsum() - - -plt.figure(figsize=(10, 5)) -plt.plot(s.index, m.ewmvol(s, span=50, min_periods=1).values, color='b') -plt.plot(s.index, m.rolling_std(s, 50, min_periods=1).values, color='r') - -plt.title('Exp-weighted std with shocks') -plt.legend(('Exp-weighted', 'Equal-weighted')) - -f = plt.gcf() -f.autofmt_xdate() - -plt.show() -plt.close('all') diff --git a/doc/plots/stats/moments_expw.py b/doc/plots/stats/moments_expw.py deleted file mode 100644 index 5fff419b3a940..0000000000000 --- a/doc/plots/stats/moments_expw.py +++ /dev/null @@ -1,35 +0,0 @@ -from moment_plots import * - -np.random.seed(1) - -ts = test_series(500) * 10 - -# ts[::100] = 20 - -s = ts.cumsum() - -fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True) - -ax0, ax1, ax2 = axes - -ax0.plot(s.index, s.values) -ax0.set_title('time series') - -ax1.plot(s.index, m.ewma(s, span=50, min_periods=1).values, color='b') -ax1.plot(s.index, m.rolling_mean(s, 50, min_periods=1).values, color='r') -ax1.set_title('rolling_mean vs. ewma') - -line1 = ax2.plot( - s.index, m.ewmstd(s, span=50, min_periods=1).values, color='b') -line2 = ax2.plot( - s.index, m.rolling_std(s, 50, min_periods=1).values, color='r') -ax2.set_title('rolling_std vs. ewmstd') - -fig.legend((line1, line2), - ('Exp-weighted', 'Equal-weighted'), - loc='upper right') -fig.autofmt_xdate() -fig.subplots_adjust(bottom=0.10, top=0.95) - -plt.show() -plt.close('all') diff --git a/doc/plots/stats/moments_rolling.py b/doc/plots/stats/moments_rolling.py deleted file mode 100644 index 30a6c5f53e20c..0000000000000 --- a/doc/plots/stats/moments_rolling.py +++ /dev/null @@ -1,24 +0,0 @@ -from moment_plots import * - -ts = test_series() -s = ts.cumsum() - -s[20:50] = np.NaN -s[120:150] = np.NaN -plot_timeseries(s, - m.rolling_count(s, 50), - m.rolling_sum(s, 50, min_periods=10), - m.rolling_mean(s, 50, min_periods=10), - m.rolling_std(s, 50, min_periods=10), - m.rolling_skew(s, 50, min_periods=10), - m.rolling_kurt(s, 50, min_periods=10), - size=(10, 12), - titles=('time series', - 'rolling_count', - 'rolling_sum', - 'rolling_mean', - 'rolling_std', - 'rolling_skew', - 'rolling_kurt')) -plt.show() -plt.close('all') diff --git a/doc/plots/stats/moments_rolling_binary.py b/doc/plots/stats/moments_rolling_binary.py deleted file mode 100644 index ab6b7b1c8ff49..0000000000000 --- a/doc/plots/stats/moments_rolling_binary.py +++ /dev/null @@ -1,30 +0,0 @@ -from moment_plots import * - -np.random.seed(1) - -ts = test_series() -s = ts.cumsum() -ts2 = test_series() -s2 = ts2.cumsum() - -s[20:50] = np.NaN -s[120:150] = np.NaN -fig, axes = plt.subplots(3, 1, figsize=(8, 10), sharex=True) - -ax0, ax1, ax2 = axes - -ax0.plot(s.index, s.values) -ax0.plot(s2.index, s2.values) -ax0.set_title('time series') - -ax1.plot(s.index, m.rolling_corr(s, s2, 50, min_periods=1).values) -ax1.set_title('rolling_corr') - -ax2.plot(s.index, m.rolling_cov(s, s2, 50, min_periods=1).values) -ax2.set_title('rolling_cov') - -fig.autofmt_xdate() -fig.subplots_adjust(bottom=0.10, top=0.95) - -plt.show() -plt.close('all') diff --git a/doc/redirects.csv b/doc/redirects.csv new file mode 100644 index 0000000000000..a7886779c97d5 --- /dev/null +++ b/doc/redirects.csv @@ -0,0 +1,1581 @@ +# This file should contain all the redirects in the documentation +# in the format `,` + +# whatsnew +whatsnew,whatsnew/index +release,whatsnew/index + +# getting started +10min,getting_started/10min +basics,getting_started/basics +comparison_with_r,getting_started/comparison/comparison_with_r +comparison_with_sql,getting_started/comparison/comparison_with_sql +comparison_with_sas,getting_started/comparison/comparison_with_sas +comparison_with_stata,getting_started/comparison/comparison_with_stata +dsintro,getting_started/dsintro +overview,getting_started/overview +tutorials,getting_started/tutorials + +# user guide +advanced,user_guide/advanced +categorical,user_guide/categorical +computation,user_guide/computation +cookbook,user_guide/cookbook +enhancingperf,user_guide/enhancingperf +gotchas,user_guide/gotchas +groupby,user_guide/groupby +indexing,user_guide/indexing +integer_na,user_guide/integer_na +io,user_guide/io +merging,user_guide/merging +missing_data,user_guide/missing_data +options,user_guide/options +reshaping,user_guide/reshaping +sparse,user_guide/sparse +style,user_guide/style +text,user_guide/text +timedeltas,user_guide/timedeltas +timeseries,user_guide/timeseries +visualization,user_guide/visualization + +# development +contributing,development/contributing +contributing_docstring,development/contributing_docstring +developer,development/developer +extending,development/extending +internals,development/internals + +# api +api,reference/index +generated/pandas.api.extensions.ExtensionArray.argsort,../reference/api/pandas.api.extensions.ExtensionArray.argsort +generated/pandas.api.extensions.ExtensionArray.astype,../reference/api/pandas.api.extensions.ExtensionArray.astype +generated/pandas.api.extensions.ExtensionArray.copy,../reference/api/pandas.api.extensions.ExtensionArray.copy +generated/pandas.api.extensions.ExtensionArray.dropna,../reference/api/pandas.api.extensions.ExtensionArray.dropna +generated/pandas.api.extensions.ExtensionArray.dtype,../reference/api/pandas.api.extensions.ExtensionArray.dtype +generated/pandas.api.extensions.ExtensionArray.factorize,../reference/api/pandas.api.extensions.ExtensionArray.factorize +generated/pandas.api.extensions.ExtensionArray.fillna,../reference/api/pandas.api.extensions.ExtensionArray.fillna +generated/pandas.api.extensions.ExtensionArray,../reference/api/pandas.api.extensions.ExtensionArray +generated/pandas.api.extensions.ExtensionArray.isna,../reference/api/pandas.api.extensions.ExtensionArray.isna +generated/pandas.api.extensions.ExtensionArray.nbytes,../reference/api/pandas.api.extensions.ExtensionArray.nbytes +generated/pandas.api.extensions.ExtensionArray.ndim,../reference/api/pandas.api.extensions.ExtensionArray.ndim +generated/pandas.api.extensions.ExtensionArray.shape,../reference/api/pandas.api.extensions.ExtensionArray.shape +generated/pandas.api.extensions.ExtensionArray.take,../reference/api/pandas.api.extensions.ExtensionArray.take +generated/pandas.api.extensions.ExtensionArray.unique,../reference/api/pandas.api.extensions.ExtensionArray.unique +generated/pandas.api.extensions.ExtensionDtype.construct_array_type,../reference/api/pandas.api.extensions.ExtensionDtype.construct_array_type +generated/pandas.api.extensions.ExtensionDtype.construct_from_string,../reference/api/pandas.api.extensions.ExtensionDtype.construct_from_string +generated/pandas.api.extensions.ExtensionDtype,../reference/api/pandas.api.extensions.ExtensionDtype +generated/pandas.api.extensions.ExtensionDtype.is_dtype,../reference/api/pandas.api.extensions.ExtensionDtype.is_dtype +generated/pandas.api.extensions.ExtensionDtype.kind,../reference/api/pandas.api.extensions.ExtensionDtype.kind +generated/pandas.api.extensions.ExtensionDtype.name,../reference/api/pandas.api.extensions.ExtensionDtype.name +generated/pandas.api.extensions.ExtensionDtype.names,../reference/api/pandas.api.extensions.ExtensionDtype.names +generated/pandas.api.extensions.ExtensionDtype.na_value,../reference/api/pandas.api.extensions.ExtensionDtype.na_value +generated/pandas.api.extensions.ExtensionDtype.type,../reference/api/pandas.api.extensions.ExtensionDtype.type +generated/pandas.api.extensions.register_dataframe_accessor,../reference/api/pandas.api.extensions.register_dataframe_accessor +generated/pandas.api.extensions.register_extension_dtype,../reference/api/pandas.api.extensions.register_extension_dtype +generated/pandas.api.extensions.register_index_accessor,../reference/api/pandas.api.extensions.register_index_accessor +generated/pandas.api.extensions.register_series_accessor,../reference/api/pandas.api.extensions.register_series_accessor +generated/pandas.api.types.infer_dtype,../reference/api/pandas.api.types.infer_dtype +generated/pandas.api.types.is_bool_dtype,../reference/api/pandas.api.types.is_bool_dtype +generated/pandas.api.types.is_bool,../reference/api/pandas.api.types.is_bool +generated/pandas.api.types.is_categorical_dtype,../reference/api/pandas.api.types.is_categorical_dtype +generated/pandas.api.types.is_categorical,../reference/api/pandas.api.types.is_categorical +generated/pandas.api.types.is_complex_dtype,../reference/api/pandas.api.types.is_complex_dtype +generated/pandas.api.types.is_complex,../reference/api/pandas.api.types.is_complex +generated/pandas.api.types.is_datetime64_any_dtype,../reference/api/pandas.api.types.is_datetime64_any_dtype +generated/pandas.api.types.is_datetime64_dtype,../reference/api/pandas.api.types.is_datetime64_dtype +generated/pandas.api.types.is_datetime64_ns_dtype,../reference/api/pandas.api.types.is_datetime64_ns_dtype +generated/pandas.api.types.is_datetime64tz_dtype,../reference/api/pandas.api.types.is_datetime64tz_dtype +generated/pandas.api.types.is_datetimetz,../reference/api/pandas.api.types.is_datetimetz +generated/pandas.api.types.is_dict_like,../reference/api/pandas.api.types.is_dict_like +generated/pandas.api.types.is_extension_array_dtype,../reference/api/pandas.api.types.is_extension_array_dtype +generated/pandas.api.types.is_extension_type,../reference/api/pandas.api.types.is_extension_type +generated/pandas.api.types.is_file_like,../reference/api/pandas.api.types.is_file_like +generated/pandas.api.types.is_float_dtype,../reference/api/pandas.api.types.is_float_dtype +generated/pandas.api.types.is_float,../reference/api/pandas.api.types.is_float +generated/pandas.api.types.is_hashable,../reference/api/pandas.api.types.is_hashable +generated/pandas.api.types.is_int64_dtype,../reference/api/pandas.api.types.is_int64_dtype +generated/pandas.api.types.is_integer_dtype,../reference/api/pandas.api.types.is_integer_dtype +generated/pandas.api.types.is_integer,../reference/api/pandas.api.types.is_integer +generated/pandas.api.types.is_interval_dtype,../reference/api/pandas.api.types.is_interval_dtype +generated/pandas.api.types.is_interval,../reference/api/pandas.api.types.is_interval +generated/pandas.api.types.is_iterator,../reference/api/pandas.api.types.is_iterator +generated/pandas.api.types.is_list_like,../reference/api/pandas.api.types.is_list_like +generated/pandas.api.types.is_named_tuple,../reference/api/pandas.api.types.is_named_tuple +generated/pandas.api.types.is_number,../reference/api/pandas.api.types.is_number +generated/pandas.api.types.is_numeric_dtype,../reference/api/pandas.api.types.is_numeric_dtype +generated/pandas.api.types.is_object_dtype,../reference/api/pandas.api.types.is_object_dtype +generated/pandas.api.types.is_period_dtype,../reference/api/pandas.api.types.is_period_dtype +generated/pandas.api.types.is_period,../reference/api/pandas.api.types.is_period +generated/pandas.api.types.is_re_compilable,../reference/api/pandas.api.types.is_re_compilable +generated/pandas.api.types.is_re,../reference/api/pandas.api.types.is_re +generated/pandas.api.types.is_scalar,../reference/api/pandas.api.types.is_scalar +generated/pandas.api.types.is_signed_integer_dtype,../reference/api/pandas.api.types.is_signed_integer_dtype +generated/pandas.api.types.is_sparse,../reference/api/pandas.api.types.is_sparse +generated/pandas.api.types.is_string_dtype,../reference/api/pandas.api.types.is_string_dtype +generated/pandas.api.types.is_timedelta64_dtype,../reference/api/pandas.api.types.is_timedelta64_dtype +generated/pandas.api.types.is_timedelta64_ns_dtype,../reference/api/pandas.api.types.is_timedelta64_ns_dtype +generated/pandas.api.types.is_unsigned_integer_dtype,../reference/api/pandas.api.types.is_unsigned_integer_dtype +generated/pandas.api.types.pandas_dtype,../reference/api/pandas.api.types.pandas_dtype +generated/pandas.api.types.union_categoricals,../reference/api/pandas.api.types.union_categoricals +generated/pandas.bdate_range,../reference/api/pandas.bdate_range +generated/pandas.Categorical.__array__,../reference/api/pandas.Categorical.__array__ +generated/pandas.Categorical.categories,../reference/api/pandas.Categorical.categories +generated/pandas.Categorical.codes,../reference/api/pandas.Categorical.codes +generated/pandas.CategoricalDtype.categories,../reference/api/pandas.CategoricalDtype.categories +generated/pandas.Categorical.dtype,../reference/api/pandas.Categorical.dtype +generated/pandas.CategoricalDtype,../reference/api/pandas.CategoricalDtype +generated/pandas.CategoricalDtype.ordered,../reference/api/pandas.CategoricalDtype.ordered +generated/pandas.Categorical.from_codes,../reference/api/pandas.Categorical.from_codes +generated/pandas.Categorical,../reference/api/pandas.Categorical +generated/pandas.CategoricalIndex.add_categories,../reference/api/pandas.CategoricalIndex.add_categories +generated/pandas.CategoricalIndex.as_ordered,../reference/api/pandas.CategoricalIndex.as_ordered +generated/pandas.CategoricalIndex.as_unordered,../reference/api/pandas.CategoricalIndex.as_unordered +generated/pandas.CategoricalIndex.categories,../reference/api/pandas.CategoricalIndex.categories +generated/pandas.CategoricalIndex.codes,../reference/api/pandas.CategoricalIndex.codes +generated/pandas.CategoricalIndex.equals,../reference/api/pandas.CategoricalIndex.equals +generated/pandas.CategoricalIndex,../reference/api/pandas.CategoricalIndex +generated/pandas.CategoricalIndex.map,../reference/api/pandas.CategoricalIndex.map +generated/pandas.CategoricalIndex.ordered,../reference/api/pandas.CategoricalIndex.ordered +generated/pandas.CategoricalIndex.remove_categories,../reference/api/pandas.CategoricalIndex.remove_categories +generated/pandas.CategoricalIndex.remove_unused_categories,../reference/api/pandas.CategoricalIndex.remove_unused_categories +generated/pandas.CategoricalIndex.rename_categories,../reference/api/pandas.CategoricalIndex.rename_categories +generated/pandas.CategoricalIndex.reorder_categories,../reference/api/pandas.CategoricalIndex.reorder_categories +generated/pandas.CategoricalIndex.set_categories,../reference/api/pandas.CategoricalIndex.set_categories +generated/pandas.Categorical.ordered,../reference/api/pandas.Categorical.ordered +generated/pandas.concat,../reference/api/pandas.concat +generated/pandas.core.groupby.DataFrameGroupBy.all,../reference/api/pandas.core.groupby.DataFrameGroupBy.all +generated/pandas.core.groupby.DataFrameGroupBy.any,../reference/api/pandas.core.groupby.DataFrameGroupBy.any +generated/pandas.core.groupby.DataFrameGroupBy.bfill,../reference/api/pandas.core.groupby.DataFrameGroupBy.bfill +generated/pandas.core.groupby.DataFrameGroupBy.boxplot,../reference/api/pandas.core.groupby.DataFrameGroupBy.boxplot +generated/pandas.core.groupby.DataFrameGroupBy.corr,../reference/api/pandas.core.groupby.DataFrameGroupBy.corr +generated/pandas.core.groupby.DataFrameGroupBy.corrwith,../reference/api/pandas.core.groupby.DataFrameGroupBy.corrwith +generated/pandas.core.groupby.DataFrameGroupBy.count,../reference/api/pandas.core.groupby.DataFrameGroupBy.count +generated/pandas.core.groupby.DataFrameGroupBy.cov,../reference/api/pandas.core.groupby.DataFrameGroupBy.cov +generated/pandas.core.groupby.DataFrameGroupBy.cummax,../reference/api/pandas.core.groupby.DataFrameGroupBy.cummax +generated/pandas.core.groupby.DataFrameGroupBy.cummin,../reference/api/pandas.core.groupby.DataFrameGroupBy.cummin +generated/pandas.core.groupby.DataFrameGroupBy.cumprod,../reference/api/pandas.core.groupby.DataFrameGroupBy.cumprod +generated/pandas.core.groupby.DataFrameGroupBy.cumsum,../reference/api/pandas.core.groupby.DataFrameGroupBy.cumsum +generated/pandas.core.groupby.DataFrameGroupBy.describe,../reference/api/pandas.core.groupby.DataFrameGroupBy.describe +generated/pandas.core.groupby.DataFrameGroupBy.diff,../reference/api/pandas.core.groupby.DataFrameGroupBy.diff +generated/pandas.core.groupby.DataFrameGroupBy.ffill,../reference/api/pandas.core.groupby.DataFrameGroupBy.ffill +generated/pandas.core.groupby.DataFrameGroupBy.fillna,../reference/api/pandas.core.groupby.DataFrameGroupBy.fillna +generated/pandas.core.groupby.DataFrameGroupBy.filter,../reference/api/pandas.core.groupby.DataFrameGroupBy.filter +generated/pandas.core.groupby.DataFrameGroupBy.hist,../reference/api/pandas.core.groupby.DataFrameGroupBy.hist +generated/pandas.core.groupby.DataFrameGroupBy.idxmax,../reference/api/pandas.core.groupby.DataFrameGroupBy.idxmax +generated/pandas.core.groupby.DataFrameGroupBy.idxmin,../reference/api/pandas.core.groupby.DataFrameGroupBy.idxmin +generated/pandas.core.groupby.DataFrameGroupBy.mad,../reference/api/pandas.core.groupby.DataFrameGroupBy.mad +generated/pandas.core.groupby.DataFrameGroupBy.pct_change,../reference/api/pandas.core.groupby.DataFrameGroupBy.pct_change +generated/pandas.core.groupby.DataFrameGroupBy.plot,../reference/api/pandas.core.groupby.DataFrameGroupBy.plot +generated/pandas.core.groupby.DataFrameGroupBy.quantile,../reference/api/pandas.core.groupby.DataFrameGroupBy.quantile +generated/pandas.core.groupby.DataFrameGroupBy.rank,../reference/api/pandas.core.groupby.DataFrameGroupBy.rank +generated/pandas.core.groupby.DataFrameGroupBy.resample,../reference/api/pandas.core.groupby.DataFrameGroupBy.resample +generated/pandas.core.groupby.DataFrameGroupBy.shift,../reference/api/pandas.core.groupby.DataFrameGroupBy.shift +generated/pandas.core.groupby.DataFrameGroupBy.size,../reference/api/pandas.core.groupby.DataFrameGroupBy.size +generated/pandas.core.groupby.DataFrameGroupBy.skew,../reference/api/pandas.core.groupby.DataFrameGroupBy.skew +generated/pandas.core.groupby.DataFrameGroupBy.take,../reference/api/pandas.core.groupby.DataFrameGroupBy.take +generated/pandas.core.groupby.DataFrameGroupBy.tshift,../reference/api/pandas.core.groupby.DataFrameGroupBy.tshift +generated/pandas.core.groupby.GroupBy.agg,../reference/api/pandas.core.groupby.GroupBy.agg +generated/pandas.core.groupby.GroupBy.aggregate,../reference/api/pandas.core.groupby.GroupBy.aggregate +generated/pandas.core.groupby.GroupBy.all,../reference/api/pandas.core.groupby.GroupBy.all +generated/pandas.core.groupby.GroupBy.any,../reference/api/pandas.core.groupby.GroupBy.any +generated/pandas.core.groupby.GroupBy.apply,../reference/api/pandas.core.groupby.GroupBy.apply +generated/pandas.core.groupby.GroupBy.bfill,../reference/api/pandas.core.groupby.GroupBy.bfill +generated/pandas.core.groupby.GroupBy.count,../reference/api/pandas.core.groupby.GroupBy.count +generated/pandas.core.groupby.GroupBy.cumcount,../reference/api/pandas.core.groupby.GroupBy.cumcount +generated/pandas.core.groupby.GroupBy.ffill,../reference/api/pandas.core.groupby.GroupBy.ffill +generated/pandas.core.groupby.GroupBy.first,../reference/api/pandas.core.groupby.GroupBy.first +generated/pandas.core.groupby.GroupBy.get_group,../reference/api/pandas.core.groupby.GroupBy.get_group +generated/pandas.core.groupby.GroupBy.groups,../reference/api/pandas.core.groupby.GroupBy.groups +generated/pandas.core.groupby.GroupBy.head,../reference/api/pandas.core.groupby.GroupBy.head +generated/pandas.core.groupby.GroupBy.indices,../reference/api/pandas.core.groupby.GroupBy.indices +generated/pandas.core.groupby.GroupBy.__iter__,../reference/api/pandas.core.groupby.GroupBy.__iter__ +generated/pandas.core.groupby.GroupBy.last,../reference/api/pandas.core.groupby.GroupBy.last +generated/pandas.core.groupby.GroupBy.max,../reference/api/pandas.core.groupby.GroupBy.max +generated/pandas.core.groupby.GroupBy.mean,../reference/api/pandas.core.groupby.GroupBy.mean +generated/pandas.core.groupby.GroupBy.median,../reference/api/pandas.core.groupby.GroupBy.median +generated/pandas.core.groupby.GroupBy.min,../reference/api/pandas.core.groupby.GroupBy.min +generated/pandas.core.groupby.GroupBy.ngroup,../reference/api/pandas.core.groupby.GroupBy.ngroup +generated/pandas.core.groupby.GroupBy.nth,../reference/api/pandas.core.groupby.GroupBy.nth +generated/pandas.core.groupby.GroupBy.ohlc,../reference/api/pandas.core.groupby.GroupBy.ohlc +generated/pandas.core.groupby.GroupBy.pct_change,../reference/api/pandas.core.groupby.GroupBy.pct_change +generated/pandas.core.groupby.GroupBy.pipe,../reference/api/pandas.core.groupby.GroupBy.pipe +generated/pandas.core.groupby.GroupBy.prod,../reference/api/pandas.core.groupby.GroupBy.prod +generated/pandas.core.groupby.GroupBy.rank,../reference/api/pandas.core.groupby.GroupBy.rank +generated/pandas.core.groupby.GroupBy.sem,../reference/api/pandas.core.groupby.GroupBy.sem +generated/pandas.core.groupby.GroupBy.size,../reference/api/pandas.core.groupby.GroupBy.size +generated/pandas.core.groupby.GroupBy.std,../reference/api/pandas.core.groupby.GroupBy.std +generated/pandas.core.groupby.GroupBy.sum,../reference/api/pandas.core.groupby.GroupBy.sum +generated/pandas.core.groupby.GroupBy.tail,../reference/api/pandas.core.groupby.GroupBy.tail +generated/pandas.core.groupby.GroupBy.transform,../reference/api/pandas.core.groupby.GroupBy.transform +generated/pandas.core.groupby.GroupBy.var,../reference/api/pandas.core.groupby.GroupBy.var +generated/pandas.core.groupby.SeriesGroupBy.is_monotonic_decreasing,../reference/api/pandas.core.groupby.SeriesGroupBy.is_monotonic_decreasing +generated/pandas.core.groupby.SeriesGroupBy.is_monotonic_increasing,../reference/api/pandas.core.groupby.SeriesGroupBy.is_monotonic_increasing +generated/pandas.core.groupby.SeriesGroupBy.nlargest,../reference/api/pandas.core.groupby.SeriesGroupBy.nlargest +generated/pandas.core.groupby.SeriesGroupBy.nsmallest,../reference/api/pandas.core.groupby.SeriesGroupBy.nsmallest +generated/pandas.core.groupby.SeriesGroupBy.nunique,../reference/api/pandas.core.groupby.SeriesGroupBy.nunique +generated/pandas.core.groupby.SeriesGroupBy.unique,../reference/api/pandas.core.groupby.SeriesGroupBy.unique +generated/pandas.core.groupby.SeriesGroupBy.value_counts,../reference/api/pandas.core.groupby.SeriesGroupBy.value_counts +generated/pandas.core.resample.Resampler.aggregate,../reference/api/pandas.core.resample.Resampler.aggregate +generated/pandas.core.resample.Resampler.apply,../reference/api/pandas.core.resample.Resampler.apply +generated/pandas.core.resample.Resampler.asfreq,../reference/api/pandas.core.resample.Resampler.asfreq +generated/pandas.core.resample.Resampler.backfill,../reference/api/pandas.core.resample.Resampler.backfill +generated/pandas.core.resample.Resampler.bfill,../reference/api/pandas.core.resample.Resampler.bfill +generated/pandas.core.resample.Resampler.count,../reference/api/pandas.core.resample.Resampler.count +generated/pandas.core.resample.Resampler.ffill,../reference/api/pandas.core.resample.Resampler.ffill +generated/pandas.core.resample.Resampler.fillna,../reference/api/pandas.core.resample.Resampler.fillna +generated/pandas.core.resample.Resampler.first,../reference/api/pandas.core.resample.Resampler.first +generated/pandas.core.resample.Resampler.get_group,../reference/api/pandas.core.resample.Resampler.get_group +generated/pandas.core.resample.Resampler.groups,../reference/api/pandas.core.resample.Resampler.groups +generated/pandas.core.resample.Resampler.indices,../reference/api/pandas.core.resample.Resampler.indices +generated/pandas.core.resample.Resampler.interpolate,../reference/api/pandas.core.resample.Resampler.interpolate +generated/pandas.core.resample.Resampler.__iter__,../reference/api/pandas.core.resample.Resampler.__iter__ +generated/pandas.core.resample.Resampler.last,../reference/api/pandas.core.resample.Resampler.last +generated/pandas.core.resample.Resampler.max,../reference/api/pandas.core.resample.Resampler.max +generated/pandas.core.resample.Resampler.mean,../reference/api/pandas.core.resample.Resampler.mean +generated/pandas.core.resample.Resampler.median,../reference/api/pandas.core.resample.Resampler.median +generated/pandas.core.resample.Resampler.min,../reference/api/pandas.core.resample.Resampler.min +generated/pandas.core.resample.Resampler.nearest,../reference/api/pandas.core.resample.Resampler.nearest +generated/pandas.core.resample.Resampler.nunique,../reference/api/pandas.core.resample.Resampler.nunique +generated/pandas.core.resample.Resampler.ohlc,../reference/api/pandas.core.resample.Resampler.ohlc +generated/pandas.core.resample.Resampler.pad,../reference/api/pandas.core.resample.Resampler.pad +generated/pandas.core.resample.Resampler.pipe,../reference/api/pandas.core.resample.Resampler.pipe +generated/pandas.core.resample.Resampler.prod,../reference/api/pandas.core.resample.Resampler.prod +generated/pandas.core.resample.Resampler.quantile,../reference/api/pandas.core.resample.Resampler.quantile +generated/pandas.core.resample.Resampler.sem,../reference/api/pandas.core.resample.Resampler.sem +generated/pandas.core.resample.Resampler.size,../reference/api/pandas.core.resample.Resampler.size +generated/pandas.core.resample.Resampler.std,../reference/api/pandas.core.resample.Resampler.std +generated/pandas.core.resample.Resampler.sum,../reference/api/pandas.core.resample.Resampler.sum +generated/pandas.core.resample.Resampler.transform,../reference/api/pandas.core.resample.Resampler.transform +generated/pandas.core.resample.Resampler.var,../reference/api/pandas.core.resample.Resampler.var +generated/pandas.core.window.EWM.corr,../reference/api/pandas.core.window.EWM.corr +generated/pandas.core.window.EWM.cov,../reference/api/pandas.core.window.EWM.cov +generated/pandas.core.window.EWM.mean,../reference/api/pandas.core.window.EWM.mean +generated/pandas.core.window.EWM.std,../reference/api/pandas.core.window.EWM.std +generated/pandas.core.window.EWM.var,../reference/api/pandas.core.window.EWM.var +generated/pandas.core.window.Expanding.aggregate,../reference/api/pandas.core.window.Expanding.aggregate +generated/pandas.core.window.Expanding.apply,../reference/api/pandas.core.window.Expanding.apply +generated/pandas.core.window.Expanding.corr,../reference/api/pandas.core.window.Expanding.corr +generated/pandas.core.window.Expanding.count,../reference/api/pandas.core.window.Expanding.count +generated/pandas.core.window.Expanding.cov,../reference/api/pandas.core.window.Expanding.cov +generated/pandas.core.window.Expanding.kurt,../reference/api/pandas.core.window.Expanding.kurt +generated/pandas.core.window.Expanding.max,../reference/api/pandas.core.window.Expanding.max +generated/pandas.core.window.Expanding.mean,../reference/api/pandas.core.window.Expanding.mean +generated/pandas.core.window.Expanding.median,../reference/api/pandas.core.window.Expanding.median +generated/pandas.core.window.Expanding.min,../reference/api/pandas.core.window.Expanding.min +generated/pandas.core.window.Expanding.quantile,../reference/api/pandas.core.window.Expanding.quantile +generated/pandas.core.window.Expanding.skew,../reference/api/pandas.core.window.Expanding.skew +generated/pandas.core.window.Expanding.std,../reference/api/pandas.core.window.Expanding.std +generated/pandas.core.window.Expanding.sum,../reference/api/pandas.core.window.Expanding.sum +generated/pandas.core.window.Expanding.var,../reference/api/pandas.core.window.Expanding.var +generated/pandas.core.window.Rolling.aggregate,../reference/api/pandas.core.window.Rolling.aggregate +generated/pandas.core.window.Rolling.apply,../reference/api/pandas.core.window.Rolling.apply +generated/pandas.core.window.Rolling.corr,../reference/api/pandas.core.window.Rolling.corr +generated/pandas.core.window.Rolling.count,../reference/api/pandas.core.window.Rolling.count +generated/pandas.core.window.Rolling.cov,../reference/api/pandas.core.window.Rolling.cov +generated/pandas.core.window.Rolling.kurt,../reference/api/pandas.core.window.Rolling.kurt +generated/pandas.core.window.Rolling.max,../reference/api/pandas.core.window.Rolling.max +generated/pandas.core.window.Rolling.mean,../reference/api/pandas.core.window.Rolling.mean +generated/pandas.core.window.Rolling.median,../reference/api/pandas.core.window.Rolling.median +generated/pandas.core.window.Rolling.min,../reference/api/pandas.core.window.Rolling.min +generated/pandas.core.window.Rolling.quantile,../reference/api/pandas.core.window.Rolling.quantile +generated/pandas.core.window.Rolling.skew,../reference/api/pandas.core.window.Rolling.skew +generated/pandas.core.window.Rolling.std,../reference/api/pandas.core.window.Rolling.std +generated/pandas.core.window.Rolling.sum,../reference/api/pandas.core.window.Rolling.sum +generated/pandas.core.window.Rolling.var,../reference/api/pandas.core.window.Rolling.var +generated/pandas.core.window.Window.mean,../reference/api/pandas.core.window.Window.mean +generated/pandas.core.window.Window.sum,../reference/api/pandas.core.window.Window.sum +generated/pandas.crosstab,../reference/api/pandas.crosstab +generated/pandas.cut,../reference/api/pandas.cut +generated/pandas.DataFrame.abs,../reference/api/pandas.DataFrame.abs +generated/pandas.DataFrame.add,../reference/api/pandas.DataFrame.add +generated/pandas.DataFrame.add_prefix,../reference/api/pandas.DataFrame.add_prefix +generated/pandas.DataFrame.add_suffix,../reference/api/pandas.DataFrame.add_suffix +generated/pandas.DataFrame.agg,../reference/api/pandas.DataFrame.agg +generated/pandas.DataFrame.aggregate,../reference/api/pandas.DataFrame.aggregate +generated/pandas.DataFrame.align,../reference/api/pandas.DataFrame.align +generated/pandas.DataFrame.all,../reference/api/pandas.DataFrame.all +generated/pandas.DataFrame.any,../reference/api/pandas.DataFrame.any +generated/pandas.DataFrame.append,../reference/api/pandas.DataFrame.append +generated/pandas.DataFrame.apply,../reference/api/pandas.DataFrame.apply +generated/pandas.DataFrame.applymap,../reference/api/pandas.DataFrame.applymap +generated/pandas.DataFrame.as_blocks,../reference/api/pandas.DataFrame.as_blocks +generated/pandas.DataFrame.asfreq,../reference/api/pandas.DataFrame.asfreq +generated/pandas.DataFrame.as_matrix,../reference/api/pandas.DataFrame.as_matrix +generated/pandas.DataFrame.asof,../reference/api/pandas.DataFrame.asof +generated/pandas.DataFrame.assign,../reference/api/pandas.DataFrame.assign +generated/pandas.DataFrame.astype,../reference/api/pandas.DataFrame.astype +generated/pandas.DataFrame.at,../reference/api/pandas.DataFrame.at +generated/pandas.DataFrame.at_time,../reference/api/pandas.DataFrame.at_time +generated/pandas.DataFrame.axes,../reference/api/pandas.DataFrame.axes +generated/pandas.DataFrame.between_time,../reference/api/pandas.DataFrame.between_time +generated/pandas.DataFrame.bfill,../reference/api/pandas.DataFrame.bfill +generated/pandas.DataFrame.blocks,../reference/api/pandas.DataFrame.blocks +generated/pandas.DataFrame.bool,../reference/api/pandas.DataFrame.bool +generated/pandas.DataFrame.boxplot,../reference/api/pandas.DataFrame.boxplot +generated/pandas.DataFrame.clip,../reference/api/pandas.DataFrame.clip +generated/pandas.DataFrame.clip_lower,../reference/api/pandas.DataFrame.clip_lower +generated/pandas.DataFrame.clip_upper,../reference/api/pandas.DataFrame.clip_upper +generated/pandas.DataFrame.columns,../reference/api/pandas.DataFrame.columns +generated/pandas.DataFrame.combine_first,../reference/api/pandas.DataFrame.combine_first +generated/pandas.DataFrame.combine,../reference/api/pandas.DataFrame.combine +generated/pandas.DataFrame.compound,../reference/api/pandas.DataFrame.compound +generated/pandas.DataFrame.convert_objects,../reference/api/pandas.DataFrame.convert_objects +generated/pandas.DataFrame.copy,../reference/api/pandas.DataFrame.copy +generated/pandas.DataFrame.corr,../reference/api/pandas.DataFrame.corr +generated/pandas.DataFrame.corrwith,../reference/api/pandas.DataFrame.corrwith +generated/pandas.DataFrame.count,../reference/api/pandas.DataFrame.count +generated/pandas.DataFrame.cov,../reference/api/pandas.DataFrame.cov +generated/pandas.DataFrame.cummax,../reference/api/pandas.DataFrame.cummax +generated/pandas.DataFrame.cummin,../reference/api/pandas.DataFrame.cummin +generated/pandas.DataFrame.cumprod,../reference/api/pandas.DataFrame.cumprod +generated/pandas.DataFrame.cumsum,../reference/api/pandas.DataFrame.cumsum +generated/pandas.DataFrame.describe,../reference/api/pandas.DataFrame.describe +generated/pandas.DataFrame.diff,../reference/api/pandas.DataFrame.diff +generated/pandas.DataFrame.div,../reference/api/pandas.DataFrame.div +generated/pandas.DataFrame.divide,../reference/api/pandas.DataFrame.divide +generated/pandas.DataFrame.dot,../reference/api/pandas.DataFrame.dot +generated/pandas.DataFrame.drop_duplicates,../reference/api/pandas.DataFrame.drop_duplicates +generated/pandas.DataFrame.drop,../reference/api/pandas.DataFrame.drop +generated/pandas.DataFrame.droplevel,../reference/api/pandas.DataFrame.droplevel +generated/pandas.DataFrame.dropna,../reference/api/pandas.DataFrame.dropna +generated/pandas.DataFrame.dtypes,../reference/api/pandas.DataFrame.dtypes +generated/pandas.DataFrame.duplicated,../reference/api/pandas.DataFrame.duplicated +generated/pandas.DataFrame.empty,../reference/api/pandas.DataFrame.empty +generated/pandas.DataFrame.eq,../reference/api/pandas.DataFrame.eq +generated/pandas.DataFrame.equals,../reference/api/pandas.DataFrame.equals +generated/pandas.DataFrame.eval,../reference/api/pandas.DataFrame.eval +generated/pandas.DataFrame.ewm,../reference/api/pandas.DataFrame.ewm +generated/pandas.DataFrame.expanding,../reference/api/pandas.DataFrame.expanding +generated/pandas.DataFrame.ffill,../reference/api/pandas.DataFrame.ffill +generated/pandas.DataFrame.fillna,../reference/api/pandas.DataFrame.fillna +generated/pandas.DataFrame.filter,../reference/api/pandas.DataFrame.filter +generated/pandas.DataFrame.first,../reference/api/pandas.DataFrame.first +generated/pandas.DataFrame.first_valid_index,../reference/api/pandas.DataFrame.first_valid_index +generated/pandas.DataFrame.floordiv,../reference/api/pandas.DataFrame.floordiv +generated/pandas.DataFrame.from_csv,../reference/api/pandas.DataFrame.from_csv +generated/pandas.DataFrame.from_dict,../reference/api/pandas.DataFrame.from_dict +generated/pandas.DataFrame.from_items,../reference/api/pandas.DataFrame.from_items +generated/pandas.DataFrame.from_records,../reference/api/pandas.DataFrame.from_records +generated/pandas.DataFrame.ftypes,../reference/api/pandas.DataFrame.ftypes +generated/pandas.DataFrame.ge,../reference/api/pandas.DataFrame.ge +generated/pandas.DataFrame.get_dtype_counts,../reference/api/pandas.DataFrame.get_dtype_counts +generated/pandas.DataFrame.get_ftype_counts,../reference/api/pandas.DataFrame.get_ftype_counts +generated/pandas.DataFrame.get,../reference/api/pandas.DataFrame.get +generated/pandas.DataFrame.get_value,../reference/api/pandas.DataFrame.get_value +generated/pandas.DataFrame.get_values,../reference/api/pandas.DataFrame.get_values +generated/pandas.DataFrame.groupby,../reference/api/pandas.DataFrame.groupby +generated/pandas.DataFrame.gt,../reference/api/pandas.DataFrame.gt +generated/pandas.DataFrame.head,../reference/api/pandas.DataFrame.head +generated/pandas.DataFrame.hist,../reference/api/pandas.DataFrame.hist +generated/pandas.DataFrame,../reference/api/pandas.DataFrame +generated/pandas.DataFrame.iat,../reference/api/pandas.DataFrame.iat +generated/pandas.DataFrame.idxmax,../reference/api/pandas.DataFrame.idxmax +generated/pandas.DataFrame.idxmin,../reference/api/pandas.DataFrame.idxmin +generated/pandas.DataFrame.iloc,../reference/api/pandas.DataFrame.iloc +generated/pandas.DataFrame.index,../reference/api/pandas.DataFrame.index +generated/pandas.DataFrame.infer_objects,../reference/api/pandas.DataFrame.infer_objects +generated/pandas.DataFrame.info,../reference/api/pandas.DataFrame.info +generated/pandas.DataFrame.insert,../reference/api/pandas.DataFrame.insert +generated/pandas.DataFrame.interpolate,../reference/api/pandas.DataFrame.interpolate +generated/pandas.DataFrame.is_copy,../reference/api/pandas.DataFrame.is_copy +generated/pandas.DataFrame.isin,../reference/api/pandas.DataFrame.isin +generated/pandas.DataFrame.isna,../reference/api/pandas.DataFrame.isna +generated/pandas.DataFrame.isnull,../reference/api/pandas.DataFrame.isnull +generated/pandas.DataFrame.items,../reference/api/pandas.DataFrame.items +generated/pandas.DataFrame.__iter__,../reference/api/pandas.DataFrame.__iter__ +generated/pandas.DataFrame.iteritems,../reference/api/pandas.DataFrame.iteritems +generated/pandas.DataFrame.iterrows,../reference/api/pandas.DataFrame.iterrows +generated/pandas.DataFrame.itertuples,../reference/api/pandas.DataFrame.itertuples +generated/pandas.DataFrame.ix,../reference/api/pandas.DataFrame.ix +generated/pandas.DataFrame.join,../reference/api/pandas.DataFrame.join +generated/pandas.DataFrame.keys,../reference/api/pandas.DataFrame.keys +generated/pandas.DataFrame.kurt,../reference/api/pandas.DataFrame.kurt +generated/pandas.DataFrame.kurtosis,../reference/api/pandas.DataFrame.kurtosis +generated/pandas.DataFrame.last,../reference/api/pandas.DataFrame.last +generated/pandas.DataFrame.last_valid_index,../reference/api/pandas.DataFrame.last_valid_index +generated/pandas.DataFrame.le,../reference/api/pandas.DataFrame.le +generated/pandas.DataFrame.loc,../reference/api/pandas.DataFrame.loc +generated/pandas.DataFrame.lookup,../reference/api/pandas.DataFrame.lookup +generated/pandas.DataFrame.lt,../reference/api/pandas.DataFrame.lt +generated/pandas.DataFrame.mad,../reference/api/pandas.DataFrame.mad +generated/pandas.DataFrame.mask,../reference/api/pandas.DataFrame.mask +generated/pandas.DataFrame.max,../reference/api/pandas.DataFrame.max +generated/pandas.DataFrame.mean,../reference/api/pandas.DataFrame.mean +generated/pandas.DataFrame.median,../reference/api/pandas.DataFrame.median +generated/pandas.DataFrame.melt,../reference/api/pandas.DataFrame.melt +generated/pandas.DataFrame.memory_usage,../reference/api/pandas.DataFrame.memory_usage +generated/pandas.DataFrame.merge,../reference/api/pandas.DataFrame.merge +generated/pandas.DataFrame.min,../reference/api/pandas.DataFrame.min +generated/pandas.DataFrame.mode,../reference/api/pandas.DataFrame.mode +generated/pandas.DataFrame.mod,../reference/api/pandas.DataFrame.mod +generated/pandas.DataFrame.mul,../reference/api/pandas.DataFrame.mul +generated/pandas.DataFrame.multiply,../reference/api/pandas.DataFrame.multiply +generated/pandas.DataFrame.ndim,../reference/api/pandas.DataFrame.ndim +generated/pandas.DataFrame.ne,../reference/api/pandas.DataFrame.ne +generated/pandas.DataFrame.nlargest,../reference/api/pandas.DataFrame.nlargest +generated/pandas.DataFrame.notna,../reference/api/pandas.DataFrame.notna +generated/pandas.DataFrame.notnull,../reference/api/pandas.DataFrame.notnull +generated/pandas.DataFrame.nsmallest,../reference/api/pandas.DataFrame.nsmallest +generated/pandas.DataFrame.nunique,../reference/api/pandas.DataFrame.nunique +generated/pandas.DataFrame.pct_change,../reference/api/pandas.DataFrame.pct_change +generated/pandas.DataFrame.pipe,../reference/api/pandas.DataFrame.pipe +generated/pandas.DataFrame.pivot,../reference/api/pandas.DataFrame.pivot +generated/pandas.DataFrame.pivot_table,../reference/api/pandas.DataFrame.pivot_table +generated/pandas.DataFrame.plot.barh,../reference/api/pandas.DataFrame.plot.barh +generated/pandas.DataFrame.plot.bar,../reference/api/pandas.DataFrame.plot.bar +generated/pandas.DataFrame.plot.box,../reference/api/pandas.DataFrame.plot.box +generated/pandas.DataFrame.plot.density,../reference/api/pandas.DataFrame.plot.density +generated/pandas.DataFrame.plot.hexbin,../reference/api/pandas.DataFrame.plot.hexbin +generated/pandas.DataFrame.plot.hist,../reference/api/pandas.DataFrame.plot.hist +generated/pandas.DataFrame.plot,../reference/api/pandas.DataFrame.plot +generated/pandas.DataFrame.plot.kde,../reference/api/pandas.DataFrame.plot.kde +generated/pandas.DataFrame.plot.line,../reference/api/pandas.DataFrame.plot.line +generated/pandas.DataFrame.plot.pie,../reference/api/pandas.DataFrame.plot.pie +generated/pandas.DataFrame.plot.scatter,../reference/api/pandas.DataFrame.plot.scatter +generated/pandas.DataFrame.pop,../reference/api/pandas.DataFrame.pop +generated/pandas.DataFrame.pow,../reference/api/pandas.DataFrame.pow +generated/pandas.DataFrame.prod,../reference/api/pandas.DataFrame.prod +generated/pandas.DataFrame.product,../reference/api/pandas.DataFrame.product +generated/pandas.DataFrame.quantile,../reference/api/pandas.DataFrame.quantile +generated/pandas.DataFrame.query,../reference/api/pandas.DataFrame.query +generated/pandas.DataFrame.radd,../reference/api/pandas.DataFrame.radd +generated/pandas.DataFrame.rank,../reference/api/pandas.DataFrame.rank +generated/pandas.DataFrame.rdiv,../reference/api/pandas.DataFrame.rdiv +generated/pandas.DataFrame.reindex_axis,../reference/api/pandas.DataFrame.reindex_axis +generated/pandas.DataFrame.reindex,../reference/api/pandas.DataFrame.reindex +generated/pandas.DataFrame.reindex_like,../reference/api/pandas.DataFrame.reindex_like +generated/pandas.DataFrame.rename_axis,../reference/api/pandas.DataFrame.rename_axis +generated/pandas.DataFrame.rename,../reference/api/pandas.DataFrame.rename +generated/pandas.DataFrame.reorder_levels,../reference/api/pandas.DataFrame.reorder_levels +generated/pandas.DataFrame.replace,../reference/api/pandas.DataFrame.replace +generated/pandas.DataFrame.resample,../reference/api/pandas.DataFrame.resample +generated/pandas.DataFrame.reset_index,../reference/api/pandas.DataFrame.reset_index +generated/pandas.DataFrame.rfloordiv,../reference/api/pandas.DataFrame.rfloordiv +generated/pandas.DataFrame.rmod,../reference/api/pandas.DataFrame.rmod +generated/pandas.DataFrame.rmul,../reference/api/pandas.DataFrame.rmul +generated/pandas.DataFrame.rolling,../reference/api/pandas.DataFrame.rolling +generated/pandas.DataFrame.round,../reference/api/pandas.DataFrame.round +generated/pandas.DataFrame.rpow,../reference/api/pandas.DataFrame.rpow +generated/pandas.DataFrame.rsub,../reference/api/pandas.DataFrame.rsub +generated/pandas.DataFrame.rtruediv,../reference/api/pandas.DataFrame.rtruediv +generated/pandas.DataFrame.sample,../reference/api/pandas.DataFrame.sample +generated/pandas.DataFrame.select_dtypes,../reference/api/pandas.DataFrame.select_dtypes +generated/pandas.DataFrame.select,../reference/api/pandas.DataFrame.select +generated/pandas.DataFrame.sem,../reference/api/pandas.DataFrame.sem +generated/pandas.DataFrame.set_axis,../reference/api/pandas.DataFrame.set_axis +generated/pandas.DataFrame.set_index,../reference/api/pandas.DataFrame.set_index +generated/pandas.DataFrame.set_value,../reference/api/pandas.DataFrame.set_value +generated/pandas.DataFrame.shape,../reference/api/pandas.DataFrame.shape +generated/pandas.DataFrame.shift,../reference/api/pandas.DataFrame.shift +generated/pandas.DataFrame.size,../reference/api/pandas.DataFrame.size +generated/pandas.DataFrame.skew,../reference/api/pandas.DataFrame.skew +generated/pandas.DataFrame.slice_shift,../reference/api/pandas.DataFrame.slice_shift +generated/pandas.DataFrame.sort_index,../reference/api/pandas.DataFrame.sort_index +generated/pandas.DataFrame.sort_values,../reference/api/pandas.DataFrame.sort_values +generated/pandas.DataFrame.squeeze,../reference/api/pandas.DataFrame.squeeze +generated/pandas.DataFrame.stack,../reference/api/pandas.DataFrame.stack +generated/pandas.DataFrame.std,../reference/api/pandas.DataFrame.std +generated/pandas.DataFrame.style,../reference/api/pandas.DataFrame.style +generated/pandas.DataFrame.sub,../reference/api/pandas.DataFrame.sub +generated/pandas.DataFrame.subtract,../reference/api/pandas.DataFrame.subtract +generated/pandas.DataFrame.sum,../reference/api/pandas.DataFrame.sum +generated/pandas.DataFrame.swapaxes,../reference/api/pandas.DataFrame.swapaxes +generated/pandas.DataFrame.swaplevel,../reference/api/pandas.DataFrame.swaplevel +generated/pandas.DataFrame.tail,../reference/api/pandas.DataFrame.tail +generated/pandas.DataFrame.take,../reference/api/pandas.DataFrame.take +generated/pandas.DataFrame.T,../reference/api/pandas.DataFrame.T +generated/pandas.DataFrame.timetuple,../reference/api/pandas.DataFrame.timetuple +generated/pandas.DataFrame.to_clipboard,../reference/api/pandas.DataFrame.to_clipboard +generated/pandas.DataFrame.to_csv,../reference/api/pandas.DataFrame.to_csv +generated/pandas.DataFrame.to_dense,../reference/api/pandas.DataFrame.to_dense +generated/pandas.DataFrame.to_dict,../reference/api/pandas.DataFrame.to_dict +generated/pandas.DataFrame.to_excel,../reference/api/pandas.DataFrame.to_excel +generated/pandas.DataFrame.to_feather,../reference/api/pandas.DataFrame.to_feather +generated/pandas.DataFrame.to_gbq,../reference/api/pandas.DataFrame.to_gbq +generated/pandas.DataFrame.to_hdf,../reference/api/pandas.DataFrame.to_hdf +generated/pandas.DataFrame.to,../reference/api/pandas.DataFrame.to +generated/pandas.DataFrame.to_json,../reference/api/pandas.DataFrame.to_json +generated/pandas.DataFrame.to_latex,../reference/api/pandas.DataFrame.to_latex +generated/pandas.DataFrame.to_msgpack,../reference/api/pandas.DataFrame.to_msgpack +generated/pandas.DataFrame.to_numpy,../reference/api/pandas.DataFrame.to_numpy +generated/pandas.DataFrame.to_panel,../reference/api/pandas.DataFrame.to_panel +generated/pandas.DataFrame.to_parquet,../reference/api/pandas.DataFrame.to_parquet +generated/pandas.DataFrame.to_period,../reference/api/pandas.DataFrame.to_period +generated/pandas.DataFrame.to_pickle,../reference/api/pandas.DataFrame.to_pickle +generated/pandas.DataFrame.to_records,../reference/api/pandas.DataFrame.to_records +generated/pandas.DataFrame.to_sparse,../reference/api/pandas.DataFrame.to_sparse +generated/pandas.DataFrame.to_sql,../reference/api/pandas.DataFrame.to_sql +generated/pandas.DataFrame.to_stata,../reference/api/pandas.DataFrame.to_stata +generated/pandas.DataFrame.to_string,../reference/api/pandas.DataFrame.to_string +generated/pandas.DataFrame.to_timestamp,../reference/api/pandas.DataFrame.to_timestamp +generated/pandas.DataFrame.to_xarray,../reference/api/pandas.DataFrame.to_xarray +generated/pandas.DataFrame.transform,../reference/api/pandas.DataFrame.transform +generated/pandas.DataFrame.transpose,../reference/api/pandas.DataFrame.transpose +generated/pandas.DataFrame.truediv,../reference/api/pandas.DataFrame.truediv +generated/pandas.DataFrame.truncate,../reference/api/pandas.DataFrame.truncate +generated/pandas.DataFrame.tshift,../reference/api/pandas.DataFrame.tshift +generated/pandas.DataFrame.tz_convert,../reference/api/pandas.DataFrame.tz_convert +generated/pandas.DataFrame.tz_localize,../reference/api/pandas.DataFrame.tz_localize +generated/pandas.DataFrame.unstack,../reference/api/pandas.DataFrame.unstack +generated/pandas.DataFrame.update,../reference/api/pandas.DataFrame.update +generated/pandas.DataFrame.values,../reference/api/pandas.DataFrame.values +generated/pandas.DataFrame.var,../reference/api/pandas.DataFrame.var +generated/pandas.DataFrame.where,../reference/api/pandas.DataFrame.where +generated/pandas.DataFrame.xs,../reference/api/pandas.DataFrame.xs +generated/pandas.date_range,../reference/api/pandas.date_range +generated/pandas.DatetimeIndex.ceil,../reference/api/pandas.DatetimeIndex.ceil +generated/pandas.DatetimeIndex.date,../reference/api/pandas.DatetimeIndex.date +generated/pandas.DatetimeIndex.day,../reference/api/pandas.DatetimeIndex.day +generated/pandas.DatetimeIndex.day_name,../reference/api/pandas.DatetimeIndex.day_name +generated/pandas.DatetimeIndex.dayofweek,../reference/api/pandas.DatetimeIndex.dayofweek +generated/pandas.DatetimeIndex.dayofyear,../reference/api/pandas.DatetimeIndex.dayofyear +generated/pandas.DatetimeIndex.floor,../reference/api/pandas.DatetimeIndex.floor +generated/pandas.DatetimeIndex.freq,../reference/api/pandas.DatetimeIndex.freq +generated/pandas.DatetimeIndex.freqstr,../reference/api/pandas.DatetimeIndex.freqstr +generated/pandas.DatetimeIndex.hour,../reference/api/pandas.DatetimeIndex.hour +generated/pandas.DatetimeIndex,../reference/api/pandas.DatetimeIndex +generated/pandas.DatetimeIndex.indexer_at_time,../reference/api/pandas.DatetimeIndex.indexer_at_time +generated/pandas.DatetimeIndex.indexer_between_time,../reference/api/pandas.DatetimeIndex.indexer_between_time +generated/pandas.DatetimeIndex.inferred_freq,../reference/api/pandas.DatetimeIndex.inferred_freq +generated/pandas.DatetimeIndex.is_leap_year,../reference/api/pandas.DatetimeIndex.is_leap_year +generated/pandas.DatetimeIndex.is_month_end,../reference/api/pandas.DatetimeIndex.is_month_end +generated/pandas.DatetimeIndex.is_month_start,../reference/api/pandas.DatetimeIndex.is_month_start +generated/pandas.DatetimeIndex.is_quarter_end,../reference/api/pandas.DatetimeIndex.is_quarter_end +generated/pandas.DatetimeIndex.is_quarter_start,../reference/api/pandas.DatetimeIndex.is_quarter_start +generated/pandas.DatetimeIndex.is_year_end,../reference/api/pandas.DatetimeIndex.is_year_end +generated/pandas.DatetimeIndex.is_year_start,../reference/api/pandas.DatetimeIndex.is_year_start +generated/pandas.DatetimeIndex.microsecond,../reference/api/pandas.DatetimeIndex.microsecond +generated/pandas.DatetimeIndex.minute,../reference/api/pandas.DatetimeIndex.minute +generated/pandas.DatetimeIndex.month,../reference/api/pandas.DatetimeIndex.month +generated/pandas.DatetimeIndex.month_name,../reference/api/pandas.DatetimeIndex.month_name +generated/pandas.DatetimeIndex.nanosecond,../reference/api/pandas.DatetimeIndex.nanosecond +generated/pandas.DatetimeIndex.normalize,../reference/api/pandas.DatetimeIndex.normalize +generated/pandas.DatetimeIndex.quarter,../reference/api/pandas.DatetimeIndex.quarter +generated/pandas.DatetimeIndex.round,../reference/api/pandas.DatetimeIndex.round +generated/pandas.DatetimeIndex.second,../reference/api/pandas.DatetimeIndex.second +generated/pandas.DatetimeIndex.snap,../reference/api/pandas.DatetimeIndex.snap +generated/pandas.DatetimeIndex.strftime,../reference/api/pandas.DatetimeIndex.strftime +generated/pandas.DatetimeIndex.time,../reference/api/pandas.DatetimeIndex.time +generated/pandas.DatetimeIndex.timetz,../reference/api/pandas.DatetimeIndex.timetz +generated/pandas.DatetimeIndex.to_frame,../reference/api/pandas.DatetimeIndex.to_frame +generated/pandas.DatetimeIndex.to_perioddelta,../reference/api/pandas.DatetimeIndex.to_perioddelta +generated/pandas.DatetimeIndex.to_period,../reference/api/pandas.DatetimeIndex.to_period +generated/pandas.DatetimeIndex.to_pydatetime,../reference/api/pandas.DatetimeIndex.to_pydatetime +generated/pandas.DatetimeIndex.to_series,../reference/api/pandas.DatetimeIndex.to_series +generated/pandas.DatetimeIndex.tz_convert,../reference/api/pandas.DatetimeIndex.tz_convert +generated/pandas.DatetimeIndex.tz,../reference/api/pandas.DatetimeIndex.tz +generated/pandas.DatetimeIndex.tz_localize,../reference/api/pandas.DatetimeIndex.tz_localize +generated/pandas.DatetimeIndex.weekday,../reference/api/pandas.DatetimeIndex.weekday +generated/pandas.DatetimeIndex.week,../reference/api/pandas.DatetimeIndex.week +generated/pandas.DatetimeIndex.weekofyear,../reference/api/pandas.DatetimeIndex.weekofyear +generated/pandas.DatetimeIndex.year,../reference/api/pandas.DatetimeIndex.year +generated/pandas.DatetimeTZDtype.base,../reference/api/pandas.DatetimeTZDtype.base +generated/pandas.DatetimeTZDtype.construct_array_type,../reference/api/pandas.DatetimeTZDtype.construct_array_type +generated/pandas.DatetimeTZDtype.construct_from_string,../reference/api/pandas.DatetimeTZDtype.construct_from_string +generated/pandas.DatetimeTZDtype,../reference/api/pandas.DatetimeTZDtype +generated/pandas.DatetimeTZDtype.isbuiltin,../reference/api/pandas.DatetimeTZDtype.isbuiltin +generated/pandas.DatetimeTZDtype.is_dtype,../reference/api/pandas.DatetimeTZDtype.is_dtype +generated/pandas.DatetimeTZDtype.isnative,../reference/api/pandas.DatetimeTZDtype.isnative +generated/pandas.DatetimeTZDtype.itemsize,../reference/api/pandas.DatetimeTZDtype.itemsize +generated/pandas.DatetimeTZDtype.kind,../reference/api/pandas.DatetimeTZDtype.kind +generated/pandas.DatetimeTZDtype.name,../reference/api/pandas.DatetimeTZDtype.name +generated/pandas.DatetimeTZDtype.names,../reference/api/pandas.DatetimeTZDtype.names +generated/pandas.DatetimeTZDtype.na_value,../reference/api/pandas.DatetimeTZDtype.na_value +generated/pandas.DatetimeTZDtype.num,../reference/api/pandas.DatetimeTZDtype.num +generated/pandas.DatetimeTZDtype.reset_cache,../reference/api/pandas.DatetimeTZDtype.reset_cache +generated/pandas.DatetimeTZDtype.shape,../reference/api/pandas.DatetimeTZDtype.shape +generated/pandas.DatetimeTZDtype.str,../reference/api/pandas.DatetimeTZDtype.str +generated/pandas.DatetimeTZDtype.subdtype,../reference/api/pandas.DatetimeTZDtype.subdtype +generated/pandas.DatetimeTZDtype.tz,../reference/api/pandas.DatetimeTZDtype.tz +generated/pandas.DatetimeTZDtype.unit,../reference/api/pandas.DatetimeTZDtype.unit +generated/pandas.describe_option,../reference/api/pandas.describe_option +generated/pandas.errors.DtypeWarning,../reference/api/pandas.errors.DtypeWarning +generated/pandas.errors.EmptyDataError,../reference/api/pandas.errors.EmptyDataError +generated/pandas.errors.OutOfBoundsDatetime,../reference/api/pandas.errors.OutOfBoundsDatetime +generated/pandas.errors.ParserError,../reference/api/pandas.errors.ParserError +generated/pandas.errors.ParserWarning,../reference/api/pandas.errors.ParserWarning +generated/pandas.errors.PerformanceWarning,../reference/api/pandas.errors.PerformanceWarning +generated/pandas.errors.UnsortedIndexError,../reference/api/pandas.errors.UnsortedIndexError +generated/pandas.errors.UnsupportedFunctionCall,../reference/api/pandas.errors.UnsupportedFunctionCall +generated/pandas.eval,../reference/api/pandas.eval +generated/pandas.ExcelFile.parse,../reference/api/pandas.ExcelFile.parse +generated/pandas.ExcelWriter,../reference/api/pandas.ExcelWriter +generated/pandas.factorize,../reference/api/pandas.factorize +generated/pandas.Float64Index,../reference/api/pandas.Float64Index +generated/pandas.get_dummies,../reference/api/pandas.get_dummies +generated/pandas.get_option,../reference/api/pandas.get_option +generated/pandas.Grouper,../reference/api/pandas.Grouper +generated/pandas.HDFStore.append,../reference/api/pandas.HDFStore.append +generated/pandas.HDFStore.get,../reference/api/pandas.HDFStore.get +generated/pandas.HDFStore.groups,../reference/api/pandas.HDFStore.groups +generated/pandas.HDFStore.info,../reference/api/pandas.HDFStore.info +generated/pandas.HDFStore.keys,../reference/api/pandas.HDFStore.keys +generated/pandas.HDFStore.put,../reference/api/pandas.HDFStore.put +generated/pandas.HDFStore.select,../reference/api/pandas.HDFStore.select +generated/pandas.HDFStore.walk,../reference/api/pandas.HDFStore.walk +generated/pandas.Index.all,../reference/api/pandas.Index.all +generated/pandas.Index.any,../reference/api/pandas.Index.any +generated/pandas.Index.append,../reference/api/pandas.Index.append +generated/pandas.Index.argmax,../reference/api/pandas.Index.argmax +generated/pandas.Index.argmin,../reference/api/pandas.Index.argmin +generated/pandas.Index.argsort,../reference/api/pandas.Index.argsort +generated/pandas.Index.array,../reference/api/pandas.Index.array +generated/pandas.Index.asi8,../reference/api/pandas.Index.asi8 +generated/pandas.Index.asof,../reference/api/pandas.Index.asof +generated/pandas.Index.asof_locs,../reference/api/pandas.Index.asof_locs +generated/pandas.Index.astype,../reference/api/pandas.Index.astype +generated/pandas.Index.base,../reference/api/pandas.Index.base +generated/pandas.Index.contains,../reference/api/pandas.Index.contains +generated/pandas.Index.copy,../reference/api/pandas.Index.copy +generated/pandas.Index.data,../reference/api/pandas.Index.data +generated/pandas.Index.delete,../reference/api/pandas.Index.delete +generated/pandas.Index.difference,../reference/api/pandas.Index.difference +generated/pandas.Index.drop_duplicates,../reference/api/pandas.Index.drop_duplicates +generated/pandas.Index.drop,../reference/api/pandas.Index.drop +generated/pandas.Index.droplevel,../reference/api/pandas.Index.droplevel +generated/pandas.Index.dropna,../reference/api/pandas.Index.dropna +generated/pandas.Index.dtype,../reference/api/pandas.Index.dtype +generated/pandas.Index.dtype_str,../reference/api/pandas.Index.dtype_str +generated/pandas.Index.duplicated,../reference/api/pandas.Index.duplicated +generated/pandas.Index.empty,../reference/api/pandas.Index.empty +generated/pandas.Index.equals,../reference/api/pandas.Index.equals +generated/pandas.Index.factorize,../reference/api/pandas.Index.factorize +generated/pandas.Index.fillna,../reference/api/pandas.Index.fillna +generated/pandas.Index.flags,../reference/api/pandas.Index.flags +generated/pandas.Index.format,../reference/api/pandas.Index.format +generated/pandas.Index.get_duplicates,../reference/api/pandas.Index.get_duplicates +generated/pandas.Index.get_indexer_for,../reference/api/pandas.Index.get_indexer_for +generated/pandas.Index.get_indexer,../reference/api/pandas.Index.get_indexer +generated/pandas.Index.get_indexer_non_unique,../reference/api/pandas.Index.get_indexer_non_unique +generated/pandas.Index.get_level_values,../reference/api/pandas.Index.get_level_values +generated/pandas.Index.get_loc,../reference/api/pandas.Index.get_loc +generated/pandas.Index.get_slice_bound,../reference/api/pandas.Index.get_slice_bound +generated/pandas.Index.get_value,../reference/api/pandas.Index.get_value +generated/pandas.Index.get_values,../reference/api/pandas.Index.get_values +generated/pandas.Index.groupby,../reference/api/pandas.Index.groupby +generated/pandas.Index.has_duplicates,../reference/api/pandas.Index.has_duplicates +generated/pandas.Index.hasnans,../reference/api/pandas.Index.hasnans +generated/pandas.Index.holds_integer,../reference/api/pandas.Index.holds_integer +generated/pandas.Index,../reference/api/pandas.Index +generated/pandas.Index.identical,../reference/api/pandas.Index.identical +generated/pandas.Index.inferred_type,../reference/api/pandas.Index.inferred_type +generated/pandas.Index.insert,../reference/api/pandas.Index.insert +generated/pandas.Index.intersection,../reference/api/pandas.Index.intersection +generated/pandas.Index.is_all_dates,../reference/api/pandas.Index.is_all_dates +generated/pandas.Index.is_boolean,../reference/api/pandas.Index.is_boolean +generated/pandas.Index.is_categorical,../reference/api/pandas.Index.is_categorical +generated/pandas.Index.is_floating,../reference/api/pandas.Index.is_floating +generated/pandas.Index.is_,../reference/api/pandas.Index.is_ +generated/pandas.Index.isin,../reference/api/pandas.Index.isin +generated/pandas.Index.is_integer,../reference/api/pandas.Index.is_integer +generated/pandas.Index.is_interval,../reference/api/pandas.Index.is_interval +generated/pandas.Index.is_lexsorted_for_tuple,../reference/api/pandas.Index.is_lexsorted_for_tuple +generated/pandas.Index.is_mixed,../reference/api/pandas.Index.is_mixed +generated/pandas.Index.is_monotonic_decreasing,../reference/api/pandas.Index.is_monotonic_decreasing +generated/pandas.Index.is_monotonic,../reference/api/pandas.Index.is_monotonic +generated/pandas.Index.is_monotonic_increasing,../reference/api/pandas.Index.is_monotonic_increasing +generated/pandas.Index.isna,../reference/api/pandas.Index.isna +generated/pandas.Index.isnull,../reference/api/pandas.Index.isnull +generated/pandas.Index.is_numeric,../reference/api/pandas.Index.is_numeric +generated/pandas.Index.is_object,../reference/api/pandas.Index.is_object +generated/pandas.Index.is_type_compatible,../reference/api/pandas.Index.is_type_compatible +generated/pandas.Index.is_unique,../reference/api/pandas.Index.is_unique +generated/pandas.Index.item,../reference/api/pandas.Index.item +generated/pandas.Index.itemsize,../reference/api/pandas.Index.itemsize +generated/pandas.Index.join,../reference/api/pandas.Index.join +generated/pandas.Index.map,../reference/api/pandas.Index.map +generated/pandas.Index.max,../reference/api/pandas.Index.max +generated/pandas.Index.memory_usage,../reference/api/pandas.Index.memory_usage +generated/pandas.Index.min,../reference/api/pandas.Index.min +generated/pandas.Index.name,../reference/api/pandas.Index.name +generated/pandas.Index.names,../reference/api/pandas.Index.names +generated/pandas.Index.nbytes,../reference/api/pandas.Index.nbytes +generated/pandas.Index.ndim,../reference/api/pandas.Index.ndim +generated/pandas.Index.nlevels,../reference/api/pandas.Index.nlevels +generated/pandas.Index.notna,../reference/api/pandas.Index.notna +generated/pandas.Index.notnull,../reference/api/pandas.Index.notnull +generated/pandas.Index.nunique,../reference/api/pandas.Index.nunique +generated/pandas.Index.putmask,../reference/api/pandas.Index.putmask +generated/pandas.Index.ravel,../reference/api/pandas.Index.ravel +generated/pandas.Index.reindex,../reference/api/pandas.Index.reindex +generated/pandas.Index.rename,../reference/api/pandas.Index.rename +generated/pandas.Index.repeat,../reference/api/pandas.Index.repeat +generated/pandas.Index.searchsorted,../reference/api/pandas.Index.searchsorted +generated/pandas.Index.set_names,../reference/api/pandas.Index.set_names +generated/pandas.Index.set_value,../reference/api/pandas.Index.set_value +generated/pandas.Index.shape,../reference/api/pandas.Index.shape +generated/pandas.Index.shift,../reference/api/pandas.Index.shift +generated/pandas.Index.size,../reference/api/pandas.Index.size +generated/pandas.IndexSlice,../reference/api/pandas.IndexSlice +generated/pandas.Index.slice_indexer,../reference/api/pandas.Index.slice_indexer +generated/pandas.Index.slice_locs,../reference/api/pandas.Index.slice_locs +generated/pandas.Index.sort,../reference/api/pandas.Index.sort +generated/pandas.Index.sortlevel,../reference/api/pandas.Index.sortlevel +generated/pandas.Index.sort_values,../reference/api/pandas.Index.sort_values +generated/pandas.Index.str,../reference/api/pandas.Index.str +generated/pandas.Index.strides,../reference/api/pandas.Index.strides +generated/pandas.Index.summary,../reference/api/pandas.Index.summary +generated/pandas.Index.symmetric_difference,../reference/api/pandas.Index.symmetric_difference +generated/pandas.Index.take,../reference/api/pandas.Index.take +generated/pandas.Index.T,../reference/api/pandas.Index.T +generated/pandas.Index.to_flat_index,../reference/api/pandas.Index.to_flat_index +generated/pandas.Index.to_frame,../reference/api/pandas.Index.to_frame +generated/pandas.Index.to_list,../reference/api/pandas.Index.to_list +generated/pandas.Index.tolist,../reference/api/pandas.Index.tolist +generated/pandas.Index.to_native_types,../reference/api/pandas.Index.to_native_types +generated/pandas.Index.to_numpy,../reference/api/pandas.Index.to_numpy +generated/pandas.Index.to_series,../reference/api/pandas.Index.to_series +generated/pandas.Index.transpose,../reference/api/pandas.Index.transpose +generated/pandas.Index.union,../reference/api/pandas.Index.union +generated/pandas.Index.unique,../reference/api/pandas.Index.unique +generated/pandas.Index.value_counts,../reference/api/pandas.Index.value_counts +generated/pandas.Index.values,../reference/api/pandas.Index.values +generated/pandas.Index.view,../reference/api/pandas.Index.view +generated/pandas.Index.where,../reference/api/pandas.Index.where +generated/pandas.infer_freq,../reference/api/pandas.infer_freq +generated/pandas.Interval.closed,../reference/api/pandas.Interval.closed +generated/pandas.Interval.closed_left,../reference/api/pandas.Interval.closed_left +generated/pandas.Interval.closed_right,../reference/api/pandas.Interval.closed_right +generated/pandas.Interval,../reference/api/pandas.Interval +generated/pandas.IntervalIndex.closed,../reference/api/pandas.IntervalIndex.closed +generated/pandas.IntervalIndex.contains,../reference/api/pandas.IntervalIndex.contains +generated/pandas.IntervalIndex.from_arrays,../reference/api/pandas.IntervalIndex.from_arrays +generated/pandas.IntervalIndex.from_breaks,../reference/api/pandas.IntervalIndex.from_breaks +generated/pandas.IntervalIndex.from_tuples,../reference/api/pandas.IntervalIndex.from_tuples +generated/pandas.IntervalIndex.get_indexer,../reference/api/pandas.IntervalIndex.get_indexer +generated/pandas.IntervalIndex.get_loc,../reference/api/pandas.IntervalIndex.get_loc +generated/pandas.IntervalIndex,../reference/api/pandas.IntervalIndex +generated/pandas.IntervalIndex.is_non_overlapping_monotonic,../reference/api/pandas.IntervalIndex.is_non_overlapping_monotonic +generated/pandas.IntervalIndex.is_overlapping,../reference/api/pandas.IntervalIndex.is_overlapping +generated/pandas.IntervalIndex.left,../reference/api/pandas.IntervalIndex.left +generated/pandas.IntervalIndex.length,../reference/api/pandas.IntervalIndex.length +generated/pandas.IntervalIndex.mid,../reference/api/pandas.IntervalIndex.mid +generated/pandas.IntervalIndex.overlaps,../reference/api/pandas.IntervalIndex.overlaps +generated/pandas.IntervalIndex.right,../reference/api/pandas.IntervalIndex.right +generated/pandas.IntervalIndex.set_closed,../reference/api/pandas.IntervalIndex.set_closed +generated/pandas.IntervalIndex.to_tuples,../reference/api/pandas.IntervalIndex.to_tuples +generated/pandas.IntervalIndex.values,../reference/api/pandas.IntervalIndex.values +generated/pandas.Interval.left,../reference/api/pandas.Interval.left +generated/pandas.Interval.length,../reference/api/pandas.Interval.length +generated/pandas.Interval.mid,../reference/api/pandas.Interval.mid +generated/pandas.Interval.open_left,../reference/api/pandas.Interval.open_left +generated/pandas.Interval.open_right,../reference/api/pandas.Interval.open_right +generated/pandas.Interval.overlaps,../reference/api/pandas.Interval.overlaps +generated/pandas.interval_range,../reference/api/pandas.interval_range +generated/pandas.Interval.right,../reference/api/pandas.Interval.right +generated/pandas.io.formats.style.Styler.apply,../reference/api/pandas.io.formats.style.Styler.apply +generated/pandas.io.formats.style.Styler.applymap,../reference/api/pandas.io.formats.style.Styler.applymap +generated/pandas.io.formats.style.Styler.background_gradient,../reference/api/pandas.io.formats.style.Styler.background_gradient +generated/pandas.io.formats.style.Styler.bar,../reference/api/pandas.io.formats.style.Styler.bar +generated/pandas.io.formats.style.Styler.clear,../reference/api/pandas.io.formats.style.Styler.clear +generated/pandas.io.formats.style.Styler.env,../reference/api/pandas.io.formats.style.Styler.env +generated/pandas.io.formats.style.Styler.export,../reference/api/pandas.io.formats.style.Styler.export +generated/pandas.io.formats.style.Styler.format,../reference/api/pandas.io.formats.style.Styler.format +generated/pandas.io.formats.style.Styler.from_custom_template,../reference/api/pandas.io.formats.style.Styler.from_custom_template +generated/pandas.io.formats.style.Styler.hide_columns,../reference/api/pandas.io.formats.style.Styler.hide_columns +generated/pandas.io.formats.style.Styler.hide_index,../reference/api/pandas.io.formats.style.Styler.hide_index +generated/pandas.io.formats.style.Styler.highlight_max,../reference/api/pandas.io.formats.style.Styler.highlight_max +generated/pandas.io.formats.style.Styler.highlight_min,../reference/api/pandas.io.formats.style.Styler.highlight_min +generated/pandas.io.formats.style.Styler.highlight_null,../reference/api/pandas.io.formats.style.Styler.highlight_null +generated/pandas.io.formats.style.Styler,../reference/api/pandas.io.formats.style.Styler +generated/pandas.io.formats.style.Styler.loader,../reference/api/pandas.io.formats.style.Styler.loader +generated/pandas.io.formats.style.Styler.pipe,../reference/api/pandas.io.formats.style.Styler.pipe +generated/pandas.io.formats.style.Styler.render,../reference/api/pandas.io.formats.style.Styler.render +generated/pandas.io.formats.style.Styler.set_caption,../reference/api/pandas.io.formats.style.Styler.set_caption +generated/pandas.io.formats.style.Styler.set_precision,../reference/api/pandas.io.formats.style.Styler.set_precision +generated/pandas.io.formats.style.Styler.set_properties,../reference/api/pandas.io.formats.style.Styler.set_properties +generated/pandas.io.formats.style.Styler.set_table_attributes,../reference/api/pandas.io.formats.style.Styler.set_table_attributes +generated/pandas.io.formats.style.Styler.set_table_styles,../reference/api/pandas.io.formats.style.Styler.set_table_styles +generated/pandas.io.formats.style.Styler.set_uuid,../reference/api/pandas.io.formats.style.Styler.set_uuid +generated/pandas.io.formats.style.Styler.template,../reference/api/pandas.io.formats.style.Styler.template +generated/pandas.io.formats.style.Styler.to_excel,../reference/api/pandas.io.formats.style.Styler.to_excel +generated/pandas.io.formats.style.Styler.use,../reference/api/pandas.io.formats.style.Styler.use +generated/pandas.io.formats.style.Styler.where,../reference/api/pandas.io.formats.style.Styler.where +generated/pandas.io.json.build_table_schema,../reference/api/pandas.io.json.build_table_schema +generated/pandas.io.json.json_normalize,../reference/api/pandas.io.json.json_normalize +generated/pandas.io.stata.StataReader.data,../reference/api/pandas.io.stata.StataReader.data +generated/pandas.io.stata.StataReader.data_label,../reference/api/pandas.io.stata.StataReader.data_label +generated/pandas.io.stata.StataReader.value_labels,../reference/api/pandas.io.stata.StataReader.value_labels +generated/pandas.io.stata.StataReader.variable_labels,../reference/api/pandas.io.stata.StataReader.variable_labels +generated/pandas.io.stata.StataWriter.write_file,../reference/api/pandas.io.stata.StataWriter.write_file +generated/pandas.isna,../reference/api/pandas.isna +generated/pandas.isnull,../reference/api/pandas.isnull +generated/pandas.melt,../reference/api/pandas.melt +generated/pandas.merge_asof,../reference/api/pandas.merge_asof +generated/pandas.merge,../reference/api/pandas.merge +generated/pandas.merge_ordered,../reference/api/pandas.merge_ordered +generated/pandas.MultiIndex.codes,../reference/api/pandas.MultiIndex.codes +generated/pandas.MultiIndex.droplevel,../reference/api/pandas.MultiIndex.droplevel +generated/pandas.MultiIndex.from_arrays,../reference/api/pandas.MultiIndex.from_arrays +generated/pandas.MultiIndex.from_frame,../reference/api/pandas.MultiIndex.from_frame +generated/pandas.MultiIndex.from_product,../reference/api/pandas.MultiIndex.from_product +generated/pandas.MultiIndex.from_tuples,../reference/api/pandas.MultiIndex.from_tuples +generated/pandas.MultiIndex.get_indexer,../reference/api/pandas.MultiIndex.get_indexer +generated/pandas.MultiIndex.get_level_values,../reference/api/pandas.MultiIndex.get_level_values +generated/pandas.MultiIndex.get_loc,../reference/api/pandas.MultiIndex.get_loc +generated/pandas.MultiIndex.get_loc_level,../reference/api/pandas.MultiIndex.get_loc_level +generated/pandas.MultiIndex,../reference/api/pandas.MultiIndex +generated/pandas.MultiIndex.is_lexsorted,../reference/api/pandas.MultiIndex.is_lexsorted +generated/pandas.MultiIndex.levels,../reference/api/pandas.MultiIndex.levels +generated/pandas.MultiIndex.levshape,../reference/api/pandas.MultiIndex.levshape +generated/pandas.MultiIndex.names,../reference/api/pandas.MultiIndex.names +generated/pandas.MultiIndex.nlevels,../reference/api/pandas.MultiIndex.nlevels +generated/pandas.MultiIndex.remove_unused_levels,../reference/api/pandas.MultiIndex.remove_unused_levels +generated/pandas.MultiIndex.reorder_levels,../reference/api/pandas.MultiIndex.reorder_levels +generated/pandas.MultiIndex.set_codes,../reference/api/pandas.MultiIndex.set_codes +generated/pandas.MultiIndex.set_levels,../reference/api/pandas.MultiIndex.set_levels +generated/pandas.MultiIndex.sortlevel,../reference/api/pandas.MultiIndex.sortlevel +generated/pandas.MultiIndex.swaplevel,../reference/api/pandas.MultiIndex.swaplevel +generated/pandas.MultiIndex.to_flat_index,../reference/api/pandas.MultiIndex.to_flat_index +generated/pandas.MultiIndex.to_frame,../reference/api/pandas.MultiIndex.to_frame +generated/pandas.MultiIndex.to_hierarchical,../reference/api/pandas.MultiIndex.to_hierarchical +generated/pandas.notna,../reference/api/pandas.notna +generated/pandas.notnull,../reference/api/pandas.notnull +generated/pandas.option_context,../reference/api/pandas.option_context +generated/pandas.Panel.abs,../reference/api/pandas.Panel.abs +generated/pandas.Panel.add,../reference/api/pandas.Panel.add +generated/pandas.Panel.add_prefix,../reference/api/pandas.Panel.add_prefix +generated/pandas.Panel.add_suffix,../reference/api/pandas.Panel.add_suffix +generated/pandas.Panel.agg,../reference/api/pandas.Panel.agg +generated/pandas.Panel.aggregate,../reference/api/pandas.Panel.aggregate +generated/pandas.Panel.align,../reference/api/pandas.Panel.align +generated/pandas.Panel.all,../reference/api/pandas.Panel.all +generated/pandas.Panel.any,../reference/api/pandas.Panel.any +generated/pandas.Panel.apply,../reference/api/pandas.Panel.apply +generated/pandas.Panel.as_blocks,../reference/api/pandas.Panel.as_blocks +generated/pandas.Panel.asfreq,../reference/api/pandas.Panel.asfreq +generated/pandas.Panel.as_matrix,../reference/api/pandas.Panel.as_matrix +generated/pandas.Panel.asof,../reference/api/pandas.Panel.asof +generated/pandas.Panel.astype,../reference/api/pandas.Panel.astype +generated/pandas.Panel.at,../reference/api/pandas.Panel.at +generated/pandas.Panel.at_time,../reference/api/pandas.Panel.at_time +generated/pandas.Panel.axes,../reference/api/pandas.Panel.axes +generated/pandas.Panel.between_time,../reference/api/pandas.Panel.between_time +generated/pandas.Panel.bfill,../reference/api/pandas.Panel.bfill +generated/pandas.Panel.blocks,../reference/api/pandas.Panel.blocks +generated/pandas.Panel.bool,../reference/api/pandas.Panel.bool +generated/pandas.Panel.clip,../reference/api/pandas.Panel.clip +generated/pandas.Panel.clip_lower,../reference/api/pandas.Panel.clip_lower +generated/pandas.Panel.clip_upper,../reference/api/pandas.Panel.clip_upper +generated/pandas.Panel.compound,../reference/api/pandas.Panel.compound +generated/pandas.Panel.conform,../reference/api/pandas.Panel.conform +generated/pandas.Panel.convert_objects,../reference/api/pandas.Panel.convert_objects +generated/pandas.Panel.copy,../reference/api/pandas.Panel.copy +generated/pandas.Panel.count,../reference/api/pandas.Panel.count +generated/pandas.Panel.cummax,../reference/api/pandas.Panel.cummax +generated/pandas.Panel.cummin,../reference/api/pandas.Panel.cummin +generated/pandas.Panel.cumprod,../reference/api/pandas.Panel.cumprod +generated/pandas.Panel.cumsum,../reference/api/pandas.Panel.cumsum +generated/pandas.Panel.describe,../reference/api/pandas.Panel.describe +generated/pandas.Panel.div,../reference/api/pandas.Panel.div +generated/pandas.Panel.divide,../reference/api/pandas.Panel.divide +generated/pandas.Panel.drop,../reference/api/pandas.Panel.drop +generated/pandas.Panel.droplevel,../reference/api/pandas.Panel.droplevel +generated/pandas.Panel.dropna,../reference/api/pandas.Panel.dropna +generated/pandas.Panel.dtypes,../reference/api/pandas.Panel.dtypes +generated/pandas.Panel.empty,../reference/api/pandas.Panel.empty +generated/pandas.Panel.eq,../reference/api/pandas.Panel.eq +generated/pandas.Panel.equals,../reference/api/pandas.Panel.equals +generated/pandas.Panel.ffill,../reference/api/pandas.Panel.ffill +generated/pandas.Panel.fillna,../reference/api/pandas.Panel.fillna +generated/pandas.Panel.filter,../reference/api/pandas.Panel.filter +generated/pandas.Panel.first,../reference/api/pandas.Panel.first +generated/pandas.Panel.first_valid_index,../reference/api/pandas.Panel.first_valid_index +generated/pandas.Panel.floordiv,../reference/api/pandas.Panel.floordiv +generated/pandas.Panel.from_dict,../reference/api/pandas.Panel.from_dict +generated/pandas.Panel.fromDict,../reference/api/pandas.Panel.fromDict +generated/pandas.Panel.ftypes,../reference/api/pandas.Panel.ftypes +generated/pandas.Panel.ge,../reference/api/pandas.Panel.ge +generated/pandas.Panel.get_dtype_counts,../reference/api/pandas.Panel.get_dtype_counts +generated/pandas.Panel.get_ftype_counts,../reference/api/pandas.Panel.get_ftype_counts +generated/pandas.Panel.get,../reference/api/pandas.Panel.get +generated/pandas.Panel.get_value,../reference/api/pandas.Panel.get_value +generated/pandas.Panel.get_values,../reference/api/pandas.Panel.get_values +generated/pandas.Panel.groupby,../reference/api/pandas.Panel.groupby +generated/pandas.Panel.gt,../reference/api/pandas.Panel.gt +generated/pandas.Panel.head,../reference/api/pandas.Panel.head +generated/pandas.Panel,../reference/api/pandas.Panel +generated/pandas.Panel.iat,../reference/api/pandas.Panel.iat +generated/pandas.Panel.iloc,../reference/api/pandas.Panel.iloc +generated/pandas.Panel.infer_objects,../reference/api/pandas.Panel.infer_objects +generated/pandas.Panel.interpolate,../reference/api/pandas.Panel.interpolate +generated/pandas.Panel.is_copy,../reference/api/pandas.Panel.is_copy +generated/pandas.Panel.isna,../reference/api/pandas.Panel.isna +generated/pandas.Panel.isnull,../reference/api/pandas.Panel.isnull +generated/pandas.Panel.items,../reference/api/pandas.Panel.items +generated/pandas.Panel.__iter__,../reference/api/pandas.Panel.__iter__ +generated/pandas.Panel.iteritems,../reference/api/pandas.Panel.iteritems +generated/pandas.Panel.ix,../reference/api/pandas.Panel.ix +generated/pandas.Panel.join,../reference/api/pandas.Panel.join +generated/pandas.Panel.keys,../reference/api/pandas.Panel.keys +generated/pandas.Panel.kurt,../reference/api/pandas.Panel.kurt +generated/pandas.Panel.kurtosis,../reference/api/pandas.Panel.kurtosis +generated/pandas.Panel.last,../reference/api/pandas.Panel.last +generated/pandas.Panel.last_valid_index,../reference/api/pandas.Panel.last_valid_index +generated/pandas.Panel.le,../reference/api/pandas.Panel.le +generated/pandas.Panel.loc,../reference/api/pandas.Panel.loc +generated/pandas.Panel.lt,../reference/api/pandas.Panel.lt +generated/pandas.Panel.mad,../reference/api/pandas.Panel.mad +generated/pandas.Panel.major_axis,../reference/api/pandas.Panel.major_axis +generated/pandas.Panel.major_xs,../reference/api/pandas.Panel.major_xs +generated/pandas.Panel.mask,../reference/api/pandas.Panel.mask +generated/pandas.Panel.max,../reference/api/pandas.Panel.max +generated/pandas.Panel.mean,../reference/api/pandas.Panel.mean +generated/pandas.Panel.median,../reference/api/pandas.Panel.median +generated/pandas.Panel.min,../reference/api/pandas.Panel.min +generated/pandas.Panel.minor_axis,../reference/api/pandas.Panel.minor_axis +generated/pandas.Panel.minor_xs,../reference/api/pandas.Panel.minor_xs +generated/pandas.Panel.mod,../reference/api/pandas.Panel.mod +generated/pandas.Panel.mul,../reference/api/pandas.Panel.mul +generated/pandas.Panel.multiply,../reference/api/pandas.Panel.multiply +generated/pandas.Panel.ndim,../reference/api/pandas.Panel.ndim +generated/pandas.Panel.ne,../reference/api/pandas.Panel.ne +generated/pandas.Panel.notna,../reference/api/pandas.Panel.notna +generated/pandas.Panel.notnull,../reference/api/pandas.Panel.notnull +generated/pandas.Panel.pct_change,../reference/api/pandas.Panel.pct_change +generated/pandas.Panel.pipe,../reference/api/pandas.Panel.pipe +generated/pandas.Panel.pop,../reference/api/pandas.Panel.pop +generated/pandas.Panel.pow,../reference/api/pandas.Panel.pow +generated/pandas.Panel.prod,../reference/api/pandas.Panel.prod +generated/pandas.Panel.product,../reference/api/pandas.Panel.product +generated/pandas.Panel.radd,../reference/api/pandas.Panel.radd +generated/pandas.Panel.rank,../reference/api/pandas.Panel.rank +generated/pandas.Panel.rdiv,../reference/api/pandas.Panel.rdiv +generated/pandas.Panel.reindex_axis,../reference/api/pandas.Panel.reindex_axis +generated/pandas.Panel.reindex,../reference/api/pandas.Panel.reindex +generated/pandas.Panel.reindex_like,../reference/api/pandas.Panel.reindex_like +generated/pandas.Panel.rename_axis,../reference/api/pandas.Panel.rename_axis +generated/pandas.Panel.rename,../reference/api/pandas.Panel.rename +generated/pandas.Panel.replace,../reference/api/pandas.Panel.replace +generated/pandas.Panel.resample,../reference/api/pandas.Panel.resample +generated/pandas.Panel.rfloordiv,../reference/api/pandas.Panel.rfloordiv +generated/pandas.Panel.rmod,../reference/api/pandas.Panel.rmod +generated/pandas.Panel.rmul,../reference/api/pandas.Panel.rmul +generated/pandas.Panel.round,../reference/api/pandas.Panel.round +generated/pandas.Panel.rpow,../reference/api/pandas.Panel.rpow +generated/pandas.Panel.rsub,../reference/api/pandas.Panel.rsub +generated/pandas.Panel.rtruediv,../reference/api/pandas.Panel.rtruediv +generated/pandas.Panel.sample,../reference/api/pandas.Panel.sample +generated/pandas.Panel.select,../reference/api/pandas.Panel.select +generated/pandas.Panel.sem,../reference/api/pandas.Panel.sem +generated/pandas.Panel.set_axis,../reference/api/pandas.Panel.set_axis +generated/pandas.Panel.set_value,../reference/api/pandas.Panel.set_value +generated/pandas.Panel.shape,../reference/api/pandas.Panel.shape +generated/pandas.Panel.shift,../reference/api/pandas.Panel.shift +generated/pandas.Panel.size,../reference/api/pandas.Panel.size +generated/pandas.Panel.skew,../reference/api/pandas.Panel.skew +generated/pandas.Panel.slice_shift,../reference/api/pandas.Panel.slice_shift +generated/pandas.Panel.sort_index,../reference/api/pandas.Panel.sort_index +generated/pandas.Panel.sort_values,../reference/api/pandas.Panel.sort_values +generated/pandas.Panel.squeeze,../reference/api/pandas.Panel.squeeze +generated/pandas.Panel.std,../reference/api/pandas.Panel.std +generated/pandas.Panel.sub,../reference/api/pandas.Panel.sub +generated/pandas.Panel.subtract,../reference/api/pandas.Panel.subtract +generated/pandas.Panel.sum,../reference/api/pandas.Panel.sum +generated/pandas.Panel.swapaxes,../reference/api/pandas.Panel.swapaxes +generated/pandas.Panel.swaplevel,../reference/api/pandas.Panel.swaplevel +generated/pandas.Panel.tail,../reference/api/pandas.Panel.tail +generated/pandas.Panel.take,../reference/api/pandas.Panel.take +generated/pandas.Panel.timetuple,../reference/api/pandas.Panel.timetuple +generated/pandas.Panel.to_clipboard,../reference/api/pandas.Panel.to_clipboard +generated/pandas.Panel.to_csv,../reference/api/pandas.Panel.to_csv +generated/pandas.Panel.to_dense,../reference/api/pandas.Panel.to_dense +generated/pandas.Panel.to_excel,../reference/api/pandas.Panel.to_excel +generated/pandas.Panel.to_frame,../reference/api/pandas.Panel.to_frame +generated/pandas.Panel.to_hdf,../reference/api/pandas.Panel.to_hdf +generated/pandas.Panel.to_json,../reference/api/pandas.Panel.to_json +generated/pandas.Panel.to_latex,../reference/api/pandas.Panel.to_latex +generated/pandas.Panel.to_msgpack,../reference/api/pandas.Panel.to_msgpack +generated/pandas.Panel.to_pickle,../reference/api/pandas.Panel.to_pickle +generated/pandas.Panel.to_sparse,../reference/api/pandas.Panel.to_sparse +generated/pandas.Panel.to_sql,../reference/api/pandas.Panel.to_sql +generated/pandas.Panel.to_xarray,../reference/api/pandas.Panel.to_xarray +generated/pandas.Panel.transform,../reference/api/pandas.Panel.transform +generated/pandas.Panel.transpose,../reference/api/pandas.Panel.transpose +generated/pandas.Panel.truediv,../reference/api/pandas.Panel.truediv +generated/pandas.Panel.truncate,../reference/api/pandas.Panel.truncate +generated/pandas.Panel.tshift,../reference/api/pandas.Panel.tshift +generated/pandas.Panel.tz_convert,../reference/api/pandas.Panel.tz_convert +generated/pandas.Panel.tz_localize,../reference/api/pandas.Panel.tz_localize +generated/pandas.Panel.update,../reference/api/pandas.Panel.update +generated/pandas.Panel.values,../reference/api/pandas.Panel.values +generated/pandas.Panel.var,../reference/api/pandas.Panel.var +generated/pandas.Panel.where,../reference/api/pandas.Panel.where +generated/pandas.Panel.xs,../reference/api/pandas.Panel.xs +generated/pandas.Period.asfreq,../reference/api/pandas.Period.asfreq +generated/pandas.Period.day,../reference/api/pandas.Period.day +generated/pandas.Period.dayofweek,../reference/api/pandas.Period.dayofweek +generated/pandas.Period.dayofyear,../reference/api/pandas.Period.dayofyear +generated/pandas.Period.days_in_month,../reference/api/pandas.Period.days_in_month +generated/pandas.Period.daysinmonth,../reference/api/pandas.Period.daysinmonth +generated/pandas.Period.end_time,../reference/api/pandas.Period.end_time +generated/pandas.Period.freq,../reference/api/pandas.Period.freq +generated/pandas.Period.freqstr,../reference/api/pandas.Period.freqstr +generated/pandas.Period.hour,../reference/api/pandas.Period.hour +generated/pandas.Period,../reference/api/pandas.Period +generated/pandas.PeriodIndex.asfreq,../reference/api/pandas.PeriodIndex.asfreq +generated/pandas.PeriodIndex.day,../reference/api/pandas.PeriodIndex.day +generated/pandas.PeriodIndex.dayofweek,../reference/api/pandas.PeriodIndex.dayofweek +generated/pandas.PeriodIndex.dayofyear,../reference/api/pandas.PeriodIndex.dayofyear +generated/pandas.PeriodIndex.days_in_month,../reference/api/pandas.PeriodIndex.days_in_month +generated/pandas.PeriodIndex.daysinmonth,../reference/api/pandas.PeriodIndex.daysinmonth +generated/pandas.PeriodIndex.end_time,../reference/api/pandas.PeriodIndex.end_time +generated/pandas.PeriodIndex.freq,../reference/api/pandas.PeriodIndex.freq +generated/pandas.PeriodIndex.freqstr,../reference/api/pandas.PeriodIndex.freqstr +generated/pandas.PeriodIndex.hour,../reference/api/pandas.PeriodIndex.hour +generated/pandas.PeriodIndex,../reference/api/pandas.PeriodIndex +generated/pandas.PeriodIndex.is_leap_year,../reference/api/pandas.PeriodIndex.is_leap_year +generated/pandas.PeriodIndex.minute,../reference/api/pandas.PeriodIndex.minute +generated/pandas.PeriodIndex.month,../reference/api/pandas.PeriodIndex.month +generated/pandas.PeriodIndex.quarter,../reference/api/pandas.PeriodIndex.quarter +generated/pandas.PeriodIndex.qyear,../reference/api/pandas.PeriodIndex.qyear +generated/pandas.PeriodIndex.second,../reference/api/pandas.PeriodIndex.second +generated/pandas.PeriodIndex.start_time,../reference/api/pandas.PeriodIndex.start_time +generated/pandas.PeriodIndex.strftime,../reference/api/pandas.PeriodIndex.strftime +generated/pandas.PeriodIndex.to_timestamp,../reference/api/pandas.PeriodIndex.to_timestamp +generated/pandas.PeriodIndex.weekday,../reference/api/pandas.PeriodIndex.weekday +generated/pandas.PeriodIndex.week,../reference/api/pandas.PeriodIndex.week +generated/pandas.PeriodIndex.weekofyear,../reference/api/pandas.PeriodIndex.weekofyear +generated/pandas.PeriodIndex.year,../reference/api/pandas.PeriodIndex.year +generated/pandas.Period.is_leap_year,../reference/api/pandas.Period.is_leap_year +generated/pandas.Period.minute,../reference/api/pandas.Period.minute +generated/pandas.Period.month,../reference/api/pandas.Period.month +generated/pandas.Period.now,../reference/api/pandas.Period.now +generated/pandas.Period.ordinal,../reference/api/pandas.Period.ordinal +generated/pandas.Period.quarter,../reference/api/pandas.Period.quarter +generated/pandas.Period.qyear,../reference/api/pandas.Period.qyear +generated/pandas.period_range,../reference/api/pandas.period_range +generated/pandas.Period.second,../reference/api/pandas.Period.second +generated/pandas.Period.start_time,../reference/api/pandas.Period.start_time +generated/pandas.Period.strftime,../reference/api/pandas.Period.strftime +generated/pandas.Period.to_timestamp,../reference/api/pandas.Period.to_timestamp +generated/pandas.Period.weekday,../reference/api/pandas.Period.weekday +generated/pandas.Period.week,../reference/api/pandas.Period.week +generated/pandas.Period.weekofyear,../reference/api/pandas.Period.weekofyear +generated/pandas.Period.year,../reference/api/pandas.Period.year +generated/pandas.pivot,../reference/api/pandas.pivot +generated/pandas.pivot_table,../reference/api/pandas.pivot_table +generated/pandas.plotting.andrews_curves,../reference/api/pandas.plotting.andrews_curves +generated/pandas.plotting.bootstrap_plot,../reference/api/pandas.plotting.bootstrap_plot +generated/pandas.plotting.deregister_matplotlib_converters,../reference/api/pandas.plotting.deregister_matplotlib_converters +generated/pandas.plotting.lag_plot,../reference/api/pandas.plotting.lag_plot +generated/pandas.plotting.parallel_coordinates,../reference/api/pandas.plotting.parallel_coordinates +generated/pandas.plotting.radviz,../reference/api/pandas.plotting.radviz +generated/pandas.plotting.register_matplotlib_converters,../reference/api/pandas.plotting.register_matplotlib_converters +generated/pandas.plotting.scatter_matrix,../reference/api/pandas.plotting.scatter_matrix +generated/pandas.qcut,../reference/api/pandas.qcut +generated/pandas.RangeIndex.from_range,../reference/api/pandas.RangeIndex.from_range +generated/pandas.RangeIndex,../reference/api/pandas.RangeIndex +generated/pandas.read_clipboard,../reference/api/pandas.read_clipboard +generated/pandas.read_csv,../reference/api/pandas.read_csv +generated/pandas.read_excel,../reference/api/pandas.read_excel +generated/pandas.read_feather,../reference/api/pandas.read_feather +generated/pandas.read_fwf,../reference/api/pandas.read_fwf +generated/pandas.read_gbq,../reference/api/pandas.read_gbq +generated/pandas.read_hdf,../reference/api/pandas.read_hdf +generated/pandas.read,../reference/api/pandas.read +generated/pandas.read_json,../reference/api/pandas.read_json +generated/pandas.read_msgpack,../reference/api/pandas.read_msgpack +generated/pandas.read_parquet,../reference/api/pandas.read_parquet +generated/pandas.read_pickle,../reference/api/pandas.read_pickle +generated/pandas.read_sas,../reference/api/pandas.read_sas +generated/pandas.read_sql,../reference/api/pandas.read_sql +generated/pandas.read_sql_query,../reference/api/pandas.read_sql_query +generated/pandas.read_sql_table,../reference/api/pandas.read_sql_table +generated/pandas.read_stata,../reference/api/pandas.read_stata +generated/pandas.read_table,../reference/api/pandas.read_table +generated/pandas.reset_option,../reference/api/pandas.reset_option +generated/pandas.Series.abs,../reference/api/pandas.Series.abs +generated/pandas.Series.add,../reference/api/pandas.Series.add +generated/pandas.Series.add_prefix,../reference/api/pandas.Series.add_prefix +generated/pandas.Series.add_suffix,../reference/api/pandas.Series.add_suffix +generated/pandas.Series.agg,../reference/api/pandas.Series.agg +generated/pandas.Series.aggregate,../reference/api/pandas.Series.aggregate +generated/pandas.Series.align,../reference/api/pandas.Series.align +generated/pandas.Series.all,../reference/api/pandas.Series.all +generated/pandas.Series.any,../reference/api/pandas.Series.any +generated/pandas.Series.append,../reference/api/pandas.Series.append +generated/pandas.Series.apply,../reference/api/pandas.Series.apply +generated/pandas.Series.argmax,../reference/api/pandas.Series.argmax +generated/pandas.Series.argmin,../reference/api/pandas.Series.argmin +generated/pandas.Series.argsort,../reference/api/pandas.Series.argsort +generated/pandas.Series.__array__,../reference/api/pandas.Series.__array__ +generated/pandas.Series.array,../reference/api/pandas.Series.array +generated/pandas.Series.as_blocks,../reference/api/pandas.Series.as_blocks +generated/pandas.Series.asfreq,../reference/api/pandas.Series.asfreq +generated/pandas.Series.as_matrix,../reference/api/pandas.Series.as_matrix +generated/pandas.Series.asobject,../reference/api/pandas.Series.asobject +generated/pandas.Series.asof,../reference/api/pandas.Series.asof +generated/pandas.Series.astype,../reference/api/pandas.Series.astype +generated/pandas.Series.at,../reference/api/pandas.Series.at +generated/pandas.Series.at_time,../reference/api/pandas.Series.at_time +generated/pandas.Series.autocorr,../reference/api/pandas.Series.autocorr +generated/pandas.Series.axes,../reference/api/pandas.Series.axes +generated/pandas.Series.base,../reference/api/pandas.Series.base +generated/pandas.Series.between,../reference/api/pandas.Series.between +generated/pandas.Series.between_time,../reference/api/pandas.Series.between_time +generated/pandas.Series.bfill,../reference/api/pandas.Series.bfill +generated/pandas.Series.blocks,../reference/api/pandas.Series.blocks +generated/pandas.Series.bool,../reference/api/pandas.Series.bool +generated/pandas.Series.cat.add_categories,../reference/api/pandas.Series.cat.add_categories +generated/pandas.Series.cat.as_ordered,../reference/api/pandas.Series.cat.as_ordered +generated/pandas.Series.cat.as_unordered,../reference/api/pandas.Series.cat.as_unordered +generated/pandas.Series.cat.categories,../reference/api/pandas.Series.cat.categories +generated/pandas.Series.cat.codes,../reference/api/pandas.Series.cat.codes +generated/pandas.Series.cat,../reference/api/pandas.Series.cat +generated/pandas.Series.cat.ordered,../reference/api/pandas.Series.cat.ordered +generated/pandas.Series.cat.remove_categories,../reference/api/pandas.Series.cat.remove_categories +generated/pandas.Series.cat.remove_unused_categories,../reference/api/pandas.Series.cat.remove_unused_categories +generated/pandas.Series.cat.rename_categories,../reference/api/pandas.Series.cat.rename_categories +generated/pandas.Series.cat.reorder_categories,../reference/api/pandas.Series.cat.reorder_categories +generated/pandas.Series.cat.set_categories,../reference/api/pandas.Series.cat.set_categories +generated/pandas.Series.clip,../reference/api/pandas.Series.clip +generated/pandas.Series.clip_lower,../reference/api/pandas.Series.clip_lower +generated/pandas.Series.clip_upper,../reference/api/pandas.Series.clip_upper +generated/pandas.Series.combine_first,../reference/api/pandas.Series.combine_first +generated/pandas.Series.combine,../reference/api/pandas.Series.combine +generated/pandas.Series.compound,../reference/api/pandas.Series.compound +generated/pandas.Series.compress,../reference/api/pandas.Series.compress +generated/pandas.Series.convert_objects,../reference/api/pandas.Series.convert_objects +generated/pandas.Series.copy,../reference/api/pandas.Series.copy +generated/pandas.Series.corr,../reference/api/pandas.Series.corr +generated/pandas.Series.count,../reference/api/pandas.Series.count +generated/pandas.Series.cov,../reference/api/pandas.Series.cov +generated/pandas.Series.cummax,../reference/api/pandas.Series.cummax +generated/pandas.Series.cummin,../reference/api/pandas.Series.cummin +generated/pandas.Series.cumprod,../reference/api/pandas.Series.cumprod +generated/pandas.Series.cumsum,../reference/api/pandas.Series.cumsum +generated/pandas.Series.data,../reference/api/pandas.Series.data +generated/pandas.Series.describe,../reference/api/pandas.Series.describe +generated/pandas.Series.diff,../reference/api/pandas.Series.diff +generated/pandas.Series.div,../reference/api/pandas.Series.div +generated/pandas.Series.divide,../reference/api/pandas.Series.divide +generated/pandas.Series.divmod,../reference/api/pandas.Series.divmod +generated/pandas.Series.dot,../reference/api/pandas.Series.dot +generated/pandas.Series.drop_duplicates,../reference/api/pandas.Series.drop_duplicates +generated/pandas.Series.drop,../reference/api/pandas.Series.drop +generated/pandas.Series.droplevel,../reference/api/pandas.Series.droplevel +generated/pandas.Series.dropna,../reference/api/pandas.Series.dropna +generated/pandas.Series.dt.ceil,../reference/api/pandas.Series.dt.ceil +generated/pandas.Series.dt.components,../reference/api/pandas.Series.dt.components +generated/pandas.Series.dt.date,../reference/api/pandas.Series.dt.date +generated/pandas.Series.dt.day,../reference/api/pandas.Series.dt.day +generated/pandas.Series.dt.day_name,../reference/api/pandas.Series.dt.day_name +generated/pandas.Series.dt.dayofweek,../reference/api/pandas.Series.dt.dayofweek +generated/pandas.Series.dt.dayofyear,../reference/api/pandas.Series.dt.dayofyear +generated/pandas.Series.dt.days,../reference/api/pandas.Series.dt.days +generated/pandas.Series.dt.days_in_month,../reference/api/pandas.Series.dt.days_in_month +generated/pandas.Series.dt.daysinmonth,../reference/api/pandas.Series.dt.daysinmonth +generated/pandas.Series.dt.end_time,../reference/api/pandas.Series.dt.end_time +generated/pandas.Series.dt.floor,../reference/api/pandas.Series.dt.floor +generated/pandas.Series.dt.freq,../reference/api/pandas.Series.dt.freq +generated/pandas.Series.dt.hour,../reference/api/pandas.Series.dt.hour +generated/pandas.Series.dt,../reference/api/pandas.Series.dt +generated/pandas.Series.dt.is_leap_year,../reference/api/pandas.Series.dt.is_leap_year +generated/pandas.Series.dt.is_month_end,../reference/api/pandas.Series.dt.is_month_end +generated/pandas.Series.dt.is_month_start,../reference/api/pandas.Series.dt.is_month_start +generated/pandas.Series.dt.is_quarter_end,../reference/api/pandas.Series.dt.is_quarter_end +generated/pandas.Series.dt.is_quarter_start,../reference/api/pandas.Series.dt.is_quarter_start +generated/pandas.Series.dt.is_year_end,../reference/api/pandas.Series.dt.is_year_end +generated/pandas.Series.dt.is_year_start,../reference/api/pandas.Series.dt.is_year_start +generated/pandas.Series.dt.microsecond,../reference/api/pandas.Series.dt.microsecond +generated/pandas.Series.dt.microseconds,../reference/api/pandas.Series.dt.microseconds +generated/pandas.Series.dt.minute,../reference/api/pandas.Series.dt.minute +generated/pandas.Series.dt.month,../reference/api/pandas.Series.dt.month +generated/pandas.Series.dt.month_name,../reference/api/pandas.Series.dt.month_name +generated/pandas.Series.dt.nanosecond,../reference/api/pandas.Series.dt.nanosecond +generated/pandas.Series.dt.nanoseconds,../reference/api/pandas.Series.dt.nanoseconds +generated/pandas.Series.dt.normalize,../reference/api/pandas.Series.dt.normalize +generated/pandas.Series.dt.quarter,../reference/api/pandas.Series.dt.quarter +generated/pandas.Series.dt.qyear,../reference/api/pandas.Series.dt.qyear +generated/pandas.Series.dt.round,../reference/api/pandas.Series.dt.round +generated/pandas.Series.dt.second,../reference/api/pandas.Series.dt.second +generated/pandas.Series.dt.seconds,../reference/api/pandas.Series.dt.seconds +generated/pandas.Series.dt.start_time,../reference/api/pandas.Series.dt.start_time +generated/pandas.Series.dt.strftime,../reference/api/pandas.Series.dt.strftime +generated/pandas.Series.dt.time,../reference/api/pandas.Series.dt.time +generated/pandas.Series.dt.timetz,../reference/api/pandas.Series.dt.timetz +generated/pandas.Series.dt.to_period,../reference/api/pandas.Series.dt.to_period +generated/pandas.Series.dt.to_pydatetime,../reference/api/pandas.Series.dt.to_pydatetime +generated/pandas.Series.dt.to_pytimedelta,../reference/api/pandas.Series.dt.to_pytimedelta +generated/pandas.Series.dt.total_seconds,../reference/api/pandas.Series.dt.total_seconds +generated/pandas.Series.dt.tz_convert,../reference/api/pandas.Series.dt.tz_convert +generated/pandas.Series.dt.tz,../reference/api/pandas.Series.dt.tz +generated/pandas.Series.dt.tz_localize,../reference/api/pandas.Series.dt.tz_localize +generated/pandas.Series.dt.weekday,../reference/api/pandas.Series.dt.weekday +generated/pandas.Series.dt.week,../reference/api/pandas.Series.dt.week +generated/pandas.Series.dt.weekofyear,../reference/api/pandas.Series.dt.weekofyear +generated/pandas.Series.dt.year,../reference/api/pandas.Series.dt.year +generated/pandas.Series.dtype,../reference/api/pandas.Series.dtype +generated/pandas.Series.dtypes,../reference/api/pandas.Series.dtypes +generated/pandas.Series.duplicated,../reference/api/pandas.Series.duplicated +generated/pandas.Series.empty,../reference/api/pandas.Series.empty +generated/pandas.Series.eq,../reference/api/pandas.Series.eq +generated/pandas.Series.equals,../reference/api/pandas.Series.equals +generated/pandas.Series.ewm,../reference/api/pandas.Series.ewm +generated/pandas.Series.expanding,../reference/api/pandas.Series.expanding +generated/pandas.Series.factorize,../reference/api/pandas.Series.factorize +generated/pandas.Series.ffill,../reference/api/pandas.Series.ffill +generated/pandas.Series.fillna,../reference/api/pandas.Series.fillna +generated/pandas.Series.filter,../reference/api/pandas.Series.filter +generated/pandas.Series.first,../reference/api/pandas.Series.first +generated/pandas.Series.first_valid_index,../reference/api/pandas.Series.first_valid_index +generated/pandas.Series.flags,../reference/api/pandas.Series.flags +generated/pandas.Series.floordiv,../reference/api/pandas.Series.floordiv +generated/pandas.Series.from_array,../reference/api/pandas.Series.from_array +generated/pandas.Series.from_csv,../reference/api/pandas.Series.from_csv +generated/pandas.Series.ftype,../reference/api/pandas.Series.ftype +generated/pandas.Series.ftypes,../reference/api/pandas.Series.ftypes +generated/pandas.Series.ge,../reference/api/pandas.Series.ge +generated/pandas.Series.get_dtype_counts,../reference/api/pandas.Series.get_dtype_counts +generated/pandas.Series.get_ftype_counts,../reference/api/pandas.Series.get_ftype_counts +generated/pandas.Series.get,../reference/api/pandas.Series.get +generated/pandas.Series.get_value,../reference/api/pandas.Series.get_value +generated/pandas.Series.get_values,../reference/api/pandas.Series.get_values +generated/pandas.Series.groupby,../reference/api/pandas.Series.groupby +generated/pandas.Series.gt,../reference/api/pandas.Series.gt +generated/pandas.Series.hasnans,../reference/api/pandas.Series.hasnans +generated/pandas.Series.head,../reference/api/pandas.Series.head +generated/pandas.Series.hist,../reference/api/pandas.Series.hist +generated/pandas.Series,../reference/api/pandas.Series +generated/pandas.Series.iat,../reference/api/pandas.Series.iat +generated/pandas.Series.idxmax,../reference/api/pandas.Series.idxmax +generated/pandas.Series.idxmin,../reference/api/pandas.Series.idxmin +generated/pandas.Series.iloc,../reference/api/pandas.Series.iloc +generated/pandas.Series.imag,../reference/api/pandas.Series.imag +generated/pandas.Series.index,../reference/api/pandas.Series.index +generated/pandas.Series.infer_objects,../reference/api/pandas.Series.infer_objects +generated/pandas.Series.interpolate,../reference/api/pandas.Series.interpolate +generated/pandas.Series.is_copy,../reference/api/pandas.Series.is_copy +generated/pandas.Series.isin,../reference/api/pandas.Series.isin +generated/pandas.Series.is_monotonic_decreasing,../reference/api/pandas.Series.is_monotonic_decreasing +generated/pandas.Series.is_monotonic,../reference/api/pandas.Series.is_monotonic +generated/pandas.Series.is_monotonic_increasing,../reference/api/pandas.Series.is_monotonic_increasing +generated/pandas.Series.isna,../reference/api/pandas.Series.isna +generated/pandas.Series.isnull,../reference/api/pandas.Series.isnull +generated/pandas.Series.is_unique,../reference/api/pandas.Series.is_unique +generated/pandas.Series.item,../reference/api/pandas.Series.item +generated/pandas.Series.items,../reference/api/pandas.Series.items +generated/pandas.Series.itemsize,../reference/api/pandas.Series.itemsize +generated/pandas.Series.__iter__,../reference/api/pandas.Series.__iter__ +generated/pandas.Series.iteritems,../reference/api/pandas.Series.iteritems +generated/pandas.Series.ix,../reference/api/pandas.Series.ix +generated/pandas.Series.keys,../reference/api/pandas.Series.keys +generated/pandas.Series.kurt,../reference/api/pandas.Series.kurt +generated/pandas.Series.kurtosis,../reference/api/pandas.Series.kurtosis +generated/pandas.Series.last,../reference/api/pandas.Series.last +generated/pandas.Series.last_valid_index,../reference/api/pandas.Series.last_valid_index +generated/pandas.Series.le,../reference/api/pandas.Series.le +generated/pandas.Series.loc,../reference/api/pandas.Series.loc +generated/pandas.Series.lt,../reference/api/pandas.Series.lt +generated/pandas.Series.mad,../reference/api/pandas.Series.mad +generated/pandas.Series.map,../reference/api/pandas.Series.map +generated/pandas.Series.mask,../reference/api/pandas.Series.mask +generated/pandas.Series.max,../reference/api/pandas.Series.max +generated/pandas.Series.mean,../reference/api/pandas.Series.mean +generated/pandas.Series.median,../reference/api/pandas.Series.median +generated/pandas.Series.memory_usage,../reference/api/pandas.Series.memory_usage +generated/pandas.Series.min,../reference/api/pandas.Series.min +generated/pandas.Series.mode,../reference/api/pandas.Series.mode +generated/pandas.Series.mod,../reference/api/pandas.Series.mod +generated/pandas.Series.mul,../reference/api/pandas.Series.mul +generated/pandas.Series.multiply,../reference/api/pandas.Series.multiply +generated/pandas.Series.name,../reference/api/pandas.Series.name +generated/pandas.Series.nbytes,../reference/api/pandas.Series.nbytes +generated/pandas.Series.ndim,../reference/api/pandas.Series.ndim +generated/pandas.Series.ne,../reference/api/pandas.Series.ne +generated/pandas.Series.nlargest,../reference/api/pandas.Series.nlargest +generated/pandas.Series.nonzero,../reference/api/pandas.Series.nonzero +generated/pandas.Series.notna,../reference/api/pandas.Series.notna +generated/pandas.Series.notnull,../reference/api/pandas.Series.notnull +generated/pandas.Series.nsmallest,../reference/api/pandas.Series.nsmallest +generated/pandas.Series.nunique,../reference/api/pandas.Series.nunique +generated/pandas.Series.pct_change,../reference/api/pandas.Series.pct_change +generated/pandas.Series.pipe,../reference/api/pandas.Series.pipe +generated/pandas.Series.plot.area,../reference/api/pandas.Series.plot.area +generated/pandas.Series.plot.barh,../reference/api/pandas.Series.plot.barh +generated/pandas.Series.plot.bar,../reference/api/pandas.Series.plot.bar +generated/pandas.Series.plot.box,../reference/api/pandas.Series.plot.box +generated/pandas.Series.plot.density,../reference/api/pandas.Series.plot.density +generated/pandas.Series.plot.hist,../reference/api/pandas.Series.plot.hist +generated/pandas.Series.plot,../reference/api/pandas.Series.plot +generated/pandas.Series.plot.kde,../reference/api/pandas.Series.plot.kde +generated/pandas.Series.plot.line,../reference/api/pandas.Series.plot.line +generated/pandas.Series.plot.pie,../reference/api/pandas.Series.plot.pie +generated/pandas.Series.pop,../reference/api/pandas.Series.pop +generated/pandas.Series.pow,../reference/api/pandas.Series.pow +generated/pandas.Series.prod,../reference/api/pandas.Series.prod +generated/pandas.Series.product,../reference/api/pandas.Series.product +generated/pandas.Series.ptp,../reference/api/pandas.Series.ptp +generated/pandas.Series.put,../reference/api/pandas.Series.put +generated/pandas.Series.quantile,../reference/api/pandas.Series.quantile +generated/pandas.Series.radd,../reference/api/pandas.Series.radd +generated/pandas.Series.rank,../reference/api/pandas.Series.rank +generated/pandas.Series.ravel,../reference/api/pandas.Series.ravel +generated/pandas.Series.rdiv,../reference/api/pandas.Series.rdiv +generated/pandas.Series.rdivmod,../reference/api/pandas.Series.rdivmod +generated/pandas.Series.real,../reference/api/pandas.Series.real +generated/pandas.Series.reindex_axis,../reference/api/pandas.Series.reindex_axis +generated/pandas.Series.reindex,../reference/api/pandas.Series.reindex +generated/pandas.Series.reindex_like,../reference/api/pandas.Series.reindex_like +generated/pandas.Series.rename_axis,../reference/api/pandas.Series.rename_axis +generated/pandas.Series.rename,../reference/api/pandas.Series.rename +generated/pandas.Series.reorder_levels,../reference/api/pandas.Series.reorder_levels +generated/pandas.Series.repeat,../reference/api/pandas.Series.repeat +generated/pandas.Series.replace,../reference/api/pandas.Series.replace +generated/pandas.Series.resample,../reference/api/pandas.Series.resample +generated/pandas.Series.reset_index,../reference/api/pandas.Series.reset_index +generated/pandas.Series.rfloordiv,../reference/api/pandas.Series.rfloordiv +generated/pandas.Series.rmod,../reference/api/pandas.Series.rmod +generated/pandas.Series.rmul,../reference/api/pandas.Series.rmul +generated/pandas.Series.rolling,../reference/api/pandas.Series.rolling +generated/pandas.Series.round,../reference/api/pandas.Series.round +generated/pandas.Series.rpow,../reference/api/pandas.Series.rpow +generated/pandas.Series.rsub,../reference/api/pandas.Series.rsub +generated/pandas.Series.rtruediv,../reference/api/pandas.Series.rtruediv +generated/pandas.Series.sample,../reference/api/pandas.Series.sample +generated/pandas.Series.searchsorted,../reference/api/pandas.Series.searchsorted +generated/pandas.Series.select,../reference/api/pandas.Series.select +generated/pandas.Series.sem,../reference/api/pandas.Series.sem +generated/pandas.Series.set_axis,../reference/api/pandas.Series.set_axis +generated/pandas.Series.set_value,../reference/api/pandas.Series.set_value +generated/pandas.Series.shape,../reference/api/pandas.Series.shape +generated/pandas.Series.shift,../reference/api/pandas.Series.shift +generated/pandas.Series.size,../reference/api/pandas.Series.size +generated/pandas.Series.skew,../reference/api/pandas.Series.skew +generated/pandas.Series.slice_shift,../reference/api/pandas.Series.slice_shift +generated/pandas.Series.sort_index,../reference/api/pandas.Series.sort_index +generated/pandas.Series.sort_values,../reference/api/pandas.Series.sort_values +generated/pandas.Series.sparse.density,../reference/api/pandas.Series.sparse.density +generated/pandas.Series.sparse.fill_value,../reference/api/pandas.Series.sparse.fill_value +generated/pandas.Series.sparse.from_coo,../reference/api/pandas.Series.sparse.from_coo +generated/pandas.Series.sparse.npoints,../reference/api/pandas.Series.sparse.npoints +generated/pandas.Series.sparse.sp_values,../reference/api/pandas.Series.sparse.sp_values +generated/pandas.Series.sparse.to_coo,../reference/api/pandas.Series.sparse.to_coo +generated/pandas.Series.squeeze,../reference/api/pandas.Series.squeeze +generated/pandas.Series.std,../reference/api/pandas.Series.std +generated/pandas.Series.str.capitalize,../reference/api/pandas.Series.str.capitalize +generated/pandas.Series.str.cat,../reference/api/pandas.Series.str.cat +generated/pandas.Series.str.center,../reference/api/pandas.Series.str.center +generated/pandas.Series.str.contains,../reference/api/pandas.Series.str.contains +generated/pandas.Series.str.count,../reference/api/pandas.Series.str.count +generated/pandas.Series.str.decode,../reference/api/pandas.Series.str.decode +generated/pandas.Series.str.encode,../reference/api/pandas.Series.str.encode +generated/pandas.Series.str.endswith,../reference/api/pandas.Series.str.endswith +generated/pandas.Series.str.extractall,../reference/api/pandas.Series.str.extractall +generated/pandas.Series.str.extract,../reference/api/pandas.Series.str.extract +generated/pandas.Series.str.findall,../reference/api/pandas.Series.str.findall +generated/pandas.Series.str.find,../reference/api/pandas.Series.str.find +generated/pandas.Series.str.get_dummies,../reference/api/pandas.Series.str.get_dummies +generated/pandas.Series.str.get,../reference/api/pandas.Series.str.get +generated/pandas.Series.str,../reference/api/pandas.Series.str +generated/pandas.Series.strides,../reference/api/pandas.Series.strides +generated/pandas.Series.str.index,../reference/api/pandas.Series.str.index +generated/pandas.Series.str.isalnum,../reference/api/pandas.Series.str.isalnum +generated/pandas.Series.str.isalpha,../reference/api/pandas.Series.str.isalpha +generated/pandas.Series.str.isdecimal,../reference/api/pandas.Series.str.isdecimal +generated/pandas.Series.str.isdigit,../reference/api/pandas.Series.str.isdigit +generated/pandas.Series.str.islower,../reference/api/pandas.Series.str.islower +generated/pandas.Series.str.isnumeric,../reference/api/pandas.Series.str.isnumeric +generated/pandas.Series.str.isspace,../reference/api/pandas.Series.str.isspace +generated/pandas.Series.str.istitle,../reference/api/pandas.Series.str.istitle +generated/pandas.Series.str.isupper,../reference/api/pandas.Series.str.isupper +generated/pandas.Series.str.join,../reference/api/pandas.Series.str.join +generated/pandas.Series.str.len,../reference/api/pandas.Series.str.len +generated/pandas.Series.str.ljust,../reference/api/pandas.Series.str.ljust +generated/pandas.Series.str.lower,../reference/api/pandas.Series.str.lower +generated/pandas.Series.str.lstrip,../reference/api/pandas.Series.str.lstrip +generated/pandas.Series.str.match,../reference/api/pandas.Series.str.match +generated/pandas.Series.str.normalize,../reference/api/pandas.Series.str.normalize +generated/pandas.Series.str.pad,../reference/api/pandas.Series.str.pad +generated/pandas.Series.str.partition,../reference/api/pandas.Series.str.partition +generated/pandas.Series.str.repeat,../reference/api/pandas.Series.str.repeat +generated/pandas.Series.str.replace,../reference/api/pandas.Series.str.replace +generated/pandas.Series.str.rfind,../reference/api/pandas.Series.str.rfind +generated/pandas.Series.str.rindex,../reference/api/pandas.Series.str.rindex +generated/pandas.Series.str.rjust,../reference/api/pandas.Series.str.rjust +generated/pandas.Series.str.rpartition,../reference/api/pandas.Series.str.rpartition +generated/pandas.Series.str.rsplit,../reference/api/pandas.Series.str.rsplit +generated/pandas.Series.str.rstrip,../reference/api/pandas.Series.str.rstrip +generated/pandas.Series.str.slice,../reference/api/pandas.Series.str.slice +generated/pandas.Series.str.slice_replace,../reference/api/pandas.Series.str.slice_replace +generated/pandas.Series.str.split,../reference/api/pandas.Series.str.split +generated/pandas.Series.str.startswith,../reference/api/pandas.Series.str.startswith +generated/pandas.Series.str.strip,../reference/api/pandas.Series.str.strip +generated/pandas.Series.str.swapcase,../reference/api/pandas.Series.str.swapcase +generated/pandas.Series.str.title,../reference/api/pandas.Series.str.title +generated/pandas.Series.str.translate,../reference/api/pandas.Series.str.translate +generated/pandas.Series.str.upper,../reference/api/pandas.Series.str.upper +generated/pandas.Series.str.wrap,../reference/api/pandas.Series.str.wrap +generated/pandas.Series.str.zfill,../reference/api/pandas.Series.str.zfill +generated/pandas.Series.sub,../reference/api/pandas.Series.sub +generated/pandas.Series.subtract,../reference/api/pandas.Series.subtract +generated/pandas.Series.sum,../reference/api/pandas.Series.sum +generated/pandas.Series.swapaxes,../reference/api/pandas.Series.swapaxes +generated/pandas.Series.swaplevel,../reference/api/pandas.Series.swaplevel +generated/pandas.Series.tail,../reference/api/pandas.Series.tail +generated/pandas.Series.take,../reference/api/pandas.Series.take +generated/pandas.Series.T,../reference/api/pandas.Series.T +generated/pandas.Series.timetuple,../reference/api/pandas.Series.timetuple +generated/pandas.Series.to_clipboard,../reference/api/pandas.Series.to_clipboard +generated/pandas.Series.to_csv,../reference/api/pandas.Series.to_csv +generated/pandas.Series.to_dense,../reference/api/pandas.Series.to_dense +generated/pandas.Series.to_dict,../reference/api/pandas.Series.to_dict +generated/pandas.Series.to_excel,../reference/api/pandas.Series.to_excel +generated/pandas.Series.to_frame,../reference/api/pandas.Series.to_frame +generated/pandas.Series.to_hdf,../reference/api/pandas.Series.to_hdf +generated/pandas.Series.to_json,../reference/api/pandas.Series.to_json +generated/pandas.Series.to_latex,../reference/api/pandas.Series.to_latex +generated/pandas.Series.to_list,../reference/api/pandas.Series.to_list +generated/pandas.Series.tolist,../reference/api/pandas.Series.tolist +generated/pandas.Series.to_msgpack,../reference/api/pandas.Series.to_msgpack +generated/pandas.Series.to_numpy,../reference/api/pandas.Series.to_numpy +generated/pandas.Series.to_period,../reference/api/pandas.Series.to_period +generated/pandas.Series.to_pickle,../reference/api/pandas.Series.to_pickle +generated/pandas.Series.to_sparse,../reference/api/pandas.Series.to_sparse +generated/pandas.Series.to_sql,../reference/api/pandas.Series.to_sql +generated/pandas.Series.to_string,../reference/api/pandas.Series.to_string +generated/pandas.Series.to_timestamp,../reference/api/pandas.Series.to_timestamp +generated/pandas.Series.to_xarray,../reference/api/pandas.Series.to_xarray +generated/pandas.Series.transform,../reference/api/pandas.Series.transform +generated/pandas.Series.transpose,../reference/api/pandas.Series.transpose +generated/pandas.Series.truediv,../reference/api/pandas.Series.truediv +generated/pandas.Series.truncate,../reference/api/pandas.Series.truncate +generated/pandas.Series.tshift,../reference/api/pandas.Series.tshift +generated/pandas.Series.tz_convert,../reference/api/pandas.Series.tz_convert +generated/pandas.Series.tz_localize,../reference/api/pandas.Series.tz_localize +generated/pandas.Series.unique,../reference/api/pandas.Series.unique +generated/pandas.Series.unstack,../reference/api/pandas.Series.unstack +generated/pandas.Series.update,../reference/api/pandas.Series.update +generated/pandas.Series.valid,../reference/api/pandas.Series.valid +generated/pandas.Series.value_counts,../reference/api/pandas.Series.value_counts +generated/pandas.Series.values,../reference/api/pandas.Series.values +generated/pandas.Series.var,../reference/api/pandas.Series.var +generated/pandas.Series.view,../reference/api/pandas.Series.view +generated/pandas.Series.where,../reference/api/pandas.Series.where +generated/pandas.Series.xs,../reference/api/pandas.Series.xs +generated/pandas.set_option,../reference/api/pandas.set_option +generated/pandas.SparseDataFrame.to_coo,../reference/api/pandas.SparseDataFrame.to_coo +generated/pandas.SparseSeries.from_coo,../reference/api/pandas.SparseSeries.from_coo +generated/pandas.SparseSeries.to_coo,../reference/api/pandas.SparseSeries.to_coo +generated/pandas.test,../reference/api/pandas.test +generated/pandas.testing.assert_frame_equal,../reference/api/pandas.testing.assert_frame_equal +generated/pandas.testing.assert_index_equal,../reference/api/pandas.testing.assert_index_equal +generated/pandas.testing.assert_series_equal,../reference/api/pandas.testing.assert_series_equal +generated/pandas.Timedelta.asm8,../reference/api/pandas.Timedelta.asm8 +generated/pandas.Timedelta.ceil,../reference/api/pandas.Timedelta.ceil +generated/pandas.Timedelta.components,../reference/api/pandas.Timedelta.components +generated/pandas.Timedelta.days,../reference/api/pandas.Timedelta.days +generated/pandas.Timedelta.delta,../reference/api/pandas.Timedelta.delta +generated/pandas.Timedelta.floor,../reference/api/pandas.Timedelta.floor +generated/pandas.Timedelta.freq,../reference/api/pandas.Timedelta.freq +generated/pandas.Timedelta,../reference/api/pandas.Timedelta +generated/pandas.TimedeltaIndex.ceil,../reference/api/pandas.TimedeltaIndex.ceil +generated/pandas.TimedeltaIndex.components,../reference/api/pandas.TimedeltaIndex.components +generated/pandas.TimedeltaIndex.days,../reference/api/pandas.TimedeltaIndex.days +generated/pandas.TimedeltaIndex.floor,../reference/api/pandas.TimedeltaIndex.floor +generated/pandas.TimedeltaIndex,../reference/api/pandas.TimedeltaIndex +generated/pandas.TimedeltaIndex.inferred_freq,../reference/api/pandas.TimedeltaIndex.inferred_freq +generated/pandas.TimedeltaIndex.microseconds,../reference/api/pandas.TimedeltaIndex.microseconds +generated/pandas.TimedeltaIndex.nanoseconds,../reference/api/pandas.TimedeltaIndex.nanoseconds +generated/pandas.TimedeltaIndex.round,../reference/api/pandas.TimedeltaIndex.round +generated/pandas.TimedeltaIndex.seconds,../reference/api/pandas.TimedeltaIndex.seconds +generated/pandas.TimedeltaIndex.to_frame,../reference/api/pandas.TimedeltaIndex.to_frame +generated/pandas.TimedeltaIndex.to_pytimedelta,../reference/api/pandas.TimedeltaIndex.to_pytimedelta +generated/pandas.TimedeltaIndex.to_series,../reference/api/pandas.TimedeltaIndex.to_series +generated/pandas.Timedelta.isoformat,../reference/api/pandas.Timedelta.isoformat +generated/pandas.Timedelta.is_populated,../reference/api/pandas.Timedelta.is_populated +generated/pandas.Timedelta.max,../reference/api/pandas.Timedelta.max +generated/pandas.Timedelta.microseconds,../reference/api/pandas.Timedelta.microseconds +generated/pandas.Timedelta.min,../reference/api/pandas.Timedelta.min +generated/pandas.Timedelta.nanoseconds,../reference/api/pandas.Timedelta.nanoseconds +generated/pandas.timedelta_range,../reference/api/pandas.timedelta_range +generated/pandas.Timedelta.resolution,../reference/api/pandas.Timedelta.resolution +generated/pandas.Timedelta.round,../reference/api/pandas.Timedelta.round +generated/pandas.Timedelta.seconds,../reference/api/pandas.Timedelta.seconds +generated/pandas.Timedelta.to_pytimedelta,../reference/api/pandas.Timedelta.to_pytimedelta +generated/pandas.Timedelta.total_seconds,../reference/api/pandas.Timedelta.total_seconds +generated/pandas.Timedelta.to_timedelta64,../reference/api/pandas.Timedelta.to_timedelta64 +generated/pandas.Timedelta.value,../reference/api/pandas.Timedelta.value +generated/pandas.Timedelta.view,../reference/api/pandas.Timedelta.view +generated/pandas.Timestamp.asm8,../reference/api/pandas.Timestamp.asm8 +generated/pandas.Timestamp.astimezone,../reference/api/pandas.Timestamp.astimezone +generated/pandas.Timestamp.ceil,../reference/api/pandas.Timestamp.ceil +generated/pandas.Timestamp.combine,../reference/api/pandas.Timestamp.combine +generated/pandas.Timestamp.ctime,../reference/api/pandas.Timestamp.ctime +generated/pandas.Timestamp.date,../reference/api/pandas.Timestamp.date +generated/pandas.Timestamp.day,../reference/api/pandas.Timestamp.day +generated/pandas.Timestamp.day_name,../reference/api/pandas.Timestamp.day_name +generated/pandas.Timestamp.dayofweek,../reference/api/pandas.Timestamp.dayofweek +generated/pandas.Timestamp.dayofyear,../reference/api/pandas.Timestamp.dayofyear +generated/pandas.Timestamp.days_in_month,../reference/api/pandas.Timestamp.days_in_month +generated/pandas.Timestamp.daysinmonth,../reference/api/pandas.Timestamp.daysinmonth +generated/pandas.Timestamp.dst,../reference/api/pandas.Timestamp.dst +generated/pandas.Timestamp.floor,../reference/api/pandas.Timestamp.floor +generated/pandas.Timestamp.fold,../reference/api/pandas.Timestamp.fold +generated/pandas.Timestamp.freq,../reference/api/pandas.Timestamp.freq +generated/pandas.Timestamp.freqstr,../reference/api/pandas.Timestamp.freqstr +generated/pandas.Timestamp.fromisoformat,../reference/api/pandas.Timestamp.fromisoformat +generated/pandas.Timestamp.fromordinal,../reference/api/pandas.Timestamp.fromordinal +generated/pandas.Timestamp.fromtimestamp,../reference/api/pandas.Timestamp.fromtimestamp +generated/pandas.Timestamp.hour,../reference/api/pandas.Timestamp.hour +generated/pandas.Timestamp,../reference/api/pandas.Timestamp +generated/pandas.Timestamp.is_leap_year,../reference/api/pandas.Timestamp.is_leap_year +generated/pandas.Timestamp.is_month_end,../reference/api/pandas.Timestamp.is_month_end +generated/pandas.Timestamp.is_month_start,../reference/api/pandas.Timestamp.is_month_start +generated/pandas.Timestamp.isocalendar,../reference/api/pandas.Timestamp.isocalendar +generated/pandas.Timestamp.isoformat,../reference/api/pandas.Timestamp.isoformat +generated/pandas.Timestamp.isoweekday,../reference/api/pandas.Timestamp.isoweekday +generated/pandas.Timestamp.is_quarter_end,../reference/api/pandas.Timestamp.is_quarter_end +generated/pandas.Timestamp.is_quarter_start,../reference/api/pandas.Timestamp.is_quarter_start +generated/pandas.Timestamp.is_year_end,../reference/api/pandas.Timestamp.is_year_end +generated/pandas.Timestamp.is_year_start,../reference/api/pandas.Timestamp.is_year_start +generated/pandas.Timestamp.max,../reference/api/pandas.Timestamp.max +generated/pandas.Timestamp.microsecond,../reference/api/pandas.Timestamp.microsecond +generated/pandas.Timestamp.min,../reference/api/pandas.Timestamp.min +generated/pandas.Timestamp.minute,../reference/api/pandas.Timestamp.minute +generated/pandas.Timestamp.month,../reference/api/pandas.Timestamp.month +generated/pandas.Timestamp.month_name,../reference/api/pandas.Timestamp.month_name +generated/pandas.Timestamp.nanosecond,../reference/api/pandas.Timestamp.nanosecond +generated/pandas.Timestamp.normalize,../reference/api/pandas.Timestamp.normalize +generated/pandas.Timestamp.now,../reference/api/pandas.Timestamp.now +generated/pandas.Timestamp.quarter,../reference/api/pandas.Timestamp.quarter +generated/pandas.Timestamp.replace,../reference/api/pandas.Timestamp.replace +generated/pandas.Timestamp.resolution,../reference/api/pandas.Timestamp.resolution +generated/pandas.Timestamp.round,../reference/api/pandas.Timestamp.round +generated/pandas.Timestamp.second,../reference/api/pandas.Timestamp.second +generated/pandas.Timestamp.strftime,../reference/api/pandas.Timestamp.strftime +generated/pandas.Timestamp.strptime,../reference/api/pandas.Timestamp.strptime +generated/pandas.Timestamp.time,../reference/api/pandas.Timestamp.time +generated/pandas.Timestamp.timestamp,../reference/api/pandas.Timestamp.timestamp +generated/pandas.Timestamp.timetuple,../reference/api/pandas.Timestamp.timetuple +generated/pandas.Timestamp.timetz,../reference/api/pandas.Timestamp.timetz +generated/pandas.Timestamp.to_datetime64,../reference/api/pandas.Timestamp.to_datetime64 +generated/pandas.Timestamp.today,../reference/api/pandas.Timestamp.today +generated/pandas.Timestamp.to_julian_date,../reference/api/pandas.Timestamp.to_julian_date +generated/pandas.Timestamp.toordinal,../reference/api/pandas.Timestamp.toordinal +generated/pandas.Timestamp.to_period,../reference/api/pandas.Timestamp.to_period +generated/pandas.Timestamp.to_pydatetime,../reference/api/pandas.Timestamp.to_pydatetime +generated/pandas.Timestamp.tz_convert,../reference/api/pandas.Timestamp.tz_convert +generated/pandas.Timestamp.tz,../reference/api/pandas.Timestamp.tz +generated/pandas.Timestamp.tzinfo,../reference/api/pandas.Timestamp.tzinfo +generated/pandas.Timestamp.tz_localize,../reference/api/pandas.Timestamp.tz_localize +generated/pandas.Timestamp.tzname,../reference/api/pandas.Timestamp.tzname +generated/pandas.Timestamp.utcfromtimestamp,../reference/api/pandas.Timestamp.utcfromtimestamp +generated/pandas.Timestamp.utcnow,../reference/api/pandas.Timestamp.utcnow +generated/pandas.Timestamp.utcoffset,../reference/api/pandas.Timestamp.utcoffset +generated/pandas.Timestamp.utctimetuple,../reference/api/pandas.Timestamp.utctimetuple +generated/pandas.Timestamp.value,../reference/api/pandas.Timestamp.value +generated/pandas.Timestamp.weekday,../reference/api/pandas.Timestamp.weekday +generated/pandas.Timestamp.weekday_name,../reference/api/pandas.Timestamp.weekday_name +generated/pandas.Timestamp.week,../reference/api/pandas.Timestamp.week +generated/pandas.Timestamp.weekofyear,../reference/api/pandas.Timestamp.weekofyear +generated/pandas.Timestamp.year,../reference/api/pandas.Timestamp.year +generated/pandas.to_datetime,../reference/api/pandas.to_datetime +generated/pandas.to_numeric,../reference/api/pandas.to_numeric +generated/pandas.to_timedelta,../reference/api/pandas.to_timedelta +generated/pandas.tseries.frequencies.to_offset,../reference/api/pandas.tseries.frequencies.to_offset +generated/pandas.unique,../reference/api/pandas.unique +generated/pandas.util.hash_array,../reference/api/pandas.util.hash_array +generated/pandas.util.hash_pandas_object,../reference/api/pandas.util.hash_pandas_object +generated/pandas.wide_to_long,../reference/api/pandas.wide_to_long diff --git a/doc/source/_static/banklist.html b/doc/source/_static/banklist.html index 8ec1561f8c394..cb07c332acbe7 100644 --- a/doc/source/_static/banklist.html +++ b/doc/source/_static/banklist.html @@ -7,7 +7,7 @@ - + @@ -37,7 +37,7 @@ else var sValue = li.selectValue; $('#googlesearch').submit(); - + } function findValue2(li) { if( li == null ) return alert("No match!"); @@ -47,7 +47,7 @@ // otherwise, let's just display the value in the text box else var sValue = li.selectValue; - + $('#googlesearch2').submit(); } function selectItem(li) { @@ -62,7 +62,7 @@ function log(event, data, formatted) { $("
  • ").html( !data ? "No match!" : "Selected: " + formatted).appendTo("#result"); } - + function formatItem(row) { return row[0] + " (id: " + row[1] + ")"; } @@ -81,7 +81,7 @@ selectFirst: false }); - + $("#search2").autocomplete("/searchjs.asp", { width: 160, autoFill: false, @@ -93,7 +93,7 @@ selectFirst: false }); - + }); @@ -232,16 +232,16 @@

    Each depositor insured to at least $250,000 per insured bank

    Failed Bank List

    The FDIC is often appointed as receiver for failed banks. This page contains useful information for the customers and vendors of these banks. This includes information on the acquiring bank (if applicable), how your accounts and loans are affected, and how vendors can file claims against the receivership. Failed Financial Institution Contact Search displays point of contact information related to failed banks.

    - +

    This list includes banks which have failed since October 1, 2000. To search for banks that failed prior to those on this page, visit this link: Failures and Assistance Transactions

    - +

    Failed Bank List - CSV file (Updated on Mondays. Also opens in Excel - Excel Help)

    - +

    Due to the small screen size some information is no longer visible.
    Full information available when viewed on a larger screen.

    @@ -253,7 +253,7 @@

    Failed Bank List

    City ST CERT - Acquiring Institution + Acquiring Institution Closing Date Updated Date @@ -294,7 +294,7 @@

    Failed Bank List

    Capital Bank, N.A. May 10, 2013 May 14, 2013 - + Douglas County Bank Douglasville @@ -383,7 +383,7 @@

    Failed Bank List

    Sunwest Bank January 11, 2013 January 24, 2013 - + Community Bank of the Ozarks Sunrise Beach @@ -392,7 +392,7 @@

    Failed Bank List

    Bank of Sullivan December 14, 2012 January 24, 2013 - + Hometown Community Bank Braselton @@ -401,7 +401,7 @@

    Failed Bank List

    CertusBank, National Association November 16, 2012 January 24, 2013 - + Citizens First National Bank Princeton @@ -518,7 +518,7 @@

    Failed Bank List

    Metcalf Bank July 20, 2012 December 17, 2012 - + First Cherokee State Bank Woodstock @@ -635,7 +635,7 @@

    Failed Bank List

    Southern States Bank May 18, 2012 May 20, 2013 - + Security Bank, National Association North Lauderdale @@ -644,7 +644,7 @@

    Failed Bank List

    Banesco USA May 4, 2012 October 31, 2012 - + Palm Desert National Bank Palm Desert @@ -734,7 +734,7 @@

    Failed Bank List

    No Acquirer March 9, 2012 October 29, 2012 - + Global Commerce Bank Doraville @@ -752,7 +752,7 @@

    Failed Bank List

    No Acquirer February 24, 2012 December 17, 2012 - + Central Bank of Georgia Ellaville @@ -761,7 +761,7 @@

    Failed Bank List

    Ameris Bank February 24, 2012 August 9, 2012 - + SCB Bank Shelbyville @@ -770,7 +770,7 @@

    Failed Bank List

    First Merchants Bank, National Association February 10, 2012 March 25, 2013 - + Charter National Bank and Trust Hoffman Estates @@ -779,7 +779,7 @@

    Failed Bank List

    Barrington Bank & Trust Company, National Association February 10, 2012 March 25, 2013 - + BankEast Knoxville @@ -788,7 +788,7 @@

    Failed Bank List

    U.S.Bank National Association January 27, 2012 March 8, 2013 - + Patriot Bank Minnesota Forest Lake @@ -797,7 +797,7 @@

    Failed Bank List

    First Resource Bank January 27, 2012 September 12, 2012 - + Tennessee Commerce Bank Franklin @@ -806,7 +806,7 @@

    Failed Bank List

    Republic Bank & Trust Company January 27, 2012 November 20, 2012 - + First Guaranty Bank and Trust Company of Jacksonville Jacksonville @@ -815,7 +815,7 @@

    Failed Bank List

    CenterState Bank of Florida, N.A. January 27, 2012 September 12, 2012 - + American Eagle Savings Bank Boothwyn @@ -824,7 +824,7 @@

    Failed Bank List

    Capital Bank, N.A. January 20, 2012 January 25, 2013 - + The First State Bank Stockbridge @@ -833,7 +833,7 @@

    Failed Bank List

    Hamilton State Bank January 20, 2012 January 25, 2013 - + Central Florida State Bank Belleview @@ -842,7 +842,7 @@

    Failed Bank List

    CenterState Bank of Florida, N.A. January 20, 2012 January 25, 2013 - + Western National Bank Phoenix @@ -869,7 +869,7 @@

    Failed Bank List

    First NBC Bank November 18, 2011 August 13, 2012 - + Polk County Bank Johnston @@ -887,7 +887,7 @@

    Failed Bank List

    Century Bank of Georgia November 10, 2011 August 13, 2012 - + SunFirst Bank Saint George @@ -896,7 +896,7 @@

    Failed Bank List

    Cache Valley Bank November 4, 2011 November 16, 2012 - + Mid City Bank, Inc. Omaha @@ -905,7 +905,7 @@

    Failed Bank List

    Premier Bank November 4, 2011 August 15, 2012 - + All American Bank Des Plaines @@ -914,7 +914,7 @@

    Failed Bank List

    International Bank of Chicago October 28, 2011 August 15, 2012 - + Community Banks of Colorado Greenwood Village @@ -959,7 +959,7 @@

    Failed Bank List

    Blackhawk Bank & Trust October 14, 2011 August 15, 2012 - + First State Bank Cranford @@ -968,7 +968,7 @@

    Failed Bank List

    Northfield Bank October 14, 2011 November 8, 2012 - + Blue Ridge Savings Bank, Inc. Asheville @@ -977,7 +977,7 @@

    Failed Bank List

    Bank of North Carolina October 14, 2011 November 8, 2012 - + Piedmont Community Bank Gray @@ -986,7 +986,7 @@

    Failed Bank List

    State Bank and Trust Company October 14, 2011 January 22, 2013 - + Sun Security Bank Ellington @@ -1202,7 +1202,7 @@

    Failed Bank List

    Ameris Bank July 15, 2011 November 2, 2012 - + One Georgia Bank Atlanta @@ -1247,7 +1247,7 @@

    Failed Bank List

    First American Bank and Trust Company June 24, 2011 November 2, 2012 - + First Commercial Bank of Tampa Bay Tampa @@ -1256,7 +1256,7 @@

    Failed Bank List

    Stonegate Bank June 17, 2011 November 2, 2012 - + McIntosh State Bank Jackson @@ -1265,7 +1265,7 @@

    Failed Bank List

    Hamilton State Bank June 17, 2011 November 2, 2012 - + Atlantic Bank and Trust Charleston @@ -1274,7 +1274,7 @@

    Failed Bank List

    First Citizens Bank and Trust Company, Inc. June 3, 2011 October 31, 2012 - + First Heritage Bank Snohomish @@ -1283,7 +1283,7 @@

    Failed Bank List

    Columbia State Bank May 27, 2011 January 28, 2013 - + Summit Bank Burlington @@ -1292,7 +1292,7 @@

    Failed Bank List

    Columbia State Bank May 20, 2011 January 22, 2013 - + First Georgia Banking Company Franklin @@ -2030,7 +2030,7 @@

    Failed Bank List

    Westamerica Bank August 20, 2010 September 12, 2012 - + Los Padres Bank Solvang @@ -2624,7 +2624,7 @@

    Failed Bank List

    MB Financial Bank, N.A. April 23, 2010 August 23, 2012 - + Amcore Bank, National Association Rockford @@ -2768,7 +2768,7 @@

    Failed Bank List

    First Citizens Bank March 19, 2010 August 23, 2012 - + Bank of Hiawassee Hiawassee @@ -3480,7 +3480,7 @@

    Failed Bank List

    October 2, 2009 August 21, 2012 - + Warren Bank Warren MI @@ -3767,7 +3767,7 @@

    Failed Bank List

    Herring Bank July 31, 2009 August 20, 2012 - + Security Bank of Jones County Gray @@ -3848,7 +3848,7 @@

    Failed Bank List

    California Bank & Trust July 17, 2009 August 20, 2012 - + BankFirst Sioux Falls @@ -4811,7 +4811,7 @@

    Failed Bank List

    Bank of the Orient October 13, 2000 March 17, 2005 - + @@ -4849,12 +4849,12 @@

    Failed Bank List

    @@ -4849,12 +4850,12 @@

    Failed Bank List

    @@ -304,8 +304,8 @@

    這個頁面上的內容需要較新版本的 Adobe Flash Player。

    - + @@ -518,14 +518,14 @@

    這個頁面上的內容需要較新版本的 Adobe Flash Player。

    MIA Geographical Information
    Scope of Service
    - + Slot Application - + Macau Freight Forwarders Cargo Tracking Platform For Rent Airport Capacity - + Airport Characteristics & Traffic Statistics @@ -539,11 +539,11 @@

    這個頁面上的內容需要較新版本的 Adobe Flash Player。

    - + - + @@ -553,3116 +553,3116 @@

    這個頁面上的內容需要較新版本的 Adobe Flash Player。

    Traffic Statistics - Passengers

    - +
    - - + +
    - + Traffic Statistics - - - - + + + +


    Passengers Figure(2008-2013)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      201320122011201020092008
    January - + 374,917 - + 362,379 - + 301,503 - + 358,902 - + 342,323 - + 420,574
    February - + 393,152 - + 312,405 - + 301,259 - + 351,654 - + 297,755 - + 442,809
    March - + 408,755 - + 334,000 - + 318,908 - + 360,365 - + 387,879 - + 468,540
    April - + 408,860 - + 358,198 - + 339,060 - + 352,976 - + 400,553 - + 492,930
    May - + 374,397 - + 329,218 - + 321,060 - + 330,407 - + 335,967 - + 465,045
    June - + 401,995 - + 356,679 - + 343,006 - + 326,724 - + 296,748 - + 426,764
    July - - + + - + 423,081 - + 378,993 - + 356,580 - + 351,110 - + 439,425
    August - - + + - + 453,391 - + 395,883 - + 364,011 - + 404,076 - + 425,814
    September - - + + - + 384,887 - + 325,124 - + 308,940 - + 317,226 - + 379,898
    October - - + + - + 383,889 - + 333,102 - + 317,040 - + 355,935 - + 415,339
    November - - + + - + 379,065 - + 327,803 - + 303,186 - + 372,104 - + 366,411
    December - - + + - + 413,873 - + 359,313 - + 348,051 - + 388,573 - + 354,253
    Total - + 2,362,076 - + 4,491,065 - + 4,045,014 - + 4,078,836 - + 4,250,249 - + 5,097,802
    - +


    Passengers Figure(2002-2007)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      200720062005200420032002
    January - + 381,887 - + 323,282 - + 289,701 - + 288,507 - + 290,140 - + 268,783
    February - + 426,014 - + 360,820 - + 348,723 - + 207,710 - + 323,264 - + 323,654
    March - + 443,805 - + 389,125 - + 321,953 - + 273,910 - + 295,052 - + 360,668
    April - + 500,917 - + 431,550 - + 367,976 - + 324,931 - + 144,082 - + 380,648
    May - + 468,637 - + 399,743 - + 359,298 - + 250,601 - + 47,333 - + 359,547
    June - + 463,676 - + 393,713 - + 360,147 - + 296,000 - + 94,294 - + 326,508
    July - + 490,404 - + 465,497 - + 413,131 - + 365,454 - + 272,784 - + 388,061
    August - + 490,830 - + 478,474 - + 409,281 - + 372,802 - + 333,840 - + 384,719
    September - + 446,594 - + 412,444 - + 354,751 - + 321,456 - + 295,447 - + 334,029
    October - + 465,757 - + 461,215 - + 390,435 - + 358,362 - + 291,193 - + 372,706
    November - + 455,132 - + 425,116 - + 323,347 - + 327,593 - + 268,282 - + 350,324
    December - + 465,225 - + 435,114 - + 308,999 - + 326,933 - + 249,855 - + 322,056
    Total - + 5,498,878 - + 4,976,093 - + 4,247,742 - + 3,714,259 - + 2,905,566 - + 4,171,703
    - +


    Passengers Figure(1996-2001)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      200120001999199819971996
    January - + 265,603 - + 184,381 - + 161,264 - + 161,432 - + 117,984 - - + +
    February - + 249,259 - + 264,066 - + 209,569 - + 168,777 - + 150,772 - - + +
    March - + 312,319 - + 226,483 - + 186,965 - + 172,060 - + 149,795 - - + +
    April - + 351,793 - + 296,541 - + 237,449 - + 180,241 - + 179,049 - - -
    May - + 338,692 - + 288,949 - + 230,691 - + 172,391 - + 189,925 - - + +
    June - + 332,630 - + 271,181 - + 231,328 - + 157,519 - + 175,402 - - + +
    July - + 344,658 - + 304,276 - + 243,534 - + 205,595 - + 173,103 - - + +
    August - + 360,899 - + 300,418 - + 257,616 - + 241,140 - + 178,118 - - + +
    September - + 291,817 - + 280,803 - + 210,885 - + 183,954 - + 163,385 - - + +
    October - + 327,232 - + 298,873 - + 231,251 - + 205,726 - + 176,879 - - + +
    November - + 315,538 - + 265,528 - + 228,637 - + 181,677 - + 146,804 - - + +
    December - + 314,866 - + 257,929 - + 210,922 - + 183,975 - + 151,362 - - + +
    Total - + 3,805,306 - + 3,239,428 - + 2,640,111 - + 2,214,487 - + 1,952,578 - + 0
    - +


    Passengers Figure(1995-1995)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      1995
    January - - + +
    February - - + +
    March - - + +
    April - - + +
    May - - + +
    June - - + +
    July - - + +
    August - - + +
    September - - + +
    October - - + +
    November - + 6,601
    December - + 37,041
    Total - + 43,642
    - +


    passenger statistic picture



    - - - - + + + +


    Movement Statistics(2008-2013)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      201320122011201020092008
    January - + 3,925 - + 3,463 - + 3,289 - + 3,184 - + 3,488 - + 4,568
    February - + 3,632 - + 2,983 - + 2,902 - + 3,053 - + 3,347 - + 4,527
    March - + 3,909 - + 3,166 - + 3,217 - + 3,175 - + 3,636 - + 4,594
    April - + 3,903 - + 3,258 - + 3,146 - + 3,023 - + 3,709 - + 4,574
    May - + 4,075 - + 3,234 - + 3,266 - + 3,033 - + 3,603 - + 4,511
    June - + 4,038 - + 3,272 - + 3,316 - + 2,909 - + 3,057 - + 4,081
    July - - + + - + 3,661 - + 3,359 - + 3,062 - + 3,354 - + 4,215
    August - - + + - + 3,942 - + 3,417 - + 3,077 - + 3,395 - + 4,139
    September - - + + - + 3,703 - + 3,169 - + 3,095 - + 3,100 - + 3,752
    October - - + + - + 3,727 - + 3,469 - + 3,179 - + 3,375 - + 3,874
    November - - + + - + 3,722 - + 3,145 - + 3,159 - + 3,213 - + 3,567
    December - - + + - + 3,866 - + 3,251 - + 3,199 - + 3,324 - + 3,362
    Total - + 23,482 - + 41,997 - + 38,946 - + 37,148 - + 40,601 - + 49,764
    - +


    Movement Statistics(2002-2007)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      200720062005200420032002
    January - + 4,384 - + 3,933 - + 3,528 - + 3,051 - + 3,257 - + 2,711
    February - + 4,131 - + 3,667 - + 3,331 - + 2,372 - + 3,003 - + 2,747
    March - + 4,349 - + 4,345 - + 3,549 - + 3,049 - + 3,109 - + 2,985
    April - + 4,460 - + 4,490 - + 3,832 - + 3,359 - + 2,033 - + 2,928
    May - + 4,629 - + 4,245 - + 3,663 - + 3,251 - + 1,229 - + 3,109
    June - + 4,365 - + 4,124 - + 3,752 - + 3,414 - + 1,217 - + 3,049
    July - + 4,612 - + 4,386 - + 3,876 - + 3,664 - + 2,423 - + 3,078
    August - + 4,446 - + 4,373 - + 3,987 - + 3,631 - + 3,040 - + 3,166
    September - + 4,414 - + 4,311 - + 3,782 - + 3,514 - + 2,809 - + 3,239
    October - + 4,445 - + 4,455 - + 3,898 - + 3,744 - + 3,052 - + 3,562
    November - + 4,563 - + 4,285 - + 3,951 - + 3,694 - + 3,125 - + 3,546
    December - + 4,588 - + 4,435 - + 3,855 - + 3,763 - + 2,996 - + 3,444
    Total - + 53,386 - + 51,049 - + 45,004 - + 40,506 - + 31,293 - + 37,564
    - +


    Movement Statistics(1996-2001)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      200120001999199819971996
    January - + 2,694 - + 2,201 - + 1,835 - + 2,177 - + 1,353 - + 744
    February - + 2,364 - + 2,357 - + 1,826 - + 1,740 - + 1,339 - + 692
    March - + 2,543 - + 2,206 - + 1,895 - + 1,911 - + 1,533 - + 872
    April - + 2,531 - + 2,311 - + 2,076 - + 1,886 - + 1,587 - + 1,026
    May - + 2,579 - + 2,383 - + 1,914 - + 2,102 - + 1,720 - + 1,115
    June - + 2,681 - + 2,370 - + 1,890 - + 2,038 - + 1,716 - + 1,037
    July - + 2,903 - + 2,609 - + 1,916 - + 2,078 - + 1,693 - + 1,209
    August - + 3,037 - + 2,487 - + 1,968 - + 2,061 - + 1,676 - + 1,241
    September - + 2,767 - + 2,329 - + 1,955 - + 1,970 - + 1,681 - + 1,263
    October - + 2,922 - + 2,417 - + 2,267 - + 1,969 - + 1,809 - + 1,368
    November - + 2,670 - + 2,273 - + 2,132 - + 2,102 - + 1,786 - + 1,433
    December - + 2,815 - + 2,749 - + 2,187 - + 1,981 - + 1,944 - + 1,386
    Total - + 32,506 - + 28,692 - + 23,861 - + 24,015 - + 19,837 - + 13,386
    - +


    Movement Statistics(1995-1995)

    - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +
      1995
    January - - + +
    February - - + +
    March - - + +
    April - - + +
    May - - + +
    June - - + +
    July - - + +
    August - - + +
    September - - + +
    October - - + +
    November - + 126
    December - + 536
    Total - + 662
    - +


    passenger statistic picture
    - +
    - +
    - - + +
    - +
    @@ -3676,11 +3676,11 @@

    Traffic Statistics - Passengers

    SEARCH -
    +
    diff --git a/pandas/tests/io/data/spam.html b/pandas/tests/io/data/spam.html index 935b39f6d6011..a8e445ff1e176 100644 --- a/pandas/tests/io/data/spam.html +++ b/pandas/tests/io/data/spam.html @@ -5,31 +5,31 @@ - - + + - + Show Foods - - - - - + + + + + - + - - - - - + + + + - - - - - - + + + + + + @@ -95,8 +95,8 @@ - -
    + +
    National Nutrient Database @@ -106,18 +106,18 @@ - - + +
    - - + +
    National Nutrient Database for Standard Reference
    Release 25
    - + - - - - - + + + + +
    Basic Report
    - +

    Nutrient data for 07908, Luncheon meat, pork with ham, minced, canned, includes SPAM (Hormel) - - + +

    - - + +
    - - + +
    Modifying household measures
    - -
    - + +
    +
    @@ -179,13 +179,13 @@

    Nutrient data for 07908, Luncheon meat, pork with ham, minced, canned, inclu handler: function() {this.cancel();}, 'isDefault': true}] }); GRAILSUI.measuresHelpDialog.render(document.body); - - + + } YAHOO.util.Event.onDOMReady(init_dlg_measuresHelpDialog); - - + +
    @@ -197,29 +197,29 @@

    Nutrient data for 07908, Luncheon meat, pork with ham, minced, canned, inclu - - + +
    - - + +

    Nutrient values and weights are for edible portion

    - - + + - - + + - - + + ")] + assert 'B' not in result + + +@pytest.mark.parametrize('columns,justify,expected', [ + (MultiIndex.from_tuples( + list(zip(np.arange(2).repeat(2), np.mod(lrange(4), 2))), + names=['CL0', 'CL1']), + 'left', + 'multiindex_1'), + + (MultiIndex.from_tuples( + list(zip(range(4), np.mod(lrange(4), 2)))), + 'right', + 'multiindex_2') +]) +def test_to_html_multiindex(columns, justify, expected, datapath): + df = DataFrame([list('abcd'), list('efgh')], columns=columns) + result = df.to_html(justify=justify) + expected = expected_html(datapath, expected) + assert result == expected + + +def test_to_html_justify(justify, datapath): + df = DataFrame({'A': [6, 30000, 2], + 'B': [1, 2, 70000], + 'C': [223442, 0, 1]}, + columns=['A', 'B', 'C']) + result = df.to_html(justify=justify) + expected = expected_html(datapath, 'justify').format(justify=justify) + assert result == expected + + +@pytest.mark.parametrize("justify", ["super-right", "small-left", + "noinherit", "tiny", "pandas"]) +def test_to_html_invalid_justify(justify): + # GH 17527 + df = DataFrame() + msg = "Invalid value for justify parameter" + + with pytest.raises(ValueError, match=msg): + df.to_html(justify=justify) + + +def test_to_html_index(datapath): + # TODO: split this test + index = ['foo', 'bar', 'baz'] + df = DataFrame({'A': [1, 2, 3], + 'B': [1.2, 3.4, 5.6], + 'C': ['one', 'two', np.nan]}, + columns=['A', 'B', 'C'], + index=index) + expected_with_index = expected_html(datapath, 'index_1') + assert df.to_html() == expected_with_index + + expected_without_index = expected_html(datapath, 'index_2') + result = df.to_html(index=False) + for i in index: + assert i not in result + assert result == expected_without_index + df.index = Index(['foo', 'bar', 'baz'], name='idx') + expected_with_index = expected_html(datapath, 'index_3') + assert df.to_html() == expected_with_index + assert df.to_html(index=False) == expected_without_index + + tuples = [('foo', 'car'), ('foo', 'bike'), ('bar', 'car')] + df.index = MultiIndex.from_tuples(tuples) + + expected_with_index = expected_html(datapath, 'index_4') + assert df.to_html() == expected_with_index + + result = df.to_html(index=False) + for i in ['foo', 'bar', 'car', 'bike']: + assert i not in result + # must be the same result as normal index + assert result == expected_without_index + + df.index = MultiIndex.from_tuples(tuples, names=['idx1', 'idx2']) + expected_with_index = expected_html(datapath, 'index_5') + assert df.to_html() == expected_with_index + assert df.to_html(index=False) == expected_without_index + + +@pytest.mark.parametrize('classes', [ + "sortable draggable", + ["sortable", "draggable"] +]) +def test_to_html_with_classes(classes, datapath): + df = DataFrame() + expected = expected_html(datapath, 'with_classes') + result = df.to_html(classes=classes) + assert result == expected + + +def test_to_html_no_index_max_rows(datapath): + # GH 14998 + df = DataFrame({"A": [1, 2, 3, 4]}) + result = df.to_html(index=False, max_rows=1) + expected = expected_html(datapath, 'gh14998_expected_output') + assert result == expected + + +def test_to_html_multiindex_max_cols(datapath): + # GH 6131 + index = MultiIndex(levels=[['ba', 'bb', 'bc'], ['ca', 'cb', 'cc']], + codes=[[0, 1, 2], [0, 1, 2]], + names=['b', 'c']) + columns = MultiIndex(levels=[['d'], ['aa', 'ab', 'ac']], + codes=[[0, 0, 0], [0, 1, 2]], + names=[None, 'a']) + data = np.array( + [[1., np.nan, np.nan], [np.nan, 2., np.nan], [np.nan, np.nan, 3.]]) + df = DataFrame(data, index, columns) + result = df.to_html(max_cols=2) + expected = expected_html(datapath, 'gh6131_expected_output') + assert result == expected + + +def test_to_html_multi_indexes_index_false(datapath): + # GH 22579 + df = DataFrame({'a': range(10), 'b': range(10, 20), 'c': range(10, 20), + 'd': range(10, 20)}) + df.columns = MultiIndex.from_product([['a', 'b'], ['c', 'd']]) + df.index = MultiIndex.from_product([['a', 'b'], + ['c', 'd', 'e', 'f', 'g']]) + result = df.to_html(index=False) + expected = expected_html(datapath, 'gh22579_expected_output') + assert result == expected + + +@pytest.mark.parametrize('index_names', [True, False]) +@pytest.mark.parametrize('header', [True, False]) +@pytest.mark.parametrize('index', [True, False]) +@pytest.mark.parametrize('column_index, column_type', [ + (Index([0, 1]), 'unnamed_standard'), + (Index([0, 1], name='columns.name'), 'named_standard'), + (MultiIndex.from_product([['a'], ['b', 'c']]), 'unnamed_multi'), + (MultiIndex.from_product( + [['a'], ['b', 'c']], names=['columns.name.0', + 'columns.name.1']), 'named_multi') +]) +@pytest.mark.parametrize('row_index, row_type', [ + (Index([0, 1]), 'unnamed_standard'), + (Index([0, 1], name='index.name'), 'named_standard'), + (MultiIndex.from_product([['a'], ['b', 'c']]), 'unnamed_multi'), + (MultiIndex.from_product( + [['a'], ['b', 'c']], names=['index.name.0', + 'index.name.1']), 'named_multi') +]) +def test_to_html_basic_alignment( + datapath, row_index, row_type, column_index, column_type, + index, header, index_names): + # GH 22747, GH 22579 + df = DataFrame(np.zeros((2, 2), dtype=int), + index=row_index, columns=column_index) + result = df.to_html( + index=index, header=header, index_names=index_names) + + if not index: + row_type = 'none' + elif not index_names and row_type.startswith('named'): + row_type = 'un' + row_type + + if not header: + column_type = 'none' + elif not index_names and column_type.startswith('named'): + column_type = 'un' + column_type + + filename = 'index_' + row_type + '_columns_' + column_type + expected = expected_html(datapath, filename) + assert result == expected + + +@pytest.mark.parametrize('index_names', [True, False]) +@pytest.mark.parametrize('header', [True, False]) +@pytest.mark.parametrize('index', [True, False]) +@pytest.mark.parametrize('column_index, column_type', [ + (Index(np.arange(8)), 'unnamed_standard'), + (Index(np.arange(8), name='columns.name'), 'named_standard'), + (MultiIndex.from_product( + [['a', 'b'], ['c', 'd'], ['e', 'f']]), 'unnamed_multi'), + (MultiIndex.from_product( + [['a', 'b'], ['c', 'd'], ['e', 'f']], names=['foo', None, 'baz']), + 'named_multi') +]) +@pytest.mark.parametrize('row_index, row_type', [ + (Index(np.arange(8)), 'unnamed_standard'), + (Index(np.arange(8), name='index.name'), 'named_standard'), + (MultiIndex.from_product( + [['a', 'b'], ['c', 'd'], ['e', 'f']]), 'unnamed_multi'), + (MultiIndex.from_product( + [['a', 'b'], ['c', 'd'], ['e', 'f']], names=['foo', None, 'baz']), + 'named_multi') +]) +def test_to_html_alignment_with_truncation( + datapath, row_index, row_type, column_index, column_type, + index, header, index_names): + # GH 22747, GH 22579 + df = DataFrame(np.arange(64).reshape(8, 8), + index=row_index, columns=column_index) + result = df.to_html( + max_rows=4, max_cols=4, + index=index, header=header, index_names=index_names) + + if not index: + row_type = 'none' + elif not index_names and row_type.startswith('named'): + row_type = 'un' + row_type + + if not header: + column_type = 'none' + elif not index_names and column_type.startswith('named'): + column_type = 'un' + column_type + + filename = 'trunc_df_index_' + row_type + '_columns_' + column_type + expected = expected_html(datapath, filename) + assert result == expected + + +@pytest.mark.parametrize('index', [False, 0]) +def test_to_html_truncation_index_false_max_rows(datapath, index): + # GH 15019 + data = [[1.764052, 0.400157], + [0.978738, 2.240893], + [1.867558, -0.977278], + [0.950088, -0.151357], + [-0.103219, 0.410599]] + df = DataFrame(data) + result = df.to_html(max_rows=4, index=index) + expected = expected_html(datapath, 'gh15019_expected_output') + assert result == expected + + +@pytest.mark.parametrize('index', [False, 0]) +@pytest.mark.parametrize('col_index_named, expected_output', [ + (False, 'gh22783_expected_output'), + (True, 'gh22783_named_columns_index') +]) +def test_to_html_truncation_index_false_max_cols( + datapath, index, col_index_named, expected_output): + # GH 22783 + data = [[1.764052, 0.400157, 0.978738, 2.240893, 1.867558], + [-0.977278, 0.950088, -0.151357, -0.103219, 0.410599]] + df = DataFrame(data) + if col_index_named: + df.columns.rename('columns.name', inplace=True) + result = df.to_html(max_cols=4, index=index) + expected = expected_html(datapath, expected_output) + assert result == expected + + +@pytest.mark.parametrize('notebook', [True, False]) +def test_to_html_notebook_has_style(notebook): + df = DataFrame({"A": [1, 2, 3]}) + result = df.to_html(notebook=notebook) + + if notebook: + assert "tbody tr th:only-of-type" in result + assert "vertical-align: middle;" in result + assert "thead th" in result + else: + assert "tbody tr th:only-of-type" not in result + assert "vertical-align: middle;" not in result + assert "thead th" not in result + + +def test_to_html_with_index_names_false(): + # GH 16493 + df = DataFrame({"A": [1, 2]}, index=Index(['a', 'b'], + name='myindexname')) + result = df.to_html(index_names=False) + assert 'myindexname' not in result + + +def test_to_html_with_id(): + # GH 8496 + df = DataFrame({"A": [1, 2]}, index=Index(['a', 'b'], + name='myindexname')) + result = df.to_html(index_names=False, table_id="TEST_ID") + assert ' id="TEST_ID"' in result + + +@pytest.mark.parametrize('value,float_format,expected', [ + (0.19999, '%.3f', 'gh21625_expected_output'), + (100.0, '%.0f', 'gh22270_expected_output'), +]) +def test_to_html_float_format_no_fixed_width( + value, float_format, expected, datapath): + # GH 21625, GH 22270 + df = DataFrame({'x': [value]}) + expected = expected_html(datapath, expected) + result = df.to_html(float_format=float_format) + assert result == expected + + +@pytest.mark.parametrize("render_links,expected", [ + (True, 'render_links_true'), + (False, 'render_links_false'), +]) +def test_to_html_render_links(render_links, expected, datapath): + # GH 2679 + data = [ + [0, 'http://pandas.pydata.org/?q1=a&q2=b', 'pydata.org'], + [0, 'www.pydata.org', 'pydata.org'] + ] + df = DataFrame(data, columns=['foo', 'bar', None]) + + result = df.to_html(render_links=render_links) + expected = expected_html(datapath, expected) + assert result == expected + + +@pytest.mark.parametrize('method,expected', [ + ('to_html', lambda x:lorem_ipsum), + ('_repr_html_', lambda x:lorem_ipsum[:x - 4] + '...') # regression case +]) +@pytest.mark.parametrize('max_colwidth', [10, 20, 50, 100]) +def test_ignore_display_max_colwidth(method, expected, max_colwidth): + # see gh-17004 + df = DataFrame([lorem_ipsum]) + with pd.option_context('display.max_colwidth', max_colwidth): + result = getattr(df, method)() + expected = expected(max_colwidth) + assert expected in result diff --git a/pandas/tests/formats/test_to_latex.py b/pandas/tests/io/formats/test_to_latex.py similarity index 57% rename from pandas/tests/formats/test_to_latex.py rename to pandas/tests/io/formats/test_to_latex.py index 29ead83f3bcd9..1653e474aa7b0 100644 --- a/pandas/tests/formats/test_to_latex.py +++ b/pandas/tests/io/formats/test_to_latex.py @@ -1,12 +1,13 @@ +import codecs from datetime import datetime import pytest +from pandas.compat import u + import pandas as pd -from pandas import DataFrame, compat +from pandas import DataFrame, Series, compat from pandas.util import testing as tm -from pandas.compat import u -import codecs @pytest.fixture @@ -91,19 +92,43 @@ def test_to_latex_format(self, frame): assert withindex_result == withindex_expected + def test_to_latex_empty(self): + df = DataFrame() + result = df.to_latex() + expected = r"""\begin{tabular}{l} +\toprule +Empty DataFrame +Columns: Index([], dtype='object') +Index: Index([], dtype='object') \\ +\bottomrule +\end{tabular} +""" + assert result == expected + + result = df.to_latex(longtable=True) + expected = r"""\begin{longtable}{l} +\toprule +Empty DataFrame +Columns: Index([], dtype='object') +Index: Index([], dtype='object') \\ +\end{longtable} +""" + assert result == expected + def test_to_latex_with_formatters(self): - df = DataFrame({'int': [1, 2, 3], + df = DataFrame({'datetime64': [datetime(2016, 1, 1), + datetime(2016, 2, 5), + datetime(2016, 3, 3)], 'float': [1.0, 2.0, 3.0], + 'int': [1, 2, 3], 'object': [(1, 2), True, False], - 'datetime64': [datetime(2016, 1, 1), - datetime(2016, 2, 5), - datetime(2016, 3, 3)]}) + }) - formatters = {'int': lambda x: '0x%x' % x, - 'float': lambda x: '[% 4.1f]' % x, - 'object': lambda x: '-%s-' % str(x), - 'datetime64': lambda x: x.strftime('%Y-%m'), - '__index__': lambda x: 'index: %s' % x} + formatters = {'datetime64': lambda x: x.strftime('%Y-%m'), + 'float': lambda x: '[{x: 4.1f}]'.format(x=x), + 'int': lambda x: '0x{x:x}'.format(x=x), + 'object': lambda x: '-{x!s}-'.format(x=x), + '__index__': lambda x: 'index: {x}'.format(x=x)} result = df.to_latex(formatters=dict(formatters)) expected = r"""\begin{tabular}{llrrl} @@ -146,11 +171,11 @@ def test_to_latex_multiindex(self): assert result == expected df = DataFrame.from_dict({ - ('c1', 0): pd.Series(dict((x, x) for x in range(4))), - ('c1', 1): pd.Series(dict((x, x + 4) for x in range(4))), - ('c2', 0): pd.Series(dict((x, x) for x in range(4))), - ('c2', 1): pd.Series(dict((x, x + 4) for x in range(4))), - ('c3', 0): pd.Series(dict((x, x) for x in range(4))), + ('c1', 0): pd.Series({x: x for x in range(4)}), + ('c1', 1): pd.Series({x: x + 4 for x in range(4)}), + ('c2', 0): pd.Series({x: x for x in range(4)}), + ('c2', 1): pd.Series({x: x + 4 for x in range(4)}), + ('c3', 0): pd.Series({x: x for x in range(4)}), }).T result = df.to_latex() expected = r"""\begin{tabular}{llrrrr} @@ -221,13 +246,35 @@ def test_to_latex_multiindex(self): assert result == expected + def test_to_latex_multiindex_dupe_level(self): + # see gh-14484 + # + # If an index is repeated in subsequent rows, it should be + # replaced with a blank in the created table. This should + # ONLY happen if all higher order indices (to the left) are + # equal too. In this test, 'c' has to be printed both times + # because the higher order index 'A' != 'B'. + df = pd.DataFrame(index=pd.MultiIndex.from_tuples( + [('A', 'c'), ('B', 'c')]), columns=['col']) + result = df.to_latex() + expected = r"""\begin{tabular}{lll} +\toprule + & & col \\ +\midrule +A & c & NaN \\ +B & c & NaN \\ +\bottomrule +\end{tabular} +""" + assert result == expected + def test_to_latex_multicolumnrow(self): df = pd.DataFrame({ - ('c1', 0): dict((x, x) for x in range(5)), - ('c1', 1): dict((x, x + 5) for x in range(5)), - ('c2', 0): dict((x, x) for x in range(5)), - ('c2', 1): dict((x, x + 5) for x in range(5)), - ('c3', 0): dict((x, x) for x in range(5)) + ('c1', 0): {x: x for x in range(5)}, + ('c1', 1): {x: x + 5 for x in range(5)}, + ('c2', 0): {x: x for x in range(5)}, + ('c2', 1): {x: x + 5 for x in range(5)}, + ('c3', 0): {x: x for x in range(5)} }) result = df.to_latex() expected = r"""\begin{tabular}{lrrrrr} @@ -302,10 +349,10 @@ def test_to_latex_escape(self): a = 'a' b = 'b' - test_dict = {u('co^l1'): {a: "a", - b: "b"}, - u('co$e^x$'): {a: "a", - b: "b"}} + test_dict = {u('co$e^x$'): {a: "a", + b: "b"}, + u('co^l1'): {a: "a", + b: "b"}} unescaped_result = DataFrame(test_dict).to_latex(escape=False) escaped_result = DataFrame(test_dict).to_latex( @@ -323,7 +370,7 @@ def test_to_latex_escape(self): escaped_expected = r'''\begin{tabular}{lll} \toprule -{} & co\$e\textasciicircumx\$ & co\textasciicircuml1 \\ +{} & co\$e\textasciicircum x\$ & co\textasciicircum l1 \\ \midrule a & a & a \\ b & b & b \\ @@ -334,6 +381,22 @@ def test_to_latex_escape(self): assert unescaped_result == unescaped_expected assert escaped_result == escaped_expected + def test_to_latex_special_escape(self): + df = DataFrame([r"a\b\c", r"^a^b^c", r"~a~b~c"]) + + escaped_result = df.to_latex() + escaped_expected = r"""\begin{tabular}{ll} +\toprule +{} & 0 \\ +\midrule +0 & a\textbackslash b\textbackslash c \\ +1 & \textasciicircum a\textasciicircum b\textasciicircum c \\ +2 & \textasciitilde a\textasciitilde b\textasciitilde c \\ +\bottomrule +\end{tabular} +""" + assert escaped_result == escaped_expected + def test_to_latex_longtable(self, frame): frame.to_latex(longtable=True) @@ -355,7 +418,6 @@ def test_to_latex_longtable(self, frame): 1 & 2 & b2 \\ \end{longtable} """ - assert withindex_result == withindex_expected withoutindex_result = df.to_latex(index=False, longtable=True) @@ -365,7 +427,7 @@ def test_to_latex_longtable(self, frame): \midrule \endhead \midrule -\multicolumn{3}{r}{{Continued on next page}} \\ +\multicolumn{2}{r}{{Continued on next page}} \\ \midrule \endfoot @@ -378,6 +440,14 @@ def test_to_latex_longtable(self, frame): assert withoutindex_result == withoutindex_expected + df = DataFrame({'a': [1, 2]}) + with1column_result = df.to_latex(index=False, longtable=True) + assert r"\multicolumn{1}" in with1column_result + + df = DataFrame({'a': [1, 2], 'b': [3, 4], 'c': [5, 6]}) + with3columns_result = df.to_latex(index=False, longtable=True) + assert r"\multicolumn{3}" in with3columns_result + def test_to_latex_escape_special_chars(self): special_characters = ['&', '%', '$', '#', '_', '{', '}', '~', '^', '\\'] @@ -394,9 +464,9 @@ def test_to_latex_escape_special_chars(self): 4 & \_ \\ 5 & \{ \\ 6 & \} \\ -7 & \textasciitilde \\ -8 & \textasciicircum \\ -9 & \textbackslash \\ +7 & \textasciitilde \\ +8 & \textasciicircum \\ +9 & \textbackslash \\ \bottomrule \end{tabular} """ @@ -470,7 +540,7 @@ def test_to_latex_specified_header(self): assert withoutescape_result == withoutescape_expected - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.to_latex(header=['A']) def test_to_latex_decimal(self, frame): @@ -491,3 +561,177 @@ def test_to_latex_decimal(self, frame): """ assert withindex_result == withindex_expected + + def test_to_latex_series(self): + s = Series(['a', 'b', 'c']) + withindex_result = s.to_latex() + withindex_expected = r"""\begin{tabular}{ll} +\toprule +{} & 0 \\ +\midrule +0 & a \\ +1 & b \\ +2 & c \\ +\bottomrule +\end{tabular} +""" + assert withindex_result == withindex_expected + + def test_to_latex_bold_rows(self): + # GH 16707 + df = pd.DataFrame({'a': [1, 2], 'b': ['b1', 'b2']}) + observed = df.to_latex(bold_rows=True) + expected = r"""\begin{tabular}{lrl} +\toprule +{} & a & b \\ +\midrule +\textbf{0} & 1 & b1 \\ +\textbf{1} & 2 & b2 \\ +\bottomrule +\end{tabular} +""" + assert observed == expected + + def test_to_latex_no_bold_rows(self): + # GH 16707 + df = pd.DataFrame({'a': [1, 2], 'b': ['b1', 'b2']}) + observed = df.to_latex(bold_rows=False) + expected = r"""\begin{tabular}{lrl} +\toprule +{} & a & b \\ +\midrule +0 & 1 & b1 \\ +1 & 2 & b2 \\ +\bottomrule +\end{tabular} +""" + assert observed == expected + + @pytest.mark.parametrize('name0', [None, 'named0']) + @pytest.mark.parametrize('name1', [None, 'named1']) + @pytest.mark.parametrize('axes', [[0], [1], [0, 1]]) + def test_to_latex_multiindex_names(self, name0, name1, axes): + # GH 18667 + names = [name0, name1] + mi = pd.MultiIndex.from_product([[1, 2], [3, 4]]) + df = pd.DataFrame(-1, index=mi.copy(), columns=mi.copy()) + for idx in axes: + df.axes[idx].names = names + + idx_names = tuple(n or '{}' for n in names) + idx_names_row = ('%s & %s & & & & \\\\\n' % idx_names + if (0 in axes and any(names)) else '') + placeholder = '{}' if any(names) and 1 in axes else ' ' + col_names = [n if (bool(n) and 1 in axes) else placeholder + for n in names] + observed = df.to_latex() + expected = r"""\begin{tabular}{llrrrr} +\toprule + & %s & \multicolumn{2}{l}{1} & \multicolumn{2}{l}{2} \\ + & %s & 3 & 4 & 3 & 4 \\ +%s\midrule +1 & 3 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ +2 & 3 & -1 & -1 & -1 & -1 \\ + & 4 & -1 & -1 & -1 & -1 \\ +\bottomrule +\end{tabular} +""" % tuple(list(col_names) + [idx_names_row]) + assert observed == expected + + @pytest.mark.parametrize('one_row', [True, False]) + def test_to_latex_multiindex_nans(self, one_row): + # GH 14249 + df = pd.DataFrame({'a': [None, 1], 'b': [2, 3], 'c': [4, 5]}) + if one_row: + df = df.iloc[[0]] + observed = df.set_index(['a', 'b']).to_latex() + expected = r"""\begin{tabular}{llr} +\toprule + & & c \\ +a & b & \\ +\midrule +NaN & 2 & 4 \\ +""" + if not one_row: + expected += r"""1.0 & 3 & 5 \\ +""" + expected += r"""\bottomrule +\end{tabular} +""" + assert observed == expected + + def test_to_latex_non_string_index(self): + # GH 19981 + observed = pd.DataFrame([[1, 2, 3]] * 2).set_index([0, 1]).to_latex() + expected = r"""\begin{tabular}{llr} +\toprule + & & 2 \\ +0 & 1 & \\ +\midrule +1 & 2 & 3 \\ + & 2 & 3 \\ +\bottomrule +\end{tabular} +""" + assert observed == expected + + def test_to_latex_midrule_location(self): + # GH 18326 + df = pd.DataFrame({'a': [1, 2]}) + df.index.name = 'foo' + observed = df.to_latex(index_names=False) + expected = r"""\begin{tabular}{lr} +\toprule +{} & a \\ +\midrule +0 & 1 \\ +1 & 2 \\ +\bottomrule +\end{tabular} +""" + + assert observed == expected + + def test_to_latex_multiindex_empty_name(self): + # GH 18669 + mi = pd.MultiIndex.from_product([[1, 2]], names=['']) + df = pd.DataFrame(-1, index=mi, columns=range(4)) + observed = df.to_latex() + expected = r"""\begin{tabular}{lrrrr} +\toprule + & 0 & 1 & 2 & 3 \\ +{} & & & & \\ +\midrule +1 & -1 & -1 & -1 & -1 \\ +2 & -1 & -1 & -1 & -1 \\ +\bottomrule +\end{tabular} +""" + assert observed == expected + + def test_to_latex_float_format_no_fixed_width(self): + + # GH 21625 + df = DataFrame({'x': [0.19999]}) + expected = r"""\begin{tabular}{lr} +\toprule +{} & x \\ +\midrule +0 & 0.200 \\ +\bottomrule +\end{tabular} +""" + assert df.to_latex(float_format='%.3f') == expected + + # GH 22270 + df = DataFrame({'x': [100.0]}) + expected = r"""\begin{tabular}{lr} +\toprule +{} & x \\ +\midrule +0 & 100 \\ +\bottomrule +\end{tabular} +""" + assert df.to_latex(float_format='%.0f') == expected diff --git a/pandas/tests/io/generate_legacy_storage_files.py b/pandas/tests/io/generate_legacy_storage_files.py old mode 100644 new mode 100755 index d0365cb2c30b3..6c6e28cb1c090 --- a/pandas/tests/io/generate_legacy_storage_files.py +++ b/pandas/tests/io/generate_legacy_storage_files.py @@ -1,18 +1,62 @@ -""" self-contained to write legacy storage (pickle/msgpack) files """ +#!/usr/bin/env python + +""" +self-contained to write legacy storage (pickle/msgpack) files + +To use this script. Create an environment where you want +generate pickles, say its for 0.18.1, with your pandas clone +in ~/pandas + +. activate pandas_0.18.1 +cd ~/ + +$ python pandas/pandas/tests/io/generate_legacy_storage_files.py \ + pandas/pandas/tests/io/data/legacy_pickle/0.18.1/ pickle + +This script generates a storage file for the current arch, system, +and python version + pandas version: 0.18.1 + output dir : pandas/pandas/tests/io/data/legacy_pickle/0.18.1/ + storage format: pickle +created pickle file: 0.18.1_x86_64_darwin_3.5.2.pickle + +The idea here is you are using the *current* version of the +generate_legacy_storage_files with an *older* version of pandas to +generate a pickle file. We will then check this file into a current +branch, and test using test_pickle.py. This will load the *older* +pickles and test versus the current data that is generated +(with master). These are then compared. + +If we have cases where we changed the signature (e.g. we renamed +offset -> freq in Timestamp). Then we have to conditionally execute +in the generate_legacy_storage_files.py to make it +run under the older AND the newer version. + +""" + from __future__ import print_function + +from datetime import timedelta from distutils.version import LooseVersion -from pandas import (Series, DataFrame, Panel, - SparseSeries, SparseDataFrame, - Index, MultiIndex, bdate_range, to_msgpack, - date_range, period_range, - Timestamp, NaT, Categorical, Period) -from pandas.compat import u import os +import platform as pl import sys + import numpy as np + +from pandas.compat import u + import pandas -import platform as pl +from pandas import ( + Categorical, DataFrame, Index, MultiIndex, NaT, Period, Series, + SparseDataFrame, SparseSeries, Timestamp, bdate_range, date_range, + period_range, timedelta_range, to_msgpack) +from pandas.tseries.offsets import ( + FY5253, BusinessDay, BusinessHour, CustomBusinessDay, DateOffset, Day, + Easter, Hour, LastWeekOfMonth, Minute, MonthBegin, MonthEnd, QuarterBegin, + QuarterEnd, SemiMonthBegin, SemiMonthEnd, Week, WeekOfMonth, YearBegin, + YearEnd) _loose_version = LooseVersion(pandas.__version__) @@ -72,7 +116,18 @@ def create_data(): index = dict(int=Index(np.arange(10)), date=date_range('20130101', periods=10), - period=period_range('2013-01-01', freq='M', periods=10)) + period=period_range('2013-01-01', freq='M', periods=10), + float=Index(np.arange(10, dtype=np.float64)), + uint=Index(np.arange(10, dtype=np.uint64)), + timedelta=timedelta_range('00:00:00', freq='30T', periods=10)) + + if _loose_version >= LooseVersion('0.18'): + from pandas import RangeIndex + index['range'] = RangeIndex(10) + + if _loose_version >= LooseVersion('0.21'): + from pandas import interval_range + index['interval'] = interval_range(0, periods=10) mi = dict(reg2=MultiIndex.from_tuples( tuple(zip(*[[u'bar', u'bar', u'baz', u'baz', u'foo', @@ -124,32 +179,55 @@ def create_data(): mixed_dup=mixed_dup_df, dt_mixed_tzs=DataFrame({ u'A': Timestamp('20130102', tz='US/Eastern'), - u'B': Timestamp('20130603', tz='CET')}, index=range(5)) + u'B': Timestamp('20130603', tz='CET')}, index=range(5)), + dt_mixed2_tzs=DataFrame({ + u'A': Timestamp('20130102', tz='US/Eastern'), + u'B': Timestamp('20130603', tz='CET'), + u'C': Timestamp('20130603', tz='UTC')}, index=range(5)) ) - mixed_dup_panel = Panel({u'ItemA': frame[u'float'], - u'ItemB': frame[u'int']}) - mixed_dup_panel.items = [u'ItemA', u'ItemA'] - panel = dict(float=Panel({u'ItemA': frame[u'float'], - u'ItemB': frame[u'float'] + 1}), - dup=Panel(np.arange(30).reshape(3, 5, 2).astype(np.float64), - items=[u'A', u'B', u'A']), - mixed_dup=mixed_dup_panel) - cat = dict(int8=Categorical(list('abcdefg')), int16=Categorical(np.arange(1000)), int32=Categorical(np.arange(10000))) timestamp = dict(normal=Timestamp('2011-01-01'), nat=NaT, - tz=Timestamp('2011-01-01', tz='US/Eastern'), - freq=Timestamp('2011-01-01', freq='D'), - both=Timestamp('2011-01-01', tz='Asia/Tokyo', - freq='M')) + tz=Timestamp('2011-01-01', tz='US/Eastern')) + + if _loose_version < LooseVersion('0.19.2'): + timestamp['freq'] = Timestamp('2011-01-01', offset='D') + timestamp['both'] = Timestamp('2011-01-01', tz='Asia/Tokyo', + offset='M') + else: + timestamp['freq'] = Timestamp('2011-01-01', freq='D') + timestamp['both'] = Timestamp('2011-01-01', tz='Asia/Tokyo', + freq='M') + + off = {'DateOffset': DateOffset(years=1), + 'DateOffset_h_ns': DateOffset(hour=6, nanoseconds=5824), + 'BusinessDay': BusinessDay(offset=timedelta(seconds=9)), + 'BusinessHour': BusinessHour(normalize=True, n=6, end='15:14'), + 'CustomBusinessDay': CustomBusinessDay(weekmask='Mon Fri'), + 'SemiMonthBegin': SemiMonthBegin(day_of_month=9), + 'SemiMonthEnd': SemiMonthEnd(day_of_month=24), + 'MonthBegin': MonthBegin(1), + 'MonthEnd': MonthEnd(1), + 'QuarterBegin': QuarterBegin(1), + 'QuarterEnd': QuarterEnd(1), + 'Day': Day(1), + 'YearBegin': YearBegin(1), + 'YearEnd': YearEnd(1), + 'Week': Week(1), + 'Week_Tues': Week(2, normalize=False, weekday=1), + 'WeekOfMonth': WeekOfMonth(week=3, weekday=4), + 'LastWeekOfMonth': LastWeekOfMonth(n=1, weekday=3), + 'FY5253': FY5253(n=2, weekday=6, startingMonth=7, variation="last"), + 'Easter': Easter(), + 'Hour': Hour(1), + 'Minute': Minute(1)} return dict(series=series, frame=frame, - panel=panel, index=index, scalars=scalars, mi=mi, @@ -157,7 +235,8 @@ def create_data(): ts=_create_sp_tsseries()), sp_frame=dict(float=_create_sp_frame()), cat=cat, - timestamp=timestamp) + timestamp=timestamp, + offsets=off) def create_pickle_data(): @@ -165,10 +244,10 @@ def create_pickle_data(): # Pre-0.14.1 versions generated non-unpicklable mixed-type frames and # panels if their columns/items were non-unique. - if _loose_version < '0.14.1': + if _loose_version < LooseVersion('0.14.1'): del data['frame']['mixed_dup'] del data['panel']['mixed_dup'] - if _loose_version < '0.17.0': + if _loose_version < LooseVersion('0.17.0'): del data['series']['period'] del data['scalars']['period'] return data @@ -180,12 +259,12 @@ def _u(x): def create_msgpack_data(): data = create_data() - if _loose_version < '0.17.0': + if _loose_version < LooseVersion('0.17.0'): del data['frame']['mixed_dup'] del data['panel']['mixed_dup'] del data['frame']['dup'] del data['panel']['dup'] - if _loose_version < '0.18.0': + if _loose_version < LooseVersion('0.18.0'): del data['series']['dt_tz'] del data['frame']['dt_mixed_tzs'] # Not supported @@ -196,6 +275,9 @@ def create_msgpack_data(): del data['frame']['cat_onecol'] del data['frame']['cat_and_float'] del data['scalars']['period'] + if _loose_version < LooseVersion('0.23.0'): + del data['index']['interval'] + del data['offsets'] return _u(data) @@ -209,7 +291,7 @@ def write_legacy_pickles(output_dir): # make sure we are < 0.13 compat (in py3) try: from pandas.compat import zip, cPickle as pickle # noqa - except: + except ImportError: import pickle version = pandas.__version__ diff --git a/pandas/tests/io/json/data/tsframe_v012.json.zip b/pandas/tests/io/json/data/tsframe_v012.json.zip new file mode 100644 index 0000000000000000000000000000000000000000..100ba0c87b2ba55c169081bb0ed60c5db7391bbb GIT binary patch literal 436 zcmWIWW@Zs#-~d8>PgidSBp}Ejz)(`0R+N~V8ee8$Xrz}_oSzpO!Nb60eJyg=i>r~} z7)2P4PTcFqY$(uj|LLnEw<6!?Th+y}ylfKDbYKphQr@pG)b!*{7t{95#=p{PX2~tP zo9VSN!2DO`Wj2tkn(477rQ0RX7Wsm1^R literal 0 HcmV?d00001 diff --git a/pandas/tests/io/json/test_compression.py b/pandas/tests/io/json/test_compression.py new file mode 100644 index 0000000000000..430acbdac804a --- /dev/null +++ b/pandas/tests/io/json/test_compression.py @@ -0,0 +1,120 @@ +import pytest + +import pandas.util._test_decorators as td + +import pandas as pd +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal + + +def test_compression_roundtrip(compression): + df = pd.DataFrame([[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + index=['A', 'B'], columns=['X', 'Y', 'Z']) + + with tm.ensure_clean() as path: + df.to_json(path, compression=compression) + assert_frame_equal(df, pd.read_json(path, + compression=compression)) + + # explicitly ensure file was compressed. + with tm.decompress_file(path, compression) as fh: + result = fh.read().decode('utf8') + assert_frame_equal(df, pd.read_json(result)) + + +def test_read_zipped_json(datapath): + uncompressed_path = datapath("io", "json", "data", "tsframe_v012.json") + uncompressed_df = pd.read_json(uncompressed_path) + + compressed_path = datapath("io", "json", "data", "tsframe_v012.json.zip") + compressed_df = pd.read_json(compressed_path, compression='zip') + + assert_frame_equal(uncompressed_df, compressed_df) + + +@td.skip_if_not_us_locale +def test_with_s3_url(compression, s3_resource): + # Bucket "pandas-test" created in tests/io/conftest.py + + df = pd.read_json('{"a": [1, 2, 3], "b": [4, 5, 6]}') + + with tm.ensure_clean() as path: + df.to_json(path, compression=compression) + with open(path, 'rb') as f: + s3_resource.Bucket("pandas-test").put_object(Key='test-1', Body=f) + + roundtripped_df = pd.read_json('s3://pandas-test/test-1', + compression=compression) + assert_frame_equal(df, roundtripped_df) + + +def test_lines_with_compression(compression): + + with tm.ensure_clean() as path: + df = pd.read_json('{"a": [1, 2, 3], "b": [4, 5, 6]}') + df.to_json(path, orient='records', lines=True, + compression=compression) + roundtripped_df = pd.read_json(path, lines=True, + compression=compression) + assert_frame_equal(df, roundtripped_df) + + +def test_chunksize_with_compression(compression): + + with tm.ensure_clean() as path: + df = pd.read_json('{"a": ["foo", "bar", "baz"], "b": [4, 5, 6]}') + df.to_json(path, orient='records', lines=True, + compression=compression) + + res = pd.read_json(path, lines=True, chunksize=1, + compression=compression) + roundtripped_df = pd.concat(res) + assert_frame_equal(df, roundtripped_df) + + +def test_write_unsupported_compression_type(): + df = pd.read_json('{"a": [1, 2, 3], "b": [4, 5, 6]}') + with tm.ensure_clean() as path: + msg = "Unrecognized compression type: unsupported" + with pytest.raises(ValueError, match=msg): + df.to_json(path, compression="unsupported") + + +def test_read_unsupported_compression_type(): + with tm.ensure_clean() as path: + msg = "Unrecognized compression type: unsupported" + with pytest.raises(ValueError, match=msg): + pd.read_json(path, compression="unsupported") + + +@pytest.mark.parametrize("to_infer", [True, False]) +@pytest.mark.parametrize("read_infer", [True, False]) +def test_to_json_compression(compression_only, + read_infer, to_infer): + # see gh-15008 + compression = compression_only + + if compression == "zip": + pytest.skip("{compression} is not supported " + "for to_csv".format(compression=compression)) + + # We'll complete file extension subsequently. + filename = "test." + + if compression == "gzip": + filename += "gz" + else: + # xz --> .xz + # bz2 --> .bz2 + filename += compression + + df = pd.DataFrame({"A": [1]}) + + to_compression = "infer" if to_infer else compression + read_compression = "infer" if read_infer else compression + + with tm.ensure_clean(filename) as path: + df.to_json(path, compression=to_compression) + result = pd.read_json(path, compression=read_compression) + tm.assert_frame_equal(result, df) diff --git a/pandas/tests/io/json/test_json_table_schema.py b/pandas/tests/io/json/test_json_table_schema.py index d1795f2816817..351b495e5d8fc 100644 --- a/pandas/tests/io/json/test_json_table_schema.py +++ b/pandas/tests/io/json/test_json_table_schema.py @@ -1,22 +1,25 @@ """Tests for Table Schema integration.""" -import json from collections import OrderedDict +import json import numpy as np -import pandas as pd import pytest +from pandas.core.dtypes.dtypes import ( + CategoricalDtype, DatetimeTZDtype, PeriodDtype) + +import pandas as pd from pandas import DataFrame -from pandas.types.dtypes import PeriodDtype, CategoricalDtype, DatetimeTZDtype import pandas.util.testing as tm + from pandas.io.json.table_schema import ( - as_json_table_type, build_table_schema, make_field, set_default_names -) + as_json_table_type, build_table_schema, convert_json_field_to_pandas_type, + convert_pandas_type_to_json_field, set_default_names) -class TestBuildSchema(tm.TestCase): +class TestBuildSchema(object): - def setUp(self): + def setup_method(self, method): self.df = DataFrame( {'A': [1, 2, 3, 4], 'B': ['a', 'b', 'c', 'c'], @@ -36,9 +39,9 @@ def test_build_table_schema(self): ], 'primaryKey': ['idx'] } - self.assertEqual(result, expected) + assert result == expected result = build_table_schema(self.df) - self.assertTrue("pandas_version" in result) + assert "pandas_version" in result def test_series(self): s = pd.Series([1, 2, 3], name='foo') @@ -46,16 +49,16 @@ def test_series(self): expected = {'fields': [{'name': 'index', 'type': 'integer'}, {'name': 'foo', 'type': 'integer'}], 'primaryKey': ['index']} - self.assertEqual(result, expected) + assert result == expected result = build_table_schema(s) - self.assertTrue('pandas_version' in result) + assert 'pandas_version' in result - def tets_series_unnamed(self): + def test_series_unnamed(self): result = build_table_schema(pd.Series([1, 2, 3]), version=False) expected = {'fields': [{'name': 'index', 'type': 'integer'}, {'name': 'values', 'type': 'integer'}], 'primaryKey': ['index']} - self.assertEqual(result, expected) + assert result == expected def test_multiindex(self): df = self.df.copy() @@ -73,104 +76,105 @@ def test_multiindex(self): ], 'primaryKey': ['level_0', 'level_1'] } - self.assertEqual(result, expected) + assert result == expected df.index.names = ['idx0', None] expected['fields'][0]['name'] = 'idx0' expected['primaryKey'] = ['idx0', 'level_1'] result = build_table_schema(df, version=False) - self.assertEqual(result, expected) + assert result == expected -class TestTableSchemaType(tm.TestCase): +class TestTableSchemaType(object): - def test_as_json_table_type_int_data(self): + @pytest.mark.parametrize('int_type', [ + np.int, np.int16, np.int32, np.int64]) + def test_as_json_table_type_int_data(self, int_type): int_data = [1, 2, 3] - int_types = [np.int, np.int16, np.int32, np.int64] - for t in int_types: - self.assertEqual(as_json_table_type(np.array(int_data, dtype=t)), - 'integer') + assert as_json_table_type(np.array( + int_data, dtype=int_type)) == 'integer' - def test_as_json_table_type_float_data(self): + @pytest.mark.parametrize('float_type', [ + np.float, np.float16, np.float32, np.float64]) + def test_as_json_table_type_float_data(self, float_type): float_data = [1., 2., 3.] - float_types = [np.float, np.float16, np.float32, np.float64] - for t in float_types: - self.assertEqual(as_json_table_type(np.array(float_data, - dtype=t)), - 'number') + assert as_json_table_type(np.array( + float_data, dtype=float_type)) == 'number' - def test_as_json_table_type_bool_data(self): + @pytest.mark.parametrize('bool_type', [bool, np.bool]) + def test_as_json_table_type_bool_data(self, bool_type): bool_data = [True, False] - bool_types = [bool, np.bool] - for t in bool_types: - self.assertEqual(as_json_table_type(np.array(bool_data, dtype=t)), - 'boolean') - - def test_as_json_table_type_date_data(self): - date_data = [pd.to_datetime(['2016']), - pd.to_datetime(['2016'], utc=True), - pd.Series(pd.to_datetime(['2016'])), - pd.Series(pd.to_datetime(['2016'], utc=True)), - pd.period_range('2016', freq='A', periods=3)] - for arr in date_data: - self.assertEqual(as_json_table_type(arr), 'datetime') - - def test_as_json_table_type_string_data(self): - strings = [pd.Series(['a', 'b']), pd.Index(['a', 'b'])] - for t in strings: - self.assertEqual(as_json_table_type(t), 'string') - - def test_as_json_table_type_categorical_data(self): - self.assertEqual(as_json_table_type(pd.Categorical(['a'])), 'any') - self.assertEqual(as_json_table_type(pd.Categorical([1])), 'any') - self.assertEqual(as_json_table_type( - pd.Series(pd.Categorical([1]))), 'any') - self.assertEqual(as_json_table_type(pd.CategoricalIndex([1])), 'any') - self.assertEqual(as_json_table_type(pd.Categorical([1])), 'any') + assert as_json_table_type(np.array( + bool_data, dtype=bool_type)) == 'boolean' + + @pytest.mark.parametrize('date_data', [ + pd.to_datetime(['2016']), + pd.to_datetime(['2016'], utc=True), + pd.Series(pd.to_datetime(['2016'])), + pd.Series(pd.to_datetime(['2016'], utc=True)), + pd.period_range('2016', freq='A', periods=3) + ]) + def test_as_json_table_type_date_data(self, date_data): + assert as_json_table_type(date_data) == 'datetime' + + @pytest.mark.parametrize('str_data', [ + pd.Series(['a', 'b']), pd.Index(['a', 'b'])]) + def test_as_json_table_type_string_data(self, str_data): + assert as_json_table_type(str_data) == 'string' + + @pytest.mark.parametrize('cat_data', [ + pd.Categorical(['a']), + pd.Categorical([1]), + pd.Series(pd.Categorical([1])), + pd.CategoricalIndex([1]), + pd.Categorical([1])]) + def test_as_json_table_type_categorical_data(self, cat_data): + assert as_json_table_type(cat_data) == 'any' # ------ # dtypes # ------ - def test_as_json_table_type_int_dtypes(self): - integers = [np.int, np.int16, np.int32, np.int64] - for t in integers: - self.assertEqual(as_json_table_type(t), 'integer') - - def test_as_json_table_type_float_dtypes(self): - floats = [np.float, np.float16, np.float32, np.float64] - for t in floats: - self.assertEqual(as_json_table_type(t), 'number') - - def test_as_json_table_type_bool_dtypes(self): - bools = [bool, np.bool] - for t in bools: - self.assertEqual(as_json_table_type(t), 'boolean') - - def test_as_json_table_type_date_dtypes(self): + @pytest.mark.parametrize('int_dtype', [ + np.int, np.int16, np.int32, np.int64]) + def test_as_json_table_type_int_dtypes(self, int_dtype): + assert as_json_table_type(int_dtype) == 'integer' + + @pytest.mark.parametrize('float_dtype', [ + np.float, np.float16, np.float32, np.float64]) + def test_as_json_table_type_float_dtypes(self, float_dtype): + assert as_json_table_type(float_dtype) == 'number' + + @pytest.mark.parametrize('bool_dtype', [bool, np.bool]) + def test_as_json_table_type_bool_dtypes(self, bool_dtype): + assert as_json_table_type(bool_dtype) == 'boolean' + + @pytest.mark.parametrize('date_dtype', [ + np.datetime64, np.dtype("=1" + + with pytest.raises(ValueError, match=msg): + pd.read_json(StringIO(lines_json_df), lines=True, + chunksize=chunksize) + + +@pytest.mark.parametrize("chunksize", [None, 1, 2]) +def test_readjson_chunks_multiple_empty_lines(chunksize): + j = """ + + {"A":1,"B":4} + + + + {"A":2,"B":5} + + + + + + + + {"A":3,"B":6} + """ + orig = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}) + test = pd.read_json(j, lines=True, chunksize=chunksize) + if chunksize is not None: + test = pd.concat(test) + tm.assert_frame_equal( + orig, test, obj="chunksize: {chunksize}".format(chunksize=chunksize)) diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index e66721beed288..63ba9bc0f0488 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -1,781 +1,613 @@ # -*- coding: utf-8 -*- -from unittest import TestCase - try: import json except ImportError: import simplejson as json -import math -import pytest -import platform -import sys -import time -import datetime import calendar -import re +import datetime import decimal from functools import partial -from pandas.compat import range, zip, StringIO, u -import pandas.io.json.libjson as ujson -import pandas.compat as compat +import locale +import math +import re +import time +import dateutil import numpy as np -from pandas import DataFrame, Series, Index, NaT, DatetimeIndex -import pandas.util.testing as tm - +import pytest +import pytz -def _skip_if_python_ver(skip_major, skip_minor=None): - major, minor = sys.version_info[:2] - if major == skip_major and (skip_minor is None or minor == skip_minor): - pytest.skip("skipping Python version %d.%d" % (major, minor)) +import pandas._libs.json as ujson +from pandas._libs.tslib import Timestamp +import pandas.compat as compat +from pandas.compat import StringIO, range, u +from pandas import DataFrame, DatetimeIndex, Index, NaT, Series, date_range +import pandas.util.testing as tm json_unicode = (json.dumps if compat.PY3 else partial(json.dumps, encoding="utf-8")) -class UltraJSONTests(TestCase): +def _clean_dict(d): + """ + Sanitize dictionary for JSON by converting all keys to strings. + + Parameters + ---------- + d : dict + The dictionary to convert. + + Returns + ------- + cleaned_dict : dict + """ + + return {str(k): v for k, v in compat.iteritems(d)} + - def test_encodeDecimal(self): +@pytest.fixture(params=[ + None, # Column indexed by default. + "split", + "records", + "values", + "index"]) +def orient(request): + return request.param + + +@pytest.fixture(params=[None, True]) +def numpy(request): + return request.param + + +class TestUltraJSONTests(object): + + @pytest.mark.skipif(compat.is_platform_32bit(), + reason="not compliant on 32-bit, xref #15865") + def test_encode_decimal(self): sut = decimal.Decimal("1337.1337") encoded = ujson.encode(sut, double_precision=15) decoded = ujson.decode(encoded) - self.assertEqual(decoded, 1337.1337) + assert decoded == 1337.1337 + + sut = decimal.Decimal("0.95") + encoded = ujson.encode(sut, double_precision=1) + assert encoded == "1.0" - def test_encodeStringConversion(self): - input = "A string \\ / \b \f \n \r \t &" + decoded = ujson.decode(encoded) + assert decoded == 1.0 + + sut = decimal.Decimal("0.94") + encoded = ujson.encode(sut, double_precision=1) + assert encoded == "0.9" + + decoded = ujson.decode(encoded) + assert decoded == 0.9 + + sut = decimal.Decimal("1.95") + encoded = ujson.encode(sut, double_precision=1) + assert encoded == "2.0" + + decoded = ujson.decode(encoded) + assert decoded == 2.0 + + sut = decimal.Decimal("-1.95") + encoded = ujson.encode(sut, double_precision=1) + assert encoded == "-2.0" + + decoded = ujson.decode(encoded) + assert decoded == -2.0 + + sut = decimal.Decimal("0.995") + encoded = ujson.encode(sut, double_precision=2) + assert encoded == "1.0" + + decoded = ujson.decode(encoded) + assert decoded == 1.0 + + sut = decimal.Decimal("0.9995") + encoded = ujson.encode(sut, double_precision=3) + assert encoded == "1.0" + + decoded = ujson.decode(encoded) + assert decoded == 1.0 + + sut = decimal.Decimal("0.99999999999999944") + encoded = ujson.encode(sut, double_precision=15) + assert encoded == "1.0" + + decoded = ujson.decode(encoded) + assert decoded == 1.0 + + @pytest.mark.parametrize("ensure_ascii", [True, False]) + def test_encode_string_conversion(self, ensure_ascii): + string_input = "A string \\ / \b \f \n \r \t &" not_html_encoded = ('"A string \\\\ \\/ \\b \\f \\n ' '\\r \\t <\\/script> &"') html_encoded = ('"A string \\\\ \\/ \\b \\f \\n \\r \\t ' '\\u003c\\/script\\u003e \\u0026"') def helper(expected_output, **encode_kwargs): - output = ujson.encode(input, **encode_kwargs) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, expected_output) - self.assertEqual(input, ujson.decode(output)) + output = ujson.encode(string_input, + ensure_ascii=ensure_ascii, + **encode_kwargs) + + assert output == expected_output + assert string_input == json.loads(output) + assert string_input == ujson.decode(output) # Default behavior assumes encode_html_chars=False. - helper(not_html_encoded, ensure_ascii=True) - helper(not_html_encoded, ensure_ascii=False) + helper(not_html_encoded) # Make sure explicit encode_html_chars=False works. - helper(not_html_encoded, ensure_ascii=True, encode_html_chars=False) - helper(not_html_encoded, ensure_ascii=False, encode_html_chars=False) + helper(not_html_encoded, encode_html_chars=False) # Make sure explicit encode_html_chars=True does the encoding. - helper(html_encoded, ensure_ascii=True, encode_html_chars=True) - helper(html_encoded, ensure_ascii=False, encode_html_chars=True) - - def test_doubleLongIssue(self): - sut = {u('a'): -4342969734183514} - encoded = json.dumps(sut) - decoded = json.loads(encoded) - self.assertEqual(sut, decoded) - encoded = ujson.encode(sut, double_precision=15) - decoded = ujson.decode(encoded) - self.assertEqual(sut, decoded) + helper(html_encoded, encode_html_chars=True) - def test_doubleLongDecimalIssue(self): - sut = {u('a'): -12345678901234.56789012} - encoded = json.dumps(sut) - decoded = json.loads(encoded) - self.assertEqual(sut, decoded) + @pytest.mark.parametrize("long_number", [ + -4342969734183514, -12345678901234.56789012, -528656961.4399388 + ]) + def test_double_long_numbers(self, long_number): + sut = {u("a"): long_number} encoded = ujson.encode(sut, double_precision=15) + decoded = ujson.decode(encoded) - self.assertEqual(sut, decoded) - - def test_encodeNonCLocale(self): - import locale - savedlocale = locale.getlocale(locale.LC_NUMERIC) - try: - locale.setlocale(locale.LC_NUMERIC, 'it_IT.UTF-8') - except: - try: - locale.setlocale(locale.LC_NUMERIC, 'Italian_Italy') - except: - pytest.skip('Could not set locale for testing') - self.assertEqual(ujson.loads(ujson.dumps(4.78e60)), 4.78e60) - self.assertEqual(ujson.loads('4.78', precise_float=True), 4.78) - locale.setlocale(locale.LC_NUMERIC, savedlocale) - - def test_encodeDecodeLongDecimal(self): - sut = {u('a'): -528656961.4399388} - encoded = ujson.dumps(sut, double_precision=15) - ujson.decode(encoded) - - def test_decimalDecodeTestPrecise(self): - sut = {u('a'): 4.56} + assert sut == decoded + + def test_encode_non_c_locale(self): + lc_category = locale.LC_NUMERIC + + # We just need one of these locales to work. + for new_locale in ("it_IT.UTF-8", "Italian_Italy"): + if tm.can_set_locale(new_locale, lc_category): + with tm.set_locale(new_locale, lc_category): + assert ujson.loads(ujson.dumps(4.78e60)) == 4.78e60 + assert ujson.loads("4.78", precise_float=True) == 4.78 + break + + def test_decimal_decode_test_precise(self): + sut = {u("a"): 4.56} encoded = ujson.encode(sut) decoded = ujson.decode(encoded, precise_float=True) - self.assertEqual(sut, decoded) - - def test_encodeDoubleTinyExponential(self): - if compat.is_platform_windows() and not compat.PY3: - pytest.skip("buggy on win-64 for py2") + assert sut == decoded + @pytest.mark.skipif(compat.is_platform_windows() and not compat.PY3, + reason="buggy on win-64 for py2") + def test_encode_double_tiny_exponential(self): num = 1e-40 - self.assertEqual(num, ujson.decode(ujson.encode(num))) + assert num == ujson.decode(ujson.encode(num)) num = 1e-100 - self.assertEqual(num, ujson.decode(ujson.encode(num))) + assert num == ujson.decode(ujson.encode(num)) num = -1e-45 - self.assertEqual(num, ujson.decode(ujson.encode(num))) + assert num == ujson.decode(ujson.encode(num)) num = -1e-145 - self.assertTrue(np.allclose(num, ujson.decode(ujson.encode(num)))) - - def test_encodeDictWithUnicodeKeys(self): - input = {u("key1"): u("value1"), u("key1"): - u("value1"), u("key1"): u("value1"), - u("key1"): u("value1"), u("key1"): - u("value1"), u("key1"): u("value1")} - output = ujson.encode(input) - - input = {u("بن"): u("value1"), u("بن"): u("value1"), - u("بن"): u("value1"), u("بن"): u("value1"), - u("بن"): u("value1"), u("بن"): u("value1"), - u("بن"): u("value1")} - output = ujson.encode(input) # noqa - - def test_encodeDoubleConversion(self): - input = math.pi - output = ujson.encode(input) - self.assertEqual(round(input, 5), round(json.loads(output), 5)) - self.assertEqual(round(input, 5), round(ujson.decode(output), 5)) - - def test_encodeWithDecimal(self): - input = 1.0 - output = ujson.encode(input) - self.assertEqual(output, "1.0") - - def test_encodeDoubleNegConversion(self): - input = -math.pi - output = ujson.encode(input) - - self.assertEqual(round(input, 5), round(json.loads(output), 5)) - self.assertEqual(round(input, 5), round(ujson.decode(output), 5)) - - def test_encodeArrayOfNestedArrays(self): - input = [[[[]]]] * 20 - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - # self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - input = np.array(input) - tm.assert_numpy_array_equal(input, ujson.decode( - output, numpy=True, dtype=input.dtype)) - - def test_encodeArrayOfDoubles(self): - input = [31337.31337, 31337.31337, 31337.31337, 31337.31337] * 10 - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - # self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - tm.assert_numpy_array_equal( - np.array(input), ujson.decode(output, numpy=True)) - - def test_doublePrecisionTest(self): - input = 30.012345678901234 - output = ujson.encode(input, double_precision=15) - self.assertEqual(input, json.loads(output)) - self.assertEqual(input, ujson.decode(output)) - - output = ujson.encode(input, double_precision=9) - self.assertEqual(round(input, 9), json.loads(output)) - self.assertEqual(round(input, 9), ujson.decode(output)) - - output = ujson.encode(input, double_precision=3) - self.assertEqual(round(input, 3), json.loads(output)) - self.assertEqual(round(input, 3), ujson.decode(output)) - - def test_invalidDoublePrecision(self): - input = 30.12345678901234567890 - - self.assertRaises(ValueError, ujson.encode, input, double_precision=20) - self.assertRaises(ValueError, ujson.encode, input, double_precision=-1) - - # will throw typeError - self.assertRaises(TypeError, ujson.encode, input, double_precision='9') - # will throw typeError - self.assertRaises(TypeError, ujson.encode, - input, double_precision=None) - - def test_encodeStringConversion2(self): - input = "A string \\ / \b \f \n \r \t" - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, '"A string \\\\ \\/ \\b \\f \\n \\r \\t"') - self.assertEqual(input, ujson.decode(output)) - pass - - def test_decodeUnicodeConversion(self): - pass - - def test_encodeUnicodeConversion1(self): - input = "Räksmörgås اسامة بن محمد بن عوض بن لادن" - enc = ujson.encode(input) + assert np.allclose(num, ujson.decode(ujson.encode(num))) + + @pytest.mark.parametrize("unicode_key", [ + u("key1"), u("بن") + ]) + def test_encode_dict_with_unicode_keys(self, unicode_key): + unicode_dict = {unicode_key: u("value1")} + assert unicode_dict == ujson.decode(ujson.encode(unicode_dict)) + + @pytest.mark.parametrize("double_input", [ + math.pi, + -math.pi # Should work with negatives too. + ]) + def test_encode_double_conversion(self, double_input): + output = ujson.encode(double_input) + assert round(double_input, 5) == round(json.loads(output), 5) + assert round(double_input, 5) == round(ujson.decode(output), 5) + + def test_encode_with_decimal(self): + decimal_input = 1.0 + output = ujson.encode(decimal_input) + + assert output == "1.0" + + def test_encode_array_of_nested_arrays(self): + nested_input = [[[[]]]] * 20 + output = ujson.encode(nested_input) + + assert nested_input == json.loads(output) + assert nested_input == ujson.decode(output) + + nested_input = np.array(nested_input) + tm.assert_numpy_array_equal(nested_input, ujson.decode( + output, numpy=True, dtype=nested_input.dtype)) + + def test_encode_array_of_doubles(self): + doubles_input = [31337.31337, 31337.31337, + 31337.31337, 31337.31337] * 10 + output = ujson.encode(doubles_input) + + assert doubles_input == json.loads(output) + assert doubles_input == ujson.decode(output) + + tm.assert_numpy_array_equal(np.array(doubles_input), + ujson.decode(output, numpy=True)) + + def test_double_precision(self): + double_input = 30.012345678901234 + output = ujson.encode(double_input, double_precision=15) + + assert double_input == json.loads(output) + assert double_input == ujson.decode(output) + + for double_precision in (3, 9): + output = ujson.encode(double_input, + double_precision=double_precision) + rounded_input = round(double_input, double_precision) + + assert rounded_input == json.loads(output) + assert rounded_input == ujson.decode(output) + + @pytest.mark.parametrize("invalid_val", [ + 20, -1, "9", None + ]) + def test_invalid_double_precision(self, invalid_val): + double_input = 30.12345678901234567890 + expected_exception = (ValueError if isinstance(invalid_val, int) + else TypeError) + + with pytest.raises(expected_exception): + ujson.encode(double_input, double_precision=invalid_val) + + def test_encode_string_conversion2(self): + string_input = "A string \\ / \b \f \n \r \t" + output = ujson.encode(string_input) + + assert string_input == json.loads(output) + assert string_input == ujson.decode(output) + assert output == '"A string \\\\ \\/ \\b \\f \\n \\r \\t"' + + @pytest.mark.parametrize("unicode_input", [ + "Räksmörgås اسامة بن محمد بن عوض بن لادن", + "\xe6\x97\xa5\xd1\x88" + ]) + def test_encode_unicode_conversion(self, unicode_input): + enc = ujson.encode(unicode_input) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input)) - self.assertEqual(dec, json.loads(enc)) - def test_encodeControlEscaping(self): - input = "\x19" - enc = ujson.encode(input) - dec = ujson.decode(enc) - self.assertEqual(input, dec) - self.assertEqual(enc, json_unicode(input)) + assert enc == json_unicode(unicode_input) + assert dec == json.loads(enc) - def test_encodeUnicodeConversion2(self): - input = "\xe6\x97\xa5\xd1\x88" - enc = ujson.encode(input) + def test_encode_control_escaping(self): + escaped_input = "\x19" + enc = ujson.encode(escaped_input) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input)) - self.assertEqual(dec, json.loads(enc)) - - def test_encodeUnicodeSurrogatePair(self): - _skip_if_python_ver(2, 5) - _skip_if_python_ver(2, 6) - input = "\xf0\x90\x8d\x86" - enc = ujson.encode(input) + + assert escaped_input == dec + assert enc == json_unicode(escaped_input) + + def test_encode_unicode_surrogate_pair(self): + surrogate_input = "\xf0\x90\x8d\x86" + enc = ujson.encode(surrogate_input) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input)) - self.assertEqual(dec, json.loads(enc)) + assert enc == json_unicode(surrogate_input) + assert dec == json.loads(enc) - def test_encodeUnicode4BytesUTF8(self): - _skip_if_python_ver(2, 5) - _skip_if_python_ver(2, 6) - input = "\xf0\x91\x80\xb0TRAILINGNORMAL" - enc = ujson.encode(input) + def test_encode_unicode_4bytes_utf8(self): + four_bytes_input = "\xf0\x91\x80\xb0TRAILINGNORMAL" + enc = ujson.encode(four_bytes_input) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input)) - self.assertEqual(dec, json.loads(enc)) + assert enc == json_unicode(four_bytes_input) + assert dec == json.loads(enc) - def test_encodeUnicode4BytesUTF8Highest(self): - _skip_if_python_ver(2, 5) - _skip_if_python_ver(2, 6) - input = "\xf3\xbf\xbf\xbfTRAILINGNORMAL" - enc = ujson.encode(input) + def test_encode_unicode_4bytes_utf8highest(self): + four_bytes_input = "\xf3\xbf\xbf\xbfTRAILINGNORMAL" + enc = ujson.encode(four_bytes_input) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input)) - self.assertEqual(dec, json.loads(enc)) + assert enc == json_unicode(four_bytes_input) + assert dec == json.loads(enc) - def test_encodeArrayInArray(self): - input = [[[[]]]] - output = ujson.encode(input) + def test_encode_array_in_array(self): + arr_in_arr_input = [[[[]]]] + output = ujson.encode(arr_in_arr_input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - tm.assert_numpy_array_equal( - np.array(input), ujson.decode(output, numpy=True)) - pass - - def test_encodeIntConversion(self): - input = 31337 - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_encodeIntNegConversion(self): - input = -31337 - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_encodeLongNegConversion(self): - input = -9223372036854775808 - output = ujson.encode(input) - - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - - def test_encodeListConversion(self): - input = [1, 2, 3, 4] - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(input, ujson.decode(output)) - tm.assert_numpy_array_equal( - np.array(input), ujson.decode(output, numpy=True)) - pass - - def test_encodeDictConversion(self): - input = {"k1": 1, "k2": 2, "k3": 3, "k4": 4} - output = ujson.encode(input) # noqa - self.assertEqual(input, json.loads(output)) - self.assertEqual(input, ujson.decode(output)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_encodeNoneConversion(self): - input = None - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_encodeTrueConversion(self): - input = True - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_encodeFalseConversion(self): - input = False - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - - def test_encodeDatetimeConversion(self): - ts = time.time() - input = datetime.datetime.fromtimestamp(ts) - output = ujson.encode(input, date_unit='s') - expected = calendar.timegm(input.utctimetuple()) - self.assertEqual(int(expected), json.loads(output)) - self.assertEqual(int(expected), ujson.decode(output)) - - def test_encodeDateConversion(self): - ts = time.time() - input = datetime.date.fromtimestamp(ts) - - output = ujson.encode(input, date_unit='s') - tup = (input.year, input.month, input.day, 0, 0, 0) + assert arr_in_arr_input == json.loads(output) + assert output == json.dumps(arr_in_arr_input) + assert arr_in_arr_input == ujson.decode(output) + + tm.assert_numpy_array_equal(np.array(arr_in_arr_input), + ujson.decode(output, numpy=True)) + + @pytest.mark.parametrize("num_input", [ + 31337, + -31337, # Negative number. + -9223372036854775808 # Large negative number. + ]) + def test_encode_num_conversion(self, num_input): + output = ujson.encode(num_input) + assert num_input == json.loads(output) + assert output == json.dumps(num_input) + assert num_input == ujson.decode(output) + + def test_encode_list_conversion(self): + list_input = [1, 2, 3, 4] + output = ujson.encode(list_input) + + assert list_input == json.loads(output) + assert list_input == ujson.decode(output) + + tm.assert_numpy_array_equal(np.array(list_input), + ujson.decode(output, numpy=True)) + + def test_encode_dict_conversion(self): + dict_input = {"k1": 1, "k2": 2, "k3": 3, "k4": 4} + output = ujson.encode(dict_input) + + assert dict_input == json.loads(output) + assert dict_input == ujson.decode(output) + @pytest.mark.parametrize("builtin_value", [None, True, False]) + def test_encode_builtin_values_conversion(self, builtin_value): + output = ujson.encode(builtin_value) + assert builtin_value == json.loads(output) + assert output == json.dumps(builtin_value) + assert builtin_value == ujson.decode(output) + + def test_encode_datetime_conversion(self): + datetime_input = datetime.datetime.fromtimestamp(time.time()) + output = ujson.encode(datetime_input, date_unit="s") + expected = calendar.timegm(datetime_input.utctimetuple()) + + assert int(expected) == json.loads(output) + assert int(expected) == ujson.decode(output) + + def test_encode_date_conversion(self): + date_input = datetime.date.fromtimestamp(time.time()) + output = ujson.encode(date_input, date_unit="s") + + tup = (date_input.year, date_input.month, date_input.day, 0, 0, 0) expected = calendar.timegm(tup) - self.assertEqual(int(expected), json.loads(output)) - self.assertEqual(int(expected), ujson.decode(output)) - - def test_encodeTimeConversion(self): - tests = [ - datetime.time(), - datetime.time(1, 2, 3), - datetime.time(10, 12, 15, 343243), - ] - for test in tests: - output = ujson.encode(test) - expected = '"%s"' % test.isoformat() - self.assertEqual(expected, output) - - def test_encodeTimeConversion_pytz(self): - # GH11473 to_json segfaults with timezone-aware datetimes - tm._skip_if_no_pytz() - import pytz + + assert int(expected) == json.loads(output) + assert int(expected) == ujson.decode(output) + + @pytest.mark.parametrize("test", [ + datetime.time(), + datetime.time(1, 2, 3), + datetime.time(10, 12, 15, 343243), + ]) + def test_encode_time_conversion_basic(self, test): + output = ujson.encode(test) + expected = '"{iso}"'.format(iso=test.isoformat()) + assert expected == output + + def test_encode_time_conversion_pytz(self): + # see gh-11473: to_json segfaults with timezone-aware datetimes test = datetime.time(10, 12, 15, 343243, pytz.utc) output = ujson.encode(test) - expected = '"%s"' % test.isoformat() - self.assertEqual(expected, output) + expected = '"{iso}"'.format(iso=test.isoformat()) + assert expected == output - def test_encodeTimeConversion_dateutil(self): - # GH11473 to_json segfaults with timezone-aware datetimes - tm._skip_if_no_dateutil() - import dateutil + def test_encode_time_conversion_dateutil(self): + # see gh-11473: to_json segfaults with timezone-aware datetimes test = datetime.time(10, 12, 15, 343243, dateutil.tz.tzutc()) output = ujson.encode(test) - expected = '"%s"' % test.isoformat() - self.assertEqual(expected, output) - - def test_nat(self): - input = NaT - assert ujson.encode(input) == 'null', "Expected null" - - def test_npy_nat(self): - from distutils.version import LooseVersion - if LooseVersion(np.__version__) < '1.7.0': - pytest.skip("numpy version < 1.7.0, is " - "{0}".format(np.__version__)) - - input = np.datetime64('NaT') - assert ujson.encode(input) == 'null', "Expected null" + expected = '"{iso}"'.format(iso=test.isoformat()) + assert expected == output + + @pytest.mark.parametrize("decoded_input", [ + NaT, + np.datetime64("NaT"), + np.nan, + np.inf, + -np.inf + ]) + def test_encode_as_null(self, decoded_input): + assert ujson.encode(decoded_input) == "null", "Expected null" def test_datetime_units(self): - from pandas._libs.lib import Timestamp - val = datetime.datetime(2013, 8, 17, 21, 17, 12, 215504) stamp = Timestamp(val) roundtrip = ujson.decode(ujson.encode(val, date_unit='s')) - self.assertEqual(roundtrip, stamp.value // 10**9) + assert roundtrip == stamp.value // 10**9 roundtrip = ujson.decode(ujson.encode(val, date_unit='ms')) - self.assertEqual(roundtrip, stamp.value // 10**6) + assert roundtrip == stamp.value // 10**6 roundtrip = ujson.decode(ujson.encode(val, date_unit='us')) - self.assertEqual(roundtrip, stamp.value // 10**3) + assert roundtrip == stamp.value // 10**3 roundtrip = ujson.decode(ujson.encode(val, date_unit='ns')) - self.assertEqual(roundtrip, stamp.value) + assert roundtrip == stamp.value + + msg = "Invalid value 'foo' for option 'date_unit'" + with pytest.raises(ValueError, match=msg): + ujson.encode(val, date_unit='foo') - self.assertRaises(ValueError, ujson.encode, val, date_unit='foo') + def test_encode_to_utf8(self): + unencoded = "\xe6\x97\xa5\xd1\x88" - def test_encodeToUTF8(self): - _skip_if_python_ver(2, 5) - input = "\xe6\x97\xa5\xd1\x88" - enc = ujson.encode(input, ensure_ascii=False) + enc = ujson.encode(unencoded, ensure_ascii=False) dec = ujson.decode(enc) - self.assertEqual(enc, json_unicode(input, ensure_ascii=False)) - self.assertEqual(dec, json.loads(enc)) - def test_decodeFromUnicode(self): - input = u("{\"obj\": 31337}") - dec1 = ujson.decode(input) - dec2 = ujson.decode(str(input)) - self.assertEqual(dec1, dec2) + assert enc == json_unicode(unencoded, ensure_ascii=False) + assert dec == json.loads(enc) - def test_encodeRecursionMax(self): + def test_decode_from_unicode(self): + unicode_input = u("{\"obj\": 31337}") + + dec1 = ujson.decode(unicode_input) + dec2 = ujson.decode(str(unicode_input)) + + assert dec1 == dec2 + + def test_encode_recursion_max(self): # 8 is the max recursion depth - class O2: + class O2(object): member = 0 pass - class O1: + class O1(object): member = 0 pass - input = O1() - input.member = O2() - input.member.member = input + decoded_input = O1() + decoded_input.member = O2() + decoded_input.member.member = decoded_input + + with pytest.raises(OverflowError): + ujson.encode(decoded_input) + + def test_decode_jibberish(self): + jibberish = "fdsa sda v9sa fdsa" + + with pytest.raises(ValueError): + ujson.decode(jibberish) + + @pytest.mark.parametrize("broken_json", [ + "[", # Broken array start. + "{", # Broken object start. + "]", # Broken array end. + "}", # Broken object end. + ]) + def test_decode_broken_json(self, broken_json): + with pytest.raises(ValueError): + ujson.decode(broken_json) + + @pytest.mark.parametrize("too_big_char", [ + "[", + "{", + ]) + def test_decode_depth_too_big(self, too_big_char): + with pytest.raises(ValueError): + ujson.decode(too_big_char * (1024 * 1024)) + + @pytest.mark.parametrize("bad_string", [ + "\"TESTING", # Unterminated. + "\"TESTING\\\"", # Unterminated escape. + "tru", # Broken True. + "fa", # Broken False. + "n", # Broken None. + ]) + def test_decode_bad_string(self, bad_string): + with pytest.raises(ValueError): + ujson.decode(bad_string) + + @pytest.mark.parametrize("broken_json", [ + '{{1337:""}}', + '{{"key":"}', + '[[[true', + ]) + def test_decode_broken_json_leak(self, broken_json): + for _ in range(1000): + with pytest.raises(ValueError): + ujson.decode(broken_json) + + @pytest.mark.parametrize("invalid_dict", [ + "{{{{31337}}}}", # No key. + "{{{{\"key\":}}}}", # No value. + "{{{{\"key\"}}}}", # No colon or value. + ]) + def test_decode_invalid_dict(self, invalid_dict): + with pytest.raises(ValueError): + ujson.decode(invalid_dict) + + @pytest.mark.parametrize("numeric_int_as_str", [ + "31337", "-31337" # Should work with negatives. + ]) + def test_decode_numeric_int(self, numeric_int_as_str): + assert int(numeric_int_as_str) == ujson.decode(numeric_int_as_str) + + @pytest.mark.skipif(compat.PY3, reason="only PY2") + def test_encode_unicode_4bytes_utf8_fail(self): + with pytest.raises(OverflowError): + ujson.encode("\xfd\xbf\xbf\xbf\xbf\xbf") + + def test_encode_null_character(self): + wrapped_input = "31337 \x00 1337" + output = ujson.encode(wrapped_input) + + assert wrapped_input == json.loads(output) + assert output == json.dumps(wrapped_input) + assert wrapped_input == ujson.decode(output) + + alone_input = "\x00" + output = ujson.encode(alone_input) + + assert alone_input == json.loads(output) + assert output == json.dumps(alone_input) + assert alone_input == ujson.decode(output) + assert '" \\u0000\\r\\n "' == ujson.dumps(u(" \u0000\r\n ")) + + def test_decode_null_character(self): + wrapped_input = "\"31337 \\u0000 31337\"" + assert ujson.decode(wrapped_input) == json.loads(wrapped_input) + + def test_encode_list_long_conversion(self): + long_input = [9223372036854775807, 9223372036854775807, + 9223372036854775807, 9223372036854775807, + 9223372036854775807, 9223372036854775807] + output = ujson.encode(long_input) + + assert long_input == json.loads(output) + assert long_input == ujson.decode(output) + + tm.assert_numpy_array_equal(np.array(long_input), + ujson.decode(output, numpy=True, + dtype=np.int64)) - try: - output = ujson.encode(input) # noqa - assert False, "Expected overflow exception" - except(OverflowError): - pass + def test_encode_long_conversion(self): + long_input = 9223372036854775807 + output = ujson.encode(long_input) - def test_encodeDoubleNan(self): - input = np.nan - assert ujson.encode(input) == 'null', "Expected null" - - def test_encodeDoubleInf(self): - input = np.inf - assert ujson.encode(input) == 'null', "Expected null" - - def test_encodeDoubleNegInf(self): - input = -np.inf - assert ujson.encode(input) == 'null', "Expected null" - - def test_decodeJibberish(self): - input = "fdsa sda v9sa fdsa" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeBrokenArrayStart(self): - input = "[" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeBrokenObjectStart(self): - input = "{" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeBrokenArrayEnd(self): - input = "]" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeArrayDepthTooBig(self): - input = '[' * (1024 * 1024) - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeBrokenObjectEnd(self): - input = "}" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeObjectDepthTooBig(self): - input = '{' * (1024 * 1024) - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeStringUnterminated(self): - input = "\"TESTING" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeStringUntermEscapeSequence(self): - input = "\"TESTING\\\"" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeStringBadEscape(self): - input = "\"TESTING\\\"" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeTrueBroken(self): - input = "tru" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeFalseBroken(self): - input = "fa" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeNullBroken(self): - input = "n" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - assert False, "Wrong exception" - - def test_decodeBrokenDictKeyTypeLeakTest(self): - input = '{{1337:""}}' - for x in range(1000): - try: - ujson.decode(input) - assert False, "Expected exception!" - except ValueError: - continue - - assert False, "Wrong exception" - - def test_decodeBrokenDictLeakTest(self): - input = '{{"key":"}' - for x in range(1000): - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - continue - - assert False, "Wrong exception" - - def test_decodeBrokenListLeakTest(self): - input = '[[[true' - for x in range(1000): - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - continue - - assert False, "Wrong exception" - - def test_decodeDictWithNoKey(self): - input = "{{{{31337}}}}" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - - assert False, "Wrong exception" - - def test_decodeDictWithNoColonOrValue(self): - input = "{{{{\"key\"}}}}" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - - assert False, "Wrong exception" - - def test_decodeDictWithNoValue(self): - input = "{{{{\"key\":}}}}" - try: - ujson.decode(input) - assert False, "Expected exception!" - except(ValueError): - return - - assert False, "Wrong exception" - - def test_decodeNumericIntPos(self): - input = "31337" - self.assertEqual(31337, ujson.decode(input)) - - def test_decodeNumericIntNeg(self): - input = "-31337" - self.assertEqual(-31337, ujson.decode(input)) - - def test_encodeUnicode4BytesUTF8Fail(self): - _skip_if_python_ver(3) - input = "\xfd\xbf\xbf\xbf\xbf\xbf" - try: - enc = ujson.encode(input) # noqa - assert False, "Expected exception" - except OverflowError: - pass + assert long_input == json.loads(output) + assert output == json.dumps(long_input) + assert long_input == ujson.decode(output) - def test_encodeNullCharacter(self): - input = "31337 \x00 1337" - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - - input = "\x00" - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - - self.assertEqual('" \\u0000\\r\\n "', ujson.dumps(u(" \u0000\r\n "))) - pass - - def test_decodeNullCharacter(self): - input = "\"31337 \\u0000 31337\"" - self.assertEqual(ujson.decode(input), json.loads(input)) - - def test_encodeListLongConversion(self): - input = [9223372036854775807, 9223372036854775807, 9223372036854775807, - 9223372036854775807, 9223372036854775807, 9223372036854775807] - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(input, ujson.decode(output)) - tm.assert_numpy_array_equal(np.array(input), - ujson.decode(output, numpy=True, - dtype=np.int64)) - pass - - def test_encodeLongConversion(self): - input = 9223372036854775807 - output = ujson.encode(input) - self.assertEqual(input, json.loads(output)) - self.assertEqual(output, json.dumps(input)) - self.assertEqual(input, ujson.decode(output)) - pass - - def test_numericIntExp(self): - input = "1337E40" - output = ujson.decode(input) - self.assertEqual(output, json.loads(input)) - - def test_numericIntFrcExp(self): - input = "1.337E40" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpEPLUS(self): - input = "1337E+9" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpePLUS(self): - input = "1.337e+40" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpE(self): - input = "1337E40" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpe(self): - input = "1337e40" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpEMinus(self): - input = "1.337E-4" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_decodeNumericIntExpeMinus(self): - input = "1.337e-4" - output = ujson.decode(input) - self.assertAlmostEqual(output, json.loads(input)) - - def test_dumpToFile(self): + @pytest.mark.parametrize("int_exp", [ + "1337E40", "1.337E40", "1337E+9", "1.337e+40", "1.337E-4" + ]) + def test_decode_numeric_int_exp(self, int_exp): + assert ujson.decode(int_exp) == json.loads(int_exp) + + def test_dump_to_file(self): f = StringIO() ujson.dump([1, 2, 3], f) - self.assertEqual("[1,2,3]", f.getvalue()) + assert "[1,2,3]" == f.getvalue() - def test_dumpToFileLikeObject(self): - class filelike: + def test_dump_to_file_like(self): + class FileLike(object): def __init__(self): self.bytes = '' - def write(self, bytes): - self.bytes += bytes - f = filelike() + def write(self, data_bytes): + self.bytes += data_bytes + + f = FileLike() ujson.dump([1, 2, 3], f) - self.assertEqual("[1,2,3]", f.bytes) + assert "[1,2,3]" == f.bytes - def test_dumpFileArgsError(self): - try: - ujson.dump([], '') - except TypeError: - pass - else: - assert False, 'expected TypeError' + def test_dump_file_args_error(self): + with pytest.raises(TypeError): + ujson.dump([], "") - def test_loadFile(self): - f = StringIO("[1,2,3,4]") - self.assertEqual([1, 2, 3, 4], ujson.load(f)) - f = StringIO("[1,2,3,4]") - tm.assert_numpy_array_equal( - np.array([1, 2, 3, 4]), ujson.load(f, numpy=True)) + def test_load_file(self): + data = "[1,2,3,4]" + exp_data = [1, 2, 3, 4] - def test_loadFileLikeObject(self): - class filelike: + f = StringIO(data) + assert exp_data == ujson.load(f) + + f = StringIO(data) + tm.assert_numpy_array_equal(np.array(exp_data), + ujson.load(f, numpy=True)) + + def test_load_file_like(self): + class FileLike(object): def read(self): try: @@ -783,94 +615,75 @@ def read(self): except AttributeError: self.end = True return "[1,2,3,4]" - f = filelike() - self.assertEqual([1, 2, 3, 4], ujson.load(f)) - f = filelike() - tm.assert_numpy_array_equal( - np.array([1, 2, 3, 4]), ujson.load(f, numpy=True)) - def test_loadFileArgsError(self): - try: + exp_data = [1, 2, 3, 4] + + f = FileLike() + assert exp_data == ujson.load(f) + + f = FileLike() + tm.assert_numpy_array_equal(np.array(exp_data), + ujson.load(f, numpy=True)) + + def test_load_file_args_error(self): + with pytest.raises(TypeError): ujson.load("[]") - except TypeError: - pass - else: - assert False, "expected TypeError" def test_version(self): assert re.match(r'^\d+\.\d+(\.\d+)?$', ujson.__version__), \ "ujson.__version__ must be a string like '1.4.0'" - def test_encodeNumericOverflow(self): - try: + def test_encode_numeric_overflow(self): + with pytest.raises(OverflowError): ujson.encode(12839128391289382193812939) - except OverflowError: - pass - else: - assert False, "expected OverflowError" - def test_encodeNumericOverflowNested(self): - for n in range(0, 100): - class Nested: - x = 12839128391289382193812939 + def test_encode_numeric_overflow_nested(self): + class Nested(object): + x = 12839128391289382193812939 - nested = Nested() + for _ in range(0, 100): + with pytest.raises(OverflowError): + ujson.encode(Nested()) - try: - ujson.encode(nested) - except OverflowError: - pass - else: - assert False, "expected OverflowError" - - def test_decodeNumberWith32bitSignBit(self): + @pytest.mark.parametrize("val", [ + 3590016419, 2**31, 2**32, (2**32) - 1 + ]) + def test_decode_number_with_32bit_sign_bit(self, val): # Test that numbers that fit within 32 bits but would have the # sign bit set (2**31 <= x < 2**32) are decoded properly. - boundary1 = 2**31 # noqa - boundary2 = 2**32 # noqa - docs = ( - '{"id": 3590016419}', - '{"id": %s}' % 2**31, - '{"id": %s}' % 2**32, - '{"id": %s}' % ((2**32) - 1), - ) - results = (3590016419, 2**31, 2**32, 2**32 - 1) - for doc, result in zip(docs, results): - self.assertEqual(ujson.decode(doc)['id'], result) - - def test_encodeBigEscape(self): - for x in range(10): - if compat.PY3: - base = '\u00e5'.encode('utf-8') - else: - base = "\xc3\xa5" - input = base * 1024 * 1024 * 2 - output = ujson.encode(input) # noqa - - def test_decodeBigEscape(self): - for x in range(10): - if compat.PY3: - base = '\u00e5'.encode('utf-8') - else: - base = "\xc3\xa5" + doc = '{{"id": {val}}}'.format(val=val) + assert ujson.decode(doc)["id"] == val + + def test_encode_big_escape(self): + # Make sure no Exception is raised. + for _ in range(10): + base = '\u00e5'.encode("utf-8") if compat.PY3 else "\xc3\xa5" + escape_input = base * 1024 * 1024 * 2 + ujson.encode(escape_input) + + def test_decode_big_escape(self): + # Make sure no Exception is raised. + for _ in range(10): + base = '\u00e5'.encode("utf-8") if compat.PY3 else "\xc3\xa5" quote = compat.str_to_bytes("\"") - input = quote + (base * 1024 * 1024 * 2) + quote - output = ujson.decode(input) # noqa - def test_toDict(self): - d = {u("key"): 31337} + escape_input = quote + (base * 1024 * 1024 * 2) + quote + ujson.decode(escape_input) - class DictTest: + def test_to_dict(self): + d = {u("key"): 31337} + class DictTest(object): def toDict(self): return d o = DictTest() output = ujson.encode(o) + dec = ujson.decode(output) - self.assertEqual(dec, d) + assert dec == d - def test_defaultHandler(self): + def test_default_handler(self): class _TestObject(object): @@ -884,730 +697,433 @@ def recursive_attr(self): def __str__(self): return str(self.val) - self.assertRaises(OverflowError, ujson.encode, _TestObject("foo")) - self.assertEqual('"foo"', ujson.encode(_TestObject("foo"), - default_handler=str)) + msg = "Maximum recursion level reached" + with pytest.raises(OverflowError, match=msg): + ujson.encode(_TestObject("foo")) + assert '"foo"' == ujson.encode(_TestObject("foo"), + default_handler=str) - def my_handler(obj): + def my_handler(_): return "foobar" - self.assertEqual('"foobar"', ujson.encode(_TestObject("foo"), - default_handler=my_handler)) - def my_handler_raises(obj): + assert '"foobar"' == ujson.encode(_TestObject("foo"), + default_handler=my_handler) + + def my_handler_raises(_): raise TypeError("I raise for anything") - with tm.assertRaisesRegexp(TypeError, "I raise for anything"): + + with pytest.raises(TypeError, match="I raise for anything"): ujson.encode(_TestObject("foo"), default_handler=my_handler_raises) - def my_int_handler(obj): + def my_int_handler(_): return 42 - self.assertEqual( - 42, ujson.decode(ujson.encode(_TestObject("foo"), - default_handler=my_int_handler))) - - def my_obj_handler(obj): - return datetime.datetime(2013, 2, 3) - self.assertEqual( - ujson.decode(ujson.encode(datetime.datetime(2013, 2, 3))), - ujson.decode(ujson.encode(_TestObject("foo"), - default_handler=my_obj_handler))) - - l = [_TestObject("foo"), _TestObject("bar")] - self.assertEqual(json.loads(json.dumps(l, default=str)), - ujson.decode(ujson.encode(l, default_handler=str))) - -class NumpyJSONTests(TestCase): + assert ujson.decode(ujson.encode(_TestObject("foo"), + default_handler=my_int_handler)) == 42 - def testBool(self): - b = np.bool(True) - self.assertEqual(ujson.decode(ujson.encode(b)), b) - - def testBoolArray(self): - inpt = np.array([True, False, True, True, False, True, False, False], - dtype=np.bool) - outp = np.array(ujson.decode(ujson.encode(inpt)), dtype=np.bool) - tm.assert_numpy_array_equal(inpt, outp) - - def testInt(self): - num = np.int(2562010) - self.assertEqual(np.int(ujson.decode(ujson.encode(num))), num) + def my_obj_handler(_): + return datetime.datetime(2013, 2, 3) - num = np.int8(127) - self.assertEqual(np.int8(ujson.decode(ujson.encode(num))), num) + assert (ujson.decode(ujson.encode(datetime.datetime(2013, 2, 3))) == + ujson.decode(ujson.encode(_TestObject("foo"), + default_handler=my_obj_handler))) - num = np.int16(2562010) - self.assertEqual(np.int16(ujson.decode(ujson.encode(num))), num) + obj_list = [_TestObject("foo"), _TestObject("bar")] + assert (json.loads(json.dumps(obj_list, default=str)) == + ujson.decode(ujson.encode(obj_list, default_handler=str))) - num = np.int32(2562010) - self.assertEqual(np.int32(ujson.decode(ujson.encode(num))), num) - num = np.int64(2562010) - self.assertEqual(np.int64(ujson.decode(ujson.encode(num))), num) +class TestNumpyJSONTests(object): - num = np.uint8(255) - self.assertEqual(np.uint8(ujson.decode(ujson.encode(num))), num) + @pytest.mark.parametrize("bool_input", [True, False]) + def test_bool(self, bool_input): + b = np.bool(bool_input) + assert ujson.decode(ujson.encode(b)) == b - num = np.uint16(2562010) - self.assertEqual(np.uint16(ujson.decode(ujson.encode(num))), num) + def test_bool_array(self): + bool_array = np.array([ + True, False, True, True, + False, True, False, False], dtype=np.bool) + output = np.array(ujson.decode( + ujson.encode(bool_array)), dtype=np.bool) + tm.assert_numpy_array_equal(bool_array, output) - num = np.uint32(2562010) - self.assertEqual(np.uint32(ujson.decode(ujson.encode(num))), num) + def test_int(self, any_int_dtype): + klass = np.dtype(any_int_dtype).type + num = klass(1) - num = np.uint64(2562010) - self.assertEqual(np.uint64(ujson.decode(ujson.encode(num))), num) + assert klass(ujson.decode(ujson.encode(num))) == num - def testIntArray(self): + def test_int_array(self, any_int_dtype): arr = np.arange(100, dtype=np.int) - dtypes = (np.int, np.int8, np.int16, np.int32, np.int64, - np.uint, np.uint8, np.uint16, np.uint32, np.uint64) - for dtype in dtypes: - inpt = arr.astype(dtype) - outp = np.array(ujson.decode(ujson.encode(inpt)), dtype=dtype) - tm.assert_numpy_array_equal(inpt, outp) - - def testIntMax(self): - num = np.int(np.iinfo(np.int).max) - self.assertEqual(np.int(ujson.decode(ujson.encode(num))), num) - - num = np.int8(np.iinfo(np.int8).max) - self.assertEqual(np.int8(ujson.decode(ujson.encode(num))), num) - - num = np.int16(np.iinfo(np.int16).max) - self.assertEqual(np.int16(ujson.decode(ujson.encode(num))), num) - - num = np.int32(np.iinfo(np.int32).max) - self.assertEqual(np.int32(ujson.decode(ujson.encode(num))), num) + arr_input = arr.astype(any_int_dtype) - num = np.uint8(np.iinfo(np.uint8).max) - self.assertEqual(np.uint8(ujson.decode(ujson.encode(num))), num) + arr_output = np.array(ujson.decode(ujson.encode(arr_input)), + dtype=any_int_dtype) + tm.assert_numpy_array_equal(arr_input, arr_output) - num = np.uint16(np.iinfo(np.uint16).max) - self.assertEqual(np.uint16(ujson.decode(ujson.encode(num))), num) + def test_int_max(self, any_int_dtype): + if any_int_dtype in ("int64", "uint64") and compat.is_platform_32bit(): + pytest.skip("Cannot test 64-bit integer on 32-bit platform") - num = np.uint32(np.iinfo(np.uint32).max) - self.assertEqual(np.uint32(ujson.decode(ujson.encode(num))), num) + klass = np.dtype(any_int_dtype).type - if platform.architecture()[0] != '32bit': - num = np.int64(np.iinfo(np.int64).max) - self.assertEqual(np.int64(ujson.decode(ujson.encode(num))), num) - - # uint64 max will always overflow as it's encoded to signed - num = np.uint64(np.iinfo(np.int64).max) - self.assertEqual(np.uint64(ujson.decode(ujson.encode(num))), num) + # uint64 max will always overflow, + # as it's encoded to signed. + if any_int_dtype == "uint64": + num = np.iinfo("int64").max + else: + num = np.iinfo(any_int_dtype).max - def testFloat(self): - num = np.float(256.2013) - self.assertEqual(np.float(ujson.decode(ujson.encode(num))), num) + assert klass(ujson.decode(ujson.encode(num))) == num - num = np.float32(256.2013) - self.assertEqual(np.float32(ujson.decode(ujson.encode(num))), num) + def test_float(self, float_dtype): + klass = np.dtype(float_dtype).type + num = klass(256.2013) - num = np.float64(256.2013) - self.assertEqual(np.float64(ujson.decode(ujson.encode(num))), num) + assert klass(ujson.decode(ujson.encode(num))) == num - def testFloatArray(self): + def test_float_array(self, float_dtype): arr = np.arange(12.5, 185.72, 1.7322, dtype=np.float) - dtypes = (np.float, np.float32, np.float64) + float_input = arr.astype(float_dtype) - for dtype in dtypes: - inpt = arr.astype(dtype) - outp = np.array(ujson.decode(ujson.encode( - inpt, double_precision=15)), dtype=dtype) - tm.assert_almost_equal(inpt, outp) + float_output = np.array(ujson.decode( + ujson.encode(float_input, double_precision=15)), + dtype=float_dtype) + tm.assert_almost_equal(float_input, float_output) - def testFloatMax(self): - num = np.float(np.finfo(np.float).max / 10) - tm.assert_almost_equal(np.float(ujson.decode( - ujson.encode(num, double_precision=15))), num, 15) + def test_float_max(self, float_dtype): + klass = np.dtype(float_dtype).type + num = klass(np.finfo(float_dtype).max / 10) - num = np.float32(np.finfo(np.float32).max / 10) - tm.assert_almost_equal(np.float32(ujson.decode( - ujson.encode(num, double_precision=15))), num, 15) + tm.assert_almost_equal(klass(ujson.decode( + ujson.encode(num, double_precision=15))), num) - num = np.float64(np.finfo(np.float64).max / 10) - tm.assert_almost_equal(np.float64(ujson.decode( - ujson.encode(num, double_precision=15))), num, 15) - - def testArrays(self): - arr = np.arange(100) + def test_array_basic(self): + arr = np.arange(96) + arr = arr.reshape((2, 2, 2, 2, 3, 2)) - arr = arr.reshape((10, 10)) tm.assert_numpy_array_equal( np.array(ujson.decode(ujson.encode(arr))), arr) tm.assert_numpy_array_equal(ujson.decode( ujson.encode(arr), numpy=True), arr) - arr = arr.reshape((5, 5, 4)) - tm.assert_numpy_array_equal( - np.array(ujson.decode(ujson.encode(arr))), arr) - tm.assert_numpy_array_equal(ujson.decode( - ujson.encode(arr), numpy=True), arr) + @pytest.mark.parametrize("shape", [ + (10, 10), + (5, 5, 4), + (100, 1), + ]) + def test_array_reshaped(self, shape): + arr = np.arange(100) + arr = arr.reshape(shape) - arr = arr.reshape((100, 1)) tm.assert_numpy_array_equal( np.array(ujson.decode(ujson.encode(arr))), arr) tm.assert_numpy_array_equal(ujson.decode( ujson.encode(arr), numpy=True), arr) - arr = np.arange(96) - arr = arr.reshape((2, 2, 2, 2, 3, 2)) + def test_array_list(self): + arr_list = ["a", list(), dict(), dict(), list(), + 42, 97.8, ["a", "b"], {"key": "val"}] + arr = np.array(arr_list) tm.assert_numpy_array_equal( np.array(ujson.decode(ujson.encode(arr))), arr) - tm.assert_numpy_array_equal(ujson.decode( - ujson.encode(arr), numpy=True), arr) - l = ['a', list(), dict(), dict(), list(), - 42, 97.8, ['a', 'b'], {'key': 'val'}] - arr = np.array(l) - tm.assert_numpy_array_equal( - np.array(ujson.decode(ujson.encode(arr))), arr) + def test_array_float(self): + dtype = np.float32 - arr = np.arange(100.202, 200.202, 1, dtype=np.float32) + arr = np.arange(100.202, 200.202, 1, dtype=dtype) arr = arr.reshape((5, 5, 4)) - outp = np.array(ujson.decode(ujson.encode(arr)), dtype=np.float32) - tm.assert_almost_equal(arr, outp) - outp = ujson.decode(ujson.encode(arr), numpy=True, dtype=np.float32) - tm.assert_almost_equal(arr, outp) - def testOdArray(self): - def will_raise(): - ujson.encode(np.array(1)) + arr_out = np.array(ujson.decode(ujson.encode(arr)), dtype=dtype) + tm.assert_almost_equal(arr, arr_out) - self.assertRaises(TypeError, will_raise) + arr_out = ujson.decode(ujson.encode(arr), numpy=True, dtype=dtype) + tm.assert_almost_equal(arr, arr_out) - def testArrayNumpyExcept(self): + def test_0d_array(self): + with pytest.raises(TypeError): + ujson.encode(np.array(1)) - input = ujson.dumps([42, {}, 'a']) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(TypeError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps(['a', 'b', [], 'c']) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([['a'], 42]) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([42, ['a'], 42]) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([{}, []]) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([42, None]) - try: - ujson.decode(input, numpy=True) - assert False, "Expected exception!" - except(TypeError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([{'a': 'b'}]) - try: - ujson.decode(input, numpy=True, labelled=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps({'a': {'b': {'c': 42}}}) - try: - ujson.decode(input, numpy=True, labelled=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - input = ujson.dumps([{'a': 42, 'b': 23}, {'c': 17}]) - try: - ujson.decode(input, numpy=True, labelled=True) - assert False, "Expected exception!" - except(ValueError): - pass - except: - assert False, "Wrong exception" - - def testArrayNumpyLabelled(self): - input = {'a': []} - output = ujson.loads(ujson.dumps(input), numpy=True, labelled=True) - self.assertTrue((np.empty((1, 0)) == output[0]).all()) - self.assertTrue((np.array(['a']) == output[1]).all()) - self.assertTrue(output[2] is None) - - input = [{'a': 42}] - output = ujson.loads(ujson.dumps(input), numpy=True, labelled=True) - self.assertTrue((np.array([42]) == output[0]).all()) - self.assertTrue(output[1] is None) - self.assertTrue((np.array([u('a')]) == output[2]).all()) - - # Write out the dump explicitly so there is no dependency on iteration - # order GH10837 + @pytest.mark.parametrize("bad_input,exc_type,kwargs", [ + ([{}, []], ValueError, {}), + ([42, None], TypeError, {}), + ([["a"], 42], ValueError, {}), + ([42, {}, "a"], TypeError, {}), + ([42, ["a"], 42], ValueError, {}), + (["a", "b", [], "c"], ValueError, {}), + ([{"a": "b"}], ValueError, dict(labelled=True)), + ({"a": {"b": {"c": 42}}}, ValueError, dict(labelled=True)), + ([{"a": 42, "b": 23}, {"c": 17}], ValueError, dict(labelled=True)) + ]) + def test_array_numpy_except(self, bad_input, exc_type, kwargs): + with pytest.raises(exc_type): + ujson.decode(ujson.dumps(bad_input), numpy=True, **kwargs) + + def test_array_numpy_labelled(self): + labelled_input = {"a": []} + output = ujson.loads(ujson.dumps(labelled_input), + numpy=True, labelled=True) + assert (np.empty((1, 0)) == output[0]).all() + assert (np.array(["a"]) == output[1]).all() + assert output[2] is None + + labelled_input = [{"a": 42}] + output = ujson.loads(ujson.dumps(labelled_input), + numpy=True, labelled=True) + assert (np.array([u("a")]) == output[2]).all() + assert (np.array([42]) == output[0]).all() + assert output[1] is None + + # see gh-10837: write out the dump explicitly + # so there is no dependency on iteration order input_dumps = ('[{"a": 42, "b":31}, {"a": 24, "c": 99}, ' '{"a": 2.4, "b": 78}]') output = ujson.loads(input_dumps, numpy=True, labelled=True) - expectedvals = np.array( + expected_vals = np.array( [42, 31, 24, 99, 2.4, 78], dtype=int).reshape((3, 2)) - self.assertTrue((expectedvals == output[0]).all()) - self.assertTrue(output[1] is None) - self.assertTrue((np.array([u('a'), 'b']) == output[2]).all()) + assert (expected_vals == output[0]).all() + assert output[1] is None + assert (np.array([u("a"), "b"]) == output[2]).all() input_dumps = ('{"1": {"a": 42, "b":31}, "2": {"a": 24, "c": 99}, ' '"3": {"a": 2.4, "b": 78}}') output = ujson.loads(input_dumps, numpy=True, labelled=True) - expectedvals = np.array( + expected_vals = np.array( [42, 31, 24, 99, 2.4, 78], dtype=int).reshape((3, 2)) - self.assertTrue((expectedvals == output[0]).all()) - self.assertTrue((np.array(['1', '2', '3']) == output[1]).all()) - self.assertTrue((np.array(['a', 'b']) == output[2]).all()) + assert (expected_vals == output[0]).all() + assert (np.array(["1", "2", "3"]) == output[1]).all() + assert (np.array(["a", "b"]) == output[2]).all() -class PandasJSONTests(TestCase): +class TestPandasJSONTests(object): - def testDataFrame(self): - df = DataFrame([[1, 2, 3], [4, 5, 6]], index=[ - 'a', 'b'], columns=['x', 'y', 'z']) - - # column indexed - outp = DataFrame(ujson.decode(ujson.encode(df))) - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - tm.assert_index_equal(df.index, outp.index) - - dec = _clean_dict(ujson.decode(ujson.encode(df, orient="split"))) - outp = DataFrame(**dec) - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - tm.assert_index_equal(df.index, outp.index) - - outp = DataFrame(ujson.decode(ujson.encode(df, orient="records"))) - outp.index = df.index - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - - outp = DataFrame(ujson.decode(ujson.encode(df, orient="values"))) - outp.index = df.index - self.assertTrue((df.values == outp.values).all()) - - outp = DataFrame(ujson.decode(ujson.encode(df, orient="index"))) - self.assertTrue((df.transpose() == outp).values.all()) - tm.assert_index_equal(df.transpose().columns, outp.columns) - tm.assert_index_equal(df.transpose().index, outp.index) - - def testDataFrameNumpy(self): - df = DataFrame([[1, 2, 3], [4, 5, 6]], index=[ - 'a', 'b'], columns=['x', 'y', 'z']) + def test_dataframe(self, orient, numpy): + if orient == "records" and numpy: + pytest.skip("Not idiomatic pandas") - # column indexed - outp = DataFrame(ujson.decode(ujson.encode(df), numpy=True)) - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - tm.assert_index_equal(df.index, outp.index) - - dec = _clean_dict(ujson.decode(ujson.encode(df, orient="split"), - numpy=True)) - outp = DataFrame(**dec) - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - tm.assert_index_equal(df.index, outp.index) - - outp = DataFrame(ujson.decode(ujson.encode(df, orient="index"), - numpy=True)) - self.assertTrue((df.transpose() == outp).values.all()) - tm.assert_index_equal(df.transpose().columns, outp.columns) - tm.assert_index_equal(df.transpose().index, outp.index) - - def testDataFrameNested(self): df = DataFrame([[1, 2, 3], [4, 5, 6]], index=[ - 'a', 'b'], columns=['x', 'y', 'z']) - - nested = {'df1': df, 'df2': df.copy()} + "a", "b"], columns=["x", "y", "z"]) + encode_kwargs = {} if orient is None else dict(orient=orient) + decode_kwargs = {} if numpy is None else dict(numpy=numpy) - exp = {'df1': ujson.decode(ujson.encode(df)), - 'df2': ujson.decode(ujson.encode(df))} - self.assertTrue(ujson.decode(ujson.encode(nested)) == exp) + output = ujson.decode(ujson.encode(df, **encode_kwargs), + **decode_kwargs) - exp = {'df1': ujson.decode(ujson.encode(df, orient="index")), - 'df2': ujson.decode(ujson.encode(df, orient="index"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="index")) == exp) - - exp = {'df1': ujson.decode(ujson.encode(df, orient="records")), - 'df2': ujson.decode(ujson.encode(df, orient="records"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="records")) == exp) + # Ensure proper DataFrame initialization. + if orient == "split": + dec = _clean_dict(output) + output = DataFrame(**dec) + else: + output = DataFrame(output) - exp = {'df1': ujson.decode(ujson.encode(df, orient="values")), - 'df2': ujson.decode(ujson.encode(df, orient="values"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="values")) == exp) + # Corrections to enable DataFrame comparison. + if orient == "values": + df.columns = [0, 1, 2] + df.index = [0, 1] + elif orient == "records": + df.index = [0, 1] + elif orient == "index": + df = df.transpose() - exp = {'df1': ujson.decode(ujson.encode(df, orient="split")), - 'df2': ujson.decode(ujson.encode(df, orient="split"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="split")) == exp) + tm.assert_frame_equal(output, df, check_dtype=False) - def testDataFrameNumpyLabelled(self): + def test_dataframe_nested(self, orient): df = DataFrame([[1, 2, 3], [4, 5, 6]], index=[ - 'a', 'b'], columns=['x', 'y', 'z']) - - # column indexed - outp = DataFrame(*ujson.decode(ujson.encode(df), - numpy=True, labelled=True)) - self.assertTrue((df.T == outp).values.all()) - tm.assert_index_equal(df.T.columns, outp.columns) - tm.assert_index_equal(df.T.index, outp.index) - - outp = DataFrame(*ujson.decode(ujson.encode(df, orient="records"), - numpy=True, labelled=True)) - outp.index = df.index - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - - outp = DataFrame(*ujson.decode(ujson.encode(df, orient="index"), - numpy=True, labelled=True)) - self.assertTrue((df == outp).values.all()) - tm.assert_index_equal(df.columns, outp.columns) - tm.assert_index_equal(df.index, outp.index) - - def testSeries(self): - s = Series([10, 20, 30, 40, 50, 60], name="series", - index=[6, 7, 8, 9, 10, 15]).sort_values() - - # column indexed - outp = Series(ujson.decode(ujson.encode(s))).sort_values() - exp = Series([10, 20, 30, 40, 50, 60], - index=['6', '7', '8', '9', '10', '15']) - tm.assert_series_equal(outp, exp) - - outp = Series(ujson.decode(ujson.encode(s), numpy=True)).sort_values() - tm.assert_series_equal(outp, exp) + "a", "b"], columns=["x", "y", "z"]) - dec = _clean_dict(ujson.decode(ujson.encode(s, orient="split"))) - outp = Series(**dec) - tm.assert_series_equal(outp, s) + nested = {"df1": df, "df2": df.copy()} + kwargs = {} if orient is None else dict(orient=orient) - dec = _clean_dict(ujson.decode(ujson.encode(s, orient="split"), - numpy=True)) - outp = Series(**dec) - - exp_np = Series(np.array([10, 20, 30, 40, 50, 60])) - exp_pd = Series([10, 20, 30, 40, 50, 60]) - outp = Series(ujson.decode(ujson.encode(s, orient="records"), - numpy=True)) - tm.assert_series_equal(outp, exp_np) + exp = {"df1": ujson.decode(ujson.encode(df, **kwargs)), + "df2": ujson.decode(ujson.encode(df, **kwargs))} + assert ujson.decode(ujson.encode(nested, **kwargs)) == exp - outp = Series(ujson.decode(ujson.encode(s, orient="records"))) - exp = Series([10, 20, 30, 40, 50, 60]) - tm.assert_series_equal(outp, exp_pd) + def test_dataframe_numpy_labelled(self, orient): + if orient in ("split", "values"): + pytest.skip("Incompatible with labelled=True") - outp = Series(ujson.decode(ujson.encode(s, orient="values"), - numpy=True)) - tm.assert_series_equal(outp, exp_np) + df = DataFrame([[1, 2, 3], [4, 5, 6]], index=[ + "a", "b"], columns=["x", "y", "z"], dtype=np.int) + kwargs = {} if orient is None else dict(orient=orient) - outp = Series(ujson.decode(ujson.encode(s, orient="values"))) - tm.assert_series_equal(outp, exp_pd) + output = DataFrame(*ujson.decode(ujson.encode(df, **kwargs), + numpy=True, labelled=True)) - outp = Series(ujson.decode(ujson.encode( - s, orient="index"))).sort_values() - exp = Series([10, 20, 30, 40, 50, 60], - index=['6', '7', '8', '9', '10', '15']) - tm.assert_series_equal(outp, exp) + if orient is None: + df = df.T + elif orient == "records": + df.index = [0, 1] - outp = Series(ujson.decode(ujson.encode( - s, orient="index"), numpy=True)).sort_values() - tm.assert_series_equal(outp, exp) + tm.assert_frame_equal(output, df) - def testSeriesNested(self): + def test_series(self, orient, numpy): s = Series([10, 20, 30, 40, 50, 60], name="series", index=[6, 7, 8, 9, 10, 15]).sort_values() - nested = {'s1': s, 's2': s.copy()} + encode_kwargs = {} if orient is None else dict(orient=orient) + decode_kwargs = {} if numpy is None else dict(numpy=numpy) - exp = {'s1': ujson.decode(ujson.encode(s)), - 's2': ujson.decode(ujson.encode(s))} - self.assertTrue(ujson.decode(ujson.encode(nested)) == exp) + output = ujson.decode(ujson.encode(s, **encode_kwargs), + **decode_kwargs) - exp = {'s1': ujson.decode(ujson.encode(s, orient="split")), - 's2': ujson.decode(ujson.encode(s, orient="split"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="split")) == exp) + if orient == "split": + dec = _clean_dict(output) + output = Series(**dec) + else: + output = Series(output) - exp = {'s1': ujson.decode(ujson.encode(s, orient="records")), - 's2': ujson.decode(ujson.encode(s, orient="records"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="records")) == exp) + if orient in (None, "index"): + s.name = None + output = output.sort_values() + s.index = ["6", "7", "8", "9", "10", "15"] + elif orient in ("records", "values"): + s.name = None + s.index = [0, 1, 2, 3, 4, 5] - exp = {'s1': ujson.decode(ujson.encode(s, orient="values")), - 's2': ujson.decode(ujson.encode(s, orient="values"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="values")) == exp) + tm.assert_series_equal(output, s, check_dtype=False) + + def test_series_nested(self, orient): + s = Series([10, 20, 30, 40, 50, 60], name="series", + index=[6, 7, 8, 9, 10, 15]).sort_values() + nested = {"s1": s, "s2": s.copy()} + kwargs = {} if orient is None else dict(orient=orient) - exp = {'s1': ujson.decode(ujson.encode(s, orient="index")), - 's2': ujson.decode(ujson.encode(s, orient="index"))} - self.assertTrue(ujson.decode( - ujson.encode(nested, orient="index")) == exp) + exp = {"s1": ujson.decode(ujson.encode(s, **kwargs)), + "s2": ujson.decode(ujson.encode(s, **kwargs))} + assert ujson.decode(ujson.encode(nested, **kwargs)) == exp - def testIndex(self): + def test_index(self): i = Index([23, 45, 18, 98, 43, 11], name="index") - # column indexed - outp = Index(ujson.decode(ujson.encode(i)), name='index') - tm.assert_index_equal(i, outp) + # Column indexed. + output = Index(ujson.decode(ujson.encode(i)), name="index") + tm.assert_index_equal(i, output) - outp = Index(ujson.decode(ujson.encode(i), numpy=True), name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i), numpy=True), name="index") + tm.assert_index_equal(i, output) dec = _clean_dict(ujson.decode(ujson.encode(i, orient="split"))) - outp = Index(**dec) - tm.assert_index_equal(i, outp) - self.assertTrue(i.name == outp.name) + output = Index(**dec) + + tm.assert_index_equal(i, output) + assert i.name == output.name dec = _clean_dict(ujson.decode(ujson.encode(i, orient="split"), numpy=True)) - outp = Index(**dec) - tm.assert_index_equal(i, outp) - self.assertTrue(i.name == outp.name) + output = Index(**dec) - outp = Index(ujson.decode(ujson.encode(i, orient="values")), - name='index') - tm.assert_index_equal(i, outp) + tm.assert_index_equal(i, output) + assert i.name == output.name - outp = Index(ujson.decode(ujson.encode(i, orient="values"), - numpy=True), name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i, orient="values")), + name="index") + tm.assert_index_equal(i, output) - outp = Index(ujson.decode(ujson.encode(i, orient="records")), - name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i, orient="values"), + numpy=True), name="index") + tm.assert_index_equal(i, output) - outp = Index(ujson.decode(ujson.encode(i, orient="records"), - numpy=True), name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i, orient="records")), + name="index") + tm.assert_index_equal(i, output) - outp = Index(ujson.decode(ujson.encode(i, orient="index")), - name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i, orient="records"), + numpy=True), name="index") + tm.assert_index_equal(i, output) - outp = Index(ujson.decode(ujson.encode(i, orient="index"), - numpy=True), name='index') - tm.assert_index_equal(i, outp) + output = Index(ujson.decode(ujson.encode(i, orient="index")), + name="index") + tm.assert_index_equal(i, output) - def test_datetimeindex(self): - from pandas.tseries.index import date_range + output = Index(ujson.decode(ujson.encode(i, orient="index"), + numpy=True), name="index") + tm.assert_index_equal(i, output) - rng = date_range('1/1/2000', periods=20) + def test_datetime_index(self): + date_unit = "ns" - encoded = ujson.encode(rng, date_unit='ns') - decoded = DatetimeIndex(np.array(ujson.decode(encoded))) + rng = date_range("1/1/2000", periods=20) + encoded = ujson.encode(rng, date_unit=date_unit) + decoded = DatetimeIndex(np.array(ujson.decode(encoded))) tm.assert_index_equal(rng, decoded) ts = Series(np.random.randn(len(rng)), index=rng) - decoded = Series(ujson.decode(ujson.encode(ts, date_unit='ns'))) + decoded = Series(ujson.decode(ujson.encode(ts, date_unit=date_unit))) + idx_values = decoded.index.values.astype(np.int64) decoded.index = DatetimeIndex(idx_values) tm.assert_series_equal(ts, decoded) - def test_decodeArrayTrailingCommaFail(self): - input = "[31337,]" - try: - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayLeadingCommaFail(self): - input = "[,31337]" - try: - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayOnlyCommaFail(self): - input = "[,]" - try: - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayUnmatchedBracketFail(self): - input = "[]]" - try: - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayEmpty(self): - input = "[]" - ujson.decode(input) - - def test_decodeArrayOneItem(self): - input = "[31337]" - ujson.decode(input) - - def test_decodeBigValue(self): - input = "9223372036854775807" - ujson.decode(input) - - def test_decodeSmallValue(self): - input = "-9223372036854775808" - ujson.decode(input) - - def test_decodeTooBigValue(self): - try: - input = "9223372036854775808" - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeTooSmallValue(self): - try: - input = "-90223372036854775809" - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeVeryTooBigValue(self): - try: - input = "9223372036854775808" - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeVeryTooSmallValue(self): - try: - input = "-90223372036854775809" - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeWithTrailingWhitespaces(self): - input = "{}\n\t " - ujson.decode(input) - - def test_decodeWithTrailingNonWhitespaces(self): - try: - input = "{}\n\t a" - ujson.decode(input) - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayWithBigInt(self): - try: - ujson.loads('[18446098363113800555]') - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeArrayFaultyUnicode(self): - try: - ujson.loads('[18446098363113800555]') - except ValueError: - pass - else: - assert False, "expected ValueError" - - def test_decodeFloatingPointAdditionalTests(self): - places = 15 - - self.assertAlmostEqual(-1.1234567893, - ujson.loads("-1.1234567893"), places=places) - self.assertAlmostEqual(-1.234567893, - ujson.loads("-1.234567893"), places=places) - self.assertAlmostEqual(-1.34567893, - ujson.loads("-1.34567893"), places=places) - self.assertAlmostEqual(-1.4567893, - ujson.loads("-1.4567893"), places=places) - self.assertAlmostEqual(-1.567893, - ujson.loads("-1.567893"), places=places) - self.assertAlmostEqual(-1.67893, - ujson.loads("-1.67893"), places=places) - self.assertAlmostEqual(-1.7893, ujson.loads("-1.7893"), places=places) - self.assertAlmostEqual(-1.893, ujson.loads("-1.893"), places=places) - self.assertAlmostEqual(-1.3, ujson.loads("-1.3"), places=places) - - self.assertAlmostEqual(1.1234567893, ujson.loads( - "1.1234567893"), places=places) - self.assertAlmostEqual(1.234567893, ujson.loads( - "1.234567893"), places=places) - self.assertAlmostEqual( - 1.34567893, ujson.loads("1.34567893"), places=places) - self.assertAlmostEqual( - 1.4567893, ujson.loads("1.4567893"), places=places) - self.assertAlmostEqual( - 1.567893, ujson.loads("1.567893"), places=places) - self.assertAlmostEqual(1.67893, ujson.loads("1.67893"), places=places) - self.assertAlmostEqual(1.7893, ujson.loads("1.7893"), places=places) - self.assertAlmostEqual(1.893, ujson.loads("1.893"), places=places) - self.assertAlmostEqual(1.3, ujson.loads("1.3"), places=places) - - def test_encodeBigSet(self): + @pytest.mark.parametrize("invalid_arr", [ + "[31337,]", # Trailing comma. + "[,31337]", # Leading comma. + "[]]", # Unmatched bracket. + "[,]", # Only comma. + ]) + def test_decode_invalid_array(self, invalid_arr): + with pytest.raises(ValueError): + ujson.decode(invalid_arr) + + @pytest.mark.parametrize("arr", [ + [], [31337] + ]) + def test_decode_array(self, arr): + assert arr == ujson.decode(str(arr)) + + @pytest.mark.parametrize("extreme_num", [ + 9223372036854775807, -9223372036854775808 + ]) + def test_decode_extreme_numbers(self, extreme_num): + assert extreme_num == ujson.decode(str(extreme_num)) + + @pytest.mark.parametrize("too_extreme_num", [ + "9223372036854775808", "-90223372036854775809" + ]) + def test_decode_too_extreme_numbers(self, too_extreme_num): + with pytest.raises(ValueError): + ujson.decode(too_extreme_num) + + def test_decode_with_trailing_whitespaces(self): + assert {} == ujson.decode("{}\n\t ") + + def test_decode_with_trailing_non_whitespaces(self): + with pytest.raises(ValueError): + ujson.decode("{}\n\t a") + + def test_decode_array_with_big_int(self): + with pytest.raises(ValueError): + ujson.loads("[18446098363113800555]") + + @pytest.mark.parametrize("float_number", [ + 1.1234567893, 1.234567893, 1.34567893, + 1.4567893, 1.567893, 1.67893, + 1.7893, 1.893, 1.3, + ]) + @pytest.mark.parametrize("sign", [-1, 1]) + def test_decode_floating_point(self, sign, float_number): + float_number *= sign + tm.assert_almost_equal(float_number, + ujson.loads(str(float_number)), + check_less_precise=15) + + def test_encode_big_set(self): s = set() + for x in range(0, 100000): s.add(x) + + # Make sure no Exception is raised. ujson.encode(s) - def test_encodeEmptySet(self): - s = set() - self.assertEqual("[]", ujson.encode(s)) + def test_encode_empty_set(self): + assert "[]" == ujson.encode(set()) - def test_encodeSet(self): - s = set([1, 2, 3, 4, 5, 6, 7, 8, 9]) + def test_encode_set(self): + s = {1, 2, 3, 4, 5, 6, 7, 8, 9} enc = ujson.encode(s) dec = ujson.decode(enc) for v in dec: - self.assertTrue(v in s) - - -def _clean_dict(d): - return dict((str(k), v) for k, v in compat.iteritems(d)) + assert v in s diff --git a/pandas/tests/io/msgpack/common.py b/pandas/tests/io/msgpack/common.py new file mode 100644 index 0000000000000..434d347c5742a --- /dev/null +++ b/pandas/tests/io/msgpack/common.py @@ -0,0 +1,9 @@ +from pandas.compat import PY3 + +# array compat +if PY3: + frombytes = lambda obj, data: obj.frombytes(data) + tobytes = lambda obj: obj.tobytes() +else: + frombytes = lambda obj, data: obj.fromstring(data) + tobytes = lambda obj: obj.tostring() diff --git a/pandas/tests/io/msgpack/data/frame.mp b/pandas/tests/io/msgpack/data/frame.mp new file mode 100644 index 0000000000000000000000000000000000000000..21e20d262b26c1a4835bdb4c00109a371e7e46f1 GIT binary patch literal 309 zcmYk2O%8%E5Jo9yGVV3T#H|+~Bc1pIl%`V+>`BSsB9Di(QO7A<_Z!tkRhFlHfXnkW7Y@kqOFZ^iNWKsK=wu;N|_+U!*!fyhnO_?A!CdXU{q@e~9VdSucd1w*T|Td;pDWe-{7% literal 0 HcmV?d00001 diff --git a/pandas/tests/io/msgpack/test_buffer.py b/pandas/tests/io/msgpack/test_buffer.py index 5a2dc3dba5dfa..e36dc5bbdb4ba 100644 --- a/pandas/tests/io/msgpack/test_buffer.py +++ b/pandas/tests/io/msgpack/test_buffer.py @@ -2,11 +2,13 @@ from pandas.io.msgpack import packb, unpackb +from .common import frombytes + def test_unpack_buffer(): from array import array buf = array('b') - buf.fromstring(packb((b'foo', b'bar'))) + frombytes(buf, packb((b'foo', b'bar'))) obj = unpackb(buf, use_list=1) assert [b'foo', b'bar'] == obj diff --git a/pandas/tests/io/msgpack/test_case.py b/pandas/tests/io/msgpack/test_case.py index 3927693a94dd8..c0e76b37ee46d 100644 --- a/pandas/tests/io/msgpack/test_case.py +++ b/pandas/tests/io/msgpack/test_case.py @@ -98,10 +98,10 @@ def test_match(): (tuple(range(16)), (b"\xdc\x00\x10\x00\x01\x02\x03\x04\x05\x06\x07" b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f")), ({}, b'\x80'), - (dict([(x, x) for x in range(15)]), + ({x: x for x in range(15)}, (b'\x8f\x00\x00\x01\x01\x02\x02\x03\x03\x04\x04\x05\x05\x06\x06\x07' b'\x07\x08\x08\t\t\n\n\x0b\x0b\x0c\x0c\r\r\x0e\x0e')), - (dict([(x, x) for x in range(16)]), + ({x: x for x in range(16)}, (b'\xde\x00\x10\x00\x00\x01\x01\x02\x02\x03\x03\x04\x04\x05\x05\x06' b'\x06\x07\x07\x08\x08\t\t\n\n\x0b\x0b\x0c\x0c\r\r\x0e\x0e' b'\x0f\x0f')), diff --git a/pandas/tests/io/msgpack/test_except.py b/pandas/tests/io/msgpack/test_except.py index 4bcef3607bfa4..cd894109e989f 100644 --- a/pandas/tests/io/msgpack/test_except.py +++ b/pandas/tests/io/msgpack/test_except.py @@ -1,6 +1,9 @@ # coding: utf-8 -import unittest +from datetime import datetime + +import pytest + from pandas.io.msgpack import packb, unpackb @@ -8,26 +11,29 @@ class DummyException(Exception): pass -class TestExceptions(unittest.TestCase): +class TestExceptions(object): def test_raise_on_find_unsupported_value(self): - import datetime - self.assertRaises(TypeError, packb, datetime.datetime.now()) + msg = "can\'t serialize datetime" + with pytest.raises(TypeError, match=msg): + packb(datetime.now()) def test_raise_from_object_hook(self): - def hook(obj): - raise DummyException - - self.assertRaises(DummyException, unpackb, packb({}), object_hook=hook) - self.assertRaises(DummyException, unpackb, packb({'fizz': 'buzz'}), - object_hook=hook) - self.assertRaises(DummyException, unpackb, packb({'fizz': 'buzz'}), - object_pairs_hook=hook) - self.assertRaises(DummyException, unpackb, - packb({'fizz': {'buzz': 'spam'}}), object_hook=hook) - self.assertRaises(DummyException, unpackb, - packb({'fizz': {'buzz': 'spam'}}), - object_pairs_hook=hook) - - def test_invalidvalue(self): - self.assertRaises(ValueError, unpackb, b'\xd9\x97#DL_') + def hook(_): + raise DummyException() + + with pytest.raises(DummyException): + unpackb(packb({}), object_hook=hook) + with pytest.raises(DummyException): + unpackb(packb({'fizz': 'buzz'}), object_hook=hook) + with pytest.raises(DummyException): + unpackb(packb({'fizz': 'buzz'}), object_pairs_hook=hook) + with pytest.raises(DummyException): + unpackb(packb({'fizz': {'buzz': 'spam'}}), object_hook=hook) + with pytest.raises(DummyException): + unpackb(packb({'fizz': {'buzz': 'spam'}}), object_pairs_hook=hook) + + def test_invalid_value(self): + msg = "Unpack failed: error" + with pytest.raises(ValueError, match=msg): + unpackb(b"\xd9\x97#DL_") diff --git a/pandas/tests/io/msgpack/test_extension.py b/pandas/tests/io/msgpack/test_extension.py index a5a111efbb835..06a0691bf4f7e 100644 --- a/pandas/tests/io/msgpack/test_extension.py +++ b/pandas/tests/io/msgpack/test_extension.py @@ -1,8 +1,12 @@ from __future__ import print_function + import array + import pandas.io.msgpack as msgpack from pandas.io.msgpack import ExtType +from .common import frombytes, tobytes + def test_pack_ext_type(): def p(s): @@ -42,15 +46,15 @@ def default(obj): print('default called', obj) if isinstance(obj, array.array): typecode = 123 # application specific typecode - data = obj.tostring() + data = tobytes(obj) return ExtType(typecode, data) - raise TypeError("Unknwon type object %r" % (obj, )) + raise TypeError("Unknown type object %r" % (obj, )) def ext_hook(code, data): print('ext_hook called', code, data) assert code == 123 obj = array.array('d') - obj.fromstring(data) + frombytes(obj, data) return obj obj = [42, b'hello', array.array('d', [1.1, 2.2, 3.3])] diff --git a/pandas/tests/io/msgpack/test_limits.py b/pandas/tests/io/msgpack/test_limits.py index a908ee3547634..dd8dc8da607a4 100644 --- a/pandas/tests/io/msgpack/test_limits.py +++ b/pandas/tests/io/msgpack/test_limits.py @@ -1,32 +1,37 @@ # coding: utf-8 -from __future__ import (absolute_import, division, print_function, - unicode_literals) -import pandas.util.testing as tm +from __future__ import ( + absolute_import, division, print_function, unicode_literals) -from pandas.io.msgpack import packb, unpackb, Packer, Unpacker, ExtType +import pytest +from pandas.io.msgpack import ExtType, Packer, Unpacker, packb, unpackb -class TestLimits(tm.TestCase): + +class TestLimits(object): def test_integer(self): x = -(2 ** 63) assert unpackb(packb(x)) == x - self.assertRaises((OverflowError, ValueError), packb, x - 1) + msg = (r"((long |Python )?(int )?too (big|large) to convert" + r"( to C (unsigned )?long))?") + with pytest.raises((OverflowError, ValueError), match=msg): + packb(x - 1) x = 2 ** 64 - 1 assert unpackb(packb(x)) == x - self.assertRaises((OverflowError, ValueError), packb, x + 1) + with pytest.raises((OverflowError, ValueError), match=msg): + packb(x + 1) def test_array_header(self): packer = Packer() packer.pack_array_header(2 ** 32 - 1) - self.assertRaises((OverflowError, ValueError), - packer.pack_array_header, 2 ** 32) + with pytest.raises((OverflowError, ValueError)): + packer.pack_array_header(2 ** 32) def test_map_header(self): packer = Packer() packer.pack_map_header(2 ** 32 - 1) - self.assertRaises((OverflowError, ValueError), - packer.pack_array_header, 2 ** 32) + with pytest.raises((OverflowError, ValueError)): + packer.pack_array_header(2 ** 32) def test_max_str_len(self): d = 'x' * 3 @@ -38,7 +43,10 @@ def test_max_str_len(self): unpacker = Unpacker(max_str_len=2, encoding='utf-8') unpacker.feed(packed) - self.assertRaises(ValueError, unpacker.unpack) + + msg = "3 exceeds max_str_len" + with pytest.raises(ValueError, match=msg): + unpacker.unpack() def test_max_bin_len(self): d = b'x' * 3 @@ -50,7 +58,10 @@ def test_max_bin_len(self): unpacker = Unpacker(max_bin_len=2) unpacker.feed(packed) - self.assertRaises(ValueError, unpacker.unpack) + + msg = "3 exceeds max_bin_len" + with pytest.raises(ValueError, match=msg): + unpacker.unpack() def test_max_array_len(self): d = [1, 2, 3] @@ -62,7 +73,10 @@ def test_max_array_len(self): unpacker = Unpacker(max_array_len=2) unpacker.feed(packed) - self.assertRaises(ValueError, unpacker.unpack) + + msg = "3 exceeds max_array_len" + with pytest.raises(ValueError, match=msg): + unpacker.unpack() def test_max_map_len(self): d = {1: 2, 3: 4, 5: 6} @@ -74,7 +88,10 @@ def test_max_map_len(self): unpacker = Unpacker(max_map_len=2) unpacker.feed(packed) - self.assertRaises(ValueError, unpacker.unpack) + + msg = "3 exceeds max_map_len" + with pytest.raises(ValueError, match=msg): + unpacker.unpack() def test_max_ext_len(self): d = ExtType(42, b"abc") @@ -86,4 +103,7 @@ def test_max_ext_len(self): unpacker = Unpacker(max_ext_len=2) unpacker.feed(packed) - self.assertRaises(ValueError, unpacker.unpack) + + msg = "4 exceeds max_ext_len" + with pytest.raises(ValueError, match=msg): + unpacker.unpack() diff --git a/pandas/tests/io/msgpack/test_newspec.py b/pandas/tests/io/msgpack/test_newspec.py index 783bfc1b364f8..d92c649c5e1ca 100644 --- a/pandas/tests/io/msgpack/test_newspec.py +++ b/pandas/tests/io/msgpack/test_newspec.py @@ -1,6 +1,6 @@ # coding: utf-8 -from pandas.io.msgpack import packb, unpackb, ExtType +from pandas.io.msgpack import ExtType, packb, unpackb def test_str8(): diff --git a/pandas/tests/io/msgpack/test_obj.py b/pandas/tests/io/msgpack/test_obj.py index b067dacb84494..471212f1bfe32 100644 --- a/pandas/tests/io/msgpack/test_obj.py +++ b/pandas/tests/io/msgpack/test_obj.py @@ -1,6 +1,7 @@ # coding: utf-8 -import unittest +import pytest + from pandas.io.msgpack import packb, unpackb @@ -8,7 +9,7 @@ class DecodeError(Exception): pass -class TestObj(unittest.TestCase): +class TestObj(object): def _arr_to_str(self, arr): return ''.join(str(c) for c in arr) @@ -46,31 +47,28 @@ def test_decode_pairs_hook(self): assert unpacked[1] == prod_sum def test_only_one_obj_hook(self): - self.assertRaises(TypeError, unpackb, b'', object_hook=lambda x: x, - object_pairs_hook=lambda x: x) + msg = "object_pairs_hook and object_hook are mutually exclusive" + with pytest.raises(TypeError, match=msg): + unpackb(b'', object_hook=lambda x: x, + object_pairs_hook=lambda x: x) def test_bad_hook(self): - def f(): + msg = r"can't serialize \(1\+2j\)" + with pytest.raises(TypeError, match=msg): packed = packb([3, 1 + 2j], default=lambda o: o) unpacked = unpackb(packed, use_list=1) # noqa - self.assertRaises(TypeError, f) - def test_array_hook(self): packed = packb([1, 2, 3]) unpacked = unpackb(packed, list_hook=self._arr_to_str, use_list=1) assert unpacked == '123' def test_an_exception_in_objecthook1(self): - def f(): + with pytest.raises(DecodeError, match='Ooops!'): packed = packb({1: {'__complex__': True, 'real': 1, 'imag': 2}}) unpackb(packed, object_hook=self.bad_complex_decoder) - self.assertRaises(DecodeError, f) - def test_an_exception_in_objecthook2(self): - def f(): + with pytest.raises(DecodeError, match='Ooops!'): packed = packb({1: [{'__complex__': True, 'real': 1, 'imag': 2}]}) unpackb(packed, list_hook=self.bad_complex_decoder, use_list=1) - - self.assertRaises(DecodeError, f) diff --git a/pandas/tests/io/msgpack/test_pack.py b/pandas/tests/io/msgpack/test_pack.py index 6f9a271cbd326..078d9f4ceb649 100644 --- a/pandas/tests/io/msgpack/test_pack.py +++ b/pandas/tests/io/msgpack/test_pack.py @@ -1,14 +1,17 @@ # coding: utf-8 +from collections import OrderedDict +import struct -import unittest +import pytest + +from pandas.compat import u -import struct from pandas import compat -from pandas.compat import u, OrderedDict -from pandas.io.msgpack import packb, unpackb, Unpacker, Packer + +from pandas.io.msgpack import Packer, Unpacker, packb, unpackb -class TestPack(unittest.TestCase): +class TestPack(object): def check(self, data, use_list=False): re = unpackb(packb(data), use_list=use_list) @@ -64,12 +67,17 @@ def testIgnoreUnicodeErrors(self): assert re == "abcdef" def testStrictUnicodeUnpack(self): - self.assertRaises(UnicodeDecodeError, unpackb, packb(b'abc\xeddef'), - encoding='utf-8', use_list=1) + msg = (r"'utf-*8' codec can't decode byte 0xed in position 3:" + " invalid continuation byte") + with pytest.raises(UnicodeDecodeError, match=msg): + unpackb(packb(b'abc\xeddef'), encoding='utf-8', use_list=1) def testStrictUnicodePack(self): - self.assertRaises(UnicodeEncodeError, packb, compat.u("abc\xeddef"), - encoding='ascii', unicode_errors='strict') + msg = (r"'ascii' codec can't encode character u*'\\xed' in position 3:" + r" ordinal not in range\(128\)") + with pytest.raises(UnicodeEncodeError, match=msg): + packb(compat.u("abc\xeddef"), encoding='ascii', + unicode_errors='strict') def testIgnoreErrorsPack(self): re = unpackb( @@ -79,7 +87,9 @@ def testIgnoreErrorsPack(self): assert re == compat.u("abcdef") def testNoEncoding(self): - self.assertRaises(TypeError, packb, compat.u("abc"), encoding=None) + msg = "Can't encode unicode string: no encoding is specified" + with pytest.raises(TypeError, match=msg): + packb(compat.u("abc"), encoding=None) def testDecodeBinary(self): re = unpackb(packb("abc"), encoding=None, use_list=1) @@ -131,7 +141,7 @@ def testMapSize(self, sizes=[0, 5, 50, 1000]): bio.seek(0) unpacker = Unpacker(bio) for size in sizes: - assert unpacker.unpack() == dict((i, i * 2) for i in range(size)) + assert unpacker.unpack() == {i: i * 2 for i in range(size)} def test_odict(self): seq = [(b'one', 1), (b'two', 2), (b'three', 3), (b'four', 4)] diff --git a/pandas/tests/io/msgpack/test_read_size.py b/pandas/tests/io/msgpack/test_read_size.py index ef521fa345637..42791b571e8e7 100644 --- a/pandas/tests/io/msgpack/test_read_size.py +++ b/pandas/tests/io/msgpack/test_read_size.py @@ -1,5 +1,6 @@ """Test Unpacker's read_array_header and read_map_header methods""" -from pandas.io.msgpack import packb, Unpacker, OutOfData +from pandas.io.msgpack import OutOfData, Unpacker, packb + UnexpectedTypeException = ValueError diff --git a/pandas/tests/io/msgpack/test_seq.py b/pandas/tests/io/msgpack/test_seq.py index 5f203e8997ccb..68be8c2d975aa 100644 --- a/pandas/tests/io/msgpack/test_seq.py +++ b/pandas/tests/io/msgpack/test_seq.py @@ -1,6 +1,7 @@ # coding: utf-8 import io + import pandas.io.msgpack as msgpack binarydata = bytes(bytearray(range(256))) @@ -25,7 +26,7 @@ def test_exceeding_unpacker_read_size(): # double free or corruption (!prev) # 40 ok for read_size=1024, while 50 introduces errors - # 7000 ok for read_size=1024*1024, while 8000 leads to glibc detected *** + # 7000 ok for read_size=1024*1024, while 8000 leads to glibc detected *** # python: double free or corruption (!prev): for idx in range(NUMBER_OF_STRINGS): diff --git a/pandas/tests/io/msgpack/test_sequnpack.py b/pandas/tests/io/msgpack/test_sequnpack.py index c9c979c4e0e44..91f5778a7ce6c 100644 --- a/pandas/tests/io/msgpack/test_sequnpack.py +++ b/pandas/tests/io/msgpack/test_sequnpack.py @@ -1,28 +1,25 @@ # coding: utf-8 -import unittest +import pytest from pandas import compat -from pandas.io.msgpack import Unpacker, BufferFull -from pandas.io.msgpack import OutOfData +from pandas.io.msgpack import BufferFull, OutOfData, Unpacker -class TestPack(unittest.TestCase): - def test_partialdata(self): +class TestPack(object): + + def test_partial_data(self): unpacker = Unpacker() - unpacker.feed(b'\xa5') - self.assertRaises(StopIteration, next, iter(unpacker)) - unpacker.feed(b'h') - self.assertRaises(StopIteration, next, iter(unpacker)) - unpacker.feed(b'a') - self.assertRaises(StopIteration, next, iter(unpacker)) - unpacker.feed(b'l') - self.assertRaises(StopIteration, next, iter(unpacker)) - unpacker.feed(b'l') - self.assertRaises(StopIteration, next, iter(unpacker)) - unpacker.feed(b'o') - assert next(iter(unpacker)) == b'hallo' + msg = "No more data to unpack" + + for data in [b"\xa5", b"h", b"a", b"l", b"l"]: + unpacker.feed(data) + with pytest.raises(StopIteration, match=msg): + next(iter(unpacker)) + + unpacker.feed(b"o") + assert next(iter(unpacker)) == b"hallo" def test_foobar(self): unpacker = Unpacker(read_size=3, use_list=1) @@ -33,7 +30,9 @@ def test_foobar(self): assert unpacker.unpack() == ord(b'b') assert unpacker.unpack() == ord(b'a') assert unpacker.unpack() == ord(b'r') - self.assertRaises(OutOfData, unpacker.unpack) + msg = "No more data to unpack" + with pytest.raises(OutOfData, match=msg): + unpacker.unpack() unpacker.feed(b'foo') unpacker.feed(b'bar') @@ -53,14 +52,24 @@ def test_foobar_skip(self): unpacker.skip() assert unpacker.unpack() == ord(b'a') unpacker.skip() - self.assertRaises(OutOfData, unpacker.unpack) + msg = "No more data to unpack" + with pytest.raises(OutOfData, match=msg): + unpacker.unpack() + + def test_maxbuffersize_read_size_exceeds_max_buffer_size(self): + msg = "read_size should be less or equal to max_buffer_size" + with pytest.raises(ValueError, match=msg): + Unpacker(read_size=5, max_buffer_size=3) + + def test_maxbuffersize_bufferfull(self): + unpacker = Unpacker(read_size=3, max_buffer_size=3, use_list=1) + unpacker.feed(b'foo') + with pytest.raises(BufferFull, match=r'^$'): + unpacker.feed(b'b') def test_maxbuffersize(self): - self.assertRaises(ValueError, Unpacker, read_size=5, max_buffer_size=3) unpacker = Unpacker(read_size=3, max_buffer_size=3, use_list=1) - unpacker.feed(b'fo') - self.assertRaises(BufferFull, unpacker.feed, b'ob') - unpacker.feed(b'o') + unpacker.feed(b'foo') assert ord('f') == next(unpacker) unpacker.feed(b'b') assert ord('o') == next(unpacker) diff --git a/pandas/tests/io/msgpack/test_subtype.py b/pandas/tests/io/msgpack/test_subtype.py index e27ec66c63e1f..8af7e0b91d9b7 100644 --- a/pandas/tests/io/msgpack/test_subtype.py +++ b/pandas/tests/io/msgpack/test_subtype.py @@ -1,8 +1,9 @@ # coding: utf-8 -from pandas.io.msgpack import packb from collections import namedtuple +from pandas.io.msgpack import packb + class MyList(list): pass diff --git a/pandas/tests/io/msgpack/test_unpack.py b/pandas/tests/io/msgpack/test_unpack.py index 24a8e885d19d6..356156296c067 100644 --- a/pandas/tests/io/msgpack/test_unpack.py +++ b/pandas/tests/io/msgpack/test_unpack.py @@ -1,11 +1,12 @@ from io import BytesIO import sys -from pandas.io.msgpack import Unpacker, packb, OutOfData, ExtType -import pandas.util.testing as tm + import pytest +from pandas.io.msgpack import ExtType, OutOfData, Unpacker, packb + -class TestUnpack(tm.TestCase): +class TestUnpack(object): def test_unpack_array_header_from_file(self): f = BytesIO(packb([1, 2, 3, 4])) @@ -15,7 +16,9 @@ def test_unpack_array_header_from_file(self): assert unpacker.unpack() == 2 assert unpacker.unpack() == 3 assert unpacker.unpack() == 4 - self.assertRaises(OutOfData, unpacker.unpack) + msg = "No more data to unpack" + with pytest.raises(OutOfData, match=msg): + unpacker.unpack() def test_unpacker_hook_refcnt(self): if not hasattr(sys, 'getrefcount'): diff --git a/pandas/tests/io/msgpack/test_unpack_raw.py b/pandas/tests/io/msgpack/test_unpack_raw.py index a261bf4cbbcd7..09ebb681d8709 100644 --- a/pandas/tests/io/msgpack/test_unpack_raw.py +++ b/pandas/tests/io/msgpack/test_unpack_raw.py @@ -1,6 +1,7 @@ """Tests for cases where the user seeks to obtain packed msgpack objects""" import io + from pandas.io.msgpack import Unpacker, packb diff --git a/pandas/tests/io/parser/c_parser_only.py b/pandas/tests/io/parser/c_parser_only.py deleted file mode 100644 index ffbd904843bfc..0000000000000 --- a/pandas/tests/io/parser/c_parser_only.py +++ /dev/null @@ -1,410 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that apply specifically to the CParser. Unless specifically stated -as a CParser-specific issue, the goal is to eventually move as many of -these tests out of this module as soon as the Python parser can accept -further arguments when parsing. -""" - -import pytest -import numpy as np - -import pandas as pd -import pandas.util.testing as tm -from pandas import DataFrame -from pandas import compat -from pandas.compat import StringIO, range, lrange - - -class CParserTests(object): - - def test_buffer_overflow(self): - # see gh-9205: test certain malformed input files that cause - # buffer overflows in tokenizer.c - - malfw = "1\r1\r1\r 1\r 1\r" # buffer overflow in words pointer - malfs = "1\r1\r1\r 1\r 1\r11\r" # buffer overflow in stream pointer - malfl = "1\r1\r1\r 1\r 1\r11\r1\r" # buffer overflow in lines pointer - - cperr = 'Buffer overflow caught - possible malformed input file.' - - for malf in (malfw, malfs, malfl): - try: - self.read_table(StringIO(malf)) - except Exception as err: - self.assertIn(cperr, str(err)) - - def test_buffer_rd_bytes(self): - # see gh-12098: src->buffer in the C parser can be freed twice leading - # to a segfault if a corrupt gzip file is read with 'read_csv' and the - # buffer is filled more than once before gzip throws an exception - - data = '\x1F\x8B\x08\x00\x00\x00\x00\x00\x00\x03\xED\xC3\x41\x09' \ - '\x00\x00\x08\x00\xB1\xB7\xB6\xBA\xFE\xA5\xCC\x21\x6C\xB0' \ - '\xA6\x4D' + '\x55' * 267 + \ - '\x7D\xF7\x00\x91\xE0\x47\x97\x14\x38\x04\x00' \ - '\x1f\x8b\x08\x00VT\x97V\x00\x03\xed]\xefO' - for i in range(100): - try: - self.read_csv(StringIO(data), - compression='gzip', - delim_whitespace=True) - except Exception: - pass - - def test_delim_whitespace_custom_terminator(self): - # See gh-12912 - data = """a b c~1 2 3~4 5 6~7 8 9""" - df = self.read_csv(StringIO(data), lineterminator='~', - delim_whitespace=True) - expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], - columns=['a', 'b', 'c']) - tm.assert_frame_equal(df, expected) - - def test_dtype_and_names_error(self): - # see gh-8833: passing both dtype and names - # resulting in an error reporting issue - data = """ -1.0 1 -2.0 2 -3.0 3 -""" - # base cases - result = self.read_csv(StringIO(data), sep=r'\s+', header=None) - expected = DataFrame([[1.0, 1], [2.0, 2], [3.0, 3]]) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), sep=r'\s+', - header=None, names=['a', 'b']) - expected = DataFrame( - [[1.0, 1], [2.0, 2], [3.0, 3]], columns=['a', 'b']) - tm.assert_frame_equal(result, expected) - - # fallback casting - result = self.read_csv(StringIO( - data), sep=r'\s+', header=None, - names=['a', 'b'], dtype={'a': np.int32}) - expected = DataFrame([[1, 1], [2, 2], [3, 3]], - columns=['a', 'b']) - expected['a'] = expected['a'].astype(np.int32) - tm.assert_frame_equal(result, expected) - - data = """ -1.0 1 -nan 2 -3.0 3 -""" - # fallback casting, but not castable - with tm.assertRaisesRegexp(ValueError, 'cannot safely convert'): - self.read_csv(StringIO(data), sep=r'\s+', header=None, - names=['a', 'b'], dtype={'a': np.int32}) - - def test_unsupported_dtype(self): - df = DataFrame(np.random.rand(5, 2), columns=list( - 'AB'), index=['1A', '1B', '1C', '1D', '1E']) - - with tm.ensure_clean('__unsupported_dtype__.csv') as path: - df.to_csv(path) - - # valid but we don't support it (date) - self.assertRaises(TypeError, self.read_csv, path, - dtype={'A': 'datetime64', 'B': 'float64'}, - index_col=0) - self.assertRaises(TypeError, self.read_csv, path, - dtype={'A': 'datetime64', 'B': 'float64'}, - index_col=0, parse_dates=['B']) - - # valid but we don't support it - self.assertRaises(TypeError, self.read_csv, path, - dtype={'A': 'timedelta64', 'B': 'float64'}, - index_col=0) - - # valid but unsupported - fixed width unicode string - self.assertRaises(TypeError, self.read_csv, path, - dtype={'A': 'U8'}, - index_col=0) - - def test_precise_conversion(self): - # see gh-8002 - tm._skip_if_32bit() - from decimal import Decimal - - normal_errors = [] - precise_errors = [] - - # test numbers between 1 and 2 - for num in np.linspace(1., 2., num=500): - # 25 decimal digits of precision - text = 'a\n{0:.25}'.format(num) - - normal_val = float(self.read_csv(StringIO(text))['a'][0]) - precise_val = float(self.read_csv( - StringIO(text), float_precision='high')['a'][0]) - roundtrip_val = float(self.read_csv( - StringIO(text), float_precision='round_trip')['a'][0]) - actual_val = Decimal(text[2:]) - - def error(val): - return abs(Decimal('{0:.100}'.format(val)) - actual_val) - - normal_errors.append(error(normal_val)) - precise_errors.append(error(precise_val)) - - # round-trip should match float() - self.assertEqual(roundtrip_val, float(text[2:])) - - self.assertTrue(sum(precise_errors) <= sum(normal_errors)) - self.assertTrue(max(precise_errors) <= max(normal_errors)) - - def test_pass_dtype_as_recarray(self): - if compat.is_platform_windows() and self.low_memory: - pytest.skip( - "segfaults on win-64, only when all tests are run") - - data = """\ -one,two -1,2.5 -2,3.5 -3,4.5 -4,5.5""" - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - result = self.read_csv(StringIO(data), dtype={ - 'one': 'u1', 1: 'S1'}, as_recarray=True) - self.assertEqual(result['one'].dtype, 'u1') - self.assertEqual(result['two'].dtype, 'S1') - - def test_usecols_dtypes(self): - data = """\ -1,2,3 -4,5,6 -7,8,9 -10,11,12""" - - result = self.read_csv(StringIO(data), usecols=(0, 1, 2), - names=('a', 'b', 'c'), - header=None, - converters={'a': str}, - dtype={'b': int, 'c': float}, - ) - result2 = self.read_csv(StringIO(data), usecols=(0, 2), - names=('a', 'b', 'c'), - header=None, - converters={'a': str}, - dtype={'b': int, 'c': float}, - ) - self.assertTrue((result.dtypes == [object, np.int, np.float]).all()) - self.assertTrue((result2.dtypes == [object, np.float]).all()) - - def test_disable_bool_parsing(self): - # #2090 - - data = """A,B,C -Yes,No,Yes -No,Yes,Yes -Yes,,Yes -No,No,No""" - - result = self.read_csv(StringIO(data), dtype=object) - self.assertTrue((result.dtypes == object).all()) - - result = self.read_csv(StringIO(data), dtype=object, na_filter=False) - self.assertEqual(result['B'][2], '') - - def test_custom_lineterminator(self): - data = 'a,b,c~1,2,3~4,5,6' - - result = self.read_csv(StringIO(data), lineterminator='~') - expected = self.read_csv(StringIO(data.replace('~', '\n'))) - - tm.assert_frame_equal(result, expected) - - def test_parse_ragged_csv(self): - data = """1,2,3 -1,2,3,4 -1,2,3,4,5 -1,2 -1,2,3,4""" - - nice_data = """1,2,3,, -1,2,3,4, -1,2,3,4,5 -1,2,,, -1,2,3,4,""" - result = self.read_csv(StringIO(data), header=None, - names=['a', 'b', 'c', 'd', 'e']) - - expected = self.read_csv(StringIO(nice_data), header=None, - names=['a', 'b', 'c', 'd', 'e']) - - tm.assert_frame_equal(result, expected) - - # too many columns, cause segfault if not careful - data = "1,2\n3,4,5" - - result = self.read_csv(StringIO(data), header=None, - names=lrange(50)) - expected = self.read_csv(StringIO(data), header=None, - names=lrange(3)).reindex(columns=lrange(50)) - - tm.assert_frame_equal(result, expected) - - def test_tokenize_CR_with_quoting(self): - # see gh-3453 - - data = ' a,b,c\r"a,b","e,d","f,f"' - - result = self.read_csv(StringIO(data), header=None) - expected = self.read_csv(StringIO(data.replace('\r', '\n')), - header=None) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data)) - expected = self.read_csv(StringIO(data.replace('\r', '\n'))) - tm.assert_frame_equal(result, expected) - - def test_grow_boundary_at_cap(self): - # See gh-12494 - # - # Cause of error was that the C parser - # was not increasing the buffer size when - # the desired space would fill the buffer - # to capacity, which would later cause a - # buffer overflow error when checking the - # EOF terminator of the CSV stream - def test_empty_header_read(count): - s = StringIO(',' * count) - expected = DataFrame(columns=[ - 'Unnamed: {i}'.format(i=i) - for i in range(count + 1)]) - df = self.read_csv(s) - tm.assert_frame_equal(df, expected) - - for count in range(1, 101): - test_empty_header_read(count) - - def test_parse_trim_buffers(self): - # This test is part of a bugfix for issue #13703. It attmepts to - # to stress the system memory allocator, to cause it to move the - # stream buffer and either let the OS reclaim the region, or let - # other memory requests of parser otherwise modify the contents - # of memory space, where it was formely located. - # This test is designed to cause a `segfault` with unpatched - # `tokenizer.c`. Sometimes the test fails on `segfault`, other - # times it fails due to memory corruption, which causes the - # loaded DataFrame to differ from the expected one. - - # Generate a large mixed-type CSV file on-the-fly (one record is - # approx 1.5KiB). - record_ = \ - """9999-9,99:99,,,,ZZ,ZZ,,,ZZZ-ZZZZ,.Z-ZZZZ,-9.99,,,9.99,Z""" \ - """ZZZZ,,-99,9,ZZZ-ZZZZ,ZZ-ZZZZ,,9.99,ZZZ-ZZZZZ,ZZZ-ZZZZZ,""" \ - """ZZZ-ZZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,9""" \ - """99,ZZZ-ZZZZ,,ZZ-ZZZZ,,,,,ZZZZ,ZZZ-ZZZZZ,ZZZ-ZZZZ,,,9,9,""" \ - """9,9,99,99,999,999,ZZZZZ,ZZZ-ZZZZZ,ZZZ-ZZZZ,9,ZZ-ZZZZ,9.""" \ - """99,ZZ-ZZZZ,ZZ-ZZZZ,,,,ZZZZ,,,ZZ,ZZ,,,,,,,,,,,,,9,,,999.""" \ - """99,999.99,,,ZZZZZ,,,Z9,,,,,,,ZZZ,ZZZ,,,,,,,,,,,ZZZZZ,ZZ""" \ - """ZZZ,ZZZ-ZZZZZZ,ZZZ-ZZZZZZ,ZZ-ZZZZ,ZZ-ZZZZ,ZZ-ZZZZ,ZZ-ZZ""" \ - """ZZ,,,999999,999999,ZZZ,ZZZ,,,ZZZ,ZZZ,999.99,999.99,,,,Z""" \ - """ZZ-ZZZ,ZZZ-ZZZ,-9.99,-9.99,9,9,,99,,9.99,9.99,9,9,9.99,""" \ - """9.99,,,,9.99,9.99,,99,,99,9.99,9.99,,,ZZZ,ZZZ,,999.99,,""" \ - """999.99,ZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,,,ZZZZZ,ZZZZZ,ZZZ,ZZZ,9,9,""" \ - """,,,,,ZZZ-ZZZZ,ZZZ999Z,,,999.99,,999.99,ZZZ-ZZZZ,,,9.999""" \ - """,9.999,9.999,9.999,-9.999,-9.999,-9.999,-9.999,9.999,9.""" \ - """999,9.999,9.999,9.999,9.999,9.999,9.999,99999,ZZZ-ZZZZ,""" \ - """,9.99,ZZZ,,,,,,,,ZZZ,,,,,9,,,,9,,,,,,,,,,ZZZ-ZZZZ,ZZZ-Z""" \ - """ZZZ,,ZZZZZ,ZZZZZ,ZZZZZ,ZZZZZ,,,9.99,,ZZ-ZZZZ,ZZ-ZZZZ,ZZ""" \ - """,999,,,,ZZ-ZZZZ,ZZZ,ZZZ,ZZZ-ZZZZ,ZZZ-ZZZZ,,,99.99,99.99""" \ - """,,,9.99,9.99,9.99,9.99,ZZZ-ZZZZ,,,ZZZ-ZZZZZ,,,,,-9.99,-""" \ - """9.99,-9.99,-9.99,,,,,,,,,ZZZ-ZZZZ,,9,9.99,9.99,99ZZ,,-9""" \ - """.99,-9.99,ZZZ-ZZZZ,,,,,,,ZZZ-ZZZZ,9.99,9.99,9999,,,,,,,""" \ - """,,,-9.9,Z/Z-ZZZZ,999.99,9.99,,999.99,ZZ-ZZZZ,ZZ-ZZZZ,9.""" \ - """99,9.99,9.99,9.99,9.99,9.99,,ZZZ-ZZZZZ,ZZZ-ZZZZZ,ZZZ-ZZ""" \ - """ZZZ,ZZZ-ZZZZZ,ZZZ-ZZZZZ,ZZZ,ZZZ,ZZZ,ZZZ,9.99,,,-9.99,ZZ""" \ - """-ZZZZ,-999.99,,-9999,,999.99,,,,999.99,99.99,,,ZZ-ZZZZZ""" \ - """ZZZ,ZZ-ZZZZ-ZZZZZZZ,,,,ZZ-ZZ-ZZZZZZZZ,ZZZZZZZZ,ZZZ-ZZZZ""" \ - """,9999,999.99,ZZZ-ZZZZ,-9.99,-9.99,ZZZ-ZZZZ,99:99:99,,99""" \ - """,99,,9.99,,-99.99,,,,,,9.99,ZZZ-ZZZZ,-9.99,-9.99,9.99,9""" \ - """.99,,ZZZ,,,,,,,ZZZ,ZZZ,,,,,""" - - # Set the number of lines so that a call to `parser_trim_buffers` - # is triggered: after a couple of full chunks are consumed a - # relatively small 'residual' chunk would cause reallocation - # within the parser. - chunksize, n_lines = 128, 2 * 128 + 15 - csv_data = "\n".join([record_] * n_lines) + "\n" - - # We will use StringIO to load the CSV from this text buffer. - # pd.read_csv() will iterate over the file in chunks and will - # finally read a residual chunk of really small size. - - # Generate the expected output: manually create the dataframe - # by splitting by comma and repeating the `n_lines` times. - row = tuple(val_ if val_ else float("nan") - for val_ in record_.split(",")) - expected = pd.DataFrame([row for _ in range(n_lines)], - dtype=object, columns=None, index=None) - - # Iterate over the CSV file in chunks of `chunksize` lines - chunks_ = self.read_csv(StringIO(csv_data), header=None, - dtype=object, chunksize=chunksize) - result = pd.concat(chunks_, axis=0, ignore_index=True) - - # Check for data corruption if there was no segfault - tm.assert_frame_equal(result, expected) - - def test_internal_null_byte(self): - # see gh-14012 - # - # The null byte ('\x00') should not be used as a - # true line terminator, escape character, or comment - # character, only as a placeholder to indicate that - # none was specified. - # - # This test should be moved to common.py ONLY when - # Python's csv class supports parsing '\x00'. - names = ['a', 'b', 'c'] - data = "1,2,3\n4,\x00,6\n7,8,9" - expected = pd.DataFrame([[1, 2.0, 3], [4, np.nan, 6], - [7, 8, 9]], columns=names) - - result = self.read_csv(StringIO(data), names=names) - tm.assert_frame_equal(result, expected) - - def test_read_nrows_large(self): - # gh-7626 - Read only nrows of data in for large inputs (>262144b) - header_narrow = '\t'.join(['COL_HEADER_' + str(i) - for i in range(10)]) + '\n' - data_narrow = '\t'.join(['somedatasomedatasomedata1' - for i in range(10)]) + '\n' - header_wide = '\t'.join(['COL_HEADER_' + str(i) - for i in range(15)]) + '\n' - data_wide = '\t'.join(['somedatasomedatasomedata2' - for i in range(15)]) + '\n' - test_input = (header_narrow + data_narrow * 1050 + - header_wide + data_wide * 2) - - df = self.read_csv(StringIO(test_input), sep='\t', nrows=1010) - - self.assertTrue(df.size == 1010 * 10) - - def test_float_precision_round_trip_with_text(self): - # gh-15140 - This should not segfault on Python 2.7+ - df = self.read_csv(StringIO('a'), - float_precision='round_trip', - header=None) - tm.assert_frame_equal(df, DataFrame({0: ['a']})) - - def test_large_difference_in_columns(self): - # gh-14125 - count = 10000 - large_row = ('X,' * count)[:-1] + '\n' - normal_row = 'XXXXXX XXXXXX,111111111111111\n' - test_input = (large_row + normal_row * 6)[:-1] - result = self.read_csv(StringIO(test_input), header=None, usecols=[0]) - rows = test_input.split('\n') - expected = DataFrame([row.split(',')[0] for row in rows]) - - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/comment.py b/pandas/tests/io/parser/comment.py deleted file mode 100644 index 9987a017cf985..0000000000000 --- a/pandas/tests/io/parser/comment.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that comments are properly handled during parsing -for all of the parsers defined in parsers.py -""" - -import numpy as np -import pandas.util.testing as tm - -from pandas import DataFrame -from pandas.compat import StringIO - - -class CommentTests(object): - - def test_comment(self): - data = """A,B,C -1,2.,4.#hello world -5.,NaN,10.0 -""" - expected = np.array([[1., 2., 4.], - [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data), comment='#') - tm.assert_numpy_array_equal(df.values, expected) - - df = self.read_table(StringIO(data), sep=',', comment='#', - na_values=['NaN']) - tm.assert_numpy_array_equal(df.values, expected) - - def test_line_comment(self): - data = """# empty -A,B,C -1,2.,4.#hello world -#ignore this line -5.,NaN,10.0 -""" - expected = np.array([[1., 2., 4.], - [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data), comment='#') - tm.assert_numpy_array_equal(df.values, expected) - - # check with delim_whitespace=True - df = self.read_csv(StringIO(data.replace(',', ' ')), comment='#', - delim_whitespace=True) - tm.assert_almost_equal(df.values, expected) - - # custom line terminator is not supported - # with the Python parser yet - if self.engine == 'c': - expected = np.array([[1., 2., 4.], - [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data.replace('\n', '*')), - comment='#', lineterminator='*') - tm.assert_numpy_array_equal(df.values, expected) - - def test_comment_skiprows(self): - data = """# empty -random line -# second empty line -1,2,3 -A,B,C -1,2.,4. -5.,NaN,10.0 -""" - # this should ignore the first four lines (including comments) - expected = np.array([[1., 2., 4.], [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data), comment='#', skiprows=4) - tm.assert_numpy_array_equal(df.values, expected) - - def test_comment_header(self): - data = """# empty -# second empty line -1,2,3 -A,B,C -1,2.,4. -5.,NaN,10.0 -""" - # header should begin at the second non-comment line - expected = np.array([[1., 2., 4.], [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data), comment='#', header=1) - tm.assert_numpy_array_equal(df.values, expected) - - def test_comment_skiprows_header(self): - data = """# empty -# second empty line -# third empty line -X,Y,Z -1,2,3 -A,B,C -1,2.,4. -5.,NaN,10.0 -""" - # skiprows should skip the first 4 lines (including comments), while - # header should start from the second non-commented line starting - # with line 5 - expected = np.array([[1., 2., 4.], [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data), comment='#', skiprows=4, header=1) - tm.assert_numpy_array_equal(df.values, expected) - - def test_custom_comment_char(self): - data = "a,b,c\n1,2,3#ignore this!\n4,5,6#ignorethistoo" - - result = self.read_csv(StringIO(data), comment='#') - expected = DataFrame({'a': [1, 4], 'b': [2, 5], 'c': [3, 6]}) - tm.assert_frame_equal(result, expected) - - def test_commment_first_line(self): - # see gh-4623 - data = '# notes\na,b,c\n# more notes\n1,2,3' - - expected = DataFrame([[1, 2, 3]], columns=['a', 'b', 'c']) - result = self.read_csv(StringIO(data), comment='#') - tm.assert_frame_equal(result, expected) - - expected = DataFrame({0: ['a', '1'], 1: ['b', '2'], 2: ['c', '3']}) - result = self.read_csv(StringIO(data), comment='#', header=None) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/common.py b/pandas/tests/io/parser/common.py deleted file mode 100644 index 2c8bca490f274..0000000000000 --- a/pandas/tests/io/parser/common.py +++ /dev/null @@ -1,1679 +0,0 @@ -# -*- coding: utf-8 -*- - -import csv -import os -import platform -import codecs - -import re -import sys -from datetime import datetime - -import pytest -import numpy as np -from pandas._libs.lib import Timestamp - -import pandas as pd -import pandas.util.testing as tm -from pandas import DataFrame, Series, Index, MultiIndex -from pandas import compat -from pandas.compat import (StringIO, BytesIO, PY3, - range, lrange, u) -from pandas.io.common import DtypeWarning, EmptyDataError, URLError -from pandas.io.parsers import TextFileReader, TextParser - - -class ParserTests(object): - """ - Want to be able to test either C+Cython or Python+Cython parsers - """ - data1 = """index,A,B,C,D -foo,2,3,4,5 -bar,7,8,9,10 -baz,12,13,14,15 -qux,12,13,14,15 -foo2,12,13,14,15 -bar2,12,13,14,15 -""" - - def test_empty_decimal_marker(self): - data = """A|B|C -1|2,334|5 -10|13|10. -""" - # Parsers support only length-1 decimals - msg = 'Only length-1 decimal markers supported' - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(data), decimal='') - - def test_bad_stream_exception(self): - # Issue 13652: - # This test validates that both python engine - # and C engine will raise UnicodeDecodeError instead of - # c engine raising ParserError and swallowing exception - # that caused read to fail. - handle = open(self.csv_shiftjs, "rb") - codec = codecs.lookup("utf-8") - utf8 = codecs.lookup('utf-8') - # stream must be binary UTF8 - stream = codecs.StreamRecoder( - handle, utf8.encode, utf8.decode, codec.streamreader, - codec.streamwriter) - if compat.PY3: - msg = "'utf-8' codec can't decode byte" - else: - msg = "'utf8' codec can't decode byte" - with tm.assertRaisesRegexp(UnicodeDecodeError, msg): - self.read_csv(stream) - stream.close() - - def test_read_csv(self): - if not compat.PY3: - if compat.is_platform_windows(): - prefix = u("file:///") - else: - prefix = u("file://") - - fname = prefix + compat.text_type(self.csv1) - self.read_csv(fname, index_col=0, parse_dates=True) - - def test_1000_sep(self): - data = """A|B|C -1|2,334|5 -10|13|10. -""" - expected = DataFrame({ - 'A': [1, 10], - 'B': [2334, 13], - 'C': [5, 10.] - }) - - df = self.read_csv(StringIO(data), sep='|', thousands=',') - tm.assert_frame_equal(df, expected) - - df = self.read_table(StringIO(data), sep='|', thousands=',') - tm.assert_frame_equal(df, expected) - - def test_squeeze(self): - data = """\ -a,1 -b,2 -c,3 -""" - idx = Index(['a', 'b', 'c'], name=0) - expected = Series([1, 2, 3], name=1, index=idx) - result = self.read_table(StringIO(data), sep=',', index_col=0, - header=None, squeeze=True) - tm.assertIsInstance(result, Series) - tm.assert_series_equal(result, expected) - - def test_squeeze_no_view(self): - # see gh-8217 - # Series should not be a view - data = """time,data\n0,10\n1,11\n2,12\n4,14\n5,15\n3,13""" - result = self.read_csv(StringIO(data), index_col='time', squeeze=True) - self.assertFalse(result._is_view) - - def test_malformed(self): - # see gh-6607 - - # all - data = """ignore -A,B,C -1,2,3 # comment -1,2,3,4,5 -2,3,4 -""" - msg = 'Expected 3 fields in line 4, saw 5' - with tm.assertRaisesRegexp(Exception, msg): - self.read_table(StringIO(data), sep=',', - header=1, comment='#') - - # first chunk - data = """ignore -A,B,C -skip -1,2,3 -3,5,10 # comment -1,2,3,4,5 -2,3,4 -""" - msg = 'Expected 3 fields in line 6, saw 5' - with tm.assertRaisesRegexp(Exception, msg): - it = self.read_table(StringIO(data), sep=',', - header=1, comment='#', - iterator=True, chunksize=1, - skiprows=[2]) - it.read(5) - - # middle chunk - data = """ignore -A,B,C -skip -1,2,3 -3,5,10 # comment -1,2,3,4,5 -2,3,4 -""" - msg = 'Expected 3 fields in line 6, saw 5' - with tm.assertRaisesRegexp(Exception, msg): - it = self.read_table(StringIO(data), sep=',', header=1, - comment='#', iterator=True, chunksize=1, - skiprows=[2]) - it.read(3) - - # last chunk - data = """ignore -A,B,C -skip -1,2,3 -3,5,10 # comment -1,2,3,4,5 -2,3,4 -""" - msg = 'Expected 3 fields in line 6, saw 5' - with tm.assertRaisesRegexp(Exception, msg): - it = self.read_table(StringIO(data), sep=',', header=1, - comment='#', iterator=True, chunksize=1, - skiprows=[2]) - it.read() - - # skipfooter is not supported with the C parser yet - if self.engine == 'python': - # skipfooter - data = """ignore -A,B,C -1,2,3 # comment -1,2,3,4,5 -2,3,4 -footer -""" - msg = 'Expected 3 fields in line 4, saw 5' - with tm.assertRaisesRegexp(Exception, msg): - self.read_table(StringIO(data), sep=',', - header=1, comment='#', - skipfooter=1) - - def test_quoting(self): - bad_line_small = """printer\tresult\tvariant_name -Klosterdruckerei\tKlosterdruckerei (1611-1804)\tMuller, Jacob -Klosterdruckerei\tKlosterdruckerei (1611-1804)\tMuller, Jakob -Klosterdruckerei\tKlosterdruckerei (1609-1805)\t"Furststiftische Hofdruckerei, (1609-1805)\tGaller, Alois -Klosterdruckerei\tKlosterdruckerei (1609-1805)\tHochfurstliche Buchhandlung """ # noqa - self.assertRaises(Exception, self.read_table, StringIO(bad_line_small), - sep='\t') - - good_line_small = bad_line_small + '"' - df = self.read_table(StringIO(good_line_small), sep='\t') - self.assertEqual(len(df), 3) - - def test_unnamed_columns(self): - data = """A,B,C,, -1,2,3,4,5 -6,7,8,9,10 -11,12,13,14,15 -""" - expected = np.array([[1, 2, 3, 4, 5], - [6, 7, 8, 9, 10], - [11, 12, 13, 14, 15]], dtype=np.int64) - df = self.read_table(StringIO(data), sep=',') - tm.assert_almost_equal(df.values, expected) - self.assert_index_equal(df.columns, - Index(['A', 'B', 'C', 'Unnamed: 3', - 'Unnamed: 4'])) - - def test_duplicate_columns(self): - # TODO: add test for condition 'mangle_dupe_cols=False' - # once it is actually supported (gh-12935) - data = """A,A,B,B,B -1,2,3,4,5 -6,7,8,9,10 -11,12,13,14,15 -""" - - for method in ('read_csv', 'read_table'): - - # check default behavior - df = getattr(self, method)(StringIO(data), sep=',') - self.assertEqual(list(df.columns), - ['A', 'A.1', 'B', 'B.1', 'B.2']) - - df = getattr(self, method)(StringIO(data), sep=',', - mangle_dupe_cols=True) - self.assertEqual(list(df.columns), - ['A', 'A.1', 'B', 'B.1', 'B.2']) - - def test_csv_mixed_type(self): - data = """A,B,C -a,1,2 -b,3,4 -c,4,5 -""" - expected = DataFrame({'A': ['a', 'b', 'c'], - 'B': [1, 3, 4], - 'C': [2, 4, 5]}) - out = self.read_csv(StringIO(data)) - tm.assert_frame_equal(out, expected) - - def test_read_csv_dataframe(self): - df = self.read_csv(self.csv1, index_col=0, parse_dates=True) - df2 = self.read_table(self.csv1, sep=',', index_col=0, - parse_dates=True) - self.assert_index_equal(df.columns, pd.Index(['A', 'B', 'C', 'D'])) - self.assertEqual(df.index.name, 'index') - self.assertIsInstance( - df.index[0], (datetime, np.datetime64, Timestamp)) - self.assertEqual(df.values.dtype, np.float64) - tm.assert_frame_equal(df, df2) - - def test_read_csv_no_index_name(self): - df = self.read_csv(self.csv2, index_col=0, parse_dates=True) - df2 = self.read_table(self.csv2, sep=',', index_col=0, - parse_dates=True) - self.assert_index_equal(df.columns, - pd.Index(['A', 'B', 'C', 'D', 'E'])) - self.assertIsInstance(df.index[0], - (datetime, np.datetime64, Timestamp)) - self.assertEqual(df.loc[:, ['A', 'B', 'C', 'D']].values.dtype, - np.float64) - tm.assert_frame_equal(df, df2) - - def test_read_table_unicode(self): - fin = BytesIO(u('\u0141aski, Jan;1').encode('utf-8')) - df1 = self.read_table(fin, sep=";", encoding="utf-8", header=None) - tm.assertIsInstance(df1[0].values[0], compat.text_type) - - def test_read_table_wrong_num_columns(self): - # too few! - data = """A,B,C,D,E,F -1,2,3,4,5,6 -6,7,8,9,10,11,12 -11,12,13,14,15,16 -""" - self.assertRaises(ValueError, self.read_csv, StringIO(data)) - - def test_read_duplicate_index_explicit(self): - data = """index,A,B,C,D -foo,2,3,4,5 -bar,7,8,9,10 -baz,12,13,14,15 -qux,12,13,14,15 -foo,12,13,14,15 -bar,12,13,14,15 -""" - - result = self.read_csv(StringIO(data), index_col=0) - expected = self.read_csv(StringIO(data)).set_index( - 'index', verify_integrity=False) - tm.assert_frame_equal(result, expected) - - result = self.read_table(StringIO(data), sep=',', index_col=0) - expected = self.read_table(StringIO(data), sep=',', ).set_index( - 'index', verify_integrity=False) - tm.assert_frame_equal(result, expected) - - def test_read_duplicate_index_implicit(self): - data = """A,B,C,D -foo,2,3,4,5 -bar,7,8,9,10 -baz,12,13,14,15 -qux,12,13,14,15 -foo,12,13,14,15 -bar,12,13,14,15 -""" - - # make sure an error isn't thrown - self.read_csv(StringIO(data)) - self.read_table(StringIO(data), sep=',') - - def test_parse_bools(self): - data = """A,B -True,1 -False,2 -True,3 -""" - data = self.read_csv(StringIO(data)) - self.assertEqual(data['A'].dtype, np.bool_) - - data = """A,B -YES,1 -no,2 -yes,3 -No,3 -Yes,3 -""" - data = self.read_csv(StringIO(data), - true_values=['yes', 'Yes', 'YES'], - false_values=['no', 'NO', 'No']) - self.assertEqual(data['A'].dtype, np.bool_) - - data = """A,B -TRUE,1 -FALSE,2 -TRUE,3 -""" - data = self.read_csv(StringIO(data)) - self.assertEqual(data['A'].dtype, np.bool_) - - data = """A,B -foo,bar -bar,foo""" - result = self.read_csv(StringIO(data), true_values=['foo'], - false_values=['bar']) - expected = DataFrame({'A': [True, False], 'B': [False, True]}) - tm.assert_frame_equal(result, expected) - - def test_int_conversion(self): - data = """A,B -1.0,1 -2.0,2 -3.0,3 -""" - data = self.read_csv(StringIO(data)) - self.assertEqual(data['A'].dtype, np.float64) - self.assertEqual(data['B'].dtype, np.int64) - - def test_read_nrows(self): - expected = self.read_csv(StringIO(self.data1))[:3] - - df = self.read_csv(StringIO(self.data1), nrows=3) - tm.assert_frame_equal(df, expected) - - # see gh-10476 - df = self.read_csv(StringIO(self.data1), nrows=3.0) - tm.assert_frame_equal(df, expected) - - msg = r"'nrows' must be an integer >=0" - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), nrows=1.2) - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), nrows='foo') - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), nrows=-1) - - def test_read_chunksize(self): - reader = self.read_csv(StringIO(self.data1), index_col=0, chunksize=2) - df = self.read_csv(StringIO(self.data1), index_col=0) - - chunks = list(reader) - - tm.assert_frame_equal(chunks[0], df[:2]) - tm.assert_frame_equal(chunks[1], df[2:4]) - tm.assert_frame_equal(chunks[2], df[4:]) - - # with invalid chunksize value: - msg = r"'chunksize' must be an integer >=1" - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), chunksize=1.3) - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), chunksize='foo') - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(self.data1), chunksize=0) - - def test_read_chunksize_and_nrows(self): - - # gh-15755 - # With nrows - reader = self.read_csv(StringIO(self.data1), index_col=0, - chunksize=2, nrows=5) - df = self.read_csv(StringIO(self.data1), index_col=0, nrows=5) - - tm.assert_frame_equal(pd.concat(reader), df) - - # chunksize > nrows - reader = self.read_csv(StringIO(self.data1), index_col=0, - chunksize=8, nrows=5) - df = self.read_csv(StringIO(self.data1), index_col=0, nrows=5) - - tm.assert_frame_equal(pd.concat(reader), df) - - # with changing "size": - reader = self.read_csv(StringIO(self.data1), index_col=0, - chunksize=8, nrows=5) - df = self.read_csv(StringIO(self.data1), index_col=0, nrows=5) - - tm.assert_frame_equal(reader.get_chunk(size=2), df.iloc[:2]) - tm.assert_frame_equal(reader.get_chunk(size=4), df.iloc[2:5]) - with tm.assertRaises(StopIteration): - reader.get_chunk(size=3) - - def test_read_chunksize_named(self): - reader = self.read_csv( - StringIO(self.data1), index_col='index', chunksize=2) - df = self.read_csv(StringIO(self.data1), index_col='index') - - chunks = list(reader) - - tm.assert_frame_equal(chunks[0], df[:2]) - tm.assert_frame_equal(chunks[1], df[2:4]) - tm.assert_frame_equal(chunks[2], df[4:]) - - def test_get_chunk_passed_chunksize(self): - data = """A,B,C -1,2,3 -4,5,6 -7,8,9 -1,2,3""" - result = self.read_csv(StringIO(data), chunksize=2) - - piece = result.get_chunk() - self.assertEqual(len(piece), 2) - - def test_read_chunksize_generated_index(self): - # GH 12185 - reader = self.read_csv(StringIO(self.data1), chunksize=2) - df = self.read_csv(StringIO(self.data1)) - - tm.assert_frame_equal(pd.concat(reader), df) - - reader = self.read_csv(StringIO(self.data1), chunksize=2, index_col=0) - df = self.read_csv(StringIO(self.data1), index_col=0) - - tm.assert_frame_equal(pd.concat(reader), df) - - def test_read_text_list(self): - data = """A,B,C\nfoo,1,2,3\nbar,4,5,6""" - as_list = [['A', 'B', 'C'], ['foo', '1', '2', '3'], ['bar', - '4', '5', '6']] - df = self.read_csv(StringIO(data), index_col=0) - - parser = TextParser(as_list, index_col=0, chunksize=2) - chunk = parser.read(None) - - tm.assert_frame_equal(chunk, df) - - def test_iterator(self): - # See gh-6607 - reader = self.read_csv(StringIO(self.data1), index_col=0, - iterator=True) - df = self.read_csv(StringIO(self.data1), index_col=0) - - chunk = reader.read(3) - tm.assert_frame_equal(chunk, df[:3]) - - last_chunk = reader.read(5) - tm.assert_frame_equal(last_chunk, df[3:]) - - # pass list - lines = list(csv.reader(StringIO(self.data1))) - parser = TextParser(lines, index_col=0, chunksize=2) - - df = self.read_csv(StringIO(self.data1), index_col=0) - - chunks = list(parser) - tm.assert_frame_equal(chunks[0], df[:2]) - tm.assert_frame_equal(chunks[1], df[2:4]) - tm.assert_frame_equal(chunks[2], df[4:]) - - # pass skiprows - parser = TextParser(lines, index_col=0, chunksize=2, skiprows=[1]) - chunks = list(parser) - tm.assert_frame_equal(chunks[0], df[1:3]) - - treader = self.read_table(StringIO(self.data1), sep=',', index_col=0, - iterator=True) - tm.assertIsInstance(treader, TextFileReader) - - # gh-3967: stopping iteration when chunksize is specified - data = """A,B,C -foo,1,2,3 -bar,4,5,6 -baz,7,8,9 -""" - reader = self.read_csv(StringIO(data), iterator=True) - result = list(reader) - expected = DataFrame(dict(A=[1, 4, 7], B=[2, 5, 8], C=[ - 3, 6, 9]), index=['foo', 'bar', 'baz']) - tm.assert_frame_equal(result[0], expected) - - # chunksize = 1 - reader = self.read_csv(StringIO(data), chunksize=1) - result = list(reader) - expected = DataFrame(dict(A=[1, 4, 7], B=[2, 5, 8], C=[ - 3, 6, 9]), index=['foo', 'bar', 'baz']) - self.assertEqual(len(result), 3) - tm.assert_frame_equal(pd.concat(result), expected) - - # skipfooter is not supported with the C parser yet - if self.engine == 'python': - # test bad parameter (skipfooter) - reader = self.read_csv(StringIO(self.data1), index_col=0, - iterator=True, skipfooter=True) - self.assertRaises(ValueError, reader.read, 3) - - def test_pass_names_with_index(self): - lines = self.data1.split('\n') - no_header = '\n'.join(lines[1:]) - - # regular index - names = ['index', 'A', 'B', 'C', 'D'] - df = self.read_csv(StringIO(no_header), index_col=0, names=names) - expected = self.read_csv(StringIO(self.data1), index_col=0) - tm.assert_frame_equal(df, expected) - - # multi index - data = """index1,index2,A,B,C,D -foo,one,2,3,4,5 -foo,two,7,8,9,10 -foo,three,12,13,14,15 -bar,one,12,13,14,15 -bar,two,12,13,14,15 -""" - lines = data.split('\n') - no_header = '\n'.join(lines[1:]) - names = ['index1', 'index2', 'A', 'B', 'C', 'D'] - df = self.read_csv(StringIO(no_header), index_col=[0, 1], - names=names) - expected = self.read_csv(StringIO(data), index_col=[0, 1]) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(data), index_col=['index1', 'index2']) - tm.assert_frame_equal(df, expected) - - def test_multi_index_no_level_names(self): - data = """index1,index2,A,B,C,D -foo,one,2,3,4,5 -foo,two,7,8,9,10 -foo,three,12,13,14,15 -bar,one,12,13,14,15 -bar,two,12,13,14,15 -""" - - data2 = """A,B,C,D -foo,one,2,3,4,5 -foo,two,7,8,9,10 -foo,three,12,13,14,15 -bar,one,12,13,14,15 -bar,two,12,13,14,15 -""" - - lines = data.split('\n') - no_header = '\n'.join(lines[1:]) - names = ['A', 'B', 'C', 'D'] - - df = self.read_csv(StringIO(no_header), index_col=[0, 1], - header=None, names=names) - expected = self.read_csv(StringIO(data), index_col=[0, 1]) - tm.assert_frame_equal(df, expected, check_names=False) - - # 2 implicit first cols - df2 = self.read_csv(StringIO(data2)) - tm.assert_frame_equal(df2, df) - - # reverse order of index - df = self.read_csv(StringIO(no_header), index_col=[1, 0], names=names, - header=None) - expected = self.read_csv(StringIO(data), index_col=[1, 0]) - tm.assert_frame_equal(df, expected, check_names=False) - - def test_multi_index_blank_df(self): - # GH 14545 - data = """a,b -""" - df = self.read_csv(StringIO(data), header=[0]) - expected = DataFrame(columns=['a', 'b']) - tm.assert_frame_equal(df, expected) - round_trip = self.read_csv(StringIO( - expected.to_csv(index=False)), header=[0]) - tm.assert_frame_equal(round_trip, expected) - - data_multiline = """a,b -c,d -""" - df2 = self.read_csv(StringIO(data_multiline), header=[0, 1]) - cols = MultiIndex.from_tuples([('a', 'c'), ('b', 'd')]) - expected2 = DataFrame(columns=cols) - tm.assert_frame_equal(df2, expected2) - round_trip = self.read_csv(StringIO( - expected2.to_csv(index=False)), header=[0, 1]) - tm.assert_frame_equal(round_trip, expected2) - - def test_no_unnamed_index(self): - data = """ id c0 c1 c2 -0 1 0 a b -1 2 0 c d -2 2 2 e f -""" - df = self.read_table(StringIO(data), sep=' ') - self.assertIsNone(df.index.name) - - def test_read_csv_parse_simple_list(self): - text = """foo -bar baz -qux foo -foo -bar""" - df = self.read_csv(StringIO(text), header=None) - expected = DataFrame({0: ['foo', 'bar baz', 'qux foo', - 'foo', 'bar']}) - tm.assert_frame_equal(df, expected) - - @tm.network - def test_url(self): - # HTTP(S) - url = ('https://raw.github.com/pandas-dev/pandas/master/' - 'pandas/tests/io/parser/data/salaries.csv') - url_table = self.read_table(url) - dirpath = tm.get_data_path() - localtable = os.path.join(dirpath, 'salaries.csv') - local_table = self.read_table(localtable) - tm.assert_frame_equal(url_table, local_table) - # TODO: ftp testing - - @tm.slow - def test_file(self): - dirpath = tm.get_data_path() - localtable = os.path.join(dirpath, 'salaries.csv') - local_table = self.read_table(localtable) - - try: - url_table = self.read_table('file://localhost/' + localtable) - except URLError: - # fails on some systems - pytest.skip("failing on %s" % - ' '.join(platform.uname()).strip()) - - tm.assert_frame_equal(url_table, local_table) - - def test_nonexistent_path(self): - # gh-2428: pls no segfault - # gh-14086: raise more helpful FileNotFoundError - path = '%s.csv' % tm.rands(10) - self.assertRaises(compat.FileNotFoundError, self.read_csv, path) - - def test_missing_trailing_delimiters(self): - data = """A,B,C,D -1,2,3,4 -1,3,3, -1,4,5""" - result = self.read_csv(StringIO(data)) - self.assertTrue(result['D'].isnull()[1:].all()) - - def test_skipinitialspace(self): - s = ('"09-Apr-2012", "01:10:18.300", 2456026.548822908, 12849, ' - '1.00361, 1.12551, 330.65659, 0355626618.16711, 73.48821, ' - '314.11625, 1917.09447, 179.71425, 80.000, 240.000, -350, ' - '70.06056, 344.98370, 1, 1, -0.689265, -0.692787, ' - '0.212036, 14.7674, 41.605, -9999.0, -9999.0, ' - '-9999.0, -9999.0, -9999.0, -9999.0, 000, 012, 128') - - sfile = StringIO(s) - # it's 33 columns - result = self.read_csv(sfile, names=lrange(33), na_values=['-9999.0'], - header=None, skipinitialspace=True) - self.assertTrue(pd.isnull(result.iloc[0, 29])) - - def test_utf16_bom_skiprows(self): - # #2298 - data = u("""skip this -skip this too -A\tB\tC -1\t2\t3 -4\t5\t6""") - - data2 = u("""skip this -skip this too -A,B,C -1,2,3 -4,5,6""") - - path = '__%s__.csv' % tm.rands(10) - - with tm.ensure_clean(path) as path: - for sep, dat in [('\t', data), (',', data2)]: - for enc in ['utf-16', 'utf-16le', 'utf-16be']: - bytes = dat.encode(enc) - with open(path, 'wb') as f: - f.write(bytes) - - s = BytesIO(dat.encode('utf-8')) - if compat.PY3: - # somewhat False since the code never sees bytes - from io import TextIOWrapper - s = TextIOWrapper(s, encoding='utf-8') - - result = self.read_csv(path, encoding=enc, skiprows=2, - sep=sep) - expected = self.read_csv(s, encoding='utf-8', skiprows=2, - sep=sep) - s.close() - - tm.assert_frame_equal(result, expected) - - def test_utf16_example(self): - path = tm.get_data_path('utf16_ex.txt') - - # it works! and is the right length - result = self.read_table(path, encoding='utf-16') - self.assertEqual(len(result), 50) - - if not compat.PY3: - buf = BytesIO(open(path, 'rb').read()) - result = self.read_table(buf, encoding='utf-16') - self.assertEqual(len(result), 50) - - def test_unicode_encoding(self): - pth = tm.get_data_path('unicode_series.csv') - - result = self.read_csv(pth, header=None, encoding='latin-1') - result = result.set_index(0) - - got = result[1][1632] - expected = u('\xc1 k\xf6ldum klaka (Cold Fever) (1994)') - - self.assertEqual(got, expected) - - def test_trailing_delimiters(self): - # #2442. grumble grumble - data = """A,B,C -1,2,3, -4,5,6, -7,8,9,""" - result = self.read_csv(StringIO(data), index_col=False) - - expected = DataFrame({'A': [1, 4, 7], 'B': [2, 5, 8], - 'C': [3, 6, 9]}) - - tm.assert_frame_equal(result, expected) - - def test_escapechar(self): - # http://stackoverflow.com/questions/13824840/feature-request-for- - # pandas-read-csv - data = '''SEARCH_TERM,ACTUAL_URL -"bra tv bord","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord" -"tv p\xc3\xa5 hjul","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord" -"SLAGBORD, \\"Bergslagen\\", IKEA:s 1700-tals serie","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord"''' # noqa - - result = self.read_csv(StringIO(data), escapechar='\\', - quotechar='"', encoding='utf-8') - self.assertEqual(result['SEARCH_TERM'][2], - 'SLAGBORD, "Bergslagen", IKEA:s 1700-tals serie') - self.assertTrue(np.array_equal(result.columns, - ['SEARCH_TERM', 'ACTUAL_URL'])) - - def test_int64_min_issues(self): - # #2599 - data = 'A,B\n0,0\n0,' - - result = self.read_csv(StringIO(data)) - expected = DataFrame({'A': [0, 0], 'B': [0, np.nan]}) - - tm.assert_frame_equal(result, expected) - - def test_parse_integers_above_fp_precision(self): - data = """Numbers -17007000002000191 -17007000002000191 -17007000002000191 -17007000002000191 -17007000002000192 -17007000002000192 -17007000002000192 -17007000002000192 -17007000002000192 -17007000002000194""" - - result = self.read_csv(StringIO(data)) - expected = DataFrame({'Numbers': [17007000002000191, - 17007000002000191, - 17007000002000191, - 17007000002000191, - 17007000002000192, - 17007000002000192, - 17007000002000192, - 17007000002000192, - 17007000002000192, - 17007000002000194]}) - - self.assertTrue(np.array_equal(result['Numbers'], expected['Numbers'])) - - def test_chunks_have_consistent_numerical_type(self): - integers = [str(i) for i in range(499999)] - data = "a\n" + "\n".join(integers + ["1.0", "2.0"] + integers) - - with tm.assert_produces_warning(False): - df = self.read_csv(StringIO(data)) - # Assert that types were coerced. - self.assertTrue(type(df.a[0]) is np.float64) - self.assertEqual(df.a.dtype, np.float) - - def test_warn_if_chunks_have_mismatched_type(self): - warning_type = False - integers = [str(i) for i in range(499999)] - data = "a\n" + "\n".join(integers + ['a', 'b'] + integers) - - # see gh-3866: if chunks are different types and can't - # be coerced using numerical types, then issue warning. - if self.engine == 'c' and self.low_memory: - warning_type = DtypeWarning - - with tm.assert_produces_warning(warning_type): - df = self.read_csv(StringIO(data)) - self.assertEqual(df.a.dtype, np.object) - - def test_integer_overflow_bug(self): - # see gh-2601 - data = "65248E10 11\n55555E55 22\n" - - result = self.read_csv(StringIO(data), header=None, sep=' ') - self.assertTrue(result[0].dtype == np.float64) - - result = self.read_csv(StringIO(data), header=None, sep=r'\s+') - self.assertTrue(result[0].dtype == np.float64) - - def test_catch_too_many_names(self): - # see gh-5156 - data = """\ -1,2,3 -4,,6 -7,8,9 -10,11,12\n""" - tm.assertRaises(ValueError, self.read_csv, StringIO(data), - header=0, names=['a', 'b', 'c', 'd']) - - def test_ignore_leading_whitespace(self): - # see gh-3374, gh-6607 - data = ' a b c\n 1 2 3\n 4 5 6\n 7 8 9' - result = self.read_table(StringIO(data), sep=r'\s+') - expected = DataFrame({'a': [1, 4, 7], 'b': [2, 5, 8], 'c': [3, 6, 9]}) - tm.assert_frame_equal(result, expected) - - def test_chunk_begins_with_newline_whitespace(self): - # see gh-10022 - data = '\n hello\nworld\n' - result = self.read_csv(StringIO(data), header=None) - self.assertEqual(len(result), 2) - - # see gh-9735: this issue is C parser-specific (bug when - # parsing whitespace and characters at chunk boundary) - if self.engine == 'c': - chunk1 = 'a' * (1024 * 256 - 2) + '\na' - chunk2 = '\n a' - result = self.read_csv(StringIO(chunk1 + chunk2), header=None) - expected = DataFrame(['a' * (1024 * 256 - 2), 'a', ' a']) - tm.assert_frame_equal(result, expected) - - def test_empty_with_index(self): - # see gh-10184 - data = 'x,y' - result = self.read_csv(StringIO(data), index_col=0) - expected = DataFrame([], columns=['y'], index=Index([], name='x')) - tm.assert_frame_equal(result, expected) - - def test_empty_with_multiindex(self): - # see gh-10467 - data = 'x,y,z' - result = self.read_csv(StringIO(data), index_col=['x', 'y']) - expected = DataFrame([], columns=['z'], - index=MultiIndex.from_arrays( - [[]] * 2, names=['x', 'y'])) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_reversed_multiindex(self): - data = 'x,y,z' - result = self.read_csv(StringIO(data), index_col=[1, 0]) - expected = DataFrame([], columns=['z'], - index=MultiIndex.from_arrays( - [[]] * 2, names=['y', 'x'])) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_float_parser(self): - # see gh-9565 - data = '45e-1,4.5,45.,inf,-inf' - result = self.read_csv(StringIO(data), header=None) - expected = DataFrame([[float(s) for s in data.split(',')]]) - tm.assert_frame_equal(result, expected) - - def test_scientific_no_exponent(self): - # see gh-12215 - df = DataFrame.from_items([('w', ['2e']), ('x', ['3E']), - ('y', ['42e']), ('z', ['632E'])]) - data = df.to_csv(index=False) - for prec in self.float_precision_choices: - df_roundtrip = self.read_csv( - StringIO(data), float_precision=prec) - tm.assert_frame_equal(df_roundtrip, df) - - def test_int64_overflow(self): - data = """ID -00013007854817840016671868 -00013007854817840016749251 -00013007854817840016754630 -00013007854817840016781876 -00013007854817840017028824 -00013007854817840017963235 -00013007854817840018860166""" - - # 13007854817840016671868 > UINT64_MAX, so this - # will overflow and return object as the dtype. - result = self.read_csv(StringIO(data)) - self.assertTrue(result['ID'].dtype == object) - - # 13007854817840016671868 > UINT64_MAX, so attempts - # to cast to either int64 or uint64 will result in - # an OverflowError being raised. - for conv in (np.int64, np.uint64): - self.assertRaises(OverflowError, self.read_csv, - StringIO(data), converters={'ID': conv}) - - # These numbers fall right inside the int64-uint64 range, - # so they should be parsed as string. - ui_max = np.iinfo(np.uint64).max - i_max = np.iinfo(np.int64).max - i_min = np.iinfo(np.int64).min - - for x in [i_max, i_min, ui_max]: - result = self.read_csv(StringIO(str(x)), header=None) - expected = DataFrame([x]) - tm.assert_frame_equal(result, expected) - - # These numbers fall just outside the int64-uint64 range, - # so they should be parsed as string. - too_big = ui_max + 1 - too_small = i_min - 1 - - for x in [too_big, too_small]: - result = self.read_csv(StringIO(str(x)), header=None) - expected = DataFrame([str(x)]) - tm.assert_frame_equal(result, expected) - - # No numerical dtype can hold both negative and uint64 values, - # so they should be cast as string. - data = '-1\n' + str(2**63) - expected = DataFrame([str(-1), str(2**63)]) - result = self.read_csv(StringIO(data), header=None) - tm.assert_frame_equal(result, expected) - - data = str(2**63) + '\n-1' - expected = DataFrame([str(2**63), str(-1)]) - result = self.read_csv(StringIO(data), header=None) - tm.assert_frame_equal(result, expected) - - def test_empty_with_nrows_chunksize(self): - # see gh-9535 - expected = DataFrame([], columns=['foo', 'bar']) - result = self.read_csv(StringIO('foo,bar\n'), nrows=10) - tm.assert_frame_equal(result, expected) - - result = next(iter(self.read_csv( - StringIO('foo,bar\n'), chunksize=10))) - tm.assert_frame_equal(result, expected) - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - result = self.read_csv(StringIO('foo,bar\n'), - nrows=10, as_recarray=True) - result = DataFrame(result[2], columns=result[1], - index=result[0]) - tm.assert_frame_equal(DataFrame.from_records( - result), expected, check_index_type=False) - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - result = next(iter(self.read_csv(StringIO('foo,bar\n'), - chunksize=10, as_recarray=True))) - result = DataFrame(result[2], columns=result[1], index=result[0]) - tm.assert_frame_equal(DataFrame.from_records(result), expected, - check_index_type=False) - - def test_eof_states(self): - # see gh-10728, gh-10548 - - # With skip_blank_lines = True - expected = DataFrame([[4, 5, 6]], columns=['a', 'b', 'c']) - - # gh-10728: WHITESPACE_LINE - data = 'a,b,c\n4,5,6\n ' - result = self.read_csv(StringIO(data)) - tm.assert_frame_equal(result, expected) - - # gh-10548: EAT_LINE_COMMENT - data = 'a,b,c\n4,5,6\n#comment' - result = self.read_csv(StringIO(data), comment='#') - tm.assert_frame_equal(result, expected) - - # EAT_CRNL_NOP - data = 'a,b,c\n4,5,6\n\r' - result = self.read_csv(StringIO(data)) - tm.assert_frame_equal(result, expected) - - # EAT_COMMENT - data = 'a,b,c\n4,5,6#comment' - result = self.read_csv(StringIO(data), comment='#') - tm.assert_frame_equal(result, expected) - - # SKIP_LINE - data = 'a,b,c\n4,5,6\nskipme' - result = self.read_csv(StringIO(data), skiprows=[2]) - tm.assert_frame_equal(result, expected) - - # With skip_blank_lines = False - - # EAT_LINE_COMMENT - data = 'a,b,c\n4,5,6\n#comment' - result = self.read_csv( - StringIO(data), comment='#', skip_blank_lines=False) - expected = DataFrame([[4, 5, 6]], columns=['a', 'b', 'c']) - tm.assert_frame_equal(result, expected) - - # IN_FIELD - data = 'a,b,c\n4,5,6\n ' - result = self.read_csv(StringIO(data), skip_blank_lines=False) - expected = DataFrame( - [['4', 5, 6], [' ', None, None]], columns=['a', 'b', 'c']) - tm.assert_frame_equal(result, expected) - - # EAT_CRNL - data = 'a,b,c\n4,5,6\n\r' - result = self.read_csv(StringIO(data), skip_blank_lines=False) - expected = DataFrame( - [[4, 5, 6], [None, None, None]], columns=['a', 'b', 'c']) - tm.assert_frame_equal(result, expected) - - # Should produce exceptions - - # ESCAPED_CHAR - data = "a,b,c\n4,5,6\n\\" - self.assertRaises(Exception, self.read_csv, - StringIO(data), escapechar='\\') - - # ESCAPE_IN_QUOTED_FIELD - data = 'a,b,c\n4,5,6\n"\\' - self.assertRaises(Exception, self.read_csv, - StringIO(data), escapechar='\\') - - # IN_QUOTED_FIELD - data = 'a,b,c\n4,5,6\n"' - self.assertRaises(Exception, self.read_csv, - StringIO(data), escapechar='\\') - - def test_uneven_lines_with_usecols(self): - # See gh-12203 - csv = r"""a,b,c - 0,1,2 - 3,4,5,6,7 - 8,9,10 - """ - - # make sure that an error is still thrown - # when the 'usecols' parameter is not provided - msg = r"Expected \d+ fields in line \d+, saw \d+" - with tm.assertRaisesRegexp(ValueError, msg): - df = self.read_csv(StringIO(csv)) - - expected = DataFrame({ - 'a': [0, 3, 8], - 'b': [1, 4, 9] - }) - - usecols = [0, 1] - df = self.read_csv(StringIO(csv), usecols=usecols) - tm.assert_frame_equal(df, expected) - - usecols = ['a', 'b'] - df = self.read_csv(StringIO(csv), usecols=usecols) - tm.assert_frame_equal(df, expected) - - def test_read_empty_with_usecols(self): - # See gh-12493 - names = ['Dummy', 'X', 'Dummy_2'] - usecols = names[1:2] # ['X'] - - # first, check to see that the response of - # parser when faced with no provided columns - # throws the correct error, with or without usecols - errmsg = "No columns to parse from file" - - with tm.assertRaisesRegexp(EmptyDataError, errmsg): - self.read_csv(StringIO('')) - - with tm.assertRaisesRegexp(EmptyDataError, errmsg): - self.read_csv(StringIO(''), usecols=usecols) - - expected = DataFrame(columns=usecols, index=[0], dtype=np.float64) - df = self.read_csv(StringIO(',,'), names=names, usecols=usecols) - tm.assert_frame_equal(df, expected) - - expected = DataFrame(columns=usecols) - df = self.read_csv(StringIO(''), names=names, usecols=usecols) - tm.assert_frame_equal(df, expected) - - def test_trailing_spaces(self): - data = "A B C \nrandom line with trailing spaces \nskip\n1,2,3\n1,2.,4.\nrandom line with trailing tabs\t\t\t\n \n5.1,NaN,10.0\n" # noqa - expected = DataFrame([[1., 2., 4.], - [5.1, np.nan, 10.]]) - - # gh-8661, gh-8679: this should ignore six lines including - # lines with trailing whitespace and blank lines - df = self.read_csv(StringIO(data.replace(',', ' ')), - header=None, delim_whitespace=True, - skiprows=[0, 1, 2, 3, 5, 6], skip_blank_lines=True) - tm.assert_frame_equal(df, expected) - df = self.read_table(StringIO(data.replace(',', ' ')), - header=None, delim_whitespace=True, - skiprows=[0, 1, 2, 3, 5, 6], - skip_blank_lines=True) - tm.assert_frame_equal(df, expected) - - # gh-8983: test skipping set of rows after a row with trailing spaces - expected = DataFrame({"A": [1., 5.1], "B": [2., np.nan], - "C": [4., 10]}) - df = self.read_table(StringIO(data.replace(',', ' ')), - delim_whitespace=True, - skiprows=[1, 2, 3, 5, 6], skip_blank_lines=True) - tm.assert_frame_equal(df, expected) - - def test_raise_on_sep_with_delim_whitespace(self): - # see gh-6607 - data = 'a b c\n1 2 3' - with tm.assertRaisesRegexp(ValueError, 'you can only specify one'): - self.read_table(StringIO(data), sep=r'\s', delim_whitespace=True) - - def test_single_char_leading_whitespace(self): - # see gh-9710 - data = """\ -MyColumn - a - b - a - b\n""" - - expected = DataFrame({'MyColumn': list('abab')}) - - result = self.read_csv(StringIO(data), delim_whitespace=True, - skipinitialspace=True) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), skipinitialspace=True) - tm.assert_frame_equal(result, expected) - - def test_empty_lines(self): - data = """\ -A,B,C -1,2.,4. - - -5.,NaN,10.0 - --70,.4,1 -""" - expected = np.array([[1., 2., 4.], - [5., np.nan, 10.], - [-70., .4, 1.]]) - df = self.read_csv(StringIO(data)) - tm.assert_numpy_array_equal(df.values, expected) - df = self.read_csv(StringIO(data.replace(',', ' ')), sep=r'\s+') - tm.assert_numpy_array_equal(df.values, expected) - expected = np.array([[1., 2., 4.], - [np.nan, np.nan, np.nan], - [np.nan, np.nan, np.nan], - [5., np.nan, 10.], - [np.nan, np.nan, np.nan], - [-70., .4, 1.]]) - df = self.read_csv(StringIO(data), skip_blank_lines=False) - tm.assert_numpy_array_equal(df.values, expected) - - def test_whitespace_lines(self): - data = """ - -\t \t\t - \t -A,B,C - \t 1,2.,4. -5.,NaN,10.0 -""" - expected = np.array([[1, 2., 4.], - [5., np.nan, 10.]]) - df = self.read_csv(StringIO(data)) - tm.assert_numpy_array_equal(df.values, expected) - - def test_regex_separator(self): - # see gh-6607 - data = """ A B C D -a 1 2 3 4 -b 1 2 3 4 -c 1 2 3 4 -""" - df = self.read_table(StringIO(data), sep=r'\s+') - expected = self.read_csv(StringIO(re.sub('[ ]+', ',', data)), - index_col=0) - self.assertIsNone(expected.index.name) - tm.assert_frame_equal(df, expected) - - data = ' a b c\n1 2 3 \n4 5 6\n 7 8 9' - result = self.read_table(StringIO(data), sep=r'\s+') - expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], - columns=['a', 'b', 'c']) - tm.assert_frame_equal(result, expected) - - def test_verbose_import(self): - text = """a,b,c,d -one,1,2,3 -one,1,2,3 -,1,2,3 -one,1,2,3 -,1,2,3 -,1,2,3 -one,1,2,3 -two,1,2,3""" - - buf = StringIO() - sys.stdout = buf - - try: # engines are verbose in different ways - self.read_csv(StringIO(text), verbose=True) - if self.engine == 'c': - self.assertIn('Tokenization took:', buf.getvalue()) - self.assertIn('Parser memory cleanup took:', buf.getvalue()) - else: # Python engine - self.assertEqual(buf.getvalue(), - 'Filled 3 NA values in column a\n') - finally: - sys.stdout = sys.__stdout__ - - buf = StringIO() - sys.stdout = buf - - text = """a,b,c,d -one,1,2,3 -two,1,2,3 -three,1,2,3 -four,1,2,3 -five,1,2,3 -,1,2,3 -seven,1,2,3 -eight,1,2,3""" - - try: # engines are verbose in different ways - self.read_csv(StringIO(text), verbose=True, index_col=0) - if self.engine == 'c': - self.assertIn('Tokenization took:', buf.getvalue()) - self.assertIn('Parser memory cleanup took:', buf.getvalue()) - else: # Python engine - self.assertEqual(buf.getvalue(), - 'Filled 1 NA values in column a\n') - finally: - sys.stdout = sys.__stdout__ - - def test_iteration_open_handle(self): - if PY3: - pytest.skip( - "won't work in Python 3 {0}".format(sys.version_info)) - - with tm.ensure_clean() as path: - with open(path, 'wb') as f: - f.write('AAA\nBBB\nCCC\nDDD\nEEE\nFFF\nGGG') - - with open(path, 'rb') as f: - for line in f: - if 'CCC' in line: - break - - if self.engine == 'c': - tm.assertRaises(Exception, self.read_table, - f, squeeze=True, header=None) - else: - result = self.read_table(f, squeeze=True, header=None) - expected = Series(['DDD', 'EEE', 'FFF', 'GGG'], name=0) - tm.assert_series_equal(result, expected) - - def test_1000_sep_with_decimal(self): - data = """A|B|C -1|2,334.01|5 -10|13|10. -""" - expected = DataFrame({ - 'A': [1, 10], - 'B': [2334.01, 13], - 'C': [5, 10.] - }) - - tm.assert_equal(expected.A.dtype, 'int64') - tm.assert_equal(expected.B.dtype, 'float') - tm.assert_equal(expected.C.dtype, 'float') - - df = self.read_csv(StringIO(data), sep='|', thousands=',', decimal='.') - tm.assert_frame_equal(df, expected) - - df = self.read_table(StringIO(data), sep='|', - thousands=',', decimal='.') - tm.assert_frame_equal(df, expected) - - data_with_odd_sep = """A|B|C -1|2.334,01|5 -10|13|10, -""" - df = self.read_csv(StringIO(data_with_odd_sep), - sep='|', thousands='.', decimal=',') - tm.assert_frame_equal(df, expected) - - df = self.read_table(StringIO(data_with_odd_sep), - sep='|', thousands='.', decimal=',') - tm.assert_frame_equal(df, expected) - - def test_euro_decimal_format(self): - data = """Id;Number1;Number2;Text1;Text2;Number3 -1;1521,1541;187101,9543;ABC;poi;4,738797819 -2;121,12;14897,76;DEF;uyt;0,377320872 -3;878,158;108013,434;GHI;rez;2,735694704""" - - df2 = self.read_csv(StringIO(data), sep=';', decimal=',') - self.assertEqual(df2['Number1'].dtype, float) - self.assertEqual(df2['Number2'].dtype, float) - self.assertEqual(df2['Number3'].dtype, float) - - def test_read_duplicate_names(self): - # See gh-7160 - data = "a,b,a\n0,1,2\n3,4,5" - df = self.read_csv(StringIO(data)) - expected = DataFrame([[0, 1, 2], [3, 4, 5]], - columns=['a', 'b', 'a.1']) - tm.assert_frame_equal(df, expected) - - data = "0,1,2\n3,4,5" - df = self.read_csv(StringIO(data), names=["a", "b", "a"]) - expected = DataFrame([[0, 1, 2], [3, 4, 5]], - columns=['a', 'b', 'a.1']) - tm.assert_frame_equal(df, expected) - - def test_inf_parsing(self): - data = """\ -,A -a,inf -b,-inf -c,+Inf -d,-Inf -e,INF -f,-INF -g,+INf -h,-INf -i,inF -j,-inF""" - inf = float('inf') - expected = Series([inf, -inf] * 5) - - df = self.read_csv(StringIO(data), index_col=0) - tm.assert_almost_equal(df['A'].values, expected.values) - - df = self.read_csv(StringIO(data), index_col=0, na_filter=False) - tm.assert_almost_equal(df['A'].values, expected.values) - - def test_raise_on_no_columns(self): - # single newline - data = "\n" - self.assertRaises(EmptyDataError, self.read_csv, StringIO(data)) - - # test with more than a single newline - data = "\n\n\n" - self.assertRaises(EmptyDataError, self.read_csv, StringIO(data)) - - def test_compact_ints_use_unsigned(self): - # see gh-13323 - data = 'a,b,c\n1,9,258' - - # sanity check - expected = DataFrame({ - 'a': np.array([1], dtype=np.int64), - 'b': np.array([9], dtype=np.int64), - 'c': np.array([258], dtype=np.int64), - }) - out = self.read_csv(StringIO(data)) - tm.assert_frame_equal(out, expected) - - expected = DataFrame({ - 'a': np.array([1], dtype=np.int8), - 'b': np.array([9], dtype=np.int8), - 'c': np.array([258], dtype=np.int16), - }) - - # default behaviour for 'use_unsigned' - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - out = self.read_csv(StringIO(data), compact_ints=True) - tm.assert_frame_equal(out, expected) - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - out = self.read_csv(StringIO(data), compact_ints=True, - use_unsigned=False) - tm.assert_frame_equal(out, expected) - - expected = DataFrame({ - 'a': np.array([1], dtype=np.uint8), - 'b': np.array([9], dtype=np.uint8), - 'c': np.array([258], dtype=np.uint16), - }) - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - out = self.read_csv(StringIO(data), compact_ints=True, - use_unsigned=True) - tm.assert_frame_equal(out, expected) - - def test_compact_ints_as_recarray(self): - data = ('0,1,0,0\n' - '1,1,0,0\n' - '0,1,0,1') - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - result = self.read_csv(StringIO(data), delimiter=',', header=None, - compact_ints=True, as_recarray=True) - ex_dtype = np.dtype([(str(i), 'i1') for i in range(4)]) - self.assertEqual(result.dtype, ex_dtype) - - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - result = self.read_csv(StringIO(data), delimiter=',', header=None, - as_recarray=True, compact_ints=True, - use_unsigned=True) - ex_dtype = np.dtype([(str(i), 'u1') for i in range(4)]) - self.assertEqual(result.dtype, ex_dtype) - - def test_as_recarray(self): - # basic test - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'a,b\n1,a\n2,b' - expected = np.array([(1, 'a'), (2, 'b')], - dtype=[('a', '=i8'), ('b', 'O')]) - out = self.read_csv(StringIO(data), as_recarray=True) - tm.assert_numpy_array_equal(out, expected) - - # index_col ignored - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'a,b\n1,a\n2,b' - expected = np.array([(1, 'a'), (2, 'b')], - dtype=[('a', '=i8'), ('b', 'O')]) - out = self.read_csv(StringIO(data), as_recarray=True, index_col=0) - tm.assert_numpy_array_equal(out, expected) - - # respects names - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = '1,a\n2,b' - expected = np.array([(1, 'a'), (2, 'b')], - dtype=[('a', '=i8'), ('b', 'O')]) - out = self.read_csv(StringIO(data), names=['a', 'b'], - header=None, as_recarray=True) - tm.assert_numpy_array_equal(out, expected) - - # header order is respected even though it conflicts - # with the natural ordering of the column names - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'b,a\n1,a\n2,b' - expected = np.array([(1, 'a'), (2, 'b')], - dtype=[('b', '=i8'), ('a', 'O')]) - out = self.read_csv(StringIO(data), as_recarray=True) - tm.assert_numpy_array_equal(out, expected) - - # overrides the squeeze parameter - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'a\n1' - expected = np.array([(1,)], dtype=[('a', '=i8')]) - out = self.read_csv(StringIO(data), as_recarray=True, squeeze=True) - tm.assert_numpy_array_equal(out, expected) - - # does data conversions before doing recarray conversion - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'a,b\n1,a\n2,b' - conv = lambda x: int(x) + 1 - expected = np.array([(2, 'a'), (3, 'b')], - dtype=[('a', '=i8'), ('b', 'O')]) - out = self.read_csv(StringIO(data), as_recarray=True, - converters={'a': conv}) - tm.assert_numpy_array_equal(out, expected) - - # filters by usecols before doing recarray conversion - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - data = 'a,b\n1,a\n2,b' - expected = np.array([(1,), (2,)], dtype=[('a', '=i8')]) - out = self.read_csv(StringIO(data), as_recarray=True, - usecols=['a']) - tm.assert_numpy_array_equal(out, expected) - - def test_memory_map(self): - mmap_file = os.path.join(self.dirpath, 'test_mmap.csv') - expected = DataFrame({ - 'a': [1, 2, 3], - 'b': ['one', 'two', 'three'], - 'c': ['I', 'II', 'III'] - }) - - out = self.read_csv(mmap_file, memory_map=True) - tm.assert_frame_equal(out, expected) - - def test_null_byte_char(self): - # see gh-2741 - data = '\x00,foo' - cols = ['a', 'b'] - - expected = DataFrame([[np.nan, 'foo']], - columns=cols) - - if self.engine == 'c': - out = self.read_csv(StringIO(data), names=cols) - tm.assert_frame_equal(out, expected) - else: - msg = "NULL byte detected" - with tm.assertRaisesRegexp(csv.Error, msg): - self.read_csv(StringIO(data), names=cols) - - def test_utf8_bom(self): - # see gh-4793 - bom = u('\ufeff') - utf8 = 'utf-8' - - def _encode_data_with_bom(_data): - bom_data = (bom + _data).encode(utf8) - return BytesIO(bom_data) - - # basic test - data = 'a\n1' - expected = DataFrame({'a': [1]}) - - out = self.read_csv(_encode_data_with_bom(data), - encoding=utf8) - tm.assert_frame_equal(out, expected) - - # test with "regular" quoting - data = '"a"\n1' - expected = DataFrame({'a': [1]}) - - out = self.read_csv(_encode_data_with_bom(data), - encoding=utf8, quotechar='"') - tm.assert_frame_equal(out, expected) - - # test in a data row instead of header - data = 'b\n1' - expected = DataFrame({'a': ['b', '1']}) - - out = self.read_csv(_encode_data_with_bom(data), - encoding=utf8, names=['a']) - tm.assert_frame_equal(out, expected) - - # test in empty data row with skipping - data = '\n1' - expected = DataFrame({'a': [1]}) - - out = self.read_csv(_encode_data_with_bom(data), - encoding=utf8, names=['a'], - skip_blank_lines=True) - tm.assert_frame_equal(out, expected) - - # test in empty data row without skipping - data = '\n1' - expected = DataFrame({'a': [np.nan, 1.0]}) - - out = self.read_csv(_encode_data_with_bom(data), - encoding=utf8, names=['a'], - skip_blank_lines=False) - tm.assert_frame_equal(out, expected) - - def test_temporary_file(self): - # see gh-13398 - data1 = "0 0" - - from tempfile import TemporaryFile - new_file = TemporaryFile("w+") - new_file.write(data1) - new_file.flush() - new_file.seek(0) - - result = self.read_csv(new_file, sep=r'\s+', header=None) - new_file.close() - expected = DataFrame([[0, 0]]) - tm.assert_frame_equal(result, expected) - - def test_read_csv_utf_aliases(self): - # see gh issue 13549 - expected = pd.DataFrame({'mb_num': [4.8], 'multibyte': ['test']}) - for byte in [8, 16]: - for fmt in ['utf-{0}', 'utf_{0}', 'UTF-{0}', 'UTF_{0}']: - encoding = fmt.format(byte) - data = 'mb_num,multibyte\n4.8,test'.encode(encoding) - result = self.read_csv(BytesIO(data), encoding=encoding) - tm.assert_frame_equal(result, expected) - - def test_internal_eof_byte(self): - # see gh-5500 - data = "a,b\n1\x1a,2" - - expected = pd.DataFrame([["1\x1a", 2]], columns=['a', 'b']) - result = self.read_csv(StringIO(data)) - tm.assert_frame_equal(result, expected) - - def test_file_handles(self): - # GH 14418 - don't close user provided file handles - - fh = StringIO('a,b\n1,2') - self.read_csv(fh) - self.assertFalse(fh.closed) - - with open(self.csv1, 'r') as f: - self.read_csv(f) - self.assertFalse(f.closed) - - # mmap not working with python engine - if self.engine != 'python': - - import mmap - with open(self.csv1, 'r') as f: - m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) - self.read_csv(m) - # closed attribute new in python 3.2 - if PY3: - self.assertFalse(m.closed) - m.close() diff --git a/pandas/tests/io/parser/compression.py b/pandas/tests/io/parser/compression.py deleted file mode 100644 index bdcd10fc64aa5..0000000000000 --- a/pandas/tests/io/parser/compression.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests compressed data parsing functionality for all -of the parsers defined in parsers.py -""" - -import pytest - -import pandas.util.testing as tm - - -class CompressionTests(object): - - def test_zip(self): - try: - import zipfile - except ImportError: - pytest.skip('need zipfile to run') - - with open(self.csv1, 'rb') as data_file: - data = data_file.read() - expected = self.read_csv(self.csv1) - - with tm.ensure_clean('test_file.zip') as path: - tmp = zipfile.ZipFile(path, mode='w') - tmp.writestr('test_file', data) - tmp.close() - - result = self.read_csv(path, compression='zip') - tm.assert_frame_equal(result, expected) - - result = self.read_csv(path, compression='infer') - tm.assert_frame_equal(result, expected) - - if self.engine is not 'python': - with open(path, 'rb') as f: - result = self.read_csv(f, compression='zip') - tm.assert_frame_equal(result, expected) - - with tm.ensure_clean('combined_zip.zip') as path: - inner_file_names = ['test_file', 'second_file'] - tmp = zipfile.ZipFile(path, mode='w') - for file_name in inner_file_names: - tmp.writestr(file_name, data) - tmp.close() - - self.assertRaisesRegexp(ValueError, 'Multiple files', - self.read_csv, path, compression='zip') - - self.assertRaisesRegexp(ValueError, 'Multiple files', - self.read_csv, path, compression='infer') - - with tm.ensure_clean() as path: - tmp = zipfile.ZipFile(path, mode='w') - tmp.close() - - self.assertRaisesRegexp(ValueError, 'Zero files', - self.read_csv, path, compression='zip') - - with tm.ensure_clean() as path: - with open(path, 'wb') as f: - self.assertRaises(zipfile.BadZipfile, self.read_csv, - f, compression='zip') - - def test_gzip(self): - try: - import gzip - except ImportError: - pytest.skip('need gzip to run') - - with open(self.csv1, 'rb') as data_file: - data = data_file.read() - expected = self.read_csv(self.csv1) - - with tm.ensure_clean() as path: - tmp = gzip.GzipFile(path, mode='wb') - tmp.write(data) - tmp.close() - - result = self.read_csv(path, compression='gzip') - tm.assert_frame_equal(result, expected) - - with open(path, 'rb') as f: - result = self.read_csv(f, compression='gzip') - tm.assert_frame_equal(result, expected) - - with tm.ensure_clean('test.gz') as path: - tmp = gzip.GzipFile(path, mode='wb') - tmp.write(data) - tmp.close() - result = self.read_csv(path, compression='infer') - tm.assert_frame_equal(result, expected) - - def test_bz2(self): - try: - import bz2 - except ImportError: - pytest.skip('need bz2 to run') - - with open(self.csv1, 'rb') as data_file: - data = data_file.read() - expected = self.read_csv(self.csv1) - - with tm.ensure_clean() as path: - tmp = bz2.BZ2File(path, mode='wb') - tmp.write(data) - tmp.close() - - result = self.read_csv(path, compression='bz2') - tm.assert_frame_equal(result, expected) - - self.assertRaises(ValueError, self.read_csv, - path, compression='bz3') - - with open(path, 'rb') as fin: - result = self.read_csv(fin, compression='bz2') - tm.assert_frame_equal(result, expected) - - with tm.ensure_clean('test.bz2') as path: - tmp = bz2.BZ2File(path, mode='wb') - tmp.write(data) - tmp.close() - result = self.read_csv(path, compression='infer') - tm.assert_frame_equal(result, expected) - - def test_xz(self): - lzma = tm._skip_if_no_lzma() - - with open(self.csv1, 'rb') as data_file: - data = data_file.read() - expected = self.read_csv(self.csv1) - - with tm.ensure_clean() as path: - tmp = lzma.LZMAFile(path, mode='wb') - tmp.write(data) - tmp.close() - - result = self.read_csv(path, compression='xz') - tm.assert_frame_equal(result, expected) - - with open(path, 'rb') as f: - result = self.read_csv(f, compression='xz') - tm.assert_frame_equal(result, expected) - - with tm.ensure_clean('test.xz') as path: - tmp = lzma.LZMAFile(path, mode='wb') - tmp.write(data) - tmp.close() - result = self.read_csv(path, compression='infer') - tm.assert_frame_equal(result, expected) - - def test_read_csv_infer_compression(self): - # see gh-9770 - expected = self.read_csv(self.csv1, index_col=0, parse_dates=True) - - inputs = [self.csv1, self.csv1 + '.gz', - self.csv1 + '.bz2', open(self.csv1)] - - for f in inputs: - df = self.read_csv(f, index_col=0, parse_dates=True, - compression='infer') - - tm.assert_frame_equal(expected, df) - - inputs[3].close() - - def test_invalid_compression(self): - msg = 'Unrecognized compression type: sfark' - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv('test_file.zip', compression='sfark') diff --git a/pandas/tests/io/parser/conftest.py b/pandas/tests/io/parser/conftest.py new file mode 100644 index 0000000000000..feb6c36b5178f --- /dev/null +++ b/pandas/tests/io/parser/conftest.py @@ -0,0 +1,85 @@ +import os + +import pytest + +from pandas import read_csv, read_table + + +class BaseParser(object): + engine = None + low_memory = True + float_precision_choices = [] + + def update_kwargs(self, kwargs): + kwargs = kwargs.copy() + kwargs.update(dict(engine=self.engine, + low_memory=self.low_memory)) + + return kwargs + + def read_csv(self, *args, **kwargs): + kwargs = self.update_kwargs(kwargs) + return read_csv(*args, **kwargs) + + def read_table(self, *args, **kwargs): + kwargs = self.update_kwargs(kwargs) + return read_table(*args, **kwargs) + + +class CParser(BaseParser): + engine = "c" + float_precision_choices = [None, "high", "round_trip"] + + +class CParserHighMemory(CParser): + low_memory = False + + +class CParserLowMemory(CParser): + low_memory = True + + +class PythonParser(BaseParser): + engine = "python" + float_precision_choices = [None] + + +@pytest.fixture +def csv_dir_path(datapath): + return datapath("io", "parser", "data") + + +@pytest.fixture +def csv1(csv_dir_path): + return os.path.join(csv_dir_path, "test1.csv") + + +_cParserHighMemory = CParserHighMemory() +_cParserLowMemory = CParserLowMemory() +_pythonParser = PythonParser() + +_py_parsers_only = [_pythonParser] +_c_parsers_only = [_cParserHighMemory, _cParserLowMemory] +_all_parsers = _c_parsers_only + _py_parsers_only + +_py_parser_ids = ["python"] +_c_parser_ids = ["c_high", "c_low"] +_all_parser_ids = _c_parser_ids + _py_parser_ids + + +@pytest.fixture(params=_all_parsers, + ids=_all_parser_ids) +def all_parsers(request): + return request.param + + +@pytest.fixture(params=_c_parsers_only, + ids=_c_parser_ids) +def c_parser_only(request): + return request.param + + +@pytest.fixture(params=_py_parsers_only, + ids=_py_parser_ids) +def python_parser_only(request): + return request.param diff --git a/pandas/tests/io/parser/converters.py b/pandas/tests/io/parser/converters.py deleted file mode 100644 index 2659d977ea747..0000000000000 --- a/pandas/tests/io/parser/converters.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests column conversion functionality during parsing -for all of the parsers defined in parsers.py -""" - -from datetime import datetime - -import pytest - -import numpy as np -import pandas as pd -import pandas.util.testing as tm - -from pandas._libs.lib import Timestamp -from pandas import DataFrame, Index -from pandas.compat import parse_date, StringIO, lmap - - -class ConverterTests(object): - - def test_converters_type_must_be_dict(self): - data = """index,A,B,C,D -foo,2,3,4,5 -""" - with tm.assertRaisesRegexp(TypeError, 'Type converters.+'): - self.read_csv(StringIO(data), converters=0) - - def test_converters(self): - data = """A,B,C,D -a,1,2,01/01/2009 -b,3,4,01/02/2009 -c,4,5,01/03/2009 -""" - result = self.read_csv(StringIO(data), converters={'D': parse_date}) - result2 = self.read_csv(StringIO(data), converters={3: parse_date}) - - expected = self.read_csv(StringIO(data)) - expected['D'] = expected['D'].map(parse_date) - - tm.assertIsInstance(result['D'][0], (datetime, Timestamp)) - tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result2, expected) - - # produce integer - converter = lambda x: int(x.split('/')[2]) - result = self.read_csv(StringIO(data), converters={'D': converter}) - expected = self.read_csv(StringIO(data)) - expected['D'] = expected['D'].map(converter) - tm.assert_frame_equal(result, expected) - - def test_converters_no_implicit_conv(self): - # see gh-2184 - data = """000102,1.2,A\n001245,2,B""" - f = lambda x: x.strip() - converter = {0: f} - df = self.read_csv(StringIO(data), header=None, converters=converter) - self.assertEqual(df[0].dtype, object) - - def test_converters_euro_decimal_format(self): - data = """Id;Number1;Number2;Text1;Text2;Number3 -1;1521,1541;187101,9543;ABC;poi;4,738797819 -2;121,12;14897,76;DEF;uyt;0,377320872 -3;878,158;108013,434;GHI;rez;2,735694704""" - f = lambda x: float(x.replace(",", ".")) - converter = {'Number1': f, 'Number2': f, 'Number3': f} - df2 = self.read_csv(StringIO(data), sep=';', converters=converter) - self.assertEqual(df2['Number1'].dtype, float) - self.assertEqual(df2['Number2'].dtype, float) - self.assertEqual(df2['Number3'].dtype, float) - - def test_converter_return_string_bug(self): - # see gh-583 - data = """Id;Number1;Number2;Text1;Text2;Number3 -1;1521,1541;187101,9543;ABC;poi;4,738797819 -2;121,12;14897,76;DEF;uyt;0,377320872 -3;878,158;108013,434;GHI;rez;2,735694704""" - f = lambda x: float(x.replace(",", ".")) - converter = {'Number1': f, 'Number2': f, 'Number3': f} - df2 = self.read_csv(StringIO(data), sep=';', converters=converter) - self.assertEqual(df2['Number1'].dtype, float) - - def test_converters_corner_with_nas(self): - # skip aberration observed on Win64 Python 3.2.2 - if hash(np.int64(-1)) != -2: - pytest.skip("skipping because of windows hash on Python" - " 3.2.2") - - data = """id,score,days -1,2,12 -2,2-5, -3,,14+ -4,6-12,2""" - - def convert_days(x): - x = x.strip() - if not x: - return np.nan - - is_plus = x.endswith('+') - if is_plus: - x = int(x[:-1]) + 1 - else: - x = int(x) - return x - - def convert_days_sentinel(x): - x = x.strip() - if not x: - return np.nan - - is_plus = x.endswith('+') - if is_plus: - x = int(x[:-1]) + 1 - else: - x = int(x) - return x - - def convert_score(x): - x = x.strip() - if not x: - return np.nan - if x.find('-') > 0: - valmin, valmax = lmap(int, x.split('-')) - val = 0.5 * (valmin + valmax) - else: - val = float(x) - - return val - - fh = StringIO(data) - result = self.read_csv(fh, converters={'score': convert_score, - 'days': convert_days}, - na_values=['', None]) - self.assertTrue(pd.isnull(result['days'][1])) - - fh = StringIO(data) - result2 = self.read_csv(fh, converters={'score': convert_score, - 'days': convert_days_sentinel}, - na_values=['', None]) - tm.assert_frame_equal(result, result2) - - def test_converter_index_col_bug(self): - # see gh-1835 - data = "A;B\n1;2\n3;4" - - rs = self.read_csv(StringIO(data), sep=';', index_col='A', - converters={'A': lambda x: x}) - - xp = DataFrame({'B': [2, 4]}, index=Index([1, 3], name='A')) - tm.assert_frame_equal(rs, xp) - self.assertEqual(rs.index.name, xp.index.name) diff --git a/pandas/tests/io/parser/data/items.jsonl b/pandas/tests/io/parser/data/items.jsonl new file mode 100644 index 0000000000000..f784d37befa82 --- /dev/null +++ b/pandas/tests/io/parser/data/items.jsonl @@ -0,0 +1,2 @@ +{"a": 1, "b": 2} +{"b":2, "a" :1} diff --git a/pandas/tests/io/parser/data/salaries.csv b/pandas/tests/io/parser/data/salaries.csv index ea7803339e98d..85631704ff6e0 100644 --- a/pandas/tests/io/parser/data/salaries.csv +++ b/pandas/tests/io/parser/data/salaries.csv @@ -1,47 +1,47 @@ S X E M -13876 1 1 1 -11608 1 3 0 -18701 1 3 1 -11283 1 2 0 -11767 1 3 0 -20872 2 2 1 -11772 2 2 0 -10535 2 1 0 -12195 2 3 0 -12313 3 2 0 -14975 3 1 1 -21371 3 2 1 -19800 3 3 1 -11417 4 1 0 -20263 4 3 1 -13231 4 3 0 -12884 4 2 0 -13245 5 2 0 -13677 5 3 0 -15965 5 1 1 -12336 6 1 0 -21352 6 3 1 -13839 6 2 0 -22884 6 2 1 -16978 7 1 1 -14803 8 2 0 -17404 8 1 1 -22184 8 3 1 -13548 8 1 0 -14467 10 1 0 -15942 10 2 0 -23174 10 3 1 -23780 10 2 1 -25410 11 2 1 -14861 11 1 0 -16882 12 2 0 -24170 12 3 1 -15990 13 1 0 -26330 13 2 1 -17949 14 2 0 -25685 15 3 1 -27837 16 2 1 -18838 16 2 0 -17483 16 1 0 -19207 17 2 0 -19346 20 1 0 +13876 1 1 1 +11608 1 3 0 +18701 1 3 1 +11283 1 2 0 +11767 1 3 0 +20872 2 2 1 +11772 2 2 0 +10535 2 1 0 +12195 2 3 0 +12313 3 2 0 +14975 3 1 1 +21371 3 2 1 +19800 3 3 1 +11417 4 1 0 +20263 4 3 1 +13231 4 3 0 +12884 4 2 0 +13245 5 2 0 +13677 5 3 0 +15965 5 1 1 +12336 6 1 0 +21352 6 3 1 +13839 6 2 0 +22884 6 2 1 +16978 7 1 1 +14803 8 2 0 +17404 8 1 1 +22184 8 3 1 +13548 8 1 0 +14467 10 1 0 +15942 10 2 0 +23174 10 3 1 +23780 10 2 1 +25410 11 2 1 +14861 11 1 0 +16882 12 2 0 +24170 12 3 1 +15990 13 1 0 +26330 13 2 1 +17949 14 2 0 +25685 15 3 1 +27837 16 2 1 +18838 16 2 0 +17483 16 1 0 +19207 17 2 0 +19346 20 1 0 diff --git a/pandas/tests/io/parser/data/sub_char.csv b/pandas/tests/io/parser/data/sub_char.csv new file mode 100644 index 0000000000000..ff1fa777832c7 --- /dev/null +++ b/pandas/tests/io/parser/data/sub_char.csv @@ -0,0 +1,2 @@ +a,"b",c +1,2,3 \ No newline at end of file diff --git a/pandas/tests/io/parser/data/tar_csv.tar b/pandas/tests/io/parser/data/tar_csv.tar new file mode 100644 index 0000000000000000000000000000000000000000..d1819550e0a0064b4d9ad829f120e49760c3ffe2 GIT binary patch literal 10240 zcmeIuK?;O03_#JW1@F)k3{BNsM}nR}J9B-TY7p4K!(9_GO$s`)68z@>0YS zW+wk!;+jjzL^~}framQ!s=W;o;!FQIwf(Nymk>_1JC}X6!*X|eRCwcUqis`RFe4E_ x009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|0009IZ32f(E6h{C6 literal 0 HcmV?d00001 diff --git a/pandas/tests/io/parser/data/tar_csv.tar.gz b/pandas/tests/io/parser/data/tar_csv.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..80505d345f1e2ffd298f35c9881d548b4e686676 GIT binary patch literal 117 zcmb2|=3oE;Cg!*24Y?W&1Y9nzWqm(E*kjs^Cl;GtaGF%rEuI>&TH(+2KS`@J&G;va zzCM;ZdHN5nr6t>TeUET$&f>6aztJ7uYuEkxQ)8*7nf3P*CtvTJzhT?OZGvEvVZ?#+ OZ+7nsQDM+vU;qF|YA}lc literal 0 HcmV?d00001 diff --git a/pandas/tests/io/parser/data/tips.csv.bz2 b/pandas/tests/io/parser/data/tips.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..1452896b05e9d41f58ffd816a0459d86796718a6 GIT binary patch literal 1316 zcmV+<1>5>UT4*^jL0KkKS@WgHUjPpp-+%xR00n>G1qTcuzHi=eU zr8L5NfM@_34^ia=nn@4@ngc)pXm>4ki*@VR?6|SRoF#LZ+TkL$)Z)}c<#mBig_KMX zruJeOi&bv;V=*04xP@hDQp(ibF*2pqxW%nuMr@F6Gix?+fsH|aKayy7UwGa_-`dVs zYfM$)R7$k8wpC6gfmM#M!-v|)iP#1h4cPkh|rkJNTD3*02| zUew#%bX<$c*~vCvMH>_%oV^S&6a+#ukskADG3ECrBRBE^v4aChy? zvDazQUv(jtyOFJd%+RitVq;Fo?$ru4tx8y4RWLAw3OQ&r5YZ6QA(|s=%EqEnNvFyDucBxbJ63X0f6|L)lrAb?vZoDHd%^>qwTK z8M-E+R_N`PibFFSF!cCl2Z7}>xeJ`*<3&DX2?dNalnbN*vYZ7QTLis}+CyTbyv{>s zl!hm_!_I4KZE}>uSzBr=*www83fCT-SPZ&+p@dCkFG(R6{D)ETHdAf-8>fnW#-GXdM4pE5VK!{hIp z4{*7H7hK39V*E6-z)7yKmA;#^4 z#PVN7@@@mJL*EhAX#`mH2SAk2lkhNXJBL>BHS&`^r&JS)>z58UjoYiOCqY*zmz*K6 z1SFlk-!Cn`6liVaz=_bPhSWpu1LJ>%Cxlk3T;w2WIQ0LRX3%vrxUPW z8d$X$uIXc_sI{9kN=EXFie6i&h29y!AZcb)r??rFOLu%3R3P<2gpt$oRe1O6gk~8T zu3j+kM{M-PhPbG60sxBGP*RgE)NL!@Yr%+f=+n7l@JL0;84IYj5yo31-0M)BHp<)Q zzkK_6UA}%i|M3mU6cFV&C+q8L8zqA-)xv!>^z@7=Fgi9q_iLEzwg+!G2w0Ts9jf*M z64F>g8RrtB4m-(FnM=?v>|@tRdI1$7H2kMsssN5^GU(*!z`p{ft@Qr;@_OlzdPSq# z=N&m=z8R{dV?dV-Iwe>fL1(0h{JJ}+<6sZ(@ePlLCs;FVmX?rYPxs1DA(^whpU+gQLdb{bOK!0;_ zkQW*TzXUDj{aqJ}zCZT`AFw?MCRq$YLmUun3sPt|TJ|F1y1->qh6EwxZc5srUOK?6 zfIOA24Gq;xs91xZWkXI-kgFkpK@VM+dImzp9WY2eRlGn`2@#FO*RJOK&vl0mX5&x| zsC*~R>SEi53Wfn0JC1s5&DImTC?CmS%t%KJn8SnJ{vz7Tu;z{(oX1Uj?2r-D=FHLg z#Nx)*tqL1*0`$uskSzVPPI~Zw87JK{kHS;|mjvLPazsSBBGTEE(XeUKcA)Oa1!1&{ ziGd~d!Xgpq$A_L=)+{U2btCFAD_NiGHe#QuSj!mhzmK3jN5V2e#ai_;@D^ZS3^-kH z6guhK*S?INWvhtT8n-^y8%I8HZbrKc2koF=btc|VG&cU-G4a~h=kf7qrTv=Ut%I~S zEXzKRMTs`<+xJ_K%nb(}Ie8d~S$W#@BiccQnPiO(+O^Yd9ou<9tf*;o$=WeUAZqAG zyzyj!F_p;rzPQ?Y92;+@To35Y<=xOSTm>@DJ;}6?*Lzr=TgaG9BIbr{y}$`b72TY! zqYYtgpVJv*bV|eFpvy$Pm>HFtbh_Na_)b19LfLd-0+3QVd;u1iG1e^0tsmq27&c@f zqhD+!jOz~T@n@5$<6yJqL9iFfH0&B9mSe(Zd*O_H&`()&cv#qX>*83gV@pnS)Uxa6 zh&!W4Kw{zbuyG*bJ30s^kL%1hKc#3Y!TLa1|HGI+q2~|%8;0j+sEAdd#O2^p#_J5{ zqk&o!uGkw*Xq2S)W72nPTLSJR3mF;xQOdr}*By;^C3XK=k7;*$ zylq6O8Vck|96AOM^M;z(GGMh%)?T{?8o*P+jIR3%VPB~S`#)bVj@Hps@zV;k&aoL? zJT_x>_m~9QgT~p5h literal 0 HcmV?d00001 diff --git a/pandas/tests/io/parser/data/utf16_ex_small.zip b/pandas/tests/io/parser/data/utf16_ex_small.zip new file mode 100644 index 0000000000000000000000000000000000000000..b0560c1b1f6c41307b575f2a86509021b49649f4 GIT binary patch literal 285 zcmWIWW@Zs#U|`^2c&?n{%@J5Cmki|10Ae8q8HUo5G()ra)Qb4x+{Bz5y^@NO&=5`r zX2v6bB0;#cf}4SnIF&U?+Ut|9%K z*xYyP>aL!jYp@|<#>O`+A~_|u(y}H_2)et1Z)sMQR;tn2-aQ>H(-D~jm`P25a**gJ;0ll4Wxq+ M2qS>>dJu;J0ImR5j{pDw literal 0 HcmV?d00001 diff --git a/pandas/tests/io/parser/dialect.py b/pandas/tests/io/parser/dialect.py deleted file mode 100644 index ee50cf812f72e..0000000000000 --- a/pandas/tests/io/parser/dialect.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that dialects are properly handled during parsing -for all of the parsers defined in parsers.py -""" - -import csv - -from pandas import DataFrame -from pandas.compat import StringIO -from pandas.io.common import ParserWarning - -import pandas.util.testing as tm - - -class DialectTests(object): - - def test_dialect(self): - data = """\ -label1,label2,label3 -index1,"a,c,e -index2,b,d,f -""" - - dia = csv.excel() - dia.quoting = csv.QUOTE_NONE - with tm.assert_produces_warning(ParserWarning): - df = self.read_csv(StringIO(data), dialect=dia) - - data = '''\ -label1,label2,label3 -index1,a,c,e -index2,b,d,f -''' - exp = self.read_csv(StringIO(data)) - exp.replace('a', '"a', inplace=True) - tm.assert_frame_equal(df, exp) - - def test_dialect_str(self): - data = """\ -fruit:vegetable -apple:brocolli -pear:tomato -""" - exp = DataFrame({ - 'fruit': ['apple', 'pear'], - 'vegetable': ['brocolli', 'tomato'] - }) - csv.register_dialect('mydialect', delimiter=':') - with tm.assert_produces_warning(ParserWarning): - df = self.read_csv(StringIO(data), dialect='mydialect') - - tm.assert_frame_equal(df, exp) - csv.unregister_dialect('mydialect') - - def test_invalid_dialect(self): - class InvalidDialect(object): - pass - - data = 'a\n1' - msg = 'Invalid dialect' - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(data), dialect=InvalidDialect) - - def test_dialect_conflict(self): - data = 'a,b\n1,2' - dialect = 'excel' - exp = DataFrame({'a': [1], 'b': [2]}) - - with tm.assert_produces_warning(None): - df = self.read_csv(StringIO(data), delimiter=',', dialect=dialect) - tm.assert_frame_equal(df, exp) - - with tm.assert_produces_warning(ParserWarning): - df = self.read_csv(StringIO(data), delimiter='.', dialect=dialect) - tm.assert_frame_equal(df, exp) diff --git a/pandas/tests/io/parser/dtypes.py b/pandas/tests/io/parser/dtypes.py deleted file mode 100644 index fa95c18c4d7a9..0000000000000 --- a/pandas/tests/io/parser/dtypes.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests dtype specification during parsing -for all of the parsers defined in parsers.py -""" - -import numpy as np -import pandas as pd -import pandas.util.testing as tm - -from pandas import DataFrame, Series, Index, MultiIndex, Categorical -from pandas.compat import StringIO -from pandas.types.dtypes import CategoricalDtype -from pandas.io.common import ParserWarning - - -class DtypeTests(object): - - def test_passing_dtype(self): - # see gh-6607 - df = DataFrame(np.random.rand(5, 2).round(4), columns=list( - 'AB'), index=['1A', '1B', '1C', '1D', '1E']) - - with tm.ensure_clean('__passing_str_as_dtype__.csv') as path: - df.to_csv(path) - - # see gh-3795: passing 'str' as the dtype - result = self.read_csv(path, dtype=str, index_col=0) - expected = df.astype(str) - tm.assert_frame_equal(result, expected) - - # for parsing, interpret object as str - result = self.read_csv(path, dtype=object, index_col=0) - tm.assert_frame_equal(result, expected) - - # we expect all object columns, so need to - # convert to test for equivalence - result = result.astype(float) - tm.assert_frame_equal(result, df) - - # invalid dtype - self.assertRaises(TypeError, self.read_csv, path, - dtype={'A': 'foo', 'B': 'float64'}, - index_col=0) - - # see gh-12048: empty frame - actual = self.read_csv(StringIO('A,B'), dtype=str) - expected = DataFrame({'A': [], 'B': []}, index=[], dtype=str) - tm.assert_frame_equal(actual, expected) - - def test_pass_dtype(self): - data = """\ -one,two -1,2.5 -2,3.5 -3,4.5 -4,5.5""" - - result = self.read_csv(StringIO(data), dtype={'one': 'u1', 1: 'S1'}) - self.assertEqual(result['one'].dtype, 'u1') - self.assertEqual(result['two'].dtype, 'object') - - def test_categorical_dtype(self): - # GH 10153 - data = """a,b,c -1,a,3.4 -1,a,3.4 -2,b,4.5""" - expected = pd.DataFrame({'a': Categorical(['1', '1', '2']), - 'b': Categorical(['a', 'a', 'b']), - 'c': Categorical(['3.4', '3.4', '4.5'])}) - actual = self.read_csv(StringIO(data), dtype='category') - tm.assert_frame_equal(actual, expected) - - actual = self.read_csv(StringIO(data), dtype=CategoricalDtype()) - tm.assert_frame_equal(actual, expected) - - actual = self.read_csv(StringIO(data), dtype={'a': 'category', - 'b': 'category', - 'c': CategoricalDtype()}) - tm.assert_frame_equal(actual, expected) - - actual = self.read_csv(StringIO(data), dtype={'b': 'category'}) - expected = pd.DataFrame({'a': [1, 1, 2], - 'b': Categorical(['a', 'a', 'b']), - 'c': [3.4, 3.4, 4.5]}) - tm.assert_frame_equal(actual, expected) - - actual = self.read_csv(StringIO(data), dtype={1: 'category'}) - tm.assert_frame_equal(actual, expected) - - # unsorted - data = """a,b,c -1,b,3.4 -1,b,3.4 -2,a,4.5""" - expected = pd.DataFrame({'a': Categorical(['1', '1', '2']), - 'b': Categorical(['b', 'b', 'a']), - 'c': Categorical(['3.4', '3.4', '4.5'])}) - actual = self.read_csv(StringIO(data), dtype='category') - tm.assert_frame_equal(actual, expected) - - # missing - data = """a,b,c -1,b,3.4 -1,nan,3.4 -2,a,4.5""" - expected = pd.DataFrame({'a': Categorical(['1', '1', '2']), - 'b': Categorical(['b', np.nan, 'a']), - 'c': Categorical(['3.4', '3.4', '4.5'])}) - actual = self.read_csv(StringIO(data), dtype='category') - tm.assert_frame_equal(actual, expected) - - def test_categorical_dtype_encoding(self): - # GH 10153 - pth = tm.get_data_path('unicode_series.csv') - encoding = 'latin-1' - expected = self.read_csv(pth, header=None, encoding=encoding) - expected[1] = Categorical(expected[1]) - actual = self.read_csv(pth, header=None, encoding=encoding, - dtype={1: 'category'}) - tm.assert_frame_equal(actual, expected) - - pth = tm.get_data_path('utf16_ex.txt') - encoding = 'utf-16' - expected = self.read_table(pth, encoding=encoding) - expected = expected.apply(Categorical) - actual = self.read_table(pth, encoding=encoding, dtype='category') - tm.assert_frame_equal(actual, expected) - - def test_categorical_dtype_chunksize(self): - # GH 10153 - data = """a,b -1,a -1,b -1,b -2,c""" - expecteds = [pd.DataFrame({'a': [1, 1], - 'b': Categorical(['a', 'b'])}), - pd.DataFrame({'a': [1, 2], - 'b': Categorical(['b', 'c'])}, - index=[2, 3])] - actuals = self.read_csv(StringIO(data), dtype={'b': 'category'}, - chunksize=2) - - for actual, expected in zip(actuals, expecteds): - tm.assert_frame_equal(actual, expected) - - def test_empty_pass_dtype(self): - data = 'one,two' - result = self.read_csv(StringIO(data), dtype={'one': 'u1'}) - - expected = DataFrame({'one': np.empty(0, dtype='u1'), - 'two': np.empty(0, dtype=np.object)}) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_index_pass_dtype(self): - data = 'one,two' - result = self.read_csv(StringIO(data), index_col=['one'], - dtype={'one': 'u1', 1: 'f'}) - - expected = DataFrame({'two': np.empty(0, dtype='f')}, - index=Index([], dtype='u1', name='one')) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_multiindex_pass_dtype(self): - data = 'one,two,three' - result = self.read_csv(StringIO(data), index_col=['one', 'two'], - dtype={'one': 'u1', 1: 'f8'}) - - exp_idx = MultiIndex.from_arrays([np.empty(0, dtype='u1'), - np.empty(0, dtype='O')], - names=['one', 'two']) - expected = DataFrame( - {'three': np.empty(0, dtype=np.object)}, index=exp_idx) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_mangled_column_pass_dtype_by_names(self): - data = 'one,one' - result = self.read_csv(StringIO(data), dtype={ - 'one': 'u1', 'one.1': 'f'}) - - expected = DataFrame( - {'one': np.empty(0, dtype='u1'), 'one.1': np.empty(0, dtype='f')}) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_mangled_column_pass_dtype_by_indexes(self): - data = 'one,one' - result = self.read_csv(StringIO(data), dtype={0: 'u1', 1: 'f'}) - - expected = DataFrame( - {'one': np.empty(0, dtype='u1'), 'one.1': np.empty(0, dtype='f')}) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_empty_with_dup_column_pass_dtype_by_indexes(self): - # see gh-9424 - expected = pd.concat([Series([], name='one', dtype='u1'), - Series([], name='one.1', dtype='f')], axis=1) - - data = 'one,one' - result = self.read_csv(StringIO(data), dtype={0: 'u1', 1: 'f'}) - tm.assert_frame_equal(result, expected, check_index_type=False) - - data = '' - result = self.read_csv(StringIO(data), names=['one', 'one'], - dtype={0: 'u1', 1: 'f'}) - tm.assert_frame_equal(result, expected, check_index_type=False) - - def test_raise_on_passed_int_dtype_with_nas(self): - # see gh-2631 - data = """YEAR, DOY, a -2001,106380451,10 -2001,,11 -2001,106380451,67""" - self.assertRaises(ValueError, self.read_csv, StringIO(data), - sep=",", skipinitialspace=True, - dtype={'DOY': np.int64}) - - def test_dtype_with_converter(self): - data = """a,b -1.1,2.2 -1.2,2.3""" - # dtype spec ignored if converted specified - with tm.assert_produces_warning(ParserWarning): - result = self.read_csv(StringIO(data), dtype={'a': 'i8'}, - converters={'a': lambda x: str(x)}) - expected = DataFrame({'a': ['1.1', '1.2'], 'b': [2.2, 2.3]}) - tm.assert_frame_equal(result, expected) - - def test_empty_dtype(self): - # see gh-14712 - data = 'a,b' - - expected = pd.DataFrame(columns=['a', 'b'], dtype=np.float64) - result = self.read_csv(StringIO(data), header=0, dtype=np.float64) - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame({'a': pd.Categorical([]), - 'b': pd.Categorical([])}, - index=[]) - result = self.read_csv(StringIO(data), header=0, - dtype='category') - tm.assert_frame_equal(result, expected) - result = self.read_csv(StringIO(data), header=0, - dtype={'a': 'category', 'b': 'category'}) - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame(columns=['a', 'b'], dtype='datetime64[ns]') - result = self.read_csv(StringIO(data), header=0, - dtype='datetime64[ns]') - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame({'a': pd.Series([], dtype='timedelta64[ns]'), - 'b': pd.Series([], dtype='timedelta64[ns]')}, - index=[]) - result = self.read_csv(StringIO(data), header=0, - dtype='timedelta64[ns]') - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame(columns=['a', 'b']) - expected['a'] = expected['a'].astype(np.float64) - result = self.read_csv(StringIO(data), header=0, - dtype={'a': np.float64}) - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame(columns=['a', 'b']) - expected['a'] = expected['a'].astype(np.float64) - result = self.read_csv(StringIO(data), header=0, - dtype={0: np.float64}) - tm.assert_frame_equal(result, expected) - - expected = pd.DataFrame(columns=['a', 'b']) - expected['a'] = expected['a'].astype(np.int32) - expected['b'] = expected['b'].astype(np.float64) - result = self.read_csv(StringIO(data), header=0, - dtype={'a': np.int32, 1: np.float64}) - tm.assert_frame_equal(result, expected) - - def test_numeric_dtype(self): - data = '0\n1' - - for dt in np.typecodes['AllInteger'] + np.typecodes['Float']: - expected = pd.DataFrame([0, 1], dtype=dt) - result = self.read_csv(StringIO(data), header=None, dtype=dt) - tm.assert_frame_equal(expected, result) diff --git a/pandas/tests/io/parser/header.py b/pandas/tests/io/parser/header.py deleted file mode 100644 index dc6d2ad1daa47..0000000000000 --- a/pandas/tests/io/parser/header.py +++ /dev/null @@ -1,277 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that the file header is properly handled or inferred -during parsing for all of the parsers defined in parsers.py -""" - -import numpy as np -import pandas.util.testing as tm - -from pandas import DataFrame, Index, MultiIndex -from pandas.compat import StringIO, lrange, u - - -class HeaderTests(object): - - def test_read_with_bad_header(self): - errmsg = r"but only \d+ lines in file" - - with tm.assertRaisesRegexp(ValueError, errmsg): - s = StringIO(',,') - self.read_csv(s, header=[10]) - - def test_bool_header_arg(self): - # see gh-6114 - data = """\ -MyColumn - a - b - a - b""" - for arg in [True, False]: - with tm.assertRaises(TypeError): - self.read_csv(StringIO(data), header=arg) - with tm.assertRaises(TypeError): - self.read_table(StringIO(data), header=arg) - - def test_no_header_prefix(self): - data = """1,2,3,4,5 -6,7,8,9,10 -11,12,13,14,15 -""" - df_pref = self.read_table(StringIO(data), sep=',', prefix='Field', - header=None) - - expected = np.array([[1, 2, 3, 4, 5], - [6, 7, 8, 9, 10], - [11, 12, 13, 14, 15]], dtype=np.int64) - tm.assert_almost_equal(df_pref.values, expected) - - self.assert_index_equal(df_pref.columns, - Index(['Field0', 'Field1', 'Field2', - 'Field3', 'Field4'])) - - def test_header_with_index_col(self): - data = """foo,1,2,3 -bar,4,5,6 -baz,7,8,9 -""" - names = ['A', 'B', 'C'] - df = self.read_csv(StringIO(data), names=names) - - self.assertEqual(names, ['A', 'B', 'C']) - - values = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] - expected = DataFrame(values, index=['foo', 'bar', 'baz'], - columns=['A', 'B', 'C']) - tm.assert_frame_equal(df, expected) - - def test_header_not_first_line(self): - data = """got,to,ignore,this,line -got,to,ignore,this,line -index,A,B,C,D -foo,2,3,4,5 -bar,7,8,9,10 -baz,12,13,14,15 -""" - data2 = """index,A,B,C,D -foo,2,3,4,5 -bar,7,8,9,10 -baz,12,13,14,15 -""" - - df = self.read_csv(StringIO(data), header=2, index_col=0) - expected = self.read_csv(StringIO(data2), header=0, index_col=0) - tm.assert_frame_equal(df, expected) - - def test_header_multi_index(self): - expected = tm.makeCustomDataframe( - 5, 3, r_idx_nlevels=2, c_idx_nlevels=4) - - data = """\ -C0,,C_l0_g0,C_l0_g1,C_l0_g2 - -C1,,C_l1_g0,C_l1_g1,C_l1_g2 -C2,,C_l2_g0,C_l2_g1,C_l2_g2 -C3,,C_l3_g0,C_l3_g1,C_l3_g2 -R0,R1,,, -R_l0_g0,R_l1_g0,R0C0,R0C1,R0C2 -R_l0_g1,R_l1_g1,R1C0,R1C1,R1C2 -R_l0_g2,R_l1_g2,R2C0,R2C1,R2C2 -R_l0_g3,R_l1_g3,R3C0,R3C1,R3C2 -R_l0_g4,R_l1_g4,R4C0,R4C1,R4C2 -""" - - df = self.read_csv(StringIO(data), header=[0, 1, 2, 3], index_col=[ - 0, 1], tupleize_cols=False) - tm.assert_frame_equal(df, expected) - - # skipping lines in the header - df = self.read_csv(StringIO(data), header=[0, 1, 2, 3], index_col=[ - 0, 1], tupleize_cols=False) - tm.assert_frame_equal(df, expected) - - # INVALID OPTIONS - - # no as_recarray - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - self.assertRaises(ValueError, self.read_csv, - StringIO(data), header=[0, 1, 2, 3], - index_col=[0, 1], as_recarray=True, - tupleize_cols=False) - - # names - self.assertRaises(ValueError, self.read_csv, - StringIO(data), header=[0, 1, 2, 3], - index_col=[0, 1], names=['foo', 'bar'], - tupleize_cols=False) - - # usecols - self.assertRaises(ValueError, self.read_csv, - StringIO(data), header=[0, 1, 2, 3], - index_col=[0, 1], usecols=['foo', 'bar'], - tupleize_cols=False) - - # non-numeric index_col - self.assertRaises(ValueError, self.read_csv, - StringIO(data), header=[0, 1, 2, 3], - index_col=['foo', 'bar'], tupleize_cols=False) - - def test_header_multiindex_common_format(self): - - df = DataFrame([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], - index=['one', 'two'], - columns=MultiIndex.from_tuples( - [('a', 'q'), ('a', 'r'), ('a', 's'), - ('b', 't'), ('c', 'u'), ('c', 'v')])) - - # to_csv - data = """,a,a,a,b,c,c -,q,r,s,t,u,v -,,,,,, -one,1,2,3,4,5,6 -two,7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=0) - tm.assert_frame_equal(df, result) - - # common - data = """,a,a,a,b,c,c -,q,r,s,t,u,v -one,1,2,3,4,5,6 -two,7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=0) - tm.assert_frame_equal(df, result) - - # common, no index_col - data = """a,a,a,b,c,c -q,r,s,t,u,v -1,2,3,4,5,6 -7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=None) - tm.assert_frame_equal(df.reset_index(drop=True), result) - - # malformed case 1 - expected = DataFrame(np.array( - [[2, 3, 4, 5, 6], [8, 9, 10, 11, 12]], dtype='int64'), - index=Index([1, 7]), - columns=MultiIndex(levels=[[u('a'), u('b'), u('c')], - [u('r'), u('s'), u('t'), - u('u'), u('v')]], - labels=[[0, 0, 1, 2, 2], [0, 1, 2, 3, 4]], - names=[u('a'), u('q')])) - - data = """a,a,a,b,c,c -q,r,s,t,u,v -1,2,3,4,5,6 -7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=0) - tm.assert_frame_equal(expected, result) - - # malformed case 2 - expected = DataFrame(np.array( - [[2, 3, 4, 5, 6], [8, 9, 10, 11, 12]], dtype='int64'), - index=Index([1, 7]), - columns=MultiIndex(levels=[[u('a'), u('b'), u('c')], - [u('r'), u('s'), u('t'), - u('u'), u('v')]], - labels=[[0, 0, 1, 2, 2], [0, 1, 2, 3, 4]], - names=[None, u('q')])) - - data = """,a,a,b,c,c -q,r,s,t,u,v -1,2,3,4,5,6 -7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=0) - tm.assert_frame_equal(expected, result) - - # mi on columns and index (malformed) - expected = DataFrame(np.array( - [[3, 4, 5, 6], [9, 10, 11, 12]], dtype='int64'), - index=MultiIndex(levels=[[1, 7], [2, 8]], - labels=[[0, 1], [0, 1]]), - columns=MultiIndex(levels=[[u('a'), u('b'), u('c')], - [u('s'), u('t'), u('u'), u('v')]], - labels=[[0, 1, 2, 2], [0, 1, 2, 3]], - names=[None, u('q')])) - - data = """,a,a,b,c,c -q,r,s,t,u,v -1,2,3,4,5,6 -7,8,9,10,11,12""" - - result = self.read_csv(StringIO(data), header=[0, 1], index_col=[0, 1]) - tm.assert_frame_equal(expected, result) - - def test_header_names_backward_compat(self): - # #2539 - data = '1,2,3\n4,5,6' - - result = self.read_csv(StringIO(data), names=['a', 'b', 'c']) - expected = self.read_csv(StringIO(data), names=['a', 'b', 'c'], - header=None) - tm.assert_frame_equal(result, expected) - - data2 = 'foo,bar,baz\n' + data - result = self.read_csv(StringIO(data2), names=['a', 'b', 'c'], - header=0) - tm.assert_frame_equal(result, expected) - - def test_read_only_header_no_rows(self): - # See gh-7773 - expected = DataFrame(columns=['a', 'b', 'c']) - - df = self.read_csv(StringIO('a,b,c')) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO('a,b,c'), index_col=False) - tm.assert_frame_equal(df, expected) - - def test_no_header(self): - data = """1,2,3,4,5 -6,7,8,9,10 -11,12,13,14,15 -""" - df = self.read_table(StringIO(data), sep=',', header=None) - df_pref = self.read_table(StringIO(data), sep=',', prefix='X', - header=None) - - names = ['foo', 'bar', 'baz', 'quux', 'panda'] - df2 = self.read_table(StringIO(data), sep=',', names=names) - expected = np.array([[1, 2, 3, 4, 5], - [6, 7, 8, 9, 10], - [11, 12, 13, 14, 15]], dtype=np.int64) - tm.assert_almost_equal(df.values, expected) - tm.assert_almost_equal(df.values, df2.values) - - self.assert_index_equal(df_pref.columns, - Index(['X0', 'X1', 'X2', 'X3', 'X4'])) - self.assert_index_equal(df.columns, Index(lrange(5))) - - self.assert_index_equal(df2.columns, Index(names)) diff --git a/pandas/tests/io/parser/index_col.py b/pandas/tests/io/parser/index_col.py deleted file mode 100644 index 6eb15eb3e043c..0000000000000 --- a/pandas/tests/io/parser/index_col.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that the specified index column (a.k.a 'index_col') -is properly handled or inferred during parsing for all of -the parsers defined in parsers.py -""" - -import pandas.util.testing as tm - -from pandas import DataFrame, Index, MultiIndex -from pandas.compat import StringIO - - -class IndexColTests(object): - - def test_index_col_named(self): - no_header = """\ -KORD1,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD2,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD3,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD4,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD5,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD6,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000""" # noqa - - h = "ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir\n" # noqa - data = h + no_header - rs = self.read_csv(StringIO(data), index_col='ID') - xp = self.read_csv(StringIO(data), header=0).set_index('ID') - tm.assert_frame_equal(rs, xp) - - self.assertRaises(ValueError, self.read_csv, StringIO(no_header), - index_col='ID') - - data = """\ -1,2,3,4,hello -5,6,7,8,world -9,10,11,12,foo -""" - names = ['a', 'b', 'c', 'd', 'message'] - xp = DataFrame({'a': [1, 5, 9], 'b': [2, 6, 10], 'c': [3, 7, 11], - 'd': [4, 8, 12]}, - index=Index(['hello', 'world', 'foo'], name='message')) - rs = self.read_csv(StringIO(data), names=names, index_col=['message']) - tm.assert_frame_equal(xp, rs) - self.assertEqual(xp.index.name, rs.index.name) - - rs = self.read_csv(StringIO(data), names=names, index_col='message') - tm.assert_frame_equal(xp, rs) - self.assertEqual(xp.index.name, rs.index.name) - - def test_index_col_is_true(self): - # see gh-9798 - self.assertRaises(ValueError, self.read_csv, - StringIO(self.ts_data), index_col=True) - - def test_infer_index_col(self): - data = """A,B,C -foo,1,2,3 -bar,4,5,6 -baz,7,8,9 -""" - data = self.read_csv(StringIO(data)) - self.assertTrue(data.index.equals(Index(['foo', 'bar', 'baz']))) - - def test_empty_index_col_scenarios(self): - data = 'x,y,z' - - # None, no index - index_col, expected = None, DataFrame([], columns=list('xyz')), - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # False, no index - index_col, expected = False, DataFrame([], columns=list('xyz')), - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # int, first column - index_col, expected = 0, DataFrame( - [], columns=['y', 'z'], index=Index([], name='x')) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # int, not first column - index_col, expected = 1, DataFrame( - [], columns=['x', 'z'], index=Index([], name='y')) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # str, first column - index_col, expected = 'x', DataFrame( - [], columns=['y', 'z'], index=Index([], name='x')) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # str, not the first column - index_col, expected = 'y', DataFrame( - [], columns=['x', 'z'], index=Index([], name='y')) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), expected) - - # list of int - index_col, expected = [0, 1], DataFrame( - [], columns=['z'], index=MultiIndex.from_arrays( - [[]] * 2, names=['x', 'y'])) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), - expected, check_index_type=False) - - # list of str - index_col = ['x', 'y'] - expected = DataFrame([], columns=['z'], - index=MultiIndex.from_arrays( - [[]] * 2, names=['x', 'y'])) - tm.assert_frame_equal(self.read_csv(StringIO( - data), index_col=index_col), - expected, check_index_type=False) - - # list of int, reversed sequence - index_col = [1, 0] - expected = DataFrame([], columns=['z'], index=MultiIndex.from_arrays( - [[]] * 2, names=['y', 'x'])) - tm.assert_frame_equal(self.read_csv( - StringIO(data), index_col=index_col), - expected, check_index_type=False) - - # list of str, reversed sequence - index_col = ['y', 'x'] - expected = DataFrame([], columns=['z'], index=MultiIndex.from_arrays( - [[]] * 2, names=['y', 'x'])) - tm.assert_frame_equal(self.read_csv(StringIO( - data), index_col=index_col), - expected, check_index_type=False) - - def test_empty_with_index_col_false(self): - # see gh-10413 - data = 'x,y' - result = self.read_csv(StringIO(data), index_col=False) - expected = DataFrame([], columns=['x', 'y']) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/multithread.py b/pandas/tests/io/parser/multithread.py deleted file mode 100644 index 2aaef889db6de..0000000000000 --- a/pandas/tests/io/parser/multithread.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests multithreading behaviour for reading and -parsing files for each parser defined in parsers.py -""" - -from __future__ import division -from multiprocessing.pool import ThreadPool - -import numpy as np -import pandas as pd -import pandas.util.testing as tm - -from pandas import DataFrame -from pandas.compat import BytesIO, range - - -def _construct_dataframe(num_rows): - - df = DataFrame(np.random.rand(num_rows, 5), columns=list('abcde')) - df['foo'] = 'foo' - df['bar'] = 'bar' - df['baz'] = 'baz' - df['date'] = pd.date_range('20000101 09:00:00', - periods=num_rows, - freq='s') - df['int'] = np.arange(num_rows, dtype='int64') - return df - - -class MultithreadTests(object): - - def _generate_multithread_dataframe(self, path, num_rows, num_tasks): - - def reader(arg): - start, nrows = arg - - if not start: - return self.read_csv(path, index_col=0, header=0, - nrows=nrows, parse_dates=['date']) - - return self.read_csv(path, - index_col=0, - header=None, - skiprows=int(start) + 1, - nrows=nrows, - parse_dates=[9]) - - tasks = [ - (num_rows * i // num_tasks, - num_rows // num_tasks) for i in range(num_tasks) - ] - - pool = ThreadPool(processes=num_tasks) - - results = pool.map(reader, tasks) - - header = results[0].columns - for r in results[1:]: - r.columns = header - - final_dataframe = pd.concat(results) - - return final_dataframe - - def test_multithread_stringio_read_csv(self): - # see gh-11786 - max_row_range = 10000 - num_files = 100 - - bytes_to_df = [ - '\n'.join( - ['%d,%d,%d' % (i, i, i) for i in range(max_row_range)] - ).encode() for j in range(num_files)] - files = [BytesIO(b) for b in bytes_to_df] - - # read all files in many threads - pool = ThreadPool(8) - results = pool.map(self.read_csv, files) - first_result = results[0] - - for result in results: - tm.assert_frame_equal(first_result, result) - - def test_multithread_path_multipart_read_csv(self): - # see gh-11786 - num_tasks = 4 - file_name = '__threadpool_reader__.csv' - num_rows = 100000 - - df = _construct_dataframe(num_rows) - - with tm.ensure_clean(file_name) as path: - df.to_csv(path) - - final_dataframe = self._generate_multithread_dataframe( - path, num_rows, num_tasks) - tm.assert_frame_equal(df, final_dataframe) diff --git a/pandas/tests/io/parser/na_values.py b/pandas/tests/io/parser/na_values.py deleted file mode 100644 index 2cbd7cdedf2ab..0000000000000 --- a/pandas/tests/io/parser/na_values.py +++ /dev/null @@ -1,305 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that NA values are properly handled during -parsing for all of the parsers defined in parsers.py -""" - -import numpy as np -from numpy import nan - -import pandas.io.parsers as parsers -import pandas.util.testing as tm - -from pandas import DataFrame, MultiIndex -from pandas.compat import StringIO, range - - -class NAvaluesTests(object): - - def test_string_nas(self): - data = """A,B,C -a,b,c -d,,f -,g,h -""" - result = self.read_csv(StringIO(data)) - expected = DataFrame([['a', 'b', 'c'], - ['d', np.nan, 'f'], - [np.nan, 'g', 'h']], - columns=['A', 'B', 'C']) - - tm.assert_frame_equal(result, expected) - - def test_detect_string_na(self): - data = """A,B -foo,bar -NA,baz -NaN,nan -""" - expected = np.array([['foo', 'bar'], [nan, 'baz'], [nan, nan]], - dtype=np.object_) - df = self.read_csv(StringIO(data)) - tm.assert_numpy_array_equal(df.values, expected) - - def test_non_string_na_values(self): - # see gh-3611: with an odd float format, we can't match - # the string '999.0' exactly but still need float matching - nice = """A,B --999,1.2 -2,-999 -3,4.5 -""" - ugly = """A,B --999,1.200 -2,-999.000 -3,4.500 -""" - na_values_param = [['-999.0', '-999'], - [-999, -999.0], - [-999.0, -999], - ['-999.0'], ['-999'], - [-999.0], [-999]] - expected = DataFrame([[np.nan, 1.2], [2.0, np.nan], - [3.0, 4.5]], columns=['A', 'B']) - - for data in (nice, ugly): - for na_values in na_values_param: - out = self.read_csv(StringIO(data), na_values=na_values) - tm.assert_frame_equal(out, expected) - - def test_default_na_values(self): - _NA_VALUES = set(['-1.#IND', '1.#QNAN', '1.#IND', '-1.#QNAN', - '#N/A', 'N/A', 'NA', '#NA', 'NULL', 'NaN', - 'nan', '-NaN', '-nan', '#N/A N/A', '']) - self.assertEqual(_NA_VALUES, parsers._NA_VALUES) - nv = len(_NA_VALUES) - - def f(i, v): - if i == 0: - buf = '' - elif i > 0: - buf = ''.join([','] * i) - - buf = "{0}{1}".format(buf, v) - - if i < nv - 1: - buf = "{0}{1}".format(buf, ''.join([','] * (nv - i - 1))) - - return buf - - data = StringIO('\n'.join([f(i, v) for i, v in enumerate(_NA_VALUES)])) - expected = DataFrame(np.nan, columns=range(nv), index=range(nv)) - df = self.read_csv(data, header=None) - tm.assert_frame_equal(df, expected) - - def test_custom_na_values(self): - data = """A,B,C -ignore,this,row -1,NA,3 --1.#IND,5,baz -7,8,NaN -""" - expected = np.array([[1., nan, 3], - [nan, 5, nan], - [7, 8, nan]]) - - df = self.read_csv(StringIO(data), na_values=['baz'], skiprows=[1]) - tm.assert_numpy_array_equal(df.values, expected) - - df2 = self.read_table(StringIO(data), sep=',', na_values=['baz'], - skiprows=[1]) - tm.assert_numpy_array_equal(df2.values, expected) - - df3 = self.read_table(StringIO(data), sep=',', na_values='baz', - skiprows=[1]) - tm.assert_numpy_array_equal(df3.values, expected) - - def test_bool_na_values(self): - data = """A,B,C -True,False,True -NA,True,False -False,NA,True""" - - result = self.read_csv(StringIO(data)) - expected = DataFrame({'A': np.array([True, nan, False], dtype=object), - 'B': np.array([False, True, nan], dtype=object), - 'C': [True, False, True]}) - - tm.assert_frame_equal(result, expected) - - def test_na_value_dict(self): - data = """A,B,C -foo,bar,NA -bar,foo,foo -foo,bar,NA -bar,foo,foo""" - - df = self.read_csv(StringIO(data), - na_values={'A': ['foo'], 'B': ['bar']}) - expected = DataFrame({'A': [np.nan, 'bar', np.nan, 'bar'], - 'B': [np.nan, 'foo', np.nan, 'foo'], - 'C': [np.nan, 'foo', np.nan, 'foo']}) - tm.assert_frame_equal(df, expected) - - data = """\ -a,b,c,d -0,NA,1,5 -""" - xp = DataFrame({'b': [np.nan], 'c': [1], 'd': [5]}, index=[0]) - xp.index.name = 'a' - df = self.read_csv(StringIO(data), na_values={}, index_col=0) - tm.assert_frame_equal(df, xp) - - xp = DataFrame({'b': [np.nan], 'd': [5]}, - MultiIndex.from_tuples([(0, 1)])) - xp.index.names = ['a', 'c'] - df = self.read_csv(StringIO(data), na_values={}, index_col=[0, 2]) - tm.assert_frame_equal(df, xp) - - xp = DataFrame({'b': [np.nan], 'd': [5]}, - MultiIndex.from_tuples([(0, 1)])) - xp.index.names = ['a', 'c'] - df = self.read_csv(StringIO(data), na_values={}, index_col=['a', 'c']) - tm.assert_frame_equal(df, xp) - - def test_na_values_keep_default(self): - data = """\ -One,Two,Three -a,1,one -b,2,two -,3,three -d,4,nan -e,5,five -nan,6, -g,7,seven -""" - df = self.read_csv(StringIO(data)) - xp = DataFrame({'One': ['a', 'b', np.nan, 'd', 'e', np.nan, 'g'], - 'Two': [1, 2, 3, 4, 5, 6, 7], - 'Three': ['one', 'two', 'three', np.nan, 'five', - np.nan, 'seven']}) - tm.assert_frame_equal(xp.reindex(columns=df.columns), df) - - df = self.read_csv(StringIO(data), na_values={'One': [], 'Three': []}, - keep_default_na=False) - xp = DataFrame({'One': ['a', 'b', '', 'd', 'e', 'nan', 'g'], - 'Two': [1, 2, 3, 4, 5, 6, 7], - 'Three': ['one', 'two', 'three', 'nan', 'five', - '', 'seven']}) - tm.assert_frame_equal(xp.reindex(columns=df.columns), df) - - df = self.read_csv( - StringIO(data), na_values=['a'], keep_default_na=False) - xp = DataFrame({'One': [np.nan, 'b', '', 'd', 'e', 'nan', 'g'], - 'Two': [1, 2, 3, 4, 5, 6, 7], - 'Three': ['one', 'two', 'three', 'nan', 'five', '', - 'seven']}) - tm.assert_frame_equal(xp.reindex(columns=df.columns), df) - - df = self.read_csv(StringIO(data), na_values={'One': [], 'Three': []}) - xp = DataFrame({'One': ['a', 'b', np.nan, 'd', 'e', np.nan, 'g'], - 'Two': [1, 2, 3, 4, 5, 6, 7], - 'Three': ['one', 'two', 'three', np.nan, 'five', - np.nan, 'seven']}) - tm.assert_frame_equal(xp.reindex(columns=df.columns), df) - - # see gh-4318: passing na_values=None and - # keep_default_na=False yields 'None' as a na_value - data = """\ -One,Two,Three -a,1,None -b,2,two -,3,None -d,4,nan -e,5,five -nan,6, -g,7,seven -""" - df = self.read_csv( - StringIO(data), keep_default_na=False) - xp = DataFrame({'One': ['a', 'b', '', 'd', 'e', 'nan', 'g'], - 'Two': [1, 2, 3, 4, 5, 6, 7], - 'Three': ['None', 'two', 'None', 'nan', 'five', '', - 'seven']}) - tm.assert_frame_equal(xp.reindex(columns=df.columns), df) - - def test_na_values_na_filter_override(self): - data = """\ -A,B -1,A -nan,B -3,C -""" - - expected = DataFrame([[1, 'A'], [np.nan, np.nan], [3, 'C']], - columns=['A', 'B']) - out = self.read_csv(StringIO(data), na_values=['B'], na_filter=True) - tm.assert_frame_equal(out, expected) - - expected = DataFrame([['1', 'A'], ['nan', 'B'], ['3', 'C']], - columns=['A', 'B']) - out = self.read_csv(StringIO(data), na_values=['B'], na_filter=False) - tm.assert_frame_equal(out, expected) - - def test_na_trailing_columns(self): - data = """Date,Currenncy,Symbol,Type,Units,UnitPrice,Cost,Tax -2012-03-14,USD,AAPL,BUY,1000 -2012-05-12,USD,SBUX,SELL,500""" - - result = self.read_csv(StringIO(data)) - self.assertEqual(result['Date'][1], '2012-05-12') - self.assertTrue(result['UnitPrice'].isnull().all()) - - def test_na_values_scalar(self): - # see gh-12224 - names = ['a', 'b'] - data = '1,2\n2,1' - - expected = DataFrame([[np.nan, 2.0], [2.0, np.nan]], - columns=names) - out = self.read_csv(StringIO(data), names=names, na_values=1) - tm.assert_frame_equal(out, expected) - - expected = DataFrame([[1.0, 2.0], [np.nan, np.nan]], - columns=names) - out = self.read_csv(StringIO(data), names=names, - na_values={'a': 2, 'b': 1}) - tm.assert_frame_equal(out, expected) - - def test_na_values_dict_aliasing(self): - na_values = {'a': 2, 'b': 1} - na_values_copy = na_values.copy() - - names = ['a', 'b'] - data = '1,2\n2,1' - - expected = DataFrame([[1.0, 2.0], [np.nan, np.nan]], columns=names) - out = self.read_csv(StringIO(data), names=names, na_values=na_values) - - tm.assert_frame_equal(out, expected) - tm.assert_dict_equal(na_values, na_values_copy) - - def test_na_values_dict_col_index(self): - # see gh-14203 - - data = 'a\nfoo\n1' - na_values = {0: 'foo'} - - out = self.read_csv(StringIO(data), na_values=na_values) - expected = DataFrame({'a': [np.nan, 1]}) - tm.assert_frame_equal(out, expected) - - def test_na_values_uint64(self): - # see gh-14983 - - na_values = [2**63] - data = str(2**63) + '\n' + str(2**63 + 1) - expected = DataFrame([str(2**63), str(2**63 + 1)]) - out = self.read_csv(StringIO(data), header=None, na_values=na_values) - tm.assert_frame_equal(out, expected) - - data = str(2**63) + ',1' + '\n,2' - expected = DataFrame([[str(2**63), 1], ['', 2]]) - out = self.read_csv(StringIO(data), header=None) - tm.assert_frame_equal(out, expected) diff --git a/pandas/tests/io/parser/parse_dates.py b/pandas/tests/io/parser/parse_dates.py deleted file mode 100644 index de4e3fbc0d943..0000000000000 --- a/pandas/tests/io/parser/parse_dates.py +++ /dev/null @@ -1,656 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests date parsing functionality for all of the -parsers defined in parsers.py -""" - -from distutils.version import LooseVersion -from datetime import datetime, date - -import pytest -import numpy as np -import pandas._libs.lib as lib -from pandas._libs.lib import Timestamp - -import pandas as pd -import pandas.io.parsers as parsers -import pandas.tseries.tools as tools -import pandas.util.testing as tm - -import pandas.io.date_converters as conv -from pandas import DataFrame, Series, Index, DatetimeIndex, MultiIndex -from pandas import compat -from pandas.compat import parse_date, StringIO, lrange -from pandas.compat.numpy import np_array_datetime64_compat -from pandas.tseries.index import date_range - - -class ParseDatesTests(object): - - def test_separator_date_conflict(self): - # Regression test for gh-4678: make sure thousands separator and - # date parsing do not conflict. - data = '06-02-2013;13:00;1-000.215' - expected = DataFrame( - [[datetime(2013, 6, 2, 13, 0, 0), 1000.215]], - columns=['Date', 2] - ) - - df = self.read_csv(StringIO(data), sep=';', thousands='-', - parse_dates={'Date': [0, 1]}, header=None) - tm.assert_frame_equal(df, expected) - - def test_multiple_date_col(self): - # Can use multiple date parsers - data = """\ -KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 -""" - - def func(*date_cols): - return lib.try_parse_dates(parsers._concat_date_cols(date_cols)) - - df = self.read_csv(StringIO(data), header=None, - date_parser=func, - prefix='X', - parse_dates={'nominal': [1, 2], - 'actual': [1, 3]}) - self.assertIn('nominal', df) - self.assertIn('actual', df) - self.assertNotIn('X1', df) - self.assertNotIn('X2', df) - self.assertNotIn('X3', df) - - d = datetime(1999, 1, 27, 19, 0) - self.assertEqual(df.loc[0, 'nominal'], d) - - df = self.read_csv(StringIO(data), header=None, - date_parser=func, - parse_dates={'nominal': [1, 2], - 'actual': [1, 3]}, - keep_date_col=True) - self.assertIn('nominal', df) - self.assertIn('actual', df) - - self.assertIn(1, df) - self.assertIn(2, df) - self.assertIn(3, df) - - data = """\ -KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 -""" - df = self.read_csv(StringIO(data), header=None, - prefix='X', parse_dates=[[1, 2], [1, 3]]) - - self.assertIn('X1_X2', df) - self.assertIn('X1_X3', df) - self.assertNotIn('X1', df) - self.assertNotIn('X2', df) - self.assertNotIn('X3', df) - - d = datetime(1999, 1, 27, 19, 0) - self.assertEqual(df.loc[0, 'X1_X2'], d) - - df = self.read_csv(StringIO(data), header=None, - parse_dates=[[1, 2], [1, 3]], keep_date_col=True) - - self.assertIn('1_2', df) - self.assertIn('1_3', df) - self.assertIn(1, df) - self.assertIn(2, df) - self.assertIn(3, df) - - data = '''\ -KORD,19990127 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -''' - df = self.read_csv(StringIO(data), sep=',', header=None, - parse_dates=[1], index_col=1) - d = datetime(1999, 1, 27, 19, 0) - self.assertEqual(df.index[0], d) - - def test_multiple_date_cols_int_cast(self): - data = ("KORD,19990127, 19:00:00, 18:56:00, 0.8100\n" - "KORD,19990127, 20:00:00, 19:56:00, 0.0100\n" - "KORD,19990127, 21:00:00, 20:56:00, -0.5900\n" - "KORD,19990127, 21:00:00, 21:18:00, -0.9900\n" - "KORD,19990127, 22:00:00, 21:56:00, -0.5900\n" - "KORD,19990127, 23:00:00, 22:56:00, -0.5900") - date_spec = {'nominal': [1, 2], 'actual': [1, 3]} - import pandas.io.date_converters as conv - - # it works! - df = self.read_csv(StringIO(data), header=None, parse_dates=date_spec, - date_parser=conv.parse_date_time) - self.assertIn('nominal', df) - - def test_multiple_date_col_timestamp_parse(self): - data = """05/31/2012,15:30:00.029,1306.25,1,E,0,,1306.25 -05/31/2012,15:30:00.029,1306.25,8,E,0,,1306.25""" - result = self.read_csv(StringIO(data), sep=',', header=None, - parse_dates=[[0, 1]], date_parser=Timestamp) - - ex_val = Timestamp('05/31/2012 15:30:00.029') - self.assertEqual(result['0_1'][0], ex_val) - - def test_multiple_date_cols_with_header(self): - data = """\ -ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir -KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000""" - - df = self.read_csv(StringIO(data), parse_dates={'nominal': [1, 2]}) - self.assertNotIsInstance(df.nominal[0], compat.string_types) - - ts_data = """\ -ID,date,nominalTime,actualTime,A,B,C,D,E -KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 -""" - - def test_multiple_date_col_name_collision(self): - self.assertRaises(ValueError, self.read_csv, StringIO(self.ts_data), - parse_dates={'ID': [1, 2]}) - - data = """\ -date_NominalTime,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir -KORD1,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD2,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD3,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD4,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD5,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD6,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000""" # noqa - - self.assertRaises(ValueError, self.read_csv, StringIO(data), - parse_dates=[[1, 2]]) - - def test_date_parser_int_bug(self): - # See gh-3071 - log_file = StringIO( - 'posix_timestamp,elapsed,sys,user,queries,query_time,rows,' - 'accountid,userid,contactid,level,silo,method\n' - '1343103150,0.062353,0,4,6,0.01690,3,' - '12345,1,-1,3,invoice_InvoiceResource,search\n' - ) - - def f(posix_string): - return datetime.utcfromtimestamp(int(posix_string)) - - # it works! - self.read_csv(log_file, index_col=0, parse_dates=[0], date_parser=f) - - def test_nat_parse(self): - # See gh-3062 - df = DataFrame(dict({ - 'A': np.asarray(lrange(10), dtype='float64'), - 'B': pd.Timestamp('20010101')})) - df.iloc[3:6, :] = np.nan - - with tm.ensure_clean('__nat_parse_.csv') as path: - df.to_csv(path) - result = self.read_csv(path, index_col=0, parse_dates=['B']) - tm.assert_frame_equal(result, df) - - expected = Series(dict(A='float64', B='datetime64[ns]')) - tm.assert_series_equal(expected, result.dtypes) - - # test with NaT for the nan_rep - # we don't have a method to specif the Datetime na_rep (it defaults - # to '') - df.to_csv(path) - result = self.read_csv(path, index_col=0, parse_dates=['B']) - tm.assert_frame_equal(result, df) - - def test_csv_custom_parser(self): - data = """A,B,C -20090101,a,1,2 -20090102,b,3,4 -20090103,c,4,5 -""" - f = lambda x: datetime.strptime(x, '%Y%m%d') - df = self.read_csv(StringIO(data), date_parser=f) - expected = self.read_csv(StringIO(data), parse_dates=True) - tm.assert_frame_equal(df, expected) - - def test_parse_dates_implicit_first_col(self): - data = """A,B,C -20090101,a,1,2 -20090102,b,3,4 -20090103,c,4,5 -""" - df = self.read_csv(StringIO(data), parse_dates=True) - expected = self.read_csv(StringIO(data), index_col=0, parse_dates=True) - self.assertIsInstance( - df.index[0], (datetime, np.datetime64, Timestamp)) - tm.assert_frame_equal(df, expected) - - def test_parse_dates_string(self): - data = """date,A,B,C -20090101,a,1,2 -20090102,b,3,4 -20090103,c,4,5 -""" - rs = self.read_csv( - StringIO(data), index_col='date', parse_dates=['date']) - idx = date_range('1/1/2009', periods=3) - idx.name = 'date' - xp = DataFrame({'A': ['a', 'b', 'c'], - 'B': [1, 3, 4], - 'C': [2, 4, 5]}, idx) - tm.assert_frame_equal(rs, xp) - - def test_yy_format_with_yearfirst(self): - data = """date,time,B,C -090131,0010,1,2 -090228,1020,3,4 -090331,0830,5,6 -""" - - # See gh-217 - import dateutil - if dateutil.__version__ >= LooseVersion('2.5.0'): - pytest.skip("testing yearfirst=True not-support" - "on datetutil < 2.5.0 this works but" - "is wrong") - - rs = self.read_csv(StringIO(data), index_col=0, - parse_dates=[['date', 'time']]) - idx = DatetimeIndex([datetime(2009, 1, 31, 0, 10, 0), - datetime(2009, 2, 28, 10, 20, 0), - datetime(2009, 3, 31, 8, 30, 0)], - dtype=object, name='date_time') - xp = DataFrame({'B': [1, 3, 5], 'C': [2, 4, 6]}, idx) - tm.assert_frame_equal(rs, xp) - - rs = self.read_csv(StringIO(data), index_col=0, - parse_dates=[[0, 1]]) - idx = DatetimeIndex([datetime(2009, 1, 31, 0, 10, 0), - datetime(2009, 2, 28, 10, 20, 0), - datetime(2009, 3, 31, 8, 30, 0)], - dtype=object, name='date_time') - xp = DataFrame({'B': [1, 3, 5], 'C': [2, 4, 6]}, idx) - tm.assert_frame_equal(rs, xp) - - def test_parse_dates_column_list(self): - data = 'a,b,c\n01/01/2010,1,15/02/2010' - - expected = DataFrame({'a': [datetime(2010, 1, 1)], 'b': [1], - 'c': [datetime(2010, 2, 15)]}) - expected = expected.set_index(['a', 'b']) - - df = self.read_csv(StringIO(data), index_col=[0, 1], - parse_dates=[0, 2], dayfirst=True) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(data), index_col=[0, 1], - parse_dates=['a', 'c'], dayfirst=True) - tm.assert_frame_equal(df, expected) - - def test_multi_index_parse_dates(self): - data = """index1,index2,A,B,C -20090101,one,a,1,2 -20090101,two,b,3,4 -20090101,three,c,4,5 -20090102,one,a,1,2 -20090102,two,b,3,4 -20090102,three,c,4,5 -20090103,one,a,1,2 -20090103,two,b,3,4 -20090103,three,c,4,5 -""" - df = self.read_csv(StringIO(data), index_col=[0, 1], parse_dates=True) - self.assertIsInstance(df.index.levels[0][0], - (datetime, np.datetime64, Timestamp)) - - # specify columns out of order! - df2 = self.read_csv(StringIO(data), index_col=[1, 0], parse_dates=True) - self.assertIsInstance(df2.index.levels[1][0], - (datetime, np.datetime64, Timestamp)) - - def test_parse_dates_custom_euroformat(self): - text = """foo,bar,baz -31/01/2010,1,2 -01/02/2010,1,NA -02/02/2010,1,2 -""" - parser = lambda d: parse_date(d, dayfirst=True) - df = self.read_csv(StringIO(text), - names=['time', 'Q', 'NTU'], header=0, - index_col=0, parse_dates=True, - date_parser=parser, na_values=['NA']) - - exp_index = Index([datetime(2010, 1, 31), datetime(2010, 2, 1), - datetime(2010, 2, 2)], name='time') - expected = DataFrame({'Q': [1, 1, 1], 'NTU': [2, np.nan, 2]}, - index=exp_index, columns=['Q', 'NTU']) - tm.assert_frame_equal(df, expected) - - parser = lambda d: parse_date(d, day_first=True) - self.assertRaises(TypeError, self.read_csv, - StringIO(text), skiprows=[0], - names=['time', 'Q', 'NTU'], index_col=0, - parse_dates=True, date_parser=parser, - na_values=['NA']) - - def test_parse_tz_aware(self): - # See gh-1693 - import pytz - data = StringIO("Date,x\n2012-06-13T01:39:00Z,0.5") - - # it works - result = self.read_csv(data, index_col=0, parse_dates=True) - stamp = result.index[0] - self.assertEqual(stamp.minute, 39) - try: - self.assertIs(result.index.tz, pytz.utc) - except AssertionError: # hello Yaroslav - arr = result.index.to_pydatetime() - result = tools.to_datetime(arr, utc=True)[0] - self.assertEqual(stamp.minute, result.minute) - self.assertEqual(stamp.hour, result.hour) - self.assertEqual(stamp.day, result.day) - - def test_multiple_date_cols_index(self): - data = """ -ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir -KORD1,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD2,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD3,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD4,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD5,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -KORD6,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 -""" - - xp = self.read_csv(StringIO(data), parse_dates={'nominal': [1, 2]}) - df = self.read_csv(StringIO(data), parse_dates={'nominal': [1, 2]}, - index_col='nominal') - tm.assert_frame_equal(xp.set_index('nominal'), df) - df2 = self.read_csv(StringIO(data), parse_dates={'nominal': [1, 2]}, - index_col=0) - tm.assert_frame_equal(df2, df) - - df3 = self.read_csv(StringIO(data), parse_dates=[[1, 2]], index_col=0) - tm.assert_frame_equal(df3, df, check_names=False) - - def test_multiple_date_cols_chunked(self): - df = self.read_csv(StringIO(self.ts_data), parse_dates={ - 'nominal': [1, 2]}, index_col='nominal') - reader = self.read_csv(StringIO(self.ts_data), - parse_dates={'nominal': [1, 2]}, - index_col='nominal', chunksize=2) - - chunks = list(reader) - - self.assertNotIn('nominalTime', df) - - tm.assert_frame_equal(chunks[0], df[:2]) - tm.assert_frame_equal(chunks[1], df[2:4]) - tm.assert_frame_equal(chunks[2], df[4:]) - - def test_multiple_date_col_named_components(self): - xp = self.read_csv(StringIO(self.ts_data), - parse_dates={'nominal': [1, 2]}, - index_col='nominal') - colspec = {'nominal': ['date', 'nominalTime']} - df = self.read_csv(StringIO(self.ts_data), parse_dates=colspec, - index_col='nominal') - tm.assert_frame_equal(df, xp) - - def test_multiple_date_col_multiple_index(self): - df = self.read_csv(StringIO(self.ts_data), - parse_dates={'nominal': [1, 2]}, - index_col=['nominal', 'ID']) - - xp = self.read_csv(StringIO(self.ts_data), - parse_dates={'nominal': [1, 2]}) - - tm.assert_frame_equal(xp.set_index(['nominal', 'ID']), df) - - def test_read_with_parse_dates_scalar_non_bool(self): - # See gh-5636 - errmsg = ("Only booleans, lists, and " - "dictionaries are accepted " - "for the 'parse_dates' parameter") - data = """A,B,C - 1,2,2003-11-1""" - - tm.assertRaisesRegexp(TypeError, errmsg, self.read_csv, - StringIO(data), parse_dates="C") - tm.assertRaisesRegexp(TypeError, errmsg, self.read_csv, - StringIO(data), parse_dates="C", - index_col="C") - - def test_read_with_parse_dates_invalid_type(self): - errmsg = ("Only booleans, lists, and " - "dictionaries are accepted " - "for the 'parse_dates' parameter") - data = """A,B,C - 1,2,2003-11-1""" - - tm.assertRaisesRegexp(TypeError, errmsg, self.read_csv, - StringIO(data), parse_dates=(1,)) - tm.assertRaisesRegexp(TypeError, errmsg, self.read_csv, - StringIO(data), parse_dates=np.array([4, 5])) - tm.assertRaisesRegexp(TypeError, errmsg, self.read_csv, - StringIO(data), parse_dates=set([1, 3, 3])) - - def test_parse_dates_empty_string(self): - # see gh-2263 - data = "Date, test\n2012-01-01, 1\n,2" - result = self.read_csv(StringIO(data), parse_dates=["Date"], - na_filter=False) - self.assertTrue(result['Date'].isnull()[1]) - - def test_parse_dates_noconvert_thousands(self): - # see gh-14066 - data = 'a\n04.15.2016' - - expected = DataFrame([datetime(2016, 4, 15)], columns=['a']) - result = self.read_csv(StringIO(data), parse_dates=['a'], - thousands='.') - tm.assert_frame_equal(result, expected) - - exp_index = DatetimeIndex(['2016-04-15'], name='a') - expected = DataFrame(index=exp_index) - result = self.read_csv(StringIO(data), index_col=0, - parse_dates=True, thousands='.') - tm.assert_frame_equal(result, expected) - - data = 'a,b\n04.15.2016,09.16.2013' - - expected = DataFrame([[datetime(2016, 4, 15), - datetime(2013, 9, 16)]], - columns=['a', 'b']) - result = self.read_csv(StringIO(data), parse_dates=['a', 'b'], - thousands='.') - tm.assert_frame_equal(result, expected) - - expected = DataFrame([[datetime(2016, 4, 15), - datetime(2013, 9, 16)]], - columns=['a', 'b']) - expected = expected.set_index(['a', 'b']) - result = self.read_csv(StringIO(data), index_col=[0, 1], - parse_dates=True, thousands='.') - tm.assert_frame_equal(result, expected) - - def test_parse_date_time_multi_level_column_name(self): - data = """\ -D,T,A,B -date, time,a,b -2001-01-05, 09:00:00, 0.0, 10. -2001-01-06, 00:00:00, 1.0, 11. -""" - datecols = {'date_time': [0, 1]} - result = self.read_csv(StringIO(data), sep=',', header=[0, 1], - parse_dates=datecols, - date_parser=conv.parse_date_time) - - expected_data = [[datetime(2001, 1, 5, 9, 0, 0), 0., 10.], - [datetime(2001, 1, 6, 0, 0, 0), 1., 11.]] - expected = DataFrame(expected_data, - columns=['date_time', ('A', 'a'), ('B', 'b')]) - tm.assert_frame_equal(result, expected) - - def test_parse_date_time(self): - dates = np.array(['2007/1/3', '2008/2/4'], dtype=object) - times = np.array(['05:07:09', '06:08:00'], dtype=object) - expected = np.array([datetime(2007, 1, 3, 5, 7, 9), - datetime(2008, 2, 4, 6, 8, 0)]) - - result = conv.parse_date_time(dates, times) - self.assertTrue((result == expected).all()) - - data = """\ -date, time, a, b -2001-01-05, 10:00:00, 0.0, 10. -2001-01-05, 00:00:00, 1., 11. -""" - datecols = {'date_time': [0, 1]} - df = self.read_csv(StringIO(data), sep=',', header=0, - parse_dates=datecols, - date_parser=conv.parse_date_time) - self.assertIn('date_time', df) - self.assertEqual(df.date_time.loc[0], datetime(2001, 1, 5, 10, 0, 0)) - - data = ("KORD,19990127, 19:00:00, 18:56:00, 0.8100\n" - "KORD,19990127, 20:00:00, 19:56:00, 0.0100\n" - "KORD,19990127, 21:00:00, 20:56:00, -0.5900\n" - "KORD,19990127, 21:00:00, 21:18:00, -0.9900\n" - "KORD,19990127, 22:00:00, 21:56:00, -0.5900\n" - "KORD,19990127, 23:00:00, 22:56:00, -0.5900") - - date_spec = {'nominal': [1, 2], 'actual': [1, 3]} - df = self.read_csv(StringIO(data), header=None, parse_dates=date_spec, - date_parser=conv.parse_date_time) - - def test_parse_date_fields(self): - years = np.array([2007, 2008]) - months = np.array([1, 2]) - days = np.array([3, 4]) - result = conv.parse_date_fields(years, months, days) - expected = np.array([datetime(2007, 1, 3), datetime(2008, 2, 4)]) - self.assertTrue((result == expected).all()) - - data = ("year, month, day, a\n 2001 , 01 , 10 , 10.\n" - "2001 , 02 , 1 , 11.") - datecols = {'ymd': [0, 1, 2]} - df = self.read_csv(StringIO(data), sep=',', header=0, - parse_dates=datecols, - date_parser=conv.parse_date_fields) - self.assertIn('ymd', df) - self.assertEqual(df.ymd.loc[0], datetime(2001, 1, 10)) - - def test_datetime_six_col(self): - years = np.array([2007, 2008]) - months = np.array([1, 2]) - days = np.array([3, 4]) - hours = np.array([5, 6]) - minutes = np.array([7, 8]) - seconds = np.array([9, 0]) - expected = np.array([datetime(2007, 1, 3, 5, 7, 9), - datetime(2008, 2, 4, 6, 8, 0)]) - - result = conv.parse_all_fields(years, months, days, - hours, minutes, seconds) - - self.assertTrue((result == expected).all()) - - data = """\ -year, month, day, hour, minute, second, a, b -2001, 01, 05, 10, 00, 0, 0.0, 10. -2001, 01, 5, 10, 0, 00, 1., 11. -""" - datecols = {'ymdHMS': [0, 1, 2, 3, 4, 5]} - df = self.read_csv(StringIO(data), sep=',', header=0, - parse_dates=datecols, - date_parser=conv.parse_all_fields) - self.assertIn('ymdHMS', df) - self.assertEqual(df.ymdHMS.loc[0], datetime(2001, 1, 5, 10, 0, 0)) - - def test_datetime_fractional_seconds(self): - data = """\ -year, month, day, hour, minute, second, a, b -2001, 01, 05, 10, 00, 0.123456, 0.0, 10. -2001, 01, 5, 10, 0, 0.500000, 1., 11. -""" - datecols = {'ymdHMS': [0, 1, 2, 3, 4, 5]} - df = self.read_csv(StringIO(data), sep=',', header=0, - parse_dates=datecols, - date_parser=conv.parse_all_fields) - self.assertIn('ymdHMS', df) - self.assertEqual(df.ymdHMS.loc[0], datetime(2001, 1, 5, 10, 0, 0, - microsecond=123456)) - self.assertEqual(df.ymdHMS.loc[1], datetime(2001, 1, 5, 10, 0, 0, - microsecond=500000)) - - def test_generic(self): - data = "year, month, day, a\n 2001, 01, 10, 10.\n 2001, 02, 1, 11." - datecols = {'ym': [0, 1]} - dateconverter = lambda y, m: date(year=int(y), month=int(m), day=1) - df = self.read_csv(StringIO(data), sep=',', header=0, - parse_dates=datecols, - date_parser=dateconverter) - self.assertIn('ym', df) - self.assertEqual(df.ym.loc[0], date(2001, 1, 1)) - - def test_dateparser_resolution_if_not_ns(self): - # GH 10245 - data = """\ -date,time,prn,rxstatus -2013-11-03,19:00:00,126,00E80000 -2013-11-03,19:00:00,23,00E80000 -2013-11-03,19:00:00,13,00E80000 -""" - - def date_parser(date, time): - datetime = np_array_datetime64_compat( - date + 'T' + time + 'Z', dtype='datetime64[s]') - return datetime - - df = self.read_csv(StringIO(data), date_parser=date_parser, - parse_dates={'datetime': ['date', 'time']}, - index_col=['datetime', 'prn']) - - datetimes = np_array_datetime64_compat(['2013-11-03T19:00:00Z'] * 3, - dtype='datetime64[s]') - df_correct = DataFrame(data={'rxstatus': ['00E80000'] * 3}, - index=MultiIndex.from_tuples( - [(datetimes[0], 126), - (datetimes[1], 23), - (datetimes[2], 13)], - names=['datetime', 'prn'])) - tm.assert_frame_equal(df, df_correct) - - def test_parse_date_column_with_empty_string(self): - # GH 6428 - data = """case,opdate - 7,10/18/2006 - 7,10/18/2008 - 621, """ - result = self.read_csv(StringIO(data), parse_dates=['opdate']) - expected_data = [[7, '10/18/2006'], - [7, '10/18/2008'], - [621, ' ']] - expected = DataFrame(expected_data, columns=['case', 'opdate']) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/python_parser_only.py b/pandas/tests/io/parser/python_parser_only.py deleted file mode 100644 index bd76070933c47..0000000000000 --- a/pandas/tests/io/parser/python_parser_only.py +++ /dev/null @@ -1,239 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that apply specifically to the Python parser. Unless specifically -stated as a Python-specific issue, the goal is to eventually move as many of -these tests out of this module as soon as the C parser can accept further -arguments when parsing. -""" - -import csv -import sys -import pytest - -import pandas.util.testing as tm -from pandas import DataFrame, Index -from pandas import compat -from pandas.compat import StringIO, BytesIO, u - - -class PythonParserTests(object): - - def test_negative_skipfooter_raises(self): - text = """#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -1/1/2000,1.,2.,3. -1/2/2000,4,5,6 -1/3/2000,7,8,9 -""" - - with tm.assertRaisesRegexp( - ValueError, 'skip footer cannot be negative'): - self.read_csv(StringIO(text), skipfooter=-1) - - def test_sniff_delimiter(self): - text = """index|A|B|C -foo|1|2|3 -bar|4|5|6 -baz|7|8|9 -""" - data = self.read_csv(StringIO(text), index_col=0, sep=None) - self.assert_index_equal(data.index, - Index(['foo', 'bar', 'baz'], name='index')) - - data2 = self.read_csv(StringIO(text), index_col=0, delimiter='|') - tm.assert_frame_equal(data, data2) - - text = """ignore this -ignore this too -index|A|B|C -foo|1|2|3 -bar|4|5|6 -baz|7|8|9 -""" - data3 = self.read_csv(StringIO(text), index_col=0, - sep=None, skiprows=2) - tm.assert_frame_equal(data, data3) - - text = u("""ignore this -ignore this too -index|A|B|C -foo|1|2|3 -bar|4|5|6 -baz|7|8|9 -""").encode('utf-8') - - s = BytesIO(text) - if compat.PY3: - # somewhat False since the code never sees bytes - from io import TextIOWrapper - s = TextIOWrapper(s, encoding='utf-8') - - data4 = self.read_csv(s, index_col=0, sep=None, skiprows=2, - encoding='utf-8') - tm.assert_frame_equal(data, data4) - - def test_BytesIO_input(self): - if not compat.PY3: - pytest.skip( - "Bytes-related test - only needs to work on Python 3") - - data = BytesIO("שלום::1234\n562::123".encode('cp1255')) - result = self.read_table(data, sep="::", encoding='cp1255') - expected = DataFrame([[562, 123]], columns=["שלום", "1234"]) - tm.assert_frame_equal(result, expected) - - def test_single_line(self): - # see gh-6607: sniff separator - - buf = StringIO() - sys.stdout = buf - - try: - df = self.read_csv(StringIO('1,2'), names=['a', 'b'], - header=None, sep=None) - tm.assert_frame_equal(DataFrame({'a': [1], 'b': [2]}), df) - finally: - sys.stdout = sys.__stdout__ - - def test_skipfooter(self): - # see gh-6607 - data = """A,B,C -1,2,3 -4,5,6 -7,8,9 -want to skip this -also also skip this -""" - result = self.read_csv(StringIO(data), skipfooter=2) - no_footer = '\n'.join(data.split('\n')[:-3]) - expected = self.read_csv(StringIO(no_footer)) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), nrows=3) - tm.assert_frame_equal(result, expected) - - # skipfooter alias - result = self.read_csv(StringIO(data), skipfooter=2) - no_footer = '\n'.join(data.split('\n')[:-3]) - expected = self.read_csv(StringIO(no_footer)) - tm.assert_frame_equal(result, expected) - - def test_decompression_regex_sep(self): - # see gh-6607 - - try: - import gzip - import bz2 - except ImportError: - pytest.skip('need gzip and bz2 to run') - - with open(self.csv1, 'rb') as f: - data = f.read() - data = data.replace(b',', b'::') - expected = self.read_csv(self.csv1) - - with tm.ensure_clean() as path: - tmp = gzip.GzipFile(path, mode='wb') - tmp.write(data) - tmp.close() - - result = self.read_csv(path, sep='::', compression='gzip') - tm.assert_frame_equal(result, expected) - - with tm.ensure_clean() as path: - tmp = bz2.BZ2File(path, mode='wb') - tmp.write(data) - tmp.close() - - result = self.read_csv(path, sep='::', compression='bz2') - tm.assert_frame_equal(result, expected) - - self.assertRaises(ValueError, self.read_csv, - path, compression='bz3') - - def test_read_table_buglet_4x_multiindex(self): - # see gh-6607 - text = """ A B C D E -one two three four -a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 -a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 -x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" - - df = self.read_table(StringIO(text), sep=r'\s+') - self.assertEqual(df.index.names, ('one', 'two', 'three', 'four')) - - # see gh-6893 - data = ' A B C\na b c\n1 3 7 0 3 6\n3 1 4 1 5 9' - expected = DataFrame.from_records( - [(1, 3, 7, 0, 3, 6), (3, 1, 4, 1, 5, 9)], - columns=list('abcABC'), index=list('abc')) - actual = self.read_table(StringIO(data), sep=r'\s+') - tm.assert_frame_equal(actual, expected) - - def test_skipfooter_with_decimal(self): - # see gh-6971 - data = '1#2\n3#4' - expected = DataFrame({'a': [1.2, 3.4]}) - - result = self.read_csv(StringIO(data), names=['a'], - decimal='#') - tm.assert_frame_equal(result, expected) - - # the stray footer line should not mess with the - # casting of the first t wo lines if we skip it - data = data + '\nFooter' - result = self.read_csv(StringIO(data), names=['a'], - decimal='#', skipfooter=1) - tm.assert_frame_equal(result, expected) - - def test_encoding_non_utf8_multichar_sep(self): - # see gh-3404 - expected = DataFrame({'a': [1], 'b': [2]}) - - for sep in ['::', '#####', '!!!', '123', '#1!c5', - '%!c!d', '@@#4:2', '_!pd#_']: - data = '1' + sep + '2' - - for encoding in ['utf-16', 'utf-16-be', 'utf-16-le', - 'utf-32', 'cp037']: - encoded_data = data.encode(encoding) - result = self.read_csv(BytesIO(encoded_data), - sep=sep, names=['a', 'b'], - encoding=encoding) - tm.assert_frame_equal(result, expected) - - def test_multi_char_sep_quotes(self): - # see gh-13374 - - data = 'a,,b\n1,,a\n2,,"2,,b"' - msg = 'ignored when a multi-char delimiter is used' - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(data), sep=',,') - - # We expect no match, so there should be an assertion - # error out of the inner context manager. - with tm.assertRaises(AssertionError): - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(data), sep=',,', - quoting=csv.QUOTE_NONE) - - def test_skipfooter_bad_row(self): - # see gh-13879 - - data = 'a,b,c\ncat,foo,bar\ndog,foo,"baz' - msg = 'parsing errors in the skipped footer rows' - - with tm.assertRaisesRegexp(csv.Error, msg): - self.read_csv(StringIO(data), skipfooter=1) - - # We expect no match, so there should be an assertion - # error out of the inner context manager. - with tm.assertRaises(AssertionError): - with tm.assertRaisesRegexp(csv.Error, msg): - self.read_csv(StringIO(data)) diff --git a/pandas/tests/io/parser/quoting.py b/pandas/tests/io/parser/quoting.py deleted file mode 100644 index a692e03e868c7..0000000000000 --- a/pandas/tests/io/parser/quoting.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that quoting specifications are properly handled -during parsing for all of the parsers defined in parsers.py -""" - -import csv -import pandas.util.testing as tm - -from pandas import DataFrame -from pandas.compat import PY3, StringIO, u - - -class QuotingTests(object): - - def test_bad_quote_char(self): - data = '1,2,3' - - # Python 2.x: "...must be an 1-character..." - # Python 3.x: "...must be a 1-character..." - msg = '"quotechar" must be a(n)? 1-character string' - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quotechar='foo') - - msg = 'quotechar must be set if quoting enabled' - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quotechar=None, - quoting=csv.QUOTE_MINIMAL) - - msg = '"quotechar" must be string, not int' - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quotechar=2) - - def test_bad_quoting(self): - data = '1,2,3' - - msg = '"quoting" must be an integer' - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quoting='foo') - - # quoting must in the range [0, 3] - msg = 'bad "quoting" value' - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quoting=5) - - def test_quote_char_basic(self): - data = 'a,b,c\n1,2,"cat"' - expected = DataFrame([[1, 2, 'cat']], - columns=['a', 'b', 'c']) - result = self.read_csv(StringIO(data), quotechar='"') - tm.assert_frame_equal(result, expected) - - def test_quote_char_various(self): - data = 'a,b,c\n1,2,"cat"' - expected = DataFrame([[1, 2, 'cat']], - columns=['a', 'b', 'c']) - quote_chars = ['~', '*', '%', '$', '@', 'P'] - - for quote_char in quote_chars: - new_data = data.replace('"', quote_char) - result = self.read_csv(StringIO(new_data), quotechar=quote_char) - tm.assert_frame_equal(result, expected) - - def test_null_quote_char(self): - data = 'a,b,c\n1,2,3' - - # sanity checks - msg = 'quotechar must be set if quoting enabled' - - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quotechar=None, - quoting=csv.QUOTE_MINIMAL) - - tm.assertRaisesRegexp(TypeError, msg, self.read_csv, - StringIO(data), quotechar='', - quoting=csv.QUOTE_MINIMAL) - - # no errors should be raised if quoting is None - expected = DataFrame([[1, 2, 3]], - columns=['a', 'b', 'c']) - - result = self.read_csv(StringIO(data), quotechar=None, - quoting=csv.QUOTE_NONE) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), quotechar='', - quoting=csv.QUOTE_NONE) - tm.assert_frame_equal(result, expected) - - def test_quoting_various(self): - data = '1,2,"foo"' - cols = ['a', 'b', 'c'] - - # QUOTE_MINIMAL and QUOTE_ALL apply only to - # the CSV writer, so they should have no - # special effect for the CSV reader - expected = DataFrame([[1, 2, 'foo']], columns=cols) - - # test default (afterwards, arguments are all explicit) - result = self.read_csv(StringIO(data), names=cols) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), quotechar='"', - quoting=csv.QUOTE_MINIMAL, names=cols) - tm.assert_frame_equal(result, expected) - - result = self.read_csv(StringIO(data), quotechar='"', - quoting=csv.QUOTE_ALL, names=cols) - tm.assert_frame_equal(result, expected) - - # QUOTE_NONE tells the reader to do no special handling - # of quote characters and leave them alone - expected = DataFrame([[1, 2, '"foo"']], columns=cols) - result = self.read_csv(StringIO(data), quotechar='"', - quoting=csv.QUOTE_NONE, names=cols) - tm.assert_frame_equal(result, expected) - - # QUOTE_NONNUMERIC tells the reader to cast - # all non-quoted fields to float - expected = DataFrame([[1.0, 2.0, 'foo']], columns=cols) - result = self.read_csv(StringIO(data), quotechar='"', - quoting=csv.QUOTE_NONNUMERIC, - names=cols) - tm.assert_frame_equal(result, expected) - - def test_double_quote(self): - data = 'a,b\n3,"4 "" 5"' - - expected = DataFrame([[3, '4 " 5']], - columns=['a', 'b']) - result = self.read_csv(StringIO(data), quotechar='"', - doublequote=True) - tm.assert_frame_equal(result, expected) - - expected = DataFrame([[3, '4 " 5"']], - columns=['a', 'b']) - result = self.read_csv(StringIO(data), quotechar='"', - doublequote=False) - tm.assert_frame_equal(result, expected) - - def test_quotechar_unicode(self): - # See gh-14477 - data = 'a\n1' - expected = DataFrame({'a': [1]}) - - result = self.read_csv(StringIO(data), quotechar=u('"')) - tm.assert_frame_equal(result, expected) - - # Compared to Python 3.x, Python 2.x does not handle unicode well. - if PY3: - result = self.read_csv(StringIO(data), quotechar=u('\u0001')) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/skiprows.py b/pandas/tests/io/parser/skiprows.py deleted file mode 100644 index c53e6a1579267..0000000000000 --- a/pandas/tests/io/parser/skiprows.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests that skipped rows are properly handled during -parsing for all of the parsers defined in parsers.py -""" - -from datetime import datetime - -import numpy as np - -import pandas.util.testing as tm - -from pandas import DataFrame -from pandas.io.common import EmptyDataError -from pandas.compat import StringIO, range, lrange - - -class SkipRowsTests(object): - - def test_skiprows_bug(self): - # see gh-505 - text = """#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -#foo,a,b,c -1/1/2000,1.,2.,3. -1/2/2000,4,5,6 -1/3/2000,7,8,9 -""" - data = self.read_csv(StringIO(text), skiprows=lrange(6), header=None, - index_col=0, parse_dates=True) - - data2 = self.read_csv(StringIO(text), skiprows=6, header=None, - index_col=0, parse_dates=True) - - expected = DataFrame(np.arange(1., 10.).reshape((3, 3)), - columns=[1, 2, 3], - index=[datetime(2000, 1, 1), datetime(2000, 1, 2), - datetime(2000, 1, 3)]) - expected.index.name = 0 - tm.assert_frame_equal(data, expected) - tm.assert_frame_equal(data, data2) - - def test_deep_skiprows(self): - # see gh-4382 - text = "a,b,c\n" + \ - "\n".join([",".join([str(i), str(i + 1), str(i + 2)]) - for i in range(10)]) - condensed_text = "a,b,c\n" + \ - "\n".join([",".join([str(i), str(i + 1), str(i + 2)]) - for i in [0, 1, 2, 3, 4, 6, 8, 9]]) - data = self.read_csv(StringIO(text), skiprows=[6, 8]) - condensed_data = self.read_csv(StringIO(condensed_text)) - tm.assert_frame_equal(data, condensed_data) - - def test_skiprows_blank(self): - # see gh-9832 - text = """#foo,a,b,c -#foo,a,b,c - -#foo,a,b,c -#foo,a,b,c - -1/1/2000,1.,2.,3. -1/2/2000,4,5,6 -1/3/2000,7,8,9 -""" - data = self.read_csv(StringIO(text), skiprows=6, header=None, - index_col=0, parse_dates=True) - - expected = DataFrame(np.arange(1., 10.).reshape((3, 3)), - columns=[1, 2, 3], - index=[datetime(2000, 1, 1), datetime(2000, 1, 2), - datetime(2000, 1, 3)]) - expected.index.name = 0 - tm.assert_frame_equal(data, expected) - - def test_skiprow_with_newline(self): - # see gh-12775 and gh-10911 - data = """id,text,num_lines -1,"line 11 -line 12",2 -2,"line 21 -line 22",2 -3,"line 31",1""" - expected = [[2, 'line 21\nline 22', 2], - [3, 'line 31', 1]] - expected = DataFrame(expected, columns=[ - 'id', 'text', 'num_lines']) - df = self.read_csv(StringIO(data), skiprows=[1]) - tm.assert_frame_equal(df, expected) - - data = ('a,b,c\n~a\n b~,~e\n d~,' - '~f\n f~\n1,2,~12\n 13\n 14~') - expected = [['a\n b', 'e\n d', 'f\n f']] - expected = DataFrame(expected, columns=[ - 'a', 'b', 'c']) - df = self.read_csv(StringIO(data), - quotechar="~", - skiprows=[2]) - tm.assert_frame_equal(df, expected) - - data = ('Text,url\n~example\n ' - 'sentence\n one~,url1\n~' - 'example\n sentence\n two~,url2\n~' - 'example\n sentence\n three~,url3') - expected = [['example\n sentence\n two', 'url2']] - expected = DataFrame(expected, columns=[ - 'Text', 'url']) - df = self.read_csv(StringIO(data), - quotechar="~", - skiprows=[1, 3]) - tm.assert_frame_equal(df, expected) - - def test_skiprow_with_quote(self): - # see gh-12775 and gh-10911 - data = """id,text,num_lines -1,"line '11' line 12",2 -2,"line '21' line 22",2 -3,"line '31' line 32",1""" - expected = [[2, "line '21' line 22", 2], - [3, "line '31' line 32", 1]] - expected = DataFrame(expected, columns=[ - 'id', 'text', 'num_lines']) - df = self.read_csv(StringIO(data), skiprows=[1]) - tm.assert_frame_equal(df, expected) - - def test_skiprow_with_newline_and_quote(self): - # see gh-12775 and gh-10911 - data = """id,text,num_lines -1,"line \n'11' line 12",2 -2,"line \n'21' line 22",2 -3,"line \n'31' line 32",1""" - expected = [[2, "line \n'21' line 22", 2], - [3, "line \n'31' line 32", 1]] - expected = DataFrame(expected, columns=[ - 'id', 'text', 'num_lines']) - df = self.read_csv(StringIO(data), skiprows=[1]) - tm.assert_frame_equal(df, expected) - - data = """id,text,num_lines -1,"line '11\n' line 12",2 -2,"line '21\n' line 22",2 -3,"line '31\n' line 32",1""" - expected = [[2, "line '21\n' line 22", 2], - [3, "line '31\n' line 32", 1]] - expected = DataFrame(expected, columns=[ - 'id', 'text', 'num_lines']) - df = self.read_csv(StringIO(data), skiprows=[1]) - tm.assert_frame_equal(df, expected) - - data = """id,text,num_lines -1,"line '11\n' \r\tline 12",2 -2,"line '21\n' \r\tline 22",2 -3,"line '31\n' \r\tline 32",1""" - expected = [[2, "line '21\n' \r\tline 22", 2], - [3, "line '31\n' \r\tline 32", 1]] - expected = DataFrame(expected, columns=[ - 'id', 'text', 'num_lines']) - df = self.read_csv(StringIO(data), skiprows=[1]) - tm.assert_frame_equal(df, expected) - - def test_skiprows_lineterminator(self): - # see gh-9079 - data = '\n'.join(['SMOSMANIA ThetaProbe-ML2X ', - '2007/01/01 01:00 0.2140 U M ', - '2007/01/01 02:00 0.2141 M O ', - '2007/01/01 04:00 0.2142 D M ']) - expected = DataFrame([['2007/01/01', '01:00', 0.2140, 'U', 'M'], - ['2007/01/01', '02:00', 0.2141, 'M', 'O'], - ['2007/01/01', '04:00', 0.2142, 'D', 'M']], - columns=['date', 'time', 'var', 'flag', - 'oflag']) - - # test with default line terminators "LF" and "CRLF" - df = self.read_csv(StringIO(data), skiprows=1, delim_whitespace=True, - names=['date', 'time', 'var', 'flag', 'oflag']) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(data.replace('\n', '\r\n')), - skiprows=1, delim_whitespace=True, - names=['date', 'time', 'var', 'flag', 'oflag']) - tm.assert_frame_equal(df, expected) - - # "CR" is not respected with the Python parser yet - if self.engine == 'c': - df = self.read_csv(StringIO(data.replace('\n', '\r')), - skiprows=1, delim_whitespace=True, - names=['date', 'time', 'var', 'flag', 'oflag']) - tm.assert_frame_equal(df, expected) - - def test_skiprows_infield_quote(self): - # see gh-14459 - data = 'a"\nb"\na\n1' - expected = DataFrame({'a': [1]}) - - df = self.read_csv(StringIO(data), skiprows=2) - tm.assert_frame_equal(df, expected) - - def test_skiprows_callable(self): - data = 'a\n1\n2\n3\n4\n5' - - skiprows = lambda x: x % 2 == 0 - expected = DataFrame({'1': [3, 5]}) - df = self.read_csv(StringIO(data), skiprows=skiprows) - tm.assert_frame_equal(df, expected) - - expected = DataFrame({'foo': [3, 5]}) - df = self.read_csv(StringIO(data), skiprows=skiprows, - header=0, names=['foo']) - tm.assert_frame_equal(df, expected) - - skiprows = lambda x: True - msg = "No columns to parse from file" - with tm.assertRaisesRegexp(EmptyDataError, msg): - self.read_csv(StringIO(data), skiprows=skiprows) - - # This is a bad callable and should raise. - msg = "by zero" - skiprows = lambda x: 1 / 0 - with tm.assertRaisesRegexp(ZeroDivisionError, msg): - self.read_csv(StringIO(data), skiprows=skiprows) diff --git a/pandas/tests/io/parser/test_c_parser_only.py b/pandas/tests/io/parser/test_c_parser_only.py new file mode 100644 index 0000000000000..c089a189ae551 --- /dev/null +++ b/pandas/tests/io/parser/test_c_parser_only.py @@ -0,0 +1,591 @@ +# -*- coding: utf-8 -*- + +""" +Tests that apply specifically to the CParser. Unless specifically stated +as a CParser-specific issue, the goal is to eventually move as many of +these tests out of this module as soon as the Python parser can accept +further arguments when parsing. +""" + +from io import TextIOWrapper +import mmap +import os +import tarfile + +import numpy as np +import pytest + +from pandas.compat import PY3, BytesIO, StringIO, lrange, range +from pandas.errors import ParserError +import pandas.util._test_decorators as td + +from pandas import DataFrame, concat +import pandas.util.testing as tm + + +@pytest.mark.parametrize( + "malformed", + ["1\r1\r1\r 1\r 1\r", + "1\r1\r1\r 1\r 1\r11\r", + "1\r1\r1\r 1\r 1\r11\r1\r"], + ids=["words pointer", "stream pointer", "lines pointer"]) +def test_buffer_overflow(c_parser_only, malformed): + # see gh-9205: test certain malformed input files that cause + # buffer overflows in tokenizer.c + msg = "Buffer overflow caught - possible malformed input file." + parser = c_parser_only + + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(malformed)) + + +def test_buffer_rd_bytes(c_parser_only): + # see gh-12098: src->buffer in the C parser can be freed twice leading + # to a segfault if a corrupt gzip file is read with 'read_csv', and the + # buffer is filled more than once before gzip raises an Exception. + + data = "\x1F\x8B\x08\x00\x00\x00\x00\x00\x00\x03\xED\xC3\x41\x09" \ + "\x00\x00\x08\x00\xB1\xB7\xB6\xBA\xFE\xA5\xCC\x21\x6C\xB0" \ + "\xA6\x4D" + "\x55" * 267 + \ + "\x7D\xF7\x00\x91\xE0\x47\x97\x14\x38\x04\x00" \ + "\x1f\x8b\x08\x00VT\x97V\x00\x03\xed]\xefO" + parser = c_parser_only + + for _ in range(100): + try: + parser.read_csv(StringIO(data), compression="gzip", + delim_whitespace=True) + except Exception: + pass + + +def test_delim_whitespace_custom_terminator(c_parser_only): + # See gh-12912 + data = "a b c~1 2 3~4 5 6~7 8 9" + parser = c_parser_only + + df = parser.read_csv(StringIO(data), lineterminator="~", + delim_whitespace=True) + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=["a", "b", "c"]) + tm.assert_frame_equal(df, expected) + + +def test_dtype_and_names_error(c_parser_only): + # see gh-8833: passing both dtype and names + # resulting in an error reporting issue + parser = c_parser_only + data = """ +1.0 1 +2.0 2 +3.0 3 +""" + # base cases + result = parser.read_csv(StringIO(data), sep=r"\s+", header=None) + expected = DataFrame([[1.0, 1], [2.0, 2], [3.0, 3]]) + tm.assert_frame_equal(result, expected) + + result = parser.read_csv(StringIO(data), sep=r"\s+", + header=None, names=["a", "b"]) + expected = DataFrame( + [[1.0, 1], [2.0, 2], [3.0, 3]], columns=["a", "b"]) + tm.assert_frame_equal(result, expected) + + # fallback casting + result = parser.read_csv(StringIO( + data), sep=r"\s+", header=None, + names=["a", "b"], dtype={"a": np.int32}) + expected = DataFrame([[1, 1], [2, 2], [3, 3]], + columns=["a", "b"]) + expected["a"] = expected["a"].astype(np.int32) + tm.assert_frame_equal(result, expected) + + data = """ +1.0 1 +nan 2 +3.0 3 +""" + # fallback casting, but not castable + with pytest.raises(ValueError, match="cannot safely convert"): + parser.read_csv(StringIO(data), sep=r"\s+", header=None, + names=["a", "b"], dtype={"a": np.int32}) + + +@pytest.mark.parametrize("match,kwargs", [ + # For each of these cases, all of the dtypes are valid, just unsupported. + (("the dtype datetime64 is not supported for parsing, " + "pass this column using parse_dates instead"), + dict(dtype={"A": "datetime64", "B": "float64"})), + + (("the dtype datetime64 is not supported for parsing, " + "pass this column using parse_dates instead"), + dict(dtype={"A": "datetime64", "B": "float64"}, + parse_dates=["B"])), + + ("the dtype timedelta64 is not supported for parsing", + dict(dtype={"A": "timedelta64", "B": "float64"})), + + ("the dtype 262144b) + parser = c_parser_only + header_narrow = "\t".join(["COL_HEADER_" + str(i) + for i in range(10)]) + "\n" + data_narrow = "\t".join(["somedatasomedatasomedata1" + for _ in range(10)]) + "\n" + header_wide = "\t".join(["COL_HEADER_" + str(i) + for i in range(15)]) + "\n" + data_wide = "\t".join(["somedatasomedatasomedata2" + for _ in range(15)]) + "\n" + test_input = (header_narrow + data_narrow * 1050 + + header_wide + data_wide * 2) + + df = parser.read_csv(StringIO(test_input), sep="\t", nrows=1010) + + assert df.size == 1010 * 10 + + +def test_float_precision_round_trip_with_text(c_parser_only): + # see gh-15140 - This should not segfault on Python 2.7+ + parser = c_parser_only + df = parser.read_csv(StringIO("a"), header=None, + float_precision="round_trip") + tm.assert_frame_equal(df, DataFrame({0: ["a"]})) + + +def test_large_difference_in_columns(c_parser_only): + # see gh-14125 + parser = c_parser_only + + count = 10000 + large_row = ("X," * count)[:-1] + "\n" + normal_row = "XXXXXX XXXXXX,111111111111111\n" + test_input = (large_row + normal_row * 6)[:-1] + + result = parser.read_csv(StringIO(test_input), header=None, usecols=[0]) + rows = test_input.split("\n") + + expected = DataFrame([row.split(",")[0] for row in rows]) + tm.assert_frame_equal(result, expected) + + +def test_data_after_quote(c_parser_only): + # see gh-15910 + parser = c_parser_only + + data = "a\n1\n\"b\"a" + result = parser.read_csv(StringIO(data)) + + expected = DataFrame({"a": ["1", "ba"]}) + tm.assert_frame_equal(result, expected) + + +def test_comment_whitespace_delimited(c_parser_only, capsys): + parser = c_parser_only + test_input = """\ +1 2 +2 2 3 +3 2 3 # 3 fields +4 2 3# 3 fields +5 2 # 2 fields +6 2# 2 fields +7 # 1 field, NaN +8# 1 field, NaN +9 2 3 # skipped line +# comment""" + df = parser.read_csv(StringIO(test_input), comment="#", header=None, + delimiter="\\s+", skiprows=0, + error_bad_lines=False) + captured = capsys.readouterr() + # skipped lines 2, 3, 4, 9 + for line_num in (2, 3, 4, 9): + assert "Skipping line {}".format(line_num) in captured.err + expected = DataFrame([[1, 2], + [5, 2], + [6, 2], + [7, np.nan], + [8, np.nan]]) + tm.assert_frame_equal(df, expected) + + +def test_file_like_no_next(c_parser_only): + # gh-16530: the file-like need not have a "next" or "__next__" + # attribute despite having an "__iter__" attribute. + # + # NOTE: This is only true for the C engine, not Python engine. + class NoNextBuffer(StringIO): + def __next__(self): + raise AttributeError("No next method") + + next = __next__ + + parser = c_parser_only + data = "a\n1" + + expected = DataFrame({"a": [1]}) + result = parser.read_csv(NoNextBuffer(data)) + + tm.assert_frame_equal(result, expected) + + +def test_buffer_rd_bytes_bad_unicode(c_parser_only): + # see gh-22748 + parser = c_parser_only + t = BytesIO(b"\xB0") + + if PY3: + msg = "'utf-8' codec can't encode character" + t = TextIOWrapper(t, encoding="ascii", errors="surrogateescape") + else: + msg = "'utf8' codec can't decode byte" + + with pytest.raises(UnicodeError, match=msg): + parser.read_csv(t, encoding="UTF-8") + + +@pytest.mark.parametrize("tar_suffix", [".tar", ".tar.gz"]) +def test_read_tarfile(c_parser_only, csv_dir_path, tar_suffix): + # see gh-16530 + # + # Unfortunately, Python's CSV library can't handle + # tarfile objects (expects string, not bytes when + # iterating through a file-like). + parser = c_parser_only + tar_path = os.path.join(csv_dir_path, "tar_csv" + tar_suffix) + + with tarfile.open(tar_path, "r") as tar: + data_file = tar.extractfile("tar_data.csv") + + out = parser.read_csv(data_file) + expected = DataFrame({"a": [1]}) + tm.assert_frame_equal(out, expected) + + +@pytest.mark.high_memory +def test_bytes_exceed_2gb(c_parser_only): + # see gh-16798 + # + # Read from a "CSV" that has a column larger than 2GB. + parser = c_parser_only + + if parser.low_memory: + pytest.skip("not a high_memory test") + + csv = StringIO("strings\n" + "\n".join( + ["x" * (1 << 20) for _ in range(2100)])) + df = parser.read_csv(csv) + assert not df.empty + + +def test_chunk_whitespace_on_boundary(c_parser_only): + # see gh-9735: this issue is C parser-specific (bug when + # parsing whitespace and characters at chunk boundary) + # + # This test case has a field too large for the Python parser / CSV library. + parser = c_parser_only + + chunk1 = "a" * (1024 * 256 - 2) + "\na" + chunk2 = "\n a" + result = parser.read_csv(StringIO(chunk1 + chunk2), header=None) + + expected = DataFrame(["a" * (1024 * 256 - 2), "a", " a"]) + tm.assert_frame_equal(result, expected) + + +def test_file_handles_mmap(c_parser_only, csv1): + # gh-14418 + # + # Don't close user provided file handles. + parser = c_parser_only + + with open(csv1, "r") as f: + m = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + parser.read_csv(m) + + if PY3: + assert not m.closed + m.close() + + +def test_file_binary_mode(c_parser_only): + # see gh-23779 + parser = c_parser_only + expected = DataFrame([[1, 2, 3], [4, 5, 6]]) + + with tm.ensure_clean() as path: + with open(path, "w") as f: + f.write("1,2,3\n4,5,6") + + with open(path, "rb") as f: + result = parser.read_csv(f, header=None) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_comment.py b/pandas/tests/io/parser/test_comment.py new file mode 100644 index 0000000000000..299a04f876bd1 --- /dev/null +++ b/pandas/tests/io/parser/test_comment.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +""" +Tests that comments are properly handled during parsing +for all of the parsers defined in parsers.py +""" + +import numpy as np +import pytest + +from pandas.compat import StringIO + +from pandas import DataFrame +import pandas.util.testing as tm + + +@pytest.mark.parametrize("na_values", [None, ["NaN"]]) +def test_comment(all_parsers, na_values): + parser = all_parsers + data = """A,B,C +1,2.,4.#hello world +5.,NaN,10.0 +""" + expected = DataFrame([[1., 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data), comment="#", + na_values=na_values) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("read_kwargs", [ + dict(), + dict(lineterminator="*"), + dict(delim_whitespace=True), +]) +def test_line_comment(all_parsers, read_kwargs): + parser = all_parsers + data = """# empty +A,B,C +1,2.,4.#hello world +#ignore this line +5.,NaN,10.0 +""" + if read_kwargs.get("delim_whitespace"): + data = data.replace(",", " ") + elif read_kwargs.get("lineterminator"): + if parser.engine != "c": + pytest.skip("Custom terminator not supported with Python engine") + + data = data.replace("\n", read_kwargs.get("lineterminator")) + + read_kwargs["comment"] = "#" + result = parser.read_csv(StringIO(data), **read_kwargs) + + expected = DataFrame([[1., 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +def test_comment_skiprows(all_parsers): + parser = all_parsers + data = """# empty +random line +# second empty line +1,2,3 +A,B,C +1,2.,4. +5.,NaN,10.0 +""" + # This should ignore the first four lines (including comments). + expected = DataFrame([[1., 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data), comment="#", skiprows=4) + tm.assert_frame_equal(result, expected) + + +def test_comment_header(all_parsers): + parser = all_parsers + data = """# empty +# second empty line +1,2,3 +A,B,C +1,2.,4. +5.,NaN,10.0 +""" + # Header should begin at the second non-comment line. + expected = DataFrame([[1., 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data), comment="#", header=1) + tm.assert_frame_equal(result, expected) + + +def test_comment_skiprows_header(all_parsers): + parser = all_parsers + data = """# empty +# second empty line +# third empty line +X,Y,Z +1,2,3 +A,B,C +1,2.,4. +5.,NaN,10.0 +""" + # Skiprows should skip the first 4 lines (including comments), + # while header should start from the second non-commented line, + # starting with line 5. + expected = DataFrame([[1., 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data), comment="#", skiprows=4, header=1) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("comment_char", ["#", "~", "&", "^", "*", "@"]) +def test_custom_comment_char(all_parsers, comment_char): + parser = all_parsers + data = "a,b,c\n1,2,3#ignore this!\n4,5,6#ignorethistoo" + result = parser.read_csv(StringIO(data.replace("#", comment_char)), + comment=comment_char) + + expected = DataFrame([[1, 2, 3], [4, 5, 6]], columns=["a", "b", "c"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("header", ["infer", None]) +def test_comment_first_line(all_parsers, header): + # see gh-4623 + parser = all_parsers + data = "# notes\na,b,c\n# more notes\n1,2,3" + + if header is None: + expected = DataFrame({0: ["a", "1"], 1: ["b", "2"], 2: ["c", "3"]}) + else: + expected = DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) + + result = parser.read_csv(StringIO(data), comment="#", header=header) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_common.py b/pandas/tests/io/parser/test_common.py new file mode 100644 index 0000000000000..05da171d7dc31 --- /dev/null +++ b/pandas/tests/io/parser/test_common.py @@ -0,0 +1,1946 @@ +# -*- coding: utf-8 -*- + +""" +Tests that work on both the Python and C engines but do not have a +specific classification into the other test modules. +""" + +import codecs +from collections import OrderedDict +import csv +from datetime import datetime +import os +import platform +from tempfile import TemporaryFile + +import numpy as np +import pytest + +from pandas._libs.tslib import Timestamp +from pandas.compat import BytesIO, StringIO, lrange, range, u +from pandas.errors import DtypeWarning, EmptyDataError, ParserError + +from pandas import DataFrame, Index, MultiIndex, Series, compat, concat +import pandas.util.testing as tm + +from pandas.io.common import URLError +from pandas.io.parsers import CParserWrapper, TextFileReader, TextParser + + +def test_override_set_noconvert_columns(): + # see gh-17351 + # + # Usecols needs to be sorted in _set_noconvert_columns based + # on the test_usecols_with_parse_dates test from test_usecols.py + class MyTextFileReader(TextFileReader): + def __init__(self): + self._currow = 0 + self.squeeze = False + + class MyCParserWrapper(CParserWrapper): + def _set_noconvert_columns(self): + if self.usecols_dtype == "integer": + # self.usecols is a set, which is documented as unordered + # but in practice, a CPython set of integers is sorted. + # In other implementations this assumption does not hold. + # The following code simulates a different order, which + # before GH 17351 would cause the wrong columns to be + # converted via the parse_dates parameter + self.usecols = list(self.usecols) + self.usecols.reverse() + return CParserWrapper._set_noconvert_columns(self) + + data = """a,b,c,d,e +0,1,20140101,0900,4 +0,1,20140102,1000,4""" + + parse_dates = [[1, 2]] + cols = { + "a": [0, 0], + "c_d": [ + Timestamp("2014-01-01 09:00:00"), + Timestamp("2014-01-02 10:00:00") + ] + } + expected = DataFrame(cols, columns=["c_d", "a"]) + + parser = MyTextFileReader() + parser.options = {"usecols": [0, 2, 3], + "parse_dates": parse_dates, + "delimiter": ","} + parser._engine = MyCParserWrapper(StringIO(data), **parser.options) + + result = parser.read() + tm.assert_frame_equal(result, expected) + + +def test_bytes_io_input(all_parsers): + if compat.PY2: + pytest.skip("Bytes-related test does not need to work on Python 2.x") + + encoding = "cp1255" + parser = all_parsers + + data = BytesIO("שלום:1234\n562:123".encode(encoding)) + result = parser.read_csv(data, sep=":", encoding=encoding) + + expected = DataFrame([[562, 123]], columns=["שלום", "1234"]) + tm.assert_frame_equal(result, expected) + + +def test_empty_decimal_marker(all_parsers): + data = """A|B|C +1|2,334|5 +10|13|10. +""" + # Parsers support only length-1 decimals + msg = "Only length-1 decimal markers supported" + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), decimal="") + + +def test_bad_stream_exception(all_parsers, csv_dir_path): + # see gh-13652 + # + # This test validates that both the Python engine and C engine will + # raise UnicodeDecodeError instead of C engine raising ParserError + # and swallowing the exception that caused read to fail. + path = os.path.join(csv_dir_path, "sauron.SHIFT_JIS.csv") + codec = codecs.lookup("utf-8") + utf8 = codecs.lookup('utf-8') + parser = all_parsers + + msg = ("'utf-8' codec can't decode byte" if compat.PY3 + else "'utf8' codec can't decode byte") + + # Stream must be binary UTF8. + with open(path, "rb") as handle, codecs.StreamRecoder( + handle, utf8.encode, utf8.decode, codec.streamreader, + codec.streamwriter) as stream: + + with pytest.raises(UnicodeDecodeError, match=msg): + parser.read_csv(stream) + + +@pytest.mark.skipif(compat.PY2, reason="PY3-only test") +def test_read_csv_local(all_parsers, csv1): + prefix = u("file:///") if compat.is_platform_windows() else u("file://") + parser = all_parsers + + fname = prefix + compat.text_type(os.path.abspath(csv1)) + result = parser.read_csv(fname, index_col=0, parse_dates=True) + + expected = DataFrame([[0.980269, 3.685731, -0.364216805298, -1.159738], + [1.047916, -0.041232, -0.16181208307, 0.212549], + [0.498581, 0.731168, -0.537677223318, 1.346270], + [1.120202, 1.567621, 0.00364077397681, 0.675253], + [-0.487094, 0.571455, -1.6116394093, 0.103469], + [0.836649, 0.246462, 0.588542635376, 1.062782], + [-0.157161, 1.340307, 1.1957779562, -1.097007]], + columns=["A", "B", "C", "D"], + index=Index([datetime(2000, 1, 3), + datetime(2000, 1, 4), + datetime(2000, 1, 5), + datetime(2000, 1, 6), + datetime(2000, 1, 7), + datetime(2000, 1, 10), + datetime(2000, 1, 11)], name="index")) + tm.assert_frame_equal(result, expected) + + +def test_1000_sep(all_parsers): + parser = all_parsers + data = """A|B|C +1|2,334|5 +10|13|10. +""" + expected = DataFrame({ + "A": [1, 10], + "B": [2334, 13], + "C": [5, 10.] + }) + + result = parser.read_csv(StringIO(data), sep="|", thousands=",") + tm.assert_frame_equal(result, expected) + + +def test_squeeze(all_parsers): + data = """\ +a,1 +b,2 +c,3 +""" + parser = all_parsers + index = Index(["a", "b", "c"], name=0) + expected = Series([1, 2, 3], name=1, index=index) + + result = parser.read_csv(StringIO(data), index_col=0, + header=None, squeeze=True) + tm.assert_series_equal(result, expected) + + # see gh-8217 + # + # Series should not be a view. + assert not result._is_view + + +def test_malformed(all_parsers): + # see gh-6607 + parser = all_parsers + data = """ignore +A,B,C +1,2,3 # comment +1,2,3,4,5 +2,3,4 +""" + msg = "Expected 3 fields in line 4, saw 5" + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), header=1, comment="#") + + +@pytest.mark.parametrize("nrows", [5, 3, None]) +def test_malformed_chunks(all_parsers, nrows): + data = """ignore +A,B,C +skip +1,2,3 +3,5,10 # comment +1,2,3,4,5 +2,3,4 +""" + parser = all_parsers + msg = 'Expected 3 fields in line 6, saw 5' + reader = parser.read_csv(StringIO(data), header=1, comment="#", + iterator=True, chunksize=1, skiprows=[2]) + + with pytest.raises(ParserError, match=msg): + reader.read(nrows) + + +def test_unnamed_columns(all_parsers): + data = """A,B,C,, +1,2,3,4,5 +6,7,8,9,10 +11,12,13,14,15 +""" + parser = all_parsers + expected = DataFrame([[1, 2, 3, 4, 5], + [6, 7, 8, 9, 10], + [11, 12, 13, 14, 15]], + dtype=np.int64, columns=["A", "B", "C", + "Unnamed: 3", + "Unnamed: 4"]) + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +def test_csv_mixed_type(all_parsers): + data = """A,B,C +a,1,2 +b,3,4 +c,4,5 +""" + parser = all_parsers + expected = DataFrame({"A": ["a", "b", "c"], + "B": [1, 3, 4], + "C": [2, 4, 5]}) + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_low_memory_no_rows_with_index(all_parsers): + # see gh-21141 + parser = all_parsers + + if not parser.low_memory: + pytest.skip("This is a low-memory specific test") + + data = """A,B,C +1,1,1,2 +2,2,3,4 +3,3,4,5 +""" + result = parser.read_csv(StringIO(data), low_memory=True, + index_col=0, nrows=0) + expected = DataFrame(columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_dataframe(all_parsers, csv1): + parser = all_parsers + result = parser.read_csv(csv1, index_col=0, parse_dates=True) + + expected = DataFrame([[0.980269, 3.685731, -0.364216805298, -1.159738], + [1.047916, -0.041232, -0.16181208307, 0.212549], + [0.498581, 0.731168, -0.537677223318, 1.346270], + [1.120202, 1.567621, 0.00364077397681, 0.675253], + [-0.487094, 0.571455, -1.6116394093, 0.103469], + [0.836649, 0.246462, 0.588542635376, 1.062782], + [-0.157161, 1.340307, 1.1957779562, -1.097007]], + columns=["A", "B", "C", "D"], + index=Index([datetime(2000, 1, 3), + datetime(2000, 1, 4), + datetime(2000, 1, 5), + datetime(2000, 1, 6), + datetime(2000, 1, 7), + datetime(2000, 1, 10), + datetime(2000, 1, 11)], name="index")) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_no_index_name(all_parsers, csv_dir_path): + parser = all_parsers + csv2 = os.path.join(csv_dir_path, "test2.csv") + result = parser.read_csv(csv2, index_col=0, parse_dates=True) + + expected = DataFrame([[0.980269, 3.685731, -0.364216805298, + -1.159738, "foo"], + [1.047916, -0.041232, -0.16181208307, + 0.212549, "bar"], + [0.498581, 0.731168, -0.537677223318, + 1.346270, "baz"], + [1.120202, 1.567621, 0.00364077397681, + 0.675253, "qux"], + [-0.487094, 0.571455, -1.6116394093, + 0.103469, "foo2"]], + columns=["A", "B", "C", "D", "E"], + index=Index([datetime(2000, 1, 3), + datetime(2000, 1, 4), + datetime(2000, 1, 5), + datetime(2000, 1, 6), + datetime(2000, 1, 7)])) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_unicode(all_parsers): + parser = all_parsers + data = BytesIO(u("\u0141aski, Jan;1").encode("utf-8")) + + result = parser.read_csv(data, sep=";", encoding="utf-8", header=None) + expected = DataFrame([[u("\u0141aski, Jan"), 1]]) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_wrong_num_columns(all_parsers): + # Too few columns. + data = """A,B,C,D,E,F +1,2,3,4,5,6 +6,7,8,9,10,11,12 +11,12,13,14,15,16 +""" + parser = all_parsers + msg = "Expected 6 fields in line 3, saw 7" + + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data)) + + +def test_read_duplicate_index_explicit(all_parsers): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo,12,13,14,15 +bar,12,13,14,15 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=0) + + expected = DataFrame([[2, 3, 4, 5], [7, 8, 9, 10], + [12, 13, 14, 15], [12, 13, 14, 15], + [12, 13, 14, 15], [12, 13, 14, 15]], + columns=["A", "B", "C", "D"], + index=Index(["foo", "bar", "baz", + "qux", "foo", "bar"], name="index")) + tm.assert_frame_equal(result, expected) + + +def test_read_duplicate_index_implicit(all_parsers): + data = """A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo,12,13,14,15 +bar,12,13,14,15 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data)) + + expected = DataFrame([[2, 3, 4, 5], [7, 8, 9, 10], + [12, 13, 14, 15], [12, 13, 14, 15], + [12, 13, 14, 15], [12, 13, 14, 15]], + columns=["A", "B", "C", "D"], + index=Index(["foo", "bar", "baz", + "qux", "foo", "bar"])) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + ("A,B\nTrue,1\nFalse,2\nTrue,3", dict(), + DataFrame([[True, 1], [False, 2], [True, 3]], columns=["A", "B"])), + ("A,B\nYES,1\nno,2\nyes,3\nNo,3\nYes,3", + dict(true_values=["yes", "Yes", "YES"], + false_values=["no", "NO", "No"]), + DataFrame([[True, 1], [False, 2], [True, 3], + [False, 3], [True, 3]], columns=["A", "B"])), + ("A,B\nTRUE,1\nFALSE,2\nTRUE,3", dict(), + DataFrame([[True, 1], [False, 2], [True, 3]], columns=["A", "B"])), + ("A,B\nfoo,bar\nbar,foo", dict(true_values=["foo"], + false_values=["bar"]), + DataFrame([[True, False], [False, True]], columns=["A", "B"])) +]) +def test_parse_bool(all_parsers, data, kwargs, expected): + parser = all_parsers + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_int_conversion(all_parsers): + data = """A,B +1.0,1 +2.0,2 +3.0,3 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data)) + + expected = DataFrame([[1.0, 1], [2.0, 2], [3.0, 3]], columns=["A", "B"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("nrows", [3, 3.0]) +def test_read_nrows(all_parsers, nrows): + # see gh-10476 + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + expected = DataFrame([["foo", 2, 3, 4, 5], + ["bar", 7, 8, 9, 10], + ["baz", 12, 13, 14, 15]], + columns=["index", "A", "B", "C", "D"]) + parser = all_parsers + + result = parser.read_csv(StringIO(data), nrows=nrows) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("nrows", [1.2, "foo", -1]) +def test_read_nrows_bad(all_parsers, nrows): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + msg = r"'nrows' must be an integer >=0" + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), nrows=nrows) + + +@pytest.mark.parametrize("index_col", [0, "index"]) +def test_read_chunksize_with_index(all_parsers, index_col): + parser = all_parsers + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + + reader = parser.read_csv(StringIO(data), index_col=0, chunksize=2) + expected = DataFrame([["foo", 2, 3, 4, 5], + ["bar", 7, 8, 9, 10], + ["baz", 12, 13, 14, 15], + ["qux", 12, 13, 14, 15], + ["foo2", 12, 13, 14, 15], + ["bar2", 12, 13, 14, 15]], + columns=["index", "A", "B", "C", "D"]) + expected = expected.set_index("index") + + chunks = list(reader) + tm.assert_frame_equal(chunks[0], expected[:2]) + tm.assert_frame_equal(chunks[1], expected[2:4]) + tm.assert_frame_equal(chunks[2], expected[4:]) + + +@pytest.mark.parametrize("chunksize", [1.3, "foo", 0]) +def test_read_chunksize_bad(all_parsers, chunksize): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + msg = r"'chunksize' must be an integer >=1" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), chunksize=chunksize) + + +@pytest.mark.parametrize("chunksize", [2, 8]) +def test_read_chunksize_and_nrows(all_parsers, chunksize): + # see gh-15755 + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + kwargs = dict(index_col=0, nrows=5) + + reader = parser.read_csv(StringIO(data), chunksize=chunksize, **kwargs) + expected = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(concat(reader), expected) + + +def test_read_chunksize_and_nrows_changing_size(all_parsers): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + kwargs = dict(index_col=0, nrows=5) + + reader = parser.read_csv(StringIO(data), chunksize=8, **kwargs) + expected = parser.read_csv(StringIO(data), **kwargs) + + tm.assert_frame_equal(reader.get_chunk(size=2), expected.iloc[:2]) + tm.assert_frame_equal(reader.get_chunk(size=4), expected.iloc[2:5]) + + with pytest.raises(StopIteration, match=""): + reader.get_chunk(size=3) + + +def test_get_chunk_passed_chunksize(all_parsers): + parser = all_parsers + data = """A,B,C +1,2,3 +4,5,6 +7,8,9 +1,2,3""" + + reader = parser.read_csv(StringIO(data), chunksize=2) + result = reader.get_chunk() + + expected = DataFrame([[1, 2, 3], [4, 5, 6]], columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [dict(), dict(index_col=0)]) +def test_read_chunksize_compat(all_parsers, kwargs): + # see gh-12185 + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + reader = parser.read_csv(StringIO(data), chunksize=2, **kwargs) + + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(concat(reader), result) + + +def test_read_chunksize_jagged_names(all_parsers): + # see gh-23509 + parser = all_parsers + data = "\n".join(["0"] * 7 + [",".join(["0"] * 10)]) + + expected = DataFrame([[0] + [np.nan] * 9] * 7 + [[0] * 10]) + reader = parser.read_csv(StringIO(data), names=range(10), chunksize=4) + + result = concat(reader) + tm.assert_frame_equal(result, expected) + + +def test_read_data_list(all_parsers): + parser = all_parsers + kwargs = dict(index_col=0) + data = "A,B,C\nfoo,1,2,3\nbar,4,5,6" + + data_list = [["A", "B", "C"], ["foo", "1", "2", "3"], + ["bar", "4", "5", "6"]] + expected = parser.read_csv(StringIO(data), **kwargs) + + parser = TextParser(data_list, chunksize=2, **kwargs) + result = parser.read() + + tm.assert_frame_equal(result, expected) + + +def test_iterator(all_parsers): + # see gh-6607 + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + kwargs = dict(index_col=0) + + expected = parser.read_csv(StringIO(data), **kwargs) + reader = parser.read_csv(StringIO(data), iterator=True, **kwargs) + + first_chunk = reader.read(3) + tm.assert_frame_equal(first_chunk, expected[:3]) + + last_chunk = reader.read(5) + tm.assert_frame_equal(last_chunk, expected[3:]) + + +def test_iterator2(all_parsers): + parser = all_parsers + data = """A,B,C +foo,1,2,3 +bar,4,5,6 +baz,7,8,9 +""" + + reader = parser.read_csv(StringIO(data), iterator=True) + result = list(reader) + + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["foo", "bar", "baz"], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result[0], expected) + + +def test_reader_list(all_parsers): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + kwargs = dict(index_col=0) + + lines = list(csv.reader(StringIO(data))) + reader = TextParser(lines, chunksize=2, **kwargs) + + expected = parser.read_csv(StringIO(data), **kwargs) + chunks = list(reader) + + tm.assert_frame_equal(chunks[0], expected[:2]) + tm.assert_frame_equal(chunks[1], expected[2:4]) + tm.assert_frame_equal(chunks[2], expected[4:]) + + +def test_reader_list_skiprows(all_parsers): + data = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""" + parser = all_parsers + kwargs = dict(index_col=0) + + lines = list(csv.reader(StringIO(data))) + reader = TextParser(lines, chunksize=2, skiprows=[1], **kwargs) + + expected = parser.read_csv(StringIO(data), **kwargs) + chunks = list(reader) + + tm.assert_frame_equal(chunks[0], expected[1:3]) + + +def test_iterator_stop_on_chunksize(all_parsers): + # gh-3967: stopping iteration when chunksize is specified + parser = all_parsers + data = """A,B,C +foo,1,2,3 +bar,4,5,6 +baz,7,8,9 +""" + + reader = parser.read_csv(StringIO(data), chunksize=1) + result = list(reader) + + assert len(result) == 3 + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["foo", "bar", "baz"], + columns=["A", "B", "C"]) + tm.assert_frame_equal(concat(result), expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(iterator=True, + chunksize=1), + dict(iterator=True), + dict(chunksize=1) +]) +def test_iterator_skipfooter_errors(all_parsers, kwargs): + msg = "'skipfooter' not supported for 'iteration'" + parser = all_parsers + data = "a\n1\n2" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), skipfooter=1, **kwargs) + + +def test_nrows_skipfooter_errors(all_parsers): + msg = "'skipfooter' not supported with 'nrows'" + data = "a\n1\n2\n3\n4\n5\n6" + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), skipfooter=1, nrows=5) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + ("""foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +qux,12,13,14,15 +foo2,12,13,14,15 +bar2,12,13,14,15 +""", dict(index_col=0, names=["index", "A", "B", "C", "D"]), + DataFrame([[2, 3, 4, 5], [7, 8, 9, 10], [12, 13, 14, 15], + [12, 13, 14, 15], [12, 13, 14, 15], [12, 13, 14, 15]], + index=Index(["foo", "bar", "baz", "qux", + "foo2", "bar2"], name="index"), + columns=["A", "B", "C", "D"])), + ("""foo,one,2,3,4,5 +foo,two,7,8,9,10 +foo,three,12,13,14,15 +bar,one,12,13,14,15 +bar,two,12,13,14,15 +""", dict(index_col=[0, 1], names=["index1", "index2", "A", "B", "C", "D"]), + DataFrame([[2, 3, 4, 5], [7, 8, 9, 10], [12, 13, 14, 15], + [12, 13, 14, 15], [12, 13, 14, 15]], + index=MultiIndex.from_tuples([ + ("foo", "one"), ("foo", "two"), ("foo", "three"), + ("bar", "one"), ("bar", "two")], + names=["index1", "index2"]), + columns=["A", "B", "C", "D"])), +]) +def test_pass_names_with_index(all_parsers, data, kwargs, expected): + parser = all_parsers + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_col", [[0, 1], [1, 0]]) +def test_multi_index_no_level_names(all_parsers, index_col): + data = """index1,index2,A,B,C,D +foo,one,2,3,4,5 +foo,two,7,8,9,10 +foo,three,12,13,14,15 +bar,one,12,13,14,15 +bar,two,12,13,14,15 +""" + headless_data = '\n'.join(data.split("\n")[1:]) + + names = ["A", "B", "C", "D"] + parser = all_parsers + + result = parser.read_csv(StringIO(headless_data), + index_col=index_col, + header=None, names=names) + expected = parser.read_csv(StringIO(data), index_col=index_col) + + # No index names in headless data. + expected.index.names = [None] * 2 + tm.assert_frame_equal(result, expected) + + +def test_multi_index_no_level_names_implicit(all_parsers): + parser = all_parsers + data = """A,B,C,D +foo,one,2,3,4,5 +foo,two,7,8,9,10 +foo,three,12,13,14,15 +bar,one,12,13,14,15 +bar,two,12,13,14,15 +""" + + result = parser.read_csv(StringIO(data)) + expected = DataFrame([[2, 3, 4, 5], [7, 8, 9, 10], [12, 13, 14, 15], + [12, 13, 14, 15], [12, 13, 14, 15]], + columns=["A", "B", "C", "D"], + index=MultiIndex.from_tuples([ + ("foo", "one"), ("foo", "two"), ("foo", "three"), + ("bar", "one"), ("bar", "two")])) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,expected,header", [ + ("a,b", DataFrame(columns=["a", "b"]), [0]), + ("a,b\nc,d", DataFrame(columns=MultiIndex.from_tuples( + [("a", "c"), ("b", "d")])), [0, 1]), +]) +@pytest.mark.parametrize("round_trip", [True, False]) +def test_multi_index_blank_df(all_parsers, data, expected, header, round_trip): + # see gh-14545 + parser = all_parsers + data = expected.to_csv(index=False) if round_trip else data + + result = parser.read_csv(StringIO(data), header=header) + tm.assert_frame_equal(result, expected) + + +def test_no_unnamed_index(all_parsers): + parser = all_parsers + data = """ id c0 c1 c2 +0 1 0 a b +1 2 0 c d +2 2 2 e f +""" + result = parser.read_csv(StringIO(data), sep=" ") + expected = DataFrame([[0, 1, 0, "a", "b"], [1, 2, 0, "c", "d"], + [2, 2, 2, "e", "f"]], columns=["Unnamed: 0", "id", + "c0", "c1", "c2"]) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_parse_simple_list(all_parsers): + parser = all_parsers + data = """foo +bar baz +qux foo +foo +bar""" + + result = parser.read_csv(StringIO(data), header=None) + expected = DataFrame(["foo", "bar baz", "qux foo", "foo", "bar"]) + tm.assert_frame_equal(result, expected) + + +@tm.network +def test_url(all_parsers, csv_dir_path): + # TODO: FTP testing + parser = all_parsers + kwargs = dict(sep="\t") + + url = ("https://raw.github.com/pandas-dev/pandas/master/" + "pandas/tests/io/parser/data/salaries.csv") + url_result = parser.read_csv(url, **kwargs) + + local_path = os.path.join(csv_dir_path, "salaries.csv") + local_result = parser.read_csv(local_path, **kwargs) + tm.assert_frame_equal(url_result, local_result) + + +@pytest.mark.slow +def test_local_file(all_parsers, csv_dir_path): + parser = all_parsers + kwargs = dict(sep="\t") + + local_path = os.path.join(csv_dir_path, "salaries.csv") + local_result = parser.read_csv(local_path, **kwargs) + url = "file://localhost/" + local_path + + try: + url_result = parser.read_csv(url, **kwargs) + tm.assert_frame_equal(url_result, local_result) + except URLError: + # Fails on some systems. + pytest.skip("Failing on: " + " ".join(platform.uname())) + + +def test_path_path_lib(all_parsers): + parser = all_parsers + df = tm.makeDataFrame() + result = tm.round_trip_pathlib( + df.to_csv, lambda p: parser.read_csv(p, index_col=0)) + tm.assert_frame_equal(df, result) + + +def test_path_local_path(all_parsers): + parser = all_parsers + df = tm.makeDataFrame() + result = tm.round_trip_localpath( + df.to_csv, lambda p: parser.read_csv(p, index_col=0)) + tm.assert_frame_equal(df, result) + + +def test_nonexistent_path(all_parsers): + # gh-2428: pls no segfault + # gh-14086: raise more helpful FileNotFoundError + parser = all_parsers + path = "%s.csv" % tm.rands(10) + + msg = ("does not exist" if parser.engine == "c" + else r"\[Errno 2\]") + with pytest.raises(compat.FileNotFoundError, match=msg) as e: + parser.read_csv(path) + + filename = e.value.filename + filename = filename.decode() if isinstance( + filename, bytes) else filename + + assert path == filename + + +def test_missing_trailing_delimiters(all_parsers): + parser = all_parsers + data = """A,B,C,D +1,2,3,4 +1,3,3, +1,4,5""" + + result = parser.read_csv(StringIO(data)) + expected = DataFrame([[1, 2, 3, 4], [1, 3, 3, np.nan], + [1, 4, 5, np.nan]], columns=["A", "B", "C", "D"]) + tm.assert_frame_equal(result, expected) + + +def test_skip_initial_space(all_parsers): + data = ('"09-Apr-2012", "01:10:18.300", 2456026.548822908, 12849, ' + '1.00361, 1.12551, 330.65659, 0355626618.16711, 73.48821, ' + '314.11625, 1917.09447, 179.71425, 80.000, 240.000, -350, ' + '70.06056, 344.98370, 1, 1, -0.689265, -0.692787, ' + '0.212036, 14.7674, 41.605, -9999.0, -9999.0, ' + '-9999.0, -9999.0, -9999.0, -9999.0, 000, 012, 128') + parser = all_parsers + + result = parser.read_csv(StringIO(data), names=lrange(33), header=None, + na_values=["-9999.0"], skipinitialspace=True) + expected = DataFrame([["09-Apr-2012", "01:10:18.300", 2456026.548822908, + 12849, 1.00361, 1.12551, 330.65659, + 355626618.16711, 73.48821, 314.11625, 1917.09447, + 179.71425, 80.0, 240.0, -350, 70.06056, 344.9837, + 1, 1, -0.689265, -0.692787, 0.212036, 14.7674, + 41.605, np.nan, np.nan, np.nan, np.nan, np.nan, + np.nan, 0, 12, 128]]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("sep", [",", "\t"]) +@pytest.mark.parametrize("encoding", ["utf-16", "utf-16le", "utf-16be"]) +def test_utf16_bom_skiprows(all_parsers, sep, encoding): + # see gh-2298 + parser = all_parsers + data = u("""skip this +skip this too +A,B,C +1,2,3 +4,5,6""").replace(",", sep) + path = "__%s__.csv" % tm.rands(10) + kwargs = dict(sep=sep, skiprows=2) + utf8 = "utf-8" + + with tm.ensure_clean(path) as path: + bytes_data = data.encode(encoding) + + with open(path, "wb") as f: + f.write(bytes_data) + + bytes_buffer = BytesIO(data.encode(utf8)) + + if compat.PY3: + from io import TextIOWrapper + bytes_buffer = TextIOWrapper(bytes_buffer, encoding=utf8) + + result = parser.read_csv(path, encoding=encoding, **kwargs) + expected = parser.read_csv(bytes_buffer, encoding=utf8, **kwargs) + + bytes_buffer.close() + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("buffer", [ + False, + pytest.param(True, marks=pytest.mark.skipif( + compat.PY3, reason="Not supported on PY3"))]) +def test_utf16_example(all_parsers, csv_dir_path, buffer): + path = os.path.join(csv_dir_path, "utf16_ex.txt") + parser = all_parsers + + src = BytesIO(open(path, "rb").read()) if buffer else path + result = parser.read_csv(src, encoding="utf-16", sep="\t") + assert len(result) == 50 + + +def test_unicode_encoding(all_parsers, csv_dir_path): + path = os.path.join(csv_dir_path, "unicode_series.csv") + parser = all_parsers + + result = parser.read_csv(path, header=None, encoding="latin-1") + result = result.set_index(0) + got = result[1][1632] + + expected = u('\xc1 k\xf6ldum klaka (Cold Fever) (1994)') + assert got == expected + + +def test_trailing_delimiters(all_parsers): + # see gh-2442 + data = """A,B,C +1,2,3, +4,5,6, +7,8,9,""" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=False) + + expected = DataFrame({"A": [1, 4, 7], "B": [2, 5, 8], "C": [3, 6, 9]}) + tm.assert_frame_equal(result, expected) + + +def test_escapechar(all_parsers): + # http://stackoverflow.com/questions/13824840/feature-request-for- + # pandas-read-csv + data = '''SEARCH_TERM,ACTUAL_URL +"bra tv bord","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord" +"tv p\xc3\xa5 hjul","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord" +"SLAGBORD, \\"Bergslagen\\", IKEA:s 1700-tals serie","http://www.ikea.com/se/sv/catalog/categories/departments/living_room/10475/?se%7cps%7cnonbranded%7cvardagsrum%7cgoogle%7ctv_bord"''' # noqa + + parser = all_parsers + result = parser.read_csv(StringIO(data), escapechar='\\', + quotechar='"', encoding='utf-8') + + assert result['SEARCH_TERM'][2] == ('SLAGBORD, "Bergslagen", ' + 'IKEA:s 1700-tals serie') + tm.assert_index_equal(result.columns, + Index(['SEARCH_TERM', 'ACTUAL_URL'])) + + +def test_int64_min_issues(all_parsers): + # see gh-2599 + parser = all_parsers + data = "A,B\n0,0\n0," + result = parser.read_csv(StringIO(data)) + + expected = DataFrame({"A": [0, 0], "B": [0, np.nan]}) + tm.assert_frame_equal(result, expected) + + +def test_parse_integers_above_fp_precision(all_parsers): + data = """Numbers +17007000002000191 +17007000002000191 +17007000002000191 +17007000002000191 +17007000002000192 +17007000002000192 +17007000002000192 +17007000002000192 +17007000002000192 +17007000002000194""" + parser = all_parsers + result = parser.read_csv(StringIO(data)) + expected = DataFrame({"Numbers": [17007000002000191, + 17007000002000191, + 17007000002000191, + 17007000002000191, + 17007000002000192, + 17007000002000192, + 17007000002000192, + 17007000002000192, + 17007000002000192, + 17007000002000194]}) + tm.assert_frame_equal(result, expected) + + +def test_chunks_have_consistent_numerical_type(all_parsers): + parser = all_parsers + integers = [str(i) for i in range(499999)] + data = "a\n" + "\n".join(integers + ["1.0", "2.0"] + integers) + + # Coercions should work without warnings. + with tm.assert_produces_warning(None): + result = parser.read_csv(StringIO(data)) + + assert type(result.a[0]) is np.float64 + assert result.a.dtype == np.float + + +def test_warn_if_chunks_have_mismatched_type(all_parsers): + warning_type = None + parser = all_parsers + integers = [str(i) for i in range(499999)] + data = "a\n" + "\n".join(integers + ["a", "b"] + integers) + + # see gh-3866: if chunks are different types and can't + # be coerced using numerical types, then issue warning. + if parser.engine == "c" and parser.low_memory: + warning_type = DtypeWarning + + with tm.assert_produces_warning(warning_type): + df = parser.read_csv(StringIO(data)) + assert df.a.dtype == np.object + + +@pytest.mark.parametrize("sep", [" ", r"\s+"]) +def test_integer_overflow_bug(all_parsers, sep): + # see gh-2601 + data = "65248E10 11\n55555E55 22\n" + parser = all_parsers + + result = parser.read_csv(StringIO(data), header=None, sep=sep) + expected = DataFrame([[6.5248e14, 11], [5.5555e59, 22]]) + tm.assert_frame_equal(result, expected) + + +def test_catch_too_many_names(all_parsers): + # see gh-5156 + data = """\ +1,2,3 +4,,6 +7,8,9 +10,11,12\n""" + parser = all_parsers + msg = ("Too many columns specified: " + "expected 4 and found 3" if parser.engine == "c" + else "Number of passed names did not match " + "number of header fields in the file") + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), header=0, names=["a", "b", "c", "d"]) + + +def test_ignore_leading_whitespace(all_parsers): + # see gh-3374, gh-6607 + parser = all_parsers + data = " a b c\n 1 2 3\n 4 5 6\n 7 8 9" + result = parser.read_csv(StringIO(data), sep=r"\s+") + + expected = DataFrame({"a": [1, 4, 7], "b": [2, 5, 8], "c": [3, 6, 9]}) + tm.assert_frame_equal(result, expected) + + +def test_chunk_begins_with_newline_whitespace(all_parsers): + # see gh-10022 + parser = all_parsers + data = "\n hello\nworld\n" + + result = parser.read_csv(StringIO(data), header=None) + expected = DataFrame([" hello", "world"]) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_index(all_parsers): + # see gh-10184 + data = "x,y" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=0) + + expected = DataFrame([], columns=["y"], index=Index([], name="x")) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_multi_index(all_parsers): + # see gh-10467 + data = "x,y,z" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=["x", "y"]) + + expected = DataFrame([], columns=["z"], + index=MultiIndex.from_arrays( + [[]] * 2, names=["x", "y"])) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_reversed_multi_index(all_parsers): + data = "x,y,z" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=[1, 0]) + + expected = DataFrame([], columns=["z"], + index=MultiIndex.from_arrays( + [[]] * 2, names=["y", "x"])) + tm.assert_frame_equal(result, expected) + + +def test_float_parser(all_parsers): + # see gh-9565 + parser = all_parsers + data = "45e-1,4.5,45.,inf,-inf" + result = parser.read_csv(StringIO(data), header=None) + + expected = DataFrame([[float(s) for s in data.split(",")]]) + tm.assert_frame_equal(result, expected) + + +def test_scientific_no_exponent(all_parsers): + # see gh-12215 + df = DataFrame.from_dict(OrderedDict([("w", ["2e"]), ("x", ["3E"]), + ("y", ["42e"]), + ("z", ["632E"])])) + data = df.to_csv(index=False) + parser = all_parsers + + for precision in parser.float_precision_choices: + df_roundtrip = parser.read_csv(StringIO(data), + float_precision=precision) + tm.assert_frame_equal(df_roundtrip, df) + + +@pytest.mark.parametrize("conv", [None, np.int64, np.uint64]) +def test_int64_overflow(all_parsers, conv): + data = """ID +00013007854817840016671868 +00013007854817840016749251 +00013007854817840016754630 +00013007854817840016781876 +00013007854817840017028824 +00013007854817840017963235 +00013007854817840018860166""" + parser = all_parsers + + if conv is None: + # 13007854817840016671868 > UINT64_MAX, so this + # will overflow and return object as the dtype. + result = parser.read_csv(StringIO(data)) + expected = DataFrame(["00013007854817840016671868", + "00013007854817840016749251", + "00013007854817840016754630", + "00013007854817840016781876", + "00013007854817840017028824", + "00013007854817840017963235", + "00013007854817840018860166"], columns=["ID"]) + tm.assert_frame_equal(result, expected) + else: + # 13007854817840016671868 > UINT64_MAX, so attempts + # to cast to either int64 or uint64 will result in + # an OverflowError being raised. + msg = ("(Python int too large to convert to C long)|" + "(long too big to convert)|" + "(int too big to convert)") + + with pytest.raises(OverflowError, match=msg): + parser.read_csv(StringIO(data), converters={"ID": conv}) + + +@pytest.mark.parametrize("val", [ + np.iinfo(np.uint64).max, + np.iinfo(np.int64).max, + np.iinfo(np.int64).min +]) +def test_int64_uint64_range(all_parsers, val): + # These numbers fall right inside the int64-uint64 + # range, so they should be parsed as string. + parser = all_parsers + result = parser.read_csv(StringIO(str(val)), header=None) + + expected = DataFrame([val]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("val", [ + np.iinfo(np.uint64).max + 1, + np.iinfo(np.int64).min - 1 +]) +def test_outside_int64_uint64_range(all_parsers, val): + # These numbers fall just outside the int64-uint64 + # range, so they should be parsed as string. + parser = all_parsers + result = parser.read_csv(StringIO(str(val)), header=None) + + expected = DataFrame([str(val)]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("exp_data", [[str(-1), str(2**63)], + [str(2**63), str(-1)]]) +def test_numeric_range_too_wide(all_parsers, exp_data): + # No numerical dtype can hold both negative and uint64 + # values, so they should be cast as string. + parser = all_parsers + data = "\n".join(exp_data) + expected = DataFrame(exp_data) + + result = parser.read_csv(StringIO(data), header=None) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("iterator", [True, False]) +def test_empty_with_nrows_chunksize(all_parsers, iterator): + # see gh-9535 + parser = all_parsers + expected = DataFrame([], columns=["foo", "bar"]) + + nrows = 10 + data = StringIO("foo,bar\n") + + if iterator: + result = next(iter(parser.read_csv(data, chunksize=nrows))) + else: + result = parser.read_csv(data, nrows=nrows) + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected,msg", [ + # gh-10728: WHITESPACE_LINE + ("a,b,c\n4,5,6\n ", dict(), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # gh-10548: EAT_LINE_COMMENT + ("a,b,c\n4,5,6\n#comment", dict(comment="#"), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # EAT_CRNL_NOP + ("a,b,c\n4,5,6\n\r", dict(), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # EAT_COMMENT + ("a,b,c\n4,5,6#comment", dict(comment="#"), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # SKIP_LINE + ("a,b,c\n4,5,6\nskipme", dict(skiprows=[2]), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # EAT_LINE_COMMENT + ("a,b,c\n4,5,6\n#comment", dict(comment="#", skip_blank_lines=False), + DataFrame([[4, 5, 6]], columns=["a", "b", "c"]), None), + + # IN_FIELD + ("a,b,c\n4,5,6\n ", dict(skip_blank_lines=False), + DataFrame([["4", 5, 6], [" ", None, None]], + columns=["a", "b", "c"]), None), + + # EAT_CRNL + ("a,b,c\n4,5,6\n\r", dict(skip_blank_lines=False), + DataFrame([[4, 5, 6], [None, None, None]], + columns=["a", "b", "c"]), None), + + # ESCAPED_CHAR + ("a,b,c\n4,5,6\n\\", dict(escapechar="\\"), + None, "(EOF following escape character)|(unexpected end of data)"), + + # ESCAPE_IN_QUOTED_FIELD + ('a,b,c\n4,5,6\n"\\', dict(escapechar="\\"), + None, "(EOF inside string starting at row 2)|(unexpected end of data)"), + + # IN_QUOTED_FIELD + ('a,b,c\n4,5,6\n"', dict(escapechar="\\"), + None, "(EOF inside string starting at row 2)|(unexpected end of data)"), +], ids=["whitespace-line", "eat-line-comment", "eat-crnl-nop", "eat-comment", + "skip-line", "eat-line-comment", "in-field", "eat-crnl", + "escaped-char", "escape-in-quoted-field", "in-quoted-field"]) +def test_eof_states(all_parsers, data, kwargs, expected, msg): + # see gh-10728, gh-10548 + parser = all_parsers + + if expected is None: + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + else: + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols", [None, [0, 1], ["a", "b"]]) +def test_uneven_lines_with_usecols(all_parsers, usecols): + # see gh-12203 + parser = all_parsers + data = r"""a,b,c +0,1,2 +3,4,5,6,7 +8,9,10""" + + if usecols is None: + # Make sure that an error is still raised + # when the "usecols" parameter is not provided. + msg = r"Expected \d+ fields in line \d+, saw \d+" + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data)) + else: + expected = DataFrame({ + "a": [0, 3, 8], + "b": [1, 4, 9] + }) + + result = parser.read_csv(StringIO(data), usecols=usecols) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + # First, check to see that the response of parser when faced with no + # provided columns raises the correct error, with or without usecols. + ("", dict(), None), + ("", dict(usecols=["X"]), None), + (",,", dict(names=["Dummy", "X", "Dummy_2"], usecols=["X"]), + DataFrame(columns=["X"], index=[0], dtype=np.float64)), + ("", dict(names=["Dummy", "X", "Dummy_2"], usecols=["X"]), + DataFrame(columns=["X"])), +]) +def test_read_empty_with_usecols(all_parsers, data, kwargs, expected): + # see gh-12493 + parser = all_parsers + + if expected is None: + msg = "No columns to parse from file" + with pytest.raises(EmptyDataError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + else: + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,expected", [ + # gh-8661, gh-8679: this should ignore six lines, including + # lines with trailing whitespace and blank lines. + (dict(header=None, delim_whitespace=True, skiprows=[0, 1, 2, 3, 5, 6], + skip_blank_lines=True), DataFrame([[1., 2., 4.], + [5.1, np.nan, 10.]])), + + # gh-8983: test skipping set of rows after a row with trailing spaces. + (dict(delim_whitespace=True, skiprows=[1, 2, 3, 5, 6], + skip_blank_lines=True), DataFrame({"A": [1., 5.1], + "B": [2., np.nan], + "C": [4., 10]})), +]) +def test_trailing_spaces(all_parsers, kwargs, expected): + data = "A B C \nrandom line with trailing spaces \nskip\n1,2,3\n1,2.,4.\nrandom line with trailing tabs\t\t\t\n \n5.1,NaN,10.0\n" # noqa + parser = all_parsers + + result = parser.read_csv(StringIO(data.replace(",", " ")), **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_raise_on_sep_with_delim_whitespace(all_parsers): + # see gh-6607 + data = "a b c\n1 2 3" + parser = all_parsers + + with pytest.raises(ValueError, match="you can only specify one"): + parser.read_csv(StringIO(data), sep=r"\s", delim_whitespace=True) + + +@pytest.mark.parametrize("delim_whitespace", [True, False]) +def test_single_char_leading_whitespace(all_parsers, delim_whitespace): + # see gh-9710 + parser = all_parsers + data = """\ +MyColumn +a +b +a +b\n""" + + expected = DataFrame({"MyColumn": list("abab")}) + result = parser.read_csv(StringIO(data), skipinitialspace=True, + delim_whitespace=delim_whitespace) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("sep,skip_blank_lines,exp_data", [ + (",", True, [[1., 2., 4.], [5., np.nan, 10.], [-70., .4, 1.]]), + (r"\s+", True, [[1., 2., 4.], [5., np.nan, 10.], [-70., .4, 1.]]), + (",", False, [[1., 2., 4.], [np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan], [5., np.nan, 10.], + [np.nan, np.nan, np.nan], [-70., .4, 1.]]), +]) +def test_empty_lines(all_parsers, sep, skip_blank_lines, exp_data): + parser = all_parsers + data = """\ +A,B,C +1,2.,4. + + +5.,NaN,10.0 + +-70,.4,1 +""" + + if sep == r"\s+": + data = data.replace(",", " ") + + result = parser.read_csv(StringIO(data), sep=sep, + skip_blank_lines=skip_blank_lines) + expected = DataFrame(exp_data, columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +def test_whitespace_lines(all_parsers): + parser = all_parsers + data = """ + +\t \t\t +\t +A,B,C +\t 1,2.,4. +5.,NaN,10.0 +""" + expected = DataFrame([[1, 2., 4.], [5., np.nan, 10.]], + columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,expected", [ + (""" A B C D +a 1 2 3 4 +b 1 2 3 4 +c 1 2 3 4 +""", DataFrame([[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + columns=["A", "B", "C", "D"], index=["a", "b", "c"])), + (" a b c\n1 2 3 \n4 5 6\n 7 8 9", + DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], columns=["a", "b", "c"])), +]) +def test_whitespace_regex_separator(all_parsers, data, expected): + # see gh-6607 + parser = all_parsers + result = parser.read_csv(StringIO(data), sep=r"\s+") + tm.assert_frame_equal(result, expected) + + +def test_verbose_read(all_parsers, capsys): + parser = all_parsers + data = """a,b,c,d +one,1,2,3 +one,1,2,3 +,1,2,3 +one,1,2,3 +,1,2,3 +,1,2,3 +one,1,2,3 +two,1,2,3""" + + # Engines are verbose in different ways. + parser.read_csv(StringIO(data), verbose=True) + captured = capsys.readouterr() + + if parser.engine == "c": + assert "Tokenization took:" in captured.out + assert "Parser memory cleanup took:" in captured.out + else: # Python engine + assert captured.out == "Filled 3 NA values in column a\n" + + +def test_verbose_read2(all_parsers, capsys): + parser = all_parsers + data = """a,b,c,d +one,1,2,3 +two,1,2,3 +three,1,2,3 +four,1,2,3 +five,1,2,3 +,1,2,3 +seven,1,2,3 +eight,1,2,3""" + + parser.read_csv(StringIO(data), verbose=True, index_col=0) + captured = capsys.readouterr() + + # Engines are verbose in different ways. + if parser.engine == "c": + assert "Tokenization took:" in captured.out + assert "Parser memory cleanup took:" in captured.out + else: # Python engine + assert captured.out == "Filled 1 NA values in column a\n" + + +def test_iteration_open_handle(all_parsers): + parser = all_parsers + kwargs = dict(squeeze=True, header=None) + + with tm.ensure_clean() as path: + with open(path, "wb" if compat.PY2 else "w") as f: + f.write("AAA\nBBB\nCCC\nDDD\nEEE\nFFF\nGGG") + + with open(path, "rb" if compat.PY2 else "r") as f: + for line in f: + if "CCC" in line: + break + + if parser.engine == "c" and compat.PY2: + msg = "Mixing iteration and read methods would lose data" + with pytest.raises(ValueError, match=msg): + parser.read_csv(f, **kwargs) + else: + result = parser.read_csv(f, **kwargs) + expected = Series(["DDD", "EEE", "FFF", "GGG"], name=0) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("data,thousands,decimal", [ + ("""A|B|C +1|2,334.01|5 +10|13|10. +""", ",", "."), + ("""A|B|C +1|2.334,01|5 +10|13|10, +""", ".", ","), +]) +def test_1000_sep_with_decimal(all_parsers, data, thousands, decimal): + parser = all_parsers + expected = DataFrame({ + "A": [1, 10], + "B": [2334.01, 13], + "C": [5, 10.] + }) + + result = parser.read_csv(StringIO(data), sep="|", + thousands=thousands, + decimal=decimal) + tm.assert_frame_equal(result, expected) + + +def test_euro_decimal_format(all_parsers): + parser = all_parsers + data = """Id;Number1;Number2;Text1;Text2;Number3 +1;1521,1541;187101,9543;ABC;poi;4,738797819 +2;121,12;14897,76;DEF;uyt;0,377320872 +3;878,158;108013,434;GHI;rez;2,735694704""" + + result = parser.read_csv(StringIO(data), sep=";", decimal=",") + expected = DataFrame([ + [1, 1521.1541, 187101.9543, "ABC", "poi", 4.738797819], + [2, 121.12, 14897.76, "DEF", "uyt", 0.377320872], + [3, 878.158, 108013.434, "GHI", "rez", 2.735694704] + ], columns=["Id", "Number1", "Number2", "Text1", "Text2", "Number3"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_filter", [True, False]) +def test_inf_parsing(all_parsers, na_filter): + parser = all_parsers + data = """\ +,A +a,inf +b,-inf +c,+Inf +d,-Inf +e,INF +f,-INF +g,+INf +h,-INf +i,inF +j,-inF""" + expected = DataFrame({"A": [float("inf"), float("-inf")] * 5}, + index=["a", "b", "c", "d", "e", + "f", "g", "h", "i", "j"]) + result = parser.read_csv(StringIO(data), index_col=0, na_filter=na_filter) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("nrows", [0, 1, 2, 3, 4, 5]) +def test_raise_on_no_columns(all_parsers, nrows): + parser = all_parsers + data = "\n" * nrows + + msg = "No columns to parse from file" + with pytest.raises(EmptyDataError, match=msg): + parser.read_csv(StringIO(data)) + + +def test_memory_map(all_parsers, csv_dir_path): + mmap_file = os.path.join(csv_dir_path, "test_mmap.csv") + parser = all_parsers + + expected = DataFrame({ + "a": [1, 2, 3], + "b": ["one", "two", "three"], + "c": ["I", "II", "III"] + }) + + result = parser.read_csv(mmap_file, memory_map=True) + tm.assert_frame_equal(result, expected) + + +def test_null_byte_char(all_parsers): + # see gh-2741 + data = "\x00,foo" + names = ["a", "b"] + parser = all_parsers + + if parser.engine == "c": + expected = DataFrame([[np.nan, "foo"]], columns=names) + out = parser.read_csv(StringIO(data), names=names) + tm.assert_frame_equal(out, expected) + else: + msg = "NULL byte detected" + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), names=names) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + # Basic test + ("a\n1", dict(), DataFrame({"a": [1]})), + + # "Regular" quoting + ('"a"\n1', dict(quotechar='"'), DataFrame({"a": [1]})), + + # Test in a data row instead of header + ("b\n1", dict(names=["a"]), DataFrame({"a": ["b", "1"]})), + + # Test in empty data row with skipping + ("\n1", dict(names=["a"], skip_blank_lines=True), DataFrame({"a": [1]})), + + # Test in empty data row without skipping + ("\n1", dict(names=["a"], skip_blank_lines=False), + DataFrame({"a": [np.nan, 1]})), +]) +def test_utf8_bom(all_parsers, data, kwargs, expected): + # see gh-4793 + parser = all_parsers + bom = u("\ufeff") + utf8 = "utf-8" + + def _encode_data_with_bom(_data): + bom_data = (bom + _data).encode(utf8) + return BytesIO(bom_data) + + result = parser.read_csv(_encode_data_with_bom(data), + encoding=utf8, **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_temporary_file(all_parsers): + # see gh-13398 + parser = all_parsers + data = "0 0" + + new_file = TemporaryFile("w+") + new_file.write(data) + new_file.flush() + new_file.seek(0) + + result = parser.read_csv(new_file, sep=r"\s+", header=None) + new_file.close() + + expected = DataFrame([[0, 0]]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("byte", [8, 16]) +@pytest.mark.parametrize("fmt", ["utf-{0}", "utf_{0}", + "UTF-{0}", "UTF_{0}"]) +def test_read_csv_utf_aliases(all_parsers, byte, fmt): + # see gh-13549 + expected = DataFrame({"mb_num": [4.8], "multibyte": ["test"]}) + parser = all_parsers + + encoding = fmt.format(byte) + data = "mb_num,multibyte\n4.8,test".encode(encoding) + + result = parser.read_csv(BytesIO(data), encoding=encoding) + tm.assert_frame_equal(result, expected) + + +def test_internal_eof_byte(all_parsers): + # see gh-5500 + parser = all_parsers + data = "a,b\n1\x1a,2" + + expected = DataFrame([["1\x1a", 2]], columns=["a", "b"]) + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +def test_internal_eof_byte_to_file(all_parsers): + # see gh-16559 + parser = all_parsers + data = b'c1,c2\r\n"test \x1a test", test\r\n' + expected = DataFrame([["test \x1a test", " test"]], + columns=["c1", "c2"]) + path = "__%s__.csv" % tm.rands(10) + + with tm.ensure_clean(path) as path: + with open(path, "wb") as f: + f.write(data) + + result = parser.read_csv(path) + tm.assert_frame_equal(result, expected) + + +def test_sub_character(all_parsers, csv_dir_path): + # see gh-16893 + filename = os.path.join(csv_dir_path, "sub_char.csv") + expected = DataFrame([[1, 2, 3]], columns=["a", "\x1ab", "c"]) + + parser = all_parsers + result = parser.read_csv(filename) + tm.assert_frame_equal(result, expected) + + +def test_file_handle_string_io(all_parsers): + # gh-14418 + # + # Don't close user provided file handles. + parser = all_parsers + data = "a,b\n1,2" + + fh = StringIO(data) + parser.read_csv(fh) + assert not fh.closed + + +def test_file_handles_with_open(all_parsers, csv1): + # gh-14418 + # + # Don't close user provided file handles. + parser = all_parsers + + with open(csv1, "r") as f: + parser.read_csv(f) + assert not f.closed + + +def test_invalid_file_buffer_class(all_parsers): + # see gh-15337 + class InvalidBuffer(object): + pass + + parser = all_parsers + msg = "Invalid file path or buffer object type" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(InvalidBuffer()) + + +def test_invalid_file_buffer_mock(all_parsers): + # see gh-15337 + parser = all_parsers + msg = "Invalid file path or buffer object type" + + class Foo(): + pass + + with pytest.raises(ValueError, match=msg): + parser.read_csv(Foo()) + + +def test_valid_file_buffer_seems_invalid(all_parsers): + # gh-16135: we want to ensure that "tell" and "seek" + # aren't actually being used when we call `read_csv` + # + # Thus, while the object may look "invalid" (these + # methods are attributes of the `StringIO` class), + # it is still a valid file-object for our purposes. + class NoSeekTellBuffer(StringIO): + def tell(self): + raise AttributeError("No tell method") + + def seek(self, pos, whence=0): + raise AttributeError("No seek method") + + data = "a\n1" + parser = all_parsers + expected = DataFrame({"a": [1]}) + + result = parser.read_csv(NoSeekTellBuffer(data)) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(), # Default is True. + dict(error_bad_lines=True), # Explicitly pass in. +]) +@pytest.mark.parametrize("warn_kwargs", [ + dict(), dict(warn_bad_lines=True), + dict(warn_bad_lines=False) +]) +def test_error_bad_lines(all_parsers, kwargs, warn_kwargs): + # see gh-15925 + parser = all_parsers + kwargs.update(**warn_kwargs) + data = "a\n1\n1,2,3\n4\n5,6,7" + + msg = "Expected 1 fields in line 3, saw 3" + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + + +def test_warn_bad_lines(all_parsers, capsys): + # see gh-15925 + parser = all_parsers + data = "a\n1\n1,2,3\n4\n5,6,7" + expected = DataFrame({"a": [1, 4]}) + + result = parser.read_csv(StringIO(data), + error_bad_lines=False, + warn_bad_lines=True) + tm.assert_frame_equal(result, expected) + + captured = capsys.readouterr() + assert "Skipping line 3" in captured.err + assert "Skipping line 5" in captured.err + + +def test_suppress_error_output(all_parsers, capsys): + # see gh-15925 + parser = all_parsers + data = "a\n1\n1,2,3\n4\n5,6,7" + expected = DataFrame({"a": [1, 4]}) + + result = parser.read_csv(StringIO(data), + error_bad_lines=False, + warn_bad_lines=False) + tm.assert_frame_equal(result, expected) + + captured = capsys.readouterr() + assert captured.err == "" + + +def test_filename_with_special_chars(all_parsers): + # see gh-15086. + parser = all_parsers + df = DataFrame({"a": [1, 2, 3]}) + + with tm.ensure_clean("sé-es-vé.csv") as path: + df.to_csv(path, index=False) + + result = parser.read_csv(path) + tm.assert_frame_equal(result, df) + + +def test_read_csv_memory_growth_chunksize(all_parsers): + # see gh-24805 + # + # Let's just make sure that we don't crash + # as we iteratively process all chunks. + parser = all_parsers + + with tm.ensure_clean() as path: + with open(path, "w") as f: + for i in range(1000): + f.write(str(i) + "\n") + + result = parser.read_csv(path, chunksize=20) + + for _ in result: + pass + + +def test_read_table_deprecated(all_parsers): + # see gh-21948 + parser = all_parsers + data = "a\tb\n1\t2\n3\t4" + expected = parser.read_csv(StringIO(data), sep="\t") + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = parser.read_table(StringIO(data)) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_compression.py b/pandas/tests/io/parser/test_compression.py new file mode 100644 index 0000000000000..6e615e795e53c --- /dev/null +++ b/pandas/tests/io/parser/test_compression.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +""" +Tests compressed data parsing functionality for all +of the parsers defined in parsers.py +""" + +import os +import zipfile + +import pytest + +import pandas as pd +import pandas.util.testing as tm + + +@pytest.fixture(params=[True, False]) +def buffer(request): + return request.param + + +@pytest.fixture +def parser_and_data(all_parsers, csv1): + parser = all_parsers + + with open(csv1, "rb") as f: + data = f.read() + expected = parser.read_csv(csv1) + + return parser, data, expected + + +@pytest.mark.parametrize("compression", ["zip", "infer", "zip2"]) +def test_zip(parser_and_data, compression): + parser, data, expected = parser_and_data + + with tm.ensure_clean("test_file.zip") as path: + with zipfile.ZipFile(path, mode="w") as tmp: + tmp.writestr("test_file", data) + + if compression == "zip2": + with open(path, "rb") as f: + result = parser.read_csv(f, compression="zip") + else: + result = parser.read_csv(path, compression=compression) + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("compression", ["zip", "infer"]) +def test_zip_error_multiple_files(parser_and_data, compression): + parser, data, expected = parser_and_data + + with tm.ensure_clean("combined_zip.zip") as path: + inner_file_names = ["test_file", "second_file"] + + with zipfile.ZipFile(path, mode="w") as tmp: + for file_name in inner_file_names: + tmp.writestr(file_name, data) + + with pytest.raises(ValueError, match="Multiple files"): + parser.read_csv(path, compression=compression) + + +def test_zip_error_no_files(parser_and_data): + parser, _, _ = parser_and_data + + with tm.ensure_clean() as path: + with zipfile.ZipFile(path, mode="w"): + pass + + with pytest.raises(ValueError, match="Zero files"): + parser.read_csv(path, compression="zip") + + +def test_zip_error_invalid_zip(parser_and_data): + parser, _, _ = parser_and_data + + with tm.ensure_clean() as path: + with open(path, "wb") as f: + with pytest.raises(zipfile.BadZipfile, + match="File is not a zip file"): + parser.read_csv(f, compression="zip") + + +@pytest.mark.parametrize("filename", [None, "test.{ext}"]) +def test_compression(parser_and_data, compression_only, buffer, filename): + parser, data, expected = parser_and_data + compress_type = compression_only + + ext = "gz" if compress_type == "gzip" else compress_type + filename = filename if filename is None else filename.format(ext=ext) + + if filename and buffer: + pytest.skip("Cannot deduce compression from " + "buffer of compressed data.") + + with tm.ensure_clean(filename=filename) as path: + tm.write_to_compressed(compress_type, path, data) + compression = "infer" if filename else compress_type + + if buffer: + with open(path, "rb") as f: + result = parser.read_csv(f, compression=compression) + else: + result = parser.read_csv(path, compression=compression) + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("ext", [None, "gz", "bz2"]) +def test_infer_compression(all_parsers, csv1, buffer, ext): + # see gh-9770 + parser = all_parsers + kwargs = dict(index_col=0, parse_dates=True) + + expected = parser.read_csv(csv1, **kwargs) + kwargs["compression"] = "infer" + + if buffer: + with open(csv1) as f: + result = parser.read_csv(f, **kwargs) + else: + ext = "." + ext if ext else "" + result = parser.read_csv(csv1 + ext, **kwargs) + + tm.assert_frame_equal(result, expected) + + +def test_compression_utf16_encoding(all_parsers, csv_dir_path): + # see gh-18071 + parser = all_parsers + path = os.path.join(csv_dir_path, "utf16_ex_small.zip") + + result = parser.read_csv(path, encoding="utf-16", + compression="zip", sep="\t") + expected = pd.DataFrame({ + u"Country": [u"Venezuela", u"Venezuela"], + u"Twitter": [u"Hugo Chávez Frías", u"Henrique Capriles R."] + }) + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("invalid_compression", ["sfark", "bz3", "zipper"]) +def test_invalid_compression(all_parsers, invalid_compression): + parser = all_parsers + compress_kwargs = dict(compression=invalid_compression) + + msg = ("Unrecognized compression " + "type: {compression}".format(**compress_kwargs)) + + with pytest.raises(ValueError, match=msg): + parser.read_csv("test_file.zip", **compress_kwargs) diff --git a/pandas/tests/io/parser/test_converters.py b/pandas/tests/io/parser/test_converters.py new file mode 100644 index 0000000000000..47bbae0274fd3 --- /dev/null +++ b/pandas/tests/io/parser/test_converters.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +""" +Tests column conversion functionality during parsing +for all of the parsers defined in parsers.py +""" + +import numpy as np +import pytest + +from pandas.compat import StringIO, lmap, parse_date + +import pandas as pd +from pandas import DataFrame, Index +import pandas.util.testing as tm + + +def test_converters_type_must_be_dict(all_parsers): + parser = all_parsers + data = """index,A,B,C,D +foo,2,3,4,5 +""" + + with pytest.raises(TypeError, match="Type converters.+"): + parser.read_csv(StringIO(data), converters=0) + + +@pytest.mark.parametrize("column", [3, "D"]) +@pytest.mark.parametrize("converter", [ + parse_date, + lambda x: int(x.split("/")[2]) # Produce integer. +]) +def test_converters(all_parsers, column, converter): + parser = all_parsers + data = """A,B,C,D +a,1,2,01/01/2009 +b,3,4,01/02/2009 +c,4,5,01/03/2009 +""" + result = parser.read_csv(StringIO(data), converters={column: converter}) + + expected = parser.read_csv(StringIO(data)) + expected["D"] = expected["D"].map(converter) + + tm.assert_frame_equal(result, expected) + + +def test_converters_no_implicit_conv(all_parsers): + # see gh-2184 + parser = all_parsers + data = """000102,1.2,A\n001245,2,B""" + + converters = {0: lambda x: x.strip()} + result = parser.read_csv(StringIO(data), header=None, + converters=converters) + + # Column 0 should not be casted to numeric and should remain as object. + expected = DataFrame([["000102", 1.2, "A"], ["001245", 2, "B"]]) + tm.assert_frame_equal(result, expected) + + +def test_converters_euro_decimal_format(all_parsers): + # see gh-583 + converters = dict() + parser = all_parsers + + data = """Id;Number1;Number2;Text1;Text2;Number3 +1;1521,1541;187101,9543;ABC;poi;4,7387 +2;121,12;14897,76;DEF;uyt;0,3773 +3;878,158;108013,434;GHI;rez;2,7356""" + converters["Number1"] = converters["Number2"] =\ + converters["Number3"] = lambda x: float(x.replace(",", ".")) + + result = parser.read_csv(StringIO(data), sep=";", converters=converters) + expected = DataFrame([[1, 1521.1541, 187101.9543, "ABC", "poi", 4.7387], + [2, 121.12, 14897.76, "DEF", "uyt", 0.3773], + [3, 878.158, 108013.434, "GHI", "rez", 2.7356]], + columns=["Id", "Number1", "Number2", + "Text1", "Text2", "Number3"]) + tm.assert_frame_equal(result, expected) + + +def test_converters_corner_with_nans(all_parsers): + parser = all_parsers + data = """id,score,days +1,2,12 +2,2-5, +3,,14+ +4,6-12,2""" + + # Example converters. + def convert_days(x): + x = x.strip() + + if not x: + return np.nan + + is_plus = x.endswith("+") + + if is_plus: + x = int(x[:-1]) + 1 + else: + x = int(x) + + return x + + def convert_days_sentinel(x): + x = x.strip() + + if not x: + return np.nan + + is_plus = x.endswith("+") + + if is_plus: + x = int(x[:-1]) + 1 + else: + x = int(x) + + return x + + def convert_score(x): + x = x.strip() + + if not x: + return np.nan + + if x.find("-") > 0: + val_min, val_max = lmap(int, x.split("-")) + val = 0.5 * (val_min + val_max) + else: + val = float(x) + + return val + + results = [] + + for day_converter in [convert_days, convert_days_sentinel]: + result = parser.read_csv(StringIO(data), + converters={"score": convert_score, + "days": day_converter}, + na_values=["", None]) + assert pd.isna(result["days"][1]) + results.append(result) + + tm.assert_frame_equal(results[0], results[1]) + + +def test_converter_index_col_bug(all_parsers): + # see gh-1835 + parser = all_parsers + data = "A;B\n1;2\n3;4" + + rs = parser.read_csv(StringIO(data), sep=";", index_col="A", + converters={"A": lambda x: x}) + + xp = DataFrame({"B": [2, 4]}, index=Index([1, 3], name="A")) + tm.assert_frame_equal(rs, xp) diff --git a/pandas/tests/io/parser/test_dialect.py b/pandas/tests/io/parser/test_dialect.py new file mode 100644 index 0000000000000..5392f793b361c --- /dev/null +++ b/pandas/tests/io/parser/test_dialect.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +""" +Tests that dialects are properly handled during parsing +for all of the parsers defined in parsers.py +""" + +import csv + +import pytest + +from pandas.compat import StringIO +from pandas.errors import ParserWarning + +from pandas import DataFrame +import pandas.util.testing as tm + + +@pytest.fixture +def custom_dialect(): + dialect_name = "weird" + dialect_kwargs = dict(doublequote=False, escapechar="~", delimiter=":", + skipinitialspace=False, quotechar="~", quoting=3) + return dialect_name, dialect_kwargs + + +def test_dialect(all_parsers): + parser = all_parsers + data = """\ +label1,label2,label3 +index1,"a,c,e +index2,b,d,f +""" + + dia = csv.excel() + dia.quoting = csv.QUOTE_NONE + df = parser.read_csv(StringIO(data), dialect=dia) + + data = """\ +label1,label2,label3 +index1,a,c,e +index2,b,d,f +""" + exp = parser.read_csv(StringIO(data)) + exp.replace("a", "\"a", inplace=True) + tm.assert_frame_equal(df, exp) + + +def test_dialect_str(all_parsers): + dialect_name = "mydialect" + parser = all_parsers + data = """\ +fruit:vegetable +apple:broccoli +pear:tomato +""" + exp = DataFrame({ + "fruit": ["apple", "pear"], + "vegetable": ["broccoli", "tomato"] + }) + + with tm.with_csv_dialect(dialect_name, delimiter=":"): + df = parser.read_csv(StringIO(data), dialect=dialect_name) + tm.assert_frame_equal(df, exp) + + +def test_invalid_dialect(all_parsers): + class InvalidDialect(object): + pass + + data = "a\n1" + parser = all_parsers + msg = "Invalid dialect" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), dialect=InvalidDialect) + + +@pytest.mark.parametrize("arg", [None, "doublequote", "escapechar", + "skipinitialspace", "quotechar", "quoting"]) +@pytest.mark.parametrize("value", ["dialect", "default", "other"]) +def test_dialect_conflict_except_delimiter(all_parsers, custom_dialect, + arg, value): + # see gh-23761. + dialect_name, dialect_kwargs = custom_dialect + parser = all_parsers + + expected = DataFrame({"a": [1], "b": [2]}) + data = "a:b\n1:2" + + warning_klass = None + kwds = dict() + + # arg=None tests when we pass in the dialect without any other arguments. + if arg is not None: + if "value" == "dialect": # No conflict --> no warning. + kwds[arg] = dialect_kwargs[arg] + elif "value" == "default": # Default --> no warning. + from pandas.io.parsers import _parser_defaults + kwds[arg] = _parser_defaults[arg] + else: # Non-default + conflict with dialect --> warning. + warning_klass = ParserWarning + kwds[arg] = "blah" + + with tm.with_csv_dialect(dialect_name, **dialect_kwargs): + with tm.assert_produces_warning(warning_klass): + result = parser.read_csv(StringIO(data), + dialect=dialect_name, **kwds) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,warning_klass", [ + (dict(sep=","), None), # sep is default --> sep_override=True + (dict(sep="."), ParserWarning), # sep isn't default --> sep_override=False + (dict(delimiter=":"), None), # No conflict + (dict(delimiter=None), None), # Default arguments --> sep_override=True + (dict(delimiter=","), ParserWarning), # Conflict + (dict(delimiter="."), ParserWarning), # Conflict +], ids=["sep-override-true", "sep-override-false", + "delimiter-no-conflict", "delimiter-default-arg", + "delimiter-conflict", "delimiter-conflict2"]) +def test_dialect_conflict_delimiter(all_parsers, custom_dialect, + kwargs, warning_klass): + # see gh-23761. + dialect_name, dialect_kwargs = custom_dialect + parser = all_parsers + + expected = DataFrame({"a": [1], "b": [2]}) + data = "a:b\n1:2" + + with tm.with_csv_dialect(dialect_name, **dialect_kwargs): + with tm.assert_produces_warning(warning_klass): + result = parser.read_csv(StringIO(data), + dialect=dialect_name, **kwargs) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_dtypes.py b/pandas/tests/io/parser/test_dtypes.py new file mode 100644 index 0000000000000..caa03fc3685f6 --- /dev/null +++ b/pandas/tests/io/parser/test_dtypes.py @@ -0,0 +1,514 @@ +# -*- coding: utf-8 -*- + +""" +Tests dtype specification during parsing +for all of the parsers defined in parsers.py +""" + +import os + +import numpy as np +import pytest + +from pandas.compat import StringIO +from pandas.errors import ParserWarning + +from pandas.core.dtypes.dtypes import CategoricalDtype + +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, MultiIndex, Series, Timestamp, concat) +import pandas.util.testing as tm + + +@pytest.mark.parametrize("dtype", [str, object]) +@pytest.mark.parametrize("check_orig", [True, False]) +def test_dtype_all_columns(all_parsers, dtype, check_orig): + # see gh-3795, gh-6607 + parser = all_parsers + + df = DataFrame(np.random.rand(5, 2).round(4), columns=list("AB"), + index=["1A", "1B", "1C", "1D", "1E"]) + + with tm.ensure_clean("__passing_str_as_dtype__.csv") as path: + df.to_csv(path) + + result = parser.read_csv(path, dtype=dtype, index_col=0) + + if check_orig: + expected = df.copy() + result = result.astype(float) + else: + expected = df.astype(str) + + tm.assert_frame_equal(result, expected) + + +def test_dtype_all_columns_empty(all_parsers): + # see gh-12048 + parser = all_parsers + result = parser.read_csv(StringIO("A,B"), dtype=str) + + expected = DataFrame({"A": [], "B": []}, index=[], dtype=str) + tm.assert_frame_equal(result, expected) + + +def test_dtype_per_column(all_parsers): + parser = all_parsers + data = """\ +one,two +1,2.5 +2,3.5 +3,4.5 +4,5.5""" + expected = DataFrame([[1, "2.5"], [2, "3.5"], [3, "4.5"], [4, "5.5"]], + columns=["one", "two"]) + expected["one"] = expected["one"].astype(np.float64) + expected["two"] = expected["two"].astype(object) + + result = parser.read_csv(StringIO(data), dtype={"one": np.float64, + 1: str}) + tm.assert_frame_equal(result, expected) + + +def test_invalid_dtype_per_column(all_parsers): + parser = all_parsers + data = """\ +one,two +1,2.5 +2,3.5 +3,4.5 +4,5.5""" + + with pytest.raises(TypeError, match="data type 'foo' not understood"): + parser.read_csv(StringIO(data), dtype={"one": "foo", 1: "int"}) + + +@pytest.mark.parametrize("dtype", [ + "category", + CategoricalDtype(), + {"a": "category", + "b": "category", + "c": CategoricalDtype()} +]) +def test_categorical_dtype(all_parsers, dtype): + # see gh-10153 + parser = all_parsers + data = """a,b,c +1,a,3.4 +1,a,3.4 +2,b,4.5""" + expected = DataFrame({"a": Categorical(["1", "1", "2"]), + "b": Categorical(["a", "a", "b"]), + "c": Categorical(["3.4", "3.4", "4.5"])}) + actual = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(actual, expected) + + +@pytest.mark.parametrize("dtype", [ + {"b": "category"}, + {1: "category"} +]) +def test_categorical_dtype_single(all_parsers, dtype): + # see gh-10153 + parser = all_parsers + data = """a,b,c +1,a,3.4 +1,a,3.4 +2,b,4.5""" + expected = DataFrame({"a": [1, 1, 2], + "b": Categorical(["a", "a", "b"]), + "c": [3.4, 3.4, 4.5]}) + actual = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_unsorted(all_parsers): + # see gh-10153 + parser = all_parsers + data = """a,b,c +1,b,3.4 +1,b,3.4 +2,a,4.5""" + expected = DataFrame({"a": Categorical(["1", "1", "2"]), + "b": Categorical(["b", "b", "a"]), + "c": Categorical(["3.4", "3.4", "4.5"])}) + actual = parser.read_csv(StringIO(data), dtype="category") + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_missing(all_parsers): + # see gh-10153 + parser = all_parsers + data = """a,b,c +1,b,3.4 +1,nan,3.4 +2,a,4.5""" + expected = DataFrame({"a": Categorical(["1", "1", "2"]), + "b": Categorical(["b", np.nan, "a"]), + "c": Categorical(["3.4", "3.4", "4.5"])}) + actual = parser.read_csv(StringIO(data), dtype="category") + tm.assert_frame_equal(actual, expected) + + +@pytest.mark.slow +def test_categorical_dtype_high_cardinality_numeric(all_parsers): + # see gh-18186 + parser = all_parsers + data = np.sort([str(i) for i in range(524289)]) + expected = DataFrame({"a": Categorical(data, ordered=True)}) + + actual = parser.read_csv(StringIO("a\n" + "\n".join(data)), + dtype="category") + actual["a"] = actual["a"].cat.reorder_categories( + np.sort(actual.a.cat.categories), ordered=True) + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_latin1(all_parsers, csv_dir_path): + # see gh-10153 + pth = os.path.join(csv_dir_path, "unicode_series.csv") + parser = all_parsers + encoding = "latin-1" + + expected = parser.read_csv(pth, header=None, encoding=encoding) + expected[1] = Categorical(expected[1]) + + actual = parser.read_csv(pth, header=None, encoding=encoding, + dtype={1: "category"}) + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_utf16(all_parsers, csv_dir_path): + # see gh-10153 + pth = os.path.join(csv_dir_path, "utf16_ex.txt") + parser = all_parsers + encoding = "utf-16" + sep = "," + + expected = parser.read_csv(pth, sep=sep, encoding=encoding) + expected = expected.apply(Categorical) + + actual = parser.read_csv(pth, sep=sep, encoding=encoding, dtype="category") + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_chunksize_infer_categories(all_parsers): + # see gh-10153 + parser = all_parsers + data = """a,b +1,a +1,b +1,b +2,c""" + expecteds = [DataFrame({"a": [1, 1], + "b": Categorical(["a", "b"])}), + DataFrame({"a": [1, 2], + "b": Categorical(["b", "c"])}, + index=[2, 3])] + actuals = parser.read_csv(StringIO(data), dtype={"b": "category"}, + chunksize=2) + + for actual, expected in zip(actuals, expecteds): + tm.assert_frame_equal(actual, expected) + + +def test_categorical_dtype_chunksize_explicit_categories(all_parsers): + # see gh-10153 + parser = all_parsers + data = """a,b +1,a +1,b +1,b +2,c""" + cats = ["a", "b", "c"] + expecteds = [DataFrame({"a": [1, 1], + "b": Categorical(["a", "b"], + categories=cats)}), + DataFrame({"a": [1, 2], + "b": Categorical(["b", "c"], + categories=cats)}, + index=[2, 3])] + dtype = CategoricalDtype(cats) + actuals = parser.read_csv(StringIO(data), dtype={"b": dtype}, chunksize=2) + + for actual, expected in zip(actuals, expecteds): + tm.assert_frame_equal(actual, expected) + + +@pytest.mark.parametrize("ordered", [False, True]) +@pytest.mark.parametrize("categories", [ + ["a", "b", "c"], + ["a", "c", "b"], + ["a", "b", "c", "d"], + ["c", "b", "a"], +]) +def test_categorical_category_dtype(all_parsers, categories, ordered): + parser = all_parsers + data = """a,b +1,a +1,b +1,b +2,c""" + expected = DataFrame({ + "a": [1, 1, 1, 2], + "b": Categorical(["a", "b", "b", "c"], + categories=categories, + ordered=ordered) + }) + + dtype = {"b": CategoricalDtype(categories=categories, + ordered=ordered)} + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_categorical_category_dtype_unsorted(all_parsers): + parser = all_parsers + data = """a,b +1,a +1,b +1,b +2,c""" + dtype = CategoricalDtype(["c", "b", "a"]) + expected = DataFrame({ + "a": [1, 1, 1, 2], + "b": Categorical(["a", "b", "b", "c"], categories=["c", "b", "a"]) + }) + + result = parser.read_csv(StringIO(data), dtype={"b": dtype}) + tm.assert_frame_equal(result, expected) + + +def test_categorical_coerces_numeric(all_parsers): + parser = all_parsers + dtype = {"b": CategoricalDtype([1, 2, 3])} + + data = "b\n1\n1\n2\n3" + expected = DataFrame({"b": Categorical([1, 1, 2, 3])}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_categorical_coerces_datetime(all_parsers): + parser = all_parsers + dtype = {"b": CategoricalDtype(pd.date_range("2017", "2019", freq="AS"))} + + data = "b\n2017-01-01\n2018-01-01\n2019-01-01" + expected = DataFrame({"b": Categorical(dtype["b"].categories)}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_categorical_coerces_timestamp(all_parsers): + parser = all_parsers + dtype = {"b": CategoricalDtype([Timestamp("2014")])} + + data = "b\n2014-01-01\n2014-01-01T00:00:00" + expected = DataFrame({"b": Categorical([Timestamp("2014")] * 2)}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_categorical_coerces_timedelta(all_parsers): + parser = all_parsers + dtype = {"b": CategoricalDtype(pd.to_timedelta(["1H", "2H", "3H"]))} + + data = "b\n1H\n2H\n3H" + expected = DataFrame({"b": Categorical(dtype["b"].categories)}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data", [ + "b\nTrue\nFalse\nNA\nFalse", + "b\ntrue\nfalse\nNA\nfalse", + "b\nTRUE\nFALSE\nNA\nFALSE", + "b\nTrue\nFalse\nNA\nFALSE", +]) +def test_categorical_dtype_coerces_boolean(all_parsers, data): + # see gh-20498 + parser = all_parsers + dtype = {"b": CategoricalDtype([False, True])} + expected = DataFrame({"b": Categorical([True, False, None, False])}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_categorical_unexpected_categories(all_parsers): + parser = all_parsers + dtype = {"b": CategoricalDtype(["a", "b", "d", "e"])} + + data = "b\nd\na\nc\nd" # Unexpected c + expected = DataFrame({"b": Categorical(list("dacd"), + dtype=dtype["b"])}) + + result = parser.read_csv(StringIO(data), dtype=dtype) + tm.assert_frame_equal(result, expected) + + +def test_empty_pass_dtype(all_parsers): + parser = all_parsers + + data = "one,two" + result = parser.read_csv(StringIO(data), dtype={"one": "u1"}) + + expected = DataFrame({"one": np.empty(0, dtype="u1"), + "two": np.empty(0, dtype=np.object)}, + index=Index([], dtype=object)) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_index_pass_dtype(all_parsers): + parser = all_parsers + + data = "one,two" + result = parser.read_csv(StringIO(data), index_col=["one"], + dtype={"one": "u1", 1: "f"}) + + expected = DataFrame({"two": np.empty(0, dtype="f")}, + index=Index([], dtype="u1", name="one")) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_multi_index_pass_dtype(all_parsers): + parser = all_parsers + + data = "one,two,three" + result = parser.read_csv(StringIO(data), index_col=["one", "two"], + dtype={"one": "u1", 1: "f8"}) + + exp_idx = MultiIndex.from_arrays([np.empty(0, dtype="u1"), + np.empty(0, dtype=np.float64)], + names=["one", "two"]) + expected = DataFrame({"three": np.empty(0, dtype=np.object)}, + index=exp_idx) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_mangled_column_pass_dtype_by_names(all_parsers): + parser = all_parsers + + data = "one,one" + result = parser.read_csv(StringIO(data), dtype={"one": "u1", "one.1": "f"}) + + expected = DataFrame({"one": np.empty(0, dtype="u1"), + "one.1": np.empty(0, dtype="f")}, + index=Index([], dtype=object)) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_mangled_column_pass_dtype_by_indexes(all_parsers): + parser = all_parsers + + data = "one,one" + result = parser.read_csv(StringIO(data), dtype={0: "u1", 1: "f"}) + + expected = DataFrame({"one": np.empty(0, dtype="u1"), + "one.1": np.empty(0, dtype="f")}, + index=Index([], dtype=object)) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_dup_column_pass_dtype_by_indexes(all_parsers): + # see gh-9424 + parser = all_parsers + expected = concat([Series([], name="one", dtype="u1"), + Series([], name="one.1", dtype="f")], axis=1) + expected.index = expected.index.astype(object) + + data = "one,one" + result = parser.read_csv(StringIO(data), dtype={0: "u1", 1: "f"}) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_dup_column_pass_dtype_by_indexes_warn(all_parsers): + # see gh-9424 + parser = all_parsers + expected = concat([Series([], name="one", dtype="u1"), + Series([], name="one.1", dtype="f")], axis=1) + expected.index = expected.index.astype(object) + + with tm.assert_produces_warning(UserWarning, check_stacklevel=False): + data = "" + result = parser.read_csv(StringIO(data), names=["one", "one"], + dtype={0: "u1", 1: "f"}) + tm.assert_frame_equal(result, expected) + + +def test_raise_on_passed_int_dtype_with_nas(all_parsers): + # see gh-2631 + parser = all_parsers + data = """YEAR, DOY, a +2001,106380451,10 +2001,,11 +2001,106380451,67""" + + msg = ("Integer column has NA values" if parser.engine == "c" else + "Unable to convert column DOY") + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), dtype={"DOY": np.int64}, + skipinitialspace=True) + + +def test_dtype_with_converters(all_parsers): + parser = all_parsers + data = """a,b +1.1,2.2 +1.2,2.3""" + + # Dtype spec ignored if converted specified. + with tm.assert_produces_warning(ParserWarning): + result = parser.read_csv(StringIO(data), dtype={"a": "i8"}, + converters={"a": lambda x: str(x)}) + expected = DataFrame({"a": ["1.1", "1.2"], "b": [2.2, 2.3]}) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("dtype,expected", [ + (np.float64, DataFrame(columns=["a", "b"], dtype=np.float64)), + ("category", DataFrame({"a": Categorical([]), + "b": Categorical([])}, + index=[])), + (dict(a="category", b="category"), + DataFrame({"a": Categorical([]), + "b": Categorical([])}, + index=[])), + ("datetime64[ns]", DataFrame(columns=["a", "b"], dtype="datetime64[ns]")), + ("timedelta64[ns]", DataFrame({"a": Series([], dtype="timedelta64[ns]"), + "b": Series([], dtype="timedelta64[ns]")}, + index=[])), + (dict(a=np.int64, + b=np.int32), DataFrame({"a": Series([], dtype=np.int64), + "b": Series([], dtype=np.int32)}, + index=[])), + ({0: np.int64, 1: np.int32}, DataFrame({"a": Series([], dtype=np.int64), + "b": Series([], dtype=np.int32)}, + index=[])), + ({"a": np.int64, 1: np.int32}, DataFrame({"a": Series([], dtype=np.int64), + "b": Series([], dtype=np.int32)}, + index=[])), +]) +def test_empty_dtype(all_parsers, dtype, expected): + # see gh-14712 + parser = all_parsers + data = "a,b" + + result = parser.read_csv(StringIO(data), header=0, dtype=dtype) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("dtype", list(np.typecodes["AllInteger"] + + np.typecodes["Float"])) +def test_numeric_dtype(all_parsers, dtype): + data = "0\n1" + parser = all_parsers + expected = DataFrame([0, 1], dtype=dtype) + + result = parser.read_csv(StringIO(data), header=None, dtype=dtype) + tm.assert_frame_equal(expected, result) diff --git a/pandas/tests/io/parser/test_header.py b/pandas/tests/io/parser/test_header.py new file mode 100644 index 0000000000000..38f4cc42357fa --- /dev/null +++ b/pandas/tests/io/parser/test_header.py @@ -0,0 +1,428 @@ +# -*- coding: utf-8 -*- + +""" +Tests that the file header is properly handled or inferred +during parsing for all of the parsers defined in parsers.py +""" + +from collections import namedtuple + +import numpy as np +import pytest + +from pandas.compat import StringIO, u +from pandas.errors import ParserError + +from pandas import DataFrame, Index, MultiIndex +import pandas.util.testing as tm + + +def test_read_with_bad_header(all_parsers): + parser = all_parsers + msg = r"but only \d+ lines in file" + + with pytest.raises(ValueError, match=msg): + s = StringIO(",,") + parser.read_csv(s, header=[10]) + + +@pytest.mark.parametrize("header", [True, False]) +def test_bool_header_arg(all_parsers, header): + # see gh-6114 + parser = all_parsers + data = """\ +MyColumn +a +b +a +b""" + msg = "Passing a bool to header is invalid" + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), header=header) + + +def test_no_header_prefix(all_parsers): + parser = all_parsers + data = """1,2,3,4,5 +6,7,8,9,10 +11,12,13,14,15 +""" + result = parser.read_csv(StringIO(data), prefix="Field", header=None) + expected = DataFrame([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], + [11, 12, 13, 14, 15]], + columns=["Field0", "Field1", "Field2", + "Field3", "Field4"]) + tm.assert_frame_equal(result, expected) + + +def test_header_with_index_col(all_parsers): + parser = all_parsers + data = """foo,1,2,3 +bar,4,5,6 +baz,7,8,9 +""" + names = ["A", "B", "C"] + result = parser.read_csv(StringIO(data), names=names) + + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["foo", "bar", "baz"], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +def test_header_not_first_line(all_parsers): + parser = all_parsers + data = """got,to,ignore,this,line +got,to,ignore,this,line +index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +""" + data2 = """index,A,B,C,D +foo,2,3,4,5 +bar,7,8,9,10 +baz,12,13,14,15 +""" + + result = parser.read_csv(StringIO(data), header=2, index_col=0) + expected = parser.read_csv(StringIO(data2), header=0, index_col=0) + tm.assert_frame_equal(result, expected) + + +def test_header_multi_index(all_parsers): + parser = all_parsers + expected = tm.makeCustomDataframe( + 5, 3, r_idx_nlevels=2, c_idx_nlevels=4) + + data = """\ +C0,,C_l0_g0,C_l0_g1,C_l0_g2 + +C1,,C_l1_g0,C_l1_g1,C_l1_g2 +C2,,C_l2_g0,C_l2_g1,C_l2_g2 +C3,,C_l3_g0,C_l3_g1,C_l3_g2 +R0,R1,,, +R_l0_g0,R_l1_g0,R0C0,R0C1,R0C2 +R_l0_g1,R_l1_g1,R1C0,R1C1,R1C2 +R_l0_g2,R_l1_g2,R2C0,R2C1,R2C2 +R_l0_g3,R_l1_g3,R3C0,R3C1,R3C2 +R_l0_g4,R_l1_g4,R4C0,R4C1,R4C2 +""" + result = parser.read_csv(StringIO(data), header=[0, 1, 2, 3], + index_col=[0, 1]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,msg", [ + (dict(index_col=["foo", "bar"]), ("index_col must only contain " + "row numbers when specifying " + "a multi-index header")), + (dict(index_col=[0, 1], names=["foo", "bar"]), ("cannot specify names " + "when specifying a " + "multi-index header")), + (dict(index_col=[0, 1], usecols=["foo", "bar"]), ("cannot specify " + "usecols when " + "specifying a " + "multi-index header")), +]) +def test_header_multi_index_invalid(all_parsers, kwargs, msg): + data = """\ +C0,,C_l0_g0,C_l0_g1,C_l0_g2 + +C1,,C_l1_g0,C_l1_g1,C_l1_g2 +C2,,C_l2_g0,C_l2_g1,C_l2_g2 +C3,,C_l3_g0,C_l3_g1,C_l3_g2 +R0,R1,,, +R_l0_g0,R_l1_g0,R0C0,R0C1,R0C2 +R_l0_g1,R_l1_g1,R1C0,R1C1,R1C2 +R_l0_g2,R_l1_g2,R2C0,R2C1,R2C2 +R_l0_g3,R_l1_g3,R3C0,R3C1,R3C2 +R_l0_g4,R_l1_g4,R4C0,R4C1,R4C2 +""" + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), header=[0, 1, 2, 3], **kwargs) + + +_TestTuple = namedtuple("names", ["first", "second"]) + + +@pytest.mark.parametrize("kwargs", [ + dict(header=[0, 1]), + dict(skiprows=3, + names=[("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")]), + dict(skiprows=3, + names=[_TestTuple("a", "q"), _TestTuple("a", "r"), + _TestTuple("a", "s"), _TestTuple("b", "t"), + _TestTuple("c", "u"), _TestTuple("c", "v")]) +]) +def test_header_multi_index_common_format1(all_parsers, kwargs): + parser = all_parsers + expected = DataFrame([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], + index=["one", "two"], + columns=MultiIndex.from_tuples( + [("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")])) + data = """,a,a,a,b,c,c +,q,r,s,t,u,v +,,,,,, +one,1,2,3,4,5,6 +two,7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), index_col=0, **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(header=[0, 1]), + dict(skiprows=2, + names=[("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")]), + dict(skiprows=2, + names=[_TestTuple("a", "q"), _TestTuple("a", "r"), + _TestTuple("a", "s"), _TestTuple("b", "t"), + _TestTuple("c", "u"), _TestTuple("c", "v")]) +]) +def test_header_multi_index_common_format2(all_parsers, kwargs): + parser = all_parsers + expected = DataFrame([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], + index=["one", "two"], + columns=MultiIndex.from_tuples( + [("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")])) + data = """,a,a,a,b,c,c +,q,r,s,t,u,v +one,1,2,3,4,5,6 +two,7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), index_col=0, **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(header=[0, 1]), + dict(skiprows=2, + names=[("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")]), + dict(skiprows=2, + names=[_TestTuple("a", "q"), _TestTuple("a", "r"), + _TestTuple("a", "s"), _TestTuple("b", "t"), + _TestTuple("c", "u"), _TestTuple("c", "v")]) +]) +def test_header_multi_index_common_format3(all_parsers, kwargs): + parser = all_parsers + expected = DataFrame([[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], + index=["one", "two"], + columns=MultiIndex.from_tuples( + [("a", "q"), ("a", "r"), ("a", "s"), + ("b", "t"), ("c", "u"), ("c", "v")])) + expected = expected.reset_index(drop=True) + data = """a,a,a,b,c,c +q,r,s,t,u,v +1,2,3,4,5,6 +7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), index_col=None, **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_header_multi_index_common_format_malformed1(all_parsers): + parser = all_parsers + expected = DataFrame(np.array( + [[2, 3, 4, 5, 6], [8, 9, 10, 11, 12]], dtype="int64"), + index=Index([1, 7]), + columns=MultiIndex(levels=[[u("a"), u("b"), u("c")], + [u("r"), u("s"), u("t"), + u("u"), u("v")]], + codes=[[0, 0, 1, 2, 2], [0, 1, 2, 3, 4]], + names=[u("a"), u("q")])) + data = """a,a,a,b,c,c +q,r,s,t,u,v +1,2,3,4,5,6 +7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), header=[0, 1], index_col=0) + tm.assert_frame_equal(expected, result) + + +def test_header_multi_index_common_format_malformed2(all_parsers): + parser = all_parsers + expected = DataFrame(np.array( + [[2, 3, 4, 5, 6], [8, 9, 10, 11, 12]], dtype="int64"), + index=Index([1, 7]), + columns=MultiIndex(levels=[[u("a"), u("b"), u("c")], + [u("r"), u("s"), u("t"), + u("u"), u("v")]], + codes=[[0, 0, 1, 2, 2], [0, 1, 2, 3, 4]], + names=[None, u("q")])) + + data = """,a,a,b,c,c +q,r,s,t,u,v +1,2,3,4,5,6 +7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), header=[0, 1], index_col=0) + tm.assert_frame_equal(expected, result) + + +def test_header_multi_index_common_format_malformed3(all_parsers): + parser = all_parsers + expected = DataFrame(np.array( + [[3, 4, 5, 6], [9, 10, 11, 12]], dtype="int64"), + index=MultiIndex(levels=[[1, 7], [2, 8]], + codes=[[0, 1], [0, 1]]), + columns=MultiIndex(levels=[[u("a"), u("b"), u("c")], + [u("s"), u("t"), u("u"), u("v")]], + codes=[[0, 1, 2, 2], [0, 1, 2, 3]], + names=[None, u("q")])) + data = """,a,a,b,c,c +q,r,s,t,u,v +1,2,3,4,5,6 +7,8,9,10,11,12""" + + result = parser.read_csv(StringIO(data), header=[0, 1], index_col=[0, 1]) + tm.assert_frame_equal(expected, result) + + +@pytest.mark.parametrize("data,header", [ + ("1,2,3\n4,5,6", None), + ("foo,bar,baz\n1,2,3\n4,5,6", 0), +]) +def test_header_names_backward_compat(all_parsers, data, header): + # see gh-2539 + parser = all_parsers + expected = parser.read_csv(StringIO("1,2,3\n4,5,6"), + names=["a", "b", "c"]) + + result = parser.read_csv(StringIO(data), names=["a", "b", "c"], + header=header) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(), dict(index_col=False) +]) +def test_read_only_header_no_rows(all_parsers, kwargs): + # See gh-7773 + parser = all_parsers + expected = DataFrame(columns=["a", "b", "c"]) + + result = parser.read_csv(StringIO("a,b,c"), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,names", [ + (dict(), [0, 1, 2, 3, 4]), + (dict(prefix="X"), ["X0", "X1", "X2", "X3", "X4"]), + (dict(names=["foo", "bar", "baz", "quux", "panda"]), + ["foo", "bar", "baz", "quux", "panda"]) +]) +def test_no_header(all_parsers, kwargs, names): + parser = all_parsers + data = """1,2,3,4,5 +6,7,8,9,10 +11,12,13,14,15 +""" + expected = DataFrame([[1, 2, 3, 4, 5], + [6, 7, 8, 9, 10], + [11, 12, 13, 14, 15]], columns=names) + result = parser.read_csv(StringIO(data), header=None, **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("header", [ + ["a", "b"], + "string_header" +]) +def test_non_int_header(all_parsers, header): + # see gh-16338 + msg = "header must be integer or list of integers" + data = """1,2\n3,4""" + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), header=header) + + +def test_singleton_header(all_parsers): + # see gh-7757 + data = """a,b,c\n0,1,2\n1,2,3""" + parser = all_parsers + + expected = DataFrame({"a": [0, 1], "b": [1, 2], "c": [2, 3]}) + result = parser.read_csv(StringIO(data), header=[0]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,expected", [ + ("A,A,A,B\none,one,one,two\n0,40,34,0.1", + DataFrame([[0, 40, 34, 0.1]], + columns=MultiIndex.from_tuples( + [("A", "one"), ("A", "one.1"), + ("A", "one.2"), ("B", "two")]))), + ("A,A,A,B\none,one,one.1,two\n0,40,34,0.1", + DataFrame([[0, 40, 34, 0.1]], + columns=MultiIndex.from_tuples( + [("A", "one"), ("A", "one.1"), + ("A", "one.1.1"), ("B", "two")]))), + ("A,A,A,B,B\none,one,one.1,two,two\n0,40,34,0.1,0.1", + DataFrame([[0, 40, 34, 0.1, 0.1]], + columns=MultiIndex.from_tuples( + [("A", "one"), ("A", "one.1"), + ("A", "one.1.1"), ("B", "two"), + ("B", "two.1")]))) +]) +def test_mangles_multi_index(all_parsers, data, expected): + # see gh-18062 + parser = all_parsers + + result = parser.read_csv(StringIO(data), header=[0, 1]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_col", [None, [0]]) +@pytest.mark.parametrize("columns", [None, + (["", "Unnamed"]), + (["Unnamed", ""]), + (["Unnamed", "NotUnnamed"])]) +def test_multi_index_unnamed(all_parsers, index_col, columns): + # see gh-23687 + # + # When specifying a multi-index header, make sure that + # we don't error just because one of the rows in our header + # has ALL column names containing the string "Unnamed". The + # correct condition to check is whether the row contains + # ALL columns that did not have names (and instead were given + # placeholder ones). + parser = all_parsers + header = [0, 1] + + if index_col is None: + data = ",".join(columns or ["", ""]) + "\n0,1\n2,3\n4,5\n" + else: + data = (",".join([""] + (columns or ["", ""])) + + "\n,0,1\n0,2,3\n1,4,5\n") + + if columns is None: + msg = (r"Passed header=\[0,1\] are too " + r"many rows for this multi_index of columns") + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), header=header, + index_col=index_col) + else: + result = parser.read_csv(StringIO(data), header=header, + index_col=index_col) + template = "Unnamed: {i}_level_0" + exp_columns = [] + + for i, col in enumerate(columns): + if not col: # Unnamed. + col = template.format(i=i if index_col is None else i + 1) + + exp_columns.append(col) + + columns = MultiIndex.from_tuples(zip(exp_columns, ["0", "1"])) + expected = DataFrame([[2, 3], [4, 5]], columns=columns) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_index_col.py b/pandas/tests/io/parser/test_index_col.py new file mode 100644 index 0000000000000..6421afba18f94 --- /dev/null +++ b/pandas/tests/io/parser/test_index_col.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +""" +Tests that the specified index column (a.k.a "index_col") +is properly handled or inferred during parsing for all of +the parsers defined in parsers.py +""" + +import pytest + +from pandas.compat import StringIO + +from pandas import DataFrame, Index, MultiIndex +import pandas.util.testing as tm + + +@pytest.mark.parametrize("with_header", [True, False]) +def test_index_col_named(all_parsers, with_header): + parser = all_parsers + no_header = """\ +KORD1,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD2,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD3,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD4,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD5,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD6,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000""" # noqa + header = "ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir\n" # noqa + + if with_header: + data = header + no_header + + result = parser.read_csv(StringIO(data), index_col="ID") + expected = parser.read_csv(StringIO(data), header=0).set_index("ID") + tm.assert_frame_equal(result, expected) + else: + data = no_header + msg = "Index ID invalid" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), index_col="ID") + + +def test_index_col_named2(all_parsers): + parser = all_parsers + data = """\ +1,2,3,4,hello +5,6,7,8,world +9,10,11,12,foo +""" + + expected = DataFrame({"a": [1, 5, 9], "b": [2, 6, 10], + "c": [3, 7, 11], "d": [4, 8, 12]}, + index=Index(["hello", "world", "foo"], + name="message")) + names = ["a", "b", "c", "d", "message"] + + result = parser.read_csv(StringIO(data), names=names, + index_col=["message"]) + tm.assert_frame_equal(result, expected) + + +def test_index_col_is_true(all_parsers): + # see gh-9798 + data = "a,b\n1,2" + parser = all_parsers + + with pytest.raises(ValueError, match="The value of index_col " + "couldn't be 'True'"): + parser.read_csv(StringIO(data), index_col=True) + + +def test_infer_index_col(all_parsers): + data = """A,B,C +foo,1,2,3 +bar,4,5,6 +baz,7,8,9 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data)) + + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=["foo", "bar", "baz"], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_col,kwargs", [ + (None, dict(columns=["x", "y", "z"])), + (False, dict(columns=["x", "y", "z"])), + (0, dict(columns=["y", "z"], index=Index([], name="x"))), + (1, dict(columns=["x", "z"], index=Index([], name="y"))), + ("x", dict(columns=["y", "z"], index=Index([], name="x"))), + ("y", dict(columns=["x", "z"], index=Index([], name="y"))), + ([0, 1], dict(columns=["z"], index=MultiIndex.from_arrays( + [[]] * 2, names=["x", "y"]))), + (["x", "y"], dict(columns=["z"], index=MultiIndex.from_arrays( + [[]] * 2, names=["x", "y"]))), + ([1, 0], dict(columns=["z"], index=MultiIndex.from_arrays( + [[]] * 2, names=["y", "x"]))), + (["y", "x"], dict(columns=["z"], index=MultiIndex.from_arrays( + [[]] * 2, names=["y", "x"]))), +]) +def test_index_col_empty_data(all_parsers, index_col, kwargs): + data = "x,y,z" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=index_col) + + expected = DataFrame([], **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_empty_with_index_col_false(all_parsers): + # see gh-10413 + data = "x,y" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=False) + + expected = DataFrame([], columns=["x", "y"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_names", [ + ["", ""], + ["foo", ""], + ["", "bar"], + ["foo", "bar"], + ["NotReallyUnnamed", "Unnamed: 0"], +]) +def test_multi_index_naming(all_parsers, index_names): + parser = all_parsers + + # We don't want empty index names being replaced with "Unnamed: 0" + data = ",".join(index_names + ["col\na,c,1\na,d,2\nb,c,3\nb,d,4"]) + result = parser.read_csv(StringIO(data), index_col=[0, 1]) + + expected = DataFrame({"col": [1, 2, 3, 4]}, + index=MultiIndex.from_product([["a", "b"], + ["c", "d"]])) + expected.index.names = [name if name else None for name in index_names] + tm.assert_frame_equal(result, expected) + + +def test_multi_index_naming_not_all_at_beginning(all_parsers): + parser = all_parsers + data = ",Unnamed: 2,\na,c,1\na,d,2\nb,c,3\nb,d,4" + result = parser.read_csv(StringIO(data), index_col=[0, 2]) + + expected = DataFrame({"Unnamed: 2": ["c", "d", "c", "d"]}, + index=MultiIndex( + levels=[['a', 'b'], [1, 2, 3, 4]], + codes=[[0, 0, 1, 1], [0, 1, 2, 3]])) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_mangle_dupes.py b/pandas/tests/io/parser/test_mangle_dupes.py new file mode 100644 index 0000000000000..0efc0c2c13557 --- /dev/null +++ b/pandas/tests/io/parser/test_mangle_dupes.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +""" +Tests that duplicate columns are handled appropriately when parsed by the +CSV engine. In general, the expected result is that they are either thoroughly +de-duplicated (if mangling requested) or ignored otherwise. +""" + +import pytest + +from pandas.compat import StringIO + +from pandas import DataFrame +import pandas.util.testing as tm + + +@pytest.mark.parametrize("kwargs", [dict(), dict(mangle_dupe_cols=True)]) +def test_basic(all_parsers, kwargs): + # TODO: add test for condition "mangle_dupe_cols=False" + # once it is actually supported (gh-12935) + parser = all_parsers + + data = "a,a,b,b,b\n1,2,3,4,5" + result = parser.read_csv(StringIO(data), sep=",", **kwargs) + + expected = DataFrame([[1, 2, 3, 4, 5]], + columns=["a", "a.1", "b", "b.1", "b.2"]) + tm.assert_frame_equal(result, expected) + + +def test_basic_names(all_parsers): + # See gh-7160 + parser = all_parsers + + data = "a,b,a\n0,1,2\n3,4,5" + expected = DataFrame([[0, 1, 2], [3, 4, 5]], + columns=["a", "b", "a.1"]) + + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +def test_basic_names_warn(all_parsers): + # See gh-7160 + parser = all_parsers + + data = "0,1,2\n3,4,5" + expected = DataFrame([[0, 1, 2], [3, 4, 5]], + columns=["a", "b", "a.1"]) + + with tm.assert_produces_warning(UserWarning, check_stacklevel=False): + result = parser.read_csv(StringIO(data), names=["a", "b", "a"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,expected", [ + ("a,a,a.1\n1,2,3", + DataFrame([[1, 2, 3]], columns=["a", "a.1", "a.1.1"])), + ("a,a,a.1,a.1.1,a.1.1.1,a.1.1.1.1\n1,2,3,4,5,6", + DataFrame([[1, 2, 3, 4, 5, 6]], columns=["a", "a.1", "a.1.1", "a.1.1.1", + "a.1.1.1.1", "a.1.1.1.1.1"])), + ("a,a,a.3,a.1,a.2,a,a\n1,2,3,4,5,6,7", + DataFrame([[1, 2, 3, 4, 5, 6, 7]], columns=["a", "a.1", "a.3", "a.1.1", + "a.2", "a.2.1", "a.3.1"])) +]) +def test_thorough_mangle_columns(all_parsers, data, expected): + # see gh-17060 + parser = all_parsers + + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,names,expected", [ + ("a,b,b\n1,2,3", + ["a.1", "a.1", "a.1.1"], + DataFrame([["a", "b", "b"], ["1", "2", "3"]], + columns=["a.1", "a.1.1", "a.1.1.1"])), + ("a,b,c,d,e,f\n1,2,3,4,5,6", + ["a", "a", "a.1", "a.1.1", "a.1.1.1", "a.1.1.1.1"], + DataFrame([["a", "b", "c", "d", "e", "f"], + ["1", "2", "3", "4", "5", "6"]], + columns=["a", "a.1", "a.1.1", "a.1.1.1", + "a.1.1.1.1", "a.1.1.1.1.1"])), + ("a,b,c,d,e,f,g\n1,2,3,4,5,6,7", + ["a", "a", "a.3", "a.1", "a.2", "a", "a"], + DataFrame([["a", "b", "c", "d", "e", "f", "g"], + ["1", "2", "3", "4", "5", "6", "7"]], + columns=["a", "a.1", "a.3", "a.1.1", + "a.2", "a.2.1", "a.3.1"])), +]) +def test_thorough_mangle_names(all_parsers, data, names, expected): + # see gh-17095 + parser = all_parsers + + with tm.assert_produces_warning(UserWarning, check_stacklevel=False): + result = parser.read_csv(StringIO(data), names=names) + tm.assert_frame_equal(result, expected) + + +def test_mangled_unnamed_placeholders(all_parsers): + # xref gh-13017 + orig_key = "0" + parser = all_parsers + + orig_value = [1, 2, 3] + df = DataFrame({orig_key: orig_value}) + + # This test recursively updates `df`. + for i in range(3): + expected = DataFrame() + + for j in range(i + 1): + expected["Unnamed: 0" + ".1" * j] = [0, 1, 2] + + expected[orig_key] = orig_value + df = parser.read_csv(StringIO(df.to_csv())) + + tm.assert_frame_equal(df, expected) diff --git a/pandas/tests/io/parser/test_multi_thread.py b/pandas/tests/io/parser/test_multi_thread.py new file mode 100644 index 0000000000000..fbf23f769e202 --- /dev/null +++ b/pandas/tests/io/parser/test_multi_thread.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +""" +Tests multithreading behaviour for reading and +parsing files for each parser defined in parsers.py +""" + +from __future__ import division + +from multiprocessing.pool import ThreadPool + +import numpy as np + +from pandas.compat import BytesIO, range + +import pandas as pd +from pandas import DataFrame +import pandas.util.testing as tm + + +def _construct_dataframe(num_rows): + """ + Construct a DataFrame for testing. + + Parameters + ---------- + num_rows : int + The number of rows for our DataFrame. + + Returns + ------- + df : DataFrame + """ + df = DataFrame(np.random.rand(num_rows, 5), columns=list("abcde")) + df["foo"] = "foo" + df["bar"] = "bar" + df["baz"] = "baz" + df["date"] = pd.date_range("20000101 09:00:00", + periods=num_rows, + freq="s") + df["int"] = np.arange(num_rows, dtype="int64") + return df + + +def test_multi_thread_string_io_read_csv(all_parsers): + # see gh-11786 + parser = all_parsers + max_row_range = 10000 + num_files = 100 + + bytes_to_df = [ + "\n".join( + ["%d,%d,%d" % (i, i, i) for i in range(max_row_range)] + ).encode() for _ in range(num_files)] + files = [BytesIO(b) for b in bytes_to_df] + + # Read all files in many threads. + pool = ThreadPool(8) + + results = pool.map(parser.read_csv, files) + first_result = results[0] + + for result in results: + tm.assert_frame_equal(first_result, result) + + +def _generate_multi_thread_dataframe(parser, path, num_rows, num_tasks): + """ + Generate a DataFrame via multi-thread. + + Parameters + ---------- + parser : BaseParser + The parser object to use for reading the data. + path : str + The location of the CSV file to read. + num_rows : int + The number of rows to read per task. + num_tasks : int + The number of tasks to use for reading this DataFrame. + + Returns + ------- + df : DataFrame + """ + def reader(arg): + """ + Create a reader for part of the CSV. + + Parameters + ---------- + arg : tuple + A tuple of the following: + + * start : int + The starting row to start for parsing CSV + * nrows : int + The number of rows to read. + + Returns + ------- + df : DataFrame + """ + start, nrows = arg + + if not start: + return parser.read_csv(path, index_col=0, header=0, + nrows=nrows, parse_dates=["date"]) + + return parser.read_csv(path, index_col=0, header=None, + skiprows=int(start) + 1, + nrows=nrows, parse_dates=[9]) + + tasks = [ + (num_rows * i // num_tasks, + num_rows // num_tasks) for i in range(num_tasks) + ] + + pool = ThreadPool(processes=num_tasks) + results = pool.map(reader, tasks) + + header = results[0].columns + + for r in results[1:]: + r.columns = header + + final_dataframe = pd.concat(results) + return final_dataframe + + +def test_multi_thread_path_multipart_read_csv(all_parsers): + # see gh-11786 + num_tasks = 4 + num_rows = 100000 + + parser = all_parsers + file_name = "__thread_pool_reader__.csv" + df = _construct_dataframe(num_rows) + + with tm.ensure_clean(file_name) as path: + df.to_csv(path) + + final_dataframe = _generate_multi_thread_dataframe(parser, path, + num_rows, num_tasks) + tm.assert_frame_equal(df, final_dataframe) diff --git a/pandas/tests/io/parser/test_na_values.py b/pandas/tests/io/parser/test_na_values.py new file mode 100644 index 0000000000000..1b6d2ee8a062e --- /dev/null +++ b/pandas/tests/io/parser/test_na_values.py @@ -0,0 +1,441 @@ +# -*- coding: utf-8 -*- + +""" +Tests that NA values are properly handled during +parsing for all of the parsers defined in parsers.py +""" + +import numpy as np +import pytest + +from pandas.compat import StringIO, range + +from pandas import DataFrame, Index, MultiIndex +import pandas.util.testing as tm + +import pandas.io.common as com + + +def test_string_nas(all_parsers): + parser = all_parsers + data = """A,B,C +a,b,c +d,,f +,g,h +""" + result = parser.read_csv(StringIO(data)) + expected = DataFrame([["a", "b", "c"], + ["d", np.nan, "f"], + [np.nan, "g", "h"]], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +def test_detect_string_na(all_parsers): + parser = all_parsers + data = """A,B +foo,bar +NA,baz +NaN,nan +""" + expected = DataFrame([["foo", "bar"], [np.nan, "baz"], + [np.nan, np.nan]], columns=["A", "B"]) + result = parser.read_csv(StringIO(data)) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_values", [ + ["-999.0", "-999"], + [-999, -999.0], + [-999.0, -999], + ["-999.0"], ["-999"], + [-999.0], [-999] +]) +@pytest.mark.parametrize("data", [ + """A,B +-999,1.2 +2,-999 +3,4.5 +""", + """A,B +-999,1.200 +2,-999.000 +3,4.500 +""" +]) +def test_non_string_na_values(all_parsers, data, na_values): + # see gh-3611: with an odd float format, we can't match + # the string "999.0" exactly but still need float matching + parser = all_parsers + expected = DataFrame([[np.nan, 1.2], [2.0, np.nan], + [3.0, 4.5]], columns=["A", "B"]) + + result = parser.read_csv(StringIO(data), na_values=na_values) + tm.assert_frame_equal(result, expected) + + +def test_default_na_values(all_parsers): + _NA_VALUES = {"-1.#IND", "1.#QNAN", "1.#IND", "-1.#QNAN", "#N/A", + "N/A", "n/a", "NA", "#NA", "NULL", "null", "NaN", "nan", + "-NaN", "-nan", "#N/A N/A", ""} + assert _NA_VALUES == com._NA_VALUES + + parser = all_parsers + nv = len(_NA_VALUES) + + def f(i, v): + if i == 0: + buf = "" + elif i > 0: + buf = "".join([","] * i) + + buf = "{0}{1}".format(buf, v) + + if i < nv - 1: + buf = "{0}{1}".format(buf, "".join([","] * (nv - i - 1))) + + return buf + + data = StringIO("\n".join(f(i, v) for i, v in enumerate(_NA_VALUES))) + expected = DataFrame(np.nan, columns=range(nv), index=range(nv)) + + result = parser.read_csv(data, header=None) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_values", ["baz", ["baz"]]) +def test_custom_na_values(all_parsers, na_values): + parser = all_parsers + data = """A,B,C +ignore,this,row +1,NA,3 +-1.#IND,5,baz +7,8,NaN +""" + expected = DataFrame([[1., np.nan, 3], [np.nan, 5, np.nan], + [7, 8, np.nan]], columns=["A", "B", "C"]) + result = parser.read_csv(StringIO(data), na_values=na_values, skiprows=[1]) + tm.assert_frame_equal(result, expected) + + +def test_bool_na_values(all_parsers): + data = """A,B,C +True,False,True +NA,True,False +False,NA,True""" + parser = all_parsers + result = parser.read_csv(StringIO(data)) + expected = DataFrame({"A": np.array([True, np.nan, False], dtype=object), + "B": np.array([False, True, np.nan], dtype=object), + "C": [True, False, True]}) + tm.assert_frame_equal(result, expected) + + +def test_na_value_dict(all_parsers): + data = """A,B,C +foo,bar,NA +bar,foo,foo +foo,bar,NA +bar,foo,foo""" + parser = all_parsers + df = parser.read_csv(StringIO(data), + na_values={"A": ["foo"], "B": ["bar"]}) + expected = DataFrame({"A": [np.nan, "bar", np.nan, "bar"], + "B": [np.nan, "foo", np.nan, "foo"], + "C": [np.nan, "foo", np.nan, "foo"]}) + tm.assert_frame_equal(df, expected) + + +@pytest.mark.parametrize("index_col,expected", [ + ([0], DataFrame({"b": [np.nan], "c": [1], "d": [5]}, + index=Index([0], name="a"))), + ([0, 2], DataFrame({"b": [np.nan], "d": [5]}, + index=MultiIndex.from_tuples( + [(0, 1)], names=["a", "c"]))), + (["a", "c"], DataFrame({"b": [np.nan], "d": [5]}, + index=MultiIndex.from_tuples( + [(0, 1)], names=["a", "c"]))), +]) +def test_na_value_dict_multi_index(all_parsers, index_col, expected): + data = """\ +a,b,c,d +0,NA,1,5 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), na_values=set(), + index_col=index_col) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,expected", [ + (dict(), DataFrame({"A": ["a", "b", np.nan, "d", "e", np.nan, "g"], + "B": [1, 2, 3, 4, 5, 6, 7], + "C": ["one", "two", "three", np.nan, "five", + np.nan, "seven"]})), + (dict(na_values={"A": [], "C": []}, keep_default_na=False), + DataFrame({"A": ["a", "b", "", "d", "e", "nan", "g"], + "B": [1, 2, 3, 4, 5, 6, 7], + "C": ["one", "two", "three", "nan", "five", "", "seven"]})), + (dict(na_values=["a"], keep_default_na=False), + DataFrame({"A": [np.nan, "b", "", "d", "e", "nan", "g"], + "B": [1, 2, 3, 4, 5, 6, 7], + "C": ["one", "two", "three", "nan", "five", "", "seven"]})), + (dict(na_values={"A": [], "C": []}), + DataFrame({"A": ["a", "b", np.nan, "d", "e", np.nan, "g"], + "B": [1, 2, 3, 4, 5, 6, 7], + "C": ["one", "two", "three", np.nan, + "five", np.nan, "seven"]})), +]) +def test_na_values_keep_default(all_parsers, kwargs, expected): + data = """\ +A,B,C +a,1,one +b,2,two +,3,three +d,4,nan +e,5,five +nan,6, +g,7,seven +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_no_na_values_no_keep_default(all_parsers): + # see gh-4318: passing na_values=None and + # keep_default_na=False yields 'None" as a na_value + data = """\ +A,B,C +a,1,None +b,2,two +,3,None +d,4,nan +e,5,five +nan,6, +g,7,seven +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), keep_default_na=False) + + expected = DataFrame({"A": ["a", "b", "", "d", "e", "nan", "g"], + "B": [1, 2, 3, 4, 5, 6, 7], + "C": ["None", "two", "None", "nan", + "five", "", "seven"]}) + tm.assert_frame_equal(result, expected) + + +def test_no_keep_default_na_dict_na_values(all_parsers): + # see gh-19227 + data = "a,b\n,2" + parser = all_parsers + result = parser.read_csv(StringIO(data), na_values={"b": ["2"]}, + keep_default_na=False) + expected = DataFrame({"a": [""], "b": [np.nan]}) + tm.assert_frame_equal(result, expected) + + +def test_no_keep_default_na_dict_na_scalar_values(all_parsers): + # see gh-19227 + # + # Scalar values shouldn't cause the parsing to crash or fail. + data = "a,b\n1,2" + parser = all_parsers + df = parser.read_csv(StringIO(data), na_values={"b": 2}, + keep_default_na=False) + expected = DataFrame({"a": [1], "b": [np.nan]}) + tm.assert_frame_equal(df, expected) + + +@pytest.mark.parametrize("col_zero_na_values", [ + 113125, "113125" +]) +def test_no_keep_default_na_dict_na_values_diff_reprs(all_parsers, + col_zero_na_values): + # see gh-19227 + data = """\ +113125,"blah","/blaha",kjsdkj,412.166,225.874,214.008 +729639,"qwer","",asdfkj,466.681,,252.373 +""" + parser = all_parsers + expected = DataFrame({0: [np.nan, 729639.0], + 1: [np.nan, "qwer"], + 2: ["/blaha", np.nan], + 3: ["kjsdkj", "asdfkj"], + 4: [412.166, 466.681], + 5: ["225.874", ""], + 6: [np.nan, 252.373]}) + + result = parser.read_csv(StringIO(data), header=None, + keep_default_na=False, + na_values={2: "", 6: "214.008", + 1: "blah", 0: col_zero_na_values}) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_filter,row_data", [ + (True, [[1, "A"], [np.nan, np.nan], [3, "C"]]), + (False, [["1", "A"], ["nan", "B"], ["3", "C"]]), +]) +def test_na_values_na_filter_override(all_parsers, na_filter, row_data): + data = """\ +A,B +1,A +nan,B +3,C +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), na_values=["B"], + na_filter=na_filter) + + expected = DataFrame(row_data, columns=["A", "B"]) + tm.assert_frame_equal(result, expected) + + +def test_na_trailing_columns(all_parsers): + parser = all_parsers + data = """Date,Currency,Symbol,Type,Units,UnitPrice,Cost,Tax +2012-03-14,USD,AAPL,BUY,1000 +2012-05-12,USD,SBUX,SELL,500""" + + # Trailing columns should be all NaN. + result = parser.read_csv(StringIO(data)) + expected = DataFrame([ + ["2012-03-14", "USD", "AAPL", "BUY", 1000, np.nan, np.nan, np.nan], + ["2012-05-12", "USD", "SBUX", "SELL", 500, np.nan, np.nan, np.nan], + ], columns=["Date", "Currency", "Symbol", "Type", + "Units", "UnitPrice", "Cost", "Tax"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_values,row_data", [ + (1, [[np.nan, 2.0], [2.0, np.nan]]), + ({"a": 2, "b": 1}, [[1.0, 2.0], [np.nan, np.nan]]), +]) +def test_na_values_scalar(all_parsers, na_values, row_data): + # see gh-12224 + parser = all_parsers + names = ["a", "b"] + data = "1,2\n2,1" + + result = parser.read_csv(StringIO(data), names=names, na_values=na_values) + expected = DataFrame(row_data, columns=names) + tm.assert_frame_equal(result, expected) + + +def test_na_values_dict_aliasing(all_parsers): + parser = all_parsers + na_values = {"a": 2, "b": 1} + na_values_copy = na_values.copy() + + names = ["a", "b"] + data = "1,2\n2,1" + + expected = DataFrame([[1.0, 2.0], [np.nan, np.nan]], columns=names) + result = parser.read_csv(StringIO(data), names=names, na_values=na_values) + + tm.assert_frame_equal(result, expected) + tm.assert_dict_equal(na_values, na_values_copy) + + +def test_na_values_dict_col_index(all_parsers): + # see gh-14203 + data = "a\nfoo\n1" + parser = all_parsers + na_values = {0: "foo"} + + result = parser.read_csv(StringIO(data), na_values=na_values) + expected = DataFrame({"a": [np.nan, 1]}) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + (str(2**63) + "\n" + str(2**63 + 1), + dict(na_values=[2**63]), DataFrame([str(2**63), str(2**63 + 1)])), + (str(2**63) + ",1" + "\n,2", + dict(), DataFrame([[str(2**63), 1], ['', 2]])), + (str(2**63) + "\n1", + dict(na_values=[2**63]), DataFrame([np.nan, 1])), +]) +def test_na_values_uint64(all_parsers, data, kwargs, expected): + # see gh-14983 + parser = all_parsers + result = parser.read_csv(StringIO(data), header=None, **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_empty_na_values_no_default_with_index(all_parsers): + # see gh-15835 + data = "a,1\nb,2" + parser = all_parsers + expected = DataFrame({"1": [2]}, index=Index(["b"], name="a")) + + result = parser.read_csv(StringIO(data), index_col=0, + keep_default_na=False) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("na_filter,index_data", [ + (False, ["", "5"]), + (True, [np.nan, 5.0]), +]) +def test_no_na_filter_on_index(all_parsers, na_filter, index_data): + # see gh-5239 + # + # Don't parse NA-values in index unless na_filter=True + parser = all_parsers + data = "a,b,c\n1,,3\n4,5,6" + + expected = DataFrame({"a": [1, 4], "c": [3, 6]}, + index=Index(index_data, name="b")) + result = parser.read_csv(StringIO(data), index_col=[1], + na_filter=na_filter) + tm.assert_frame_equal(result, expected) + + +def test_inf_na_values_with_int_index(all_parsers): + # see gh-17128 + parser = all_parsers + data = "idx,col1,col2\n1,3,4\n2,inf,-inf" + + # Don't fail with OverflowError with inf's and integer index column. + out = parser.read_csv(StringIO(data), index_col=[0], + na_values=["inf", "-inf"]) + expected = DataFrame({"col1": [3, np.nan], "col2": [4, np.nan]}, + index=Index([1, 2], name="idx")) + tm.assert_frame_equal(out, expected) + + +@pytest.mark.parametrize("na_filter", [True, False]) +def test_na_values_with_dtype_str_and_na_filter(all_parsers, na_filter): + # see gh-20377 + parser = all_parsers + data = "a,b,c\n1,,3\n4,5,6" + + # na_filter=True --> missing value becomes NaN. + # na_filter=False --> missing value remains empty string. + empty = np.nan if na_filter else "" + expected = DataFrame({"a": ["1", "4"], + "b": [empty, "5"], + "c": ["3", "6"]}) + + result = parser.read_csv(StringIO(data), na_filter=na_filter, dtype=str) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data, na_values", [ + ("false,1\n,1\ntrue", None), + ("false,1\nnull,1\ntrue", None), + ("false,1\nnan,1\ntrue", None), + ("false,1\nfoo,1\ntrue", 'foo'), + ("false,1\nfoo,1\ntrue", ['foo']), + ("false,1\nfoo,1\ntrue", {'a': 'foo'}), +]) +def test_cast_NA_to_bool_raises_error(all_parsers, data, na_values): + parser = all_parsers + msg = ("(Bool column has NA values in column [0a])|" + "(cannot safely convert passed user dtype of " + "bool for object dtyped data in column 0)") + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), header=None, names=['a', 'b'], + dtype={'a': 'bool'}, na_values=na_values) diff --git a/pandas/tests/io/parser/test_network.py b/pandas/tests/io/parser/test_network.py index 4d6b6c7daa3c6..e54da94089cfd 100644 --- a/pandas/tests/io/parser/test_network.py +++ b/pandas/tests/io/parser/test_network.py @@ -4,29 +4,33 @@ Tests parsers ability to read and parse non-local files and hence require a network connection to be read. """ +import logging -import os +import numpy as np import pytest -import pandas.util.testing as tm -from pandas import DataFrame -from pandas.io.parsers import read_csv, read_table +from pandas.compat import BytesIO, StringIO +import pandas.util._test_decorators as td +from pandas import DataFrame +import pandas.util.testing as tm -@pytest.fixture(scope='module') -def salaries_table(): - path = os.path.join(tm.get_data_path(), 'salaries.csv') - return read_table(path) +from pandas.io.parsers import read_csv +@pytest.mark.network @pytest.mark.parametrize( - "compression,extension", - [('gzip', '.gz'), ('bz2', '.bz2'), ('zip', '.zip'), - tm._mark_skipif_no_lzma(('xz', '.xz'))]) + "compress_type, extension", [ + ('gzip', '.gz'), ('bz2', '.bz2'), ('zip', '.zip'), + pytest.param('xz', '.xz', marks=td.skip_if_no_lzma) + ] +) @pytest.mark.parametrize('mode', ['explicit', 'infer']) @pytest.mark.parametrize('engine', ['python', 'c']) -def test_compressed_urls(salaries_table, compression, extension, mode, engine): - check_compressed_urls(salaries_table, compression, extension, mode, engine) +def test_compressed_urls(salaries_table, compress_type, extension, mode, + engine): + check_compressed_urls(salaries_table, compress_type, extension, mode, + engine) @tm.network @@ -42,136 +46,159 @@ def check_compressed_urls(salaries_table, compression, extension, mode, if mode != 'explicit': compression = mode - url_table = read_table(url, compression=compression, engine=engine) + url_table = read_csv(url, sep='\t', compression=compression, engine=engine) tm.assert_frame_equal(url_table, salaries_table) -class TestS3(tm.TestCase): +@pytest.fixture +def tips_df(datapath): + """DataFrame with the tips dataset.""" + return read_csv(datapath('io', 'parser', 'data', 'tips.csv')) - def setUp(self): - try: - import s3fs # noqa - except ImportError: - pytest.skip("s3fs not installed") - @tm.network - def test_parse_public_s3_bucket(self): +@pytest.mark.usefixtures("s3_resource") +@td.skip_if_not_us_locale() +class TestS3(object): + + def test_parse_public_s3_bucket(self, tips_df): + pytest.importorskip('s3fs') + + # more of an integration test due to the not-public contents portion + # can probably mock this though. for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df = read_csv('s3://pandas-test/tips.csv' + ext, compression=comp) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')), df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(df, tips_df) # Read public file from bucket with not-public contents df = read_csv('s3://cant_get_it/tips.csv') - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv(tm.get_data_path('tips.csv')), df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(df, tips_df) + + def test_parse_public_s3n_bucket(self, tips_df): - @tm.network - def test_parse_public_s3n_bucket(self): # Read from AWS s3 as "s3n" URL df = read_csv('s3n://pandas-test/tips.csv', nrows=10) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')).iloc[:10], df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(tips_df.iloc[:10], df) - @tm.network - def test_parse_public_s3a_bucket(self): + def test_parse_public_s3a_bucket(self, tips_df): # Read from AWS s3 as "s3a" URL df = read_csv('s3a://pandas-test/tips.csv', nrows=10) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')).iloc[:10], df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(tips_df.iloc[:10], df) - @tm.network - def test_parse_public_s3_bucket_nrows(self): + def test_parse_public_s3_bucket_nrows(self, tips_df): for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df = read_csv('s3://pandas-test/tips.csv' + ext, nrows=10, compression=comp) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')).iloc[:10], df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(tips_df.iloc[:10], df) - @tm.network - def test_parse_public_s3_bucket_chunked(self): + def test_parse_public_s3_bucket_chunked(self, tips_df): # Read with a chunksize chunksize = 5 - local_tips = read_csv(tm.get_data_path('tips.csv')) for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df_reader = read_csv('s3://pandas-test/tips.csv' + ext, chunksize=chunksize, compression=comp) - self.assertEqual(df_reader.chunksize, chunksize) + assert df_reader.chunksize == chunksize for i_chunk in [0, 1, 2]: # Read a couple of chunks and make sure we see them # properly. df = df_reader.get_chunk() - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - true_df = local_tips.iloc[ + assert isinstance(df, DataFrame) + assert not df.empty + true_df = tips_df.iloc[ chunksize * i_chunk: chunksize * (i_chunk + 1)] tm.assert_frame_equal(true_df, df) - @tm.network - def test_parse_public_s3_bucket_chunked_python(self): + def test_parse_public_s3_bucket_chunked_python(self, tips_df): # Read with a chunksize using the Python parser chunksize = 5 - local_tips = read_csv(tm.get_data_path('tips.csv')) for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df_reader = read_csv('s3://pandas-test/tips.csv' + ext, chunksize=chunksize, compression=comp, engine='python') - self.assertEqual(df_reader.chunksize, chunksize) + assert df_reader.chunksize == chunksize for i_chunk in [0, 1, 2]: # Read a couple of chunks and make sure we see them properly. df = df_reader.get_chunk() - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - true_df = local_tips.iloc[ + assert isinstance(df, DataFrame) + assert not df.empty + true_df = tips_df.iloc[ chunksize * i_chunk: chunksize * (i_chunk + 1)] tm.assert_frame_equal(true_df, df) - @tm.network - def test_parse_public_s3_bucket_python(self): + def test_parse_public_s3_bucket_python(self, tips_df): for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df = read_csv('s3://pandas-test/tips.csv' + ext, engine='python', compression=comp) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')), df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(df, tips_df) - @tm.network - def test_infer_s3_compression(self): + def test_infer_s3_compression(self, tips_df): for ext in ['', '.gz', '.bz2']: df = read_csv('s3://pandas-test/tips.csv' + ext, engine='python', compression='infer') - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')), df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(df, tips_df) - @tm.network - def test_parse_public_s3_bucket_nrows_python(self): + def test_parse_public_s3_bucket_nrows_python(self, tips_df): for ext, comp in [('', None), ('.gz', 'gzip'), ('.bz2', 'bz2')]: df = read_csv('s3://pandas-test/tips.csv' + ext, engine='python', nrows=10, compression=comp) - self.assertTrue(isinstance(df, DataFrame)) - self.assertFalse(df.empty) - tm.assert_frame_equal(read_csv( - tm.get_data_path('tips.csv')).iloc[:10], df) + assert isinstance(df, DataFrame) + assert not df.empty + tm.assert_frame_equal(tips_df.iloc[:10], df) - @tm.network def test_s3_fails(self): - with tm.assertRaises(IOError): + with pytest.raises(IOError): read_csv('s3://nyqpug/asdf.csv') # Receive a permission error when trying to read a private bucket. # It's irrelevant here that this isn't actually a table. - with tm.assertRaises(IOError): + with pytest.raises(IOError): read_csv('s3://cant_get_it/') + + def test_read_csv_handles_boto_s3_object(self, + s3_resource, + tips_file): + # see gh-16135 + + s3_object = s3_resource.meta.client.get_object( + Bucket='pandas-test', + Key='tips.csv') + + result = read_csv(BytesIO(s3_object["Body"].read()), encoding='utf8') + assert isinstance(result, DataFrame) + assert not result.empty + + expected = read_csv(tips_file) + tm.assert_frame_equal(result, expected) + + def test_read_csv_chunked_download(self, s3_resource, caplog): + # 8 MB, S3FS usees 5MB chunks + df = DataFrame(np.random.randn(100000, 4), columns=list('abcd')) + buf = BytesIO() + str_buf = StringIO() + + df.to_csv(str_buf) + + buf = BytesIO(str_buf.getvalue().encode('utf-8')) + + s3_resource.Bucket("pandas-test").put_object( + Key="large-file.csv", + Body=buf) + + with caplog.at_level(logging.DEBUG, logger='s3fs.core'): + read_csv("s3://pandas-test/large-file.csv", nrows=5) + # log of fetch_range (start, stop) + assert ((0, 5505024) in {x.args[-2:] for x in caplog.records}) diff --git a/pandas/tests/io/parser/test_parse_dates.py b/pandas/tests/io/parser/test_parse_dates.py new file mode 100644 index 0000000000000..ffc8af09bf239 --- /dev/null +++ b/pandas/tests/io/parser/test_parse_dates.py @@ -0,0 +1,849 @@ +# -*- coding: utf-8 -*- + +""" +Tests date parsing functionality for all of the +parsers defined in parsers.py +""" + +from datetime import date, datetime + +import numpy as np +import pytest +import pytz + +from pandas._libs.tslib import Timestamp +from pandas._libs.tslibs import parsing +from pandas.compat import StringIO, lrange, parse_date +from pandas.compat.numpy import np_array_datetime64_compat + +import pandas as pd +from pandas import DataFrame, DatetimeIndex, Index, MultiIndex +from pandas.core.indexes.datetimes import date_range +import pandas.util.testing as tm + +import pandas.io.date_converters as conv +import pandas.io.parsers as parsers + + +def test_separator_date_conflict(all_parsers): + # Regression test for gh-4678 + # + # Make sure thousands separator and + # date parsing do not conflict. + parser = all_parsers + data = "06-02-2013;13:00;1-000.215" + expected = DataFrame([[datetime(2013, 6, 2, 13, 0, 0), 1000.215]], + columns=["Date", 2]) + + df = parser.read_csv(StringIO(data), sep=";", thousands="-", + parse_dates={"Date": [0, 1]}, header=None) + tm.assert_frame_equal(df, expected) + + +@pytest.mark.parametrize("keep_date_col", [True, False]) +def test_multiple_date_col_custom(all_parsers, keep_date_col): + data = """\ +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + parser = all_parsers + + def date_parser(*date_cols): + """ + Test date parser. + + Parameters + ---------- + date_cols : args + The list of data columns to parse. + + Returns + ------- + parsed : Series + """ + return parsing.try_parse_dates(parsers._concat_date_cols(date_cols)) + + result = parser.read_csv(StringIO(data), header=None, + date_parser=date_parser, prefix="X", + parse_dates={"actual": [1, 2], + "nominal": [1, 3]}, + keep_date_col=keep_date_col) + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), datetime(1999, 1, 27, 18, 56), + "KORD", "19990127", " 19:00:00", " 18:56:00", + 0.81, 2.81, 7.2, 0.0, 280.0], + [datetime(1999, 1, 27, 20, 0), datetime(1999, 1, 27, 19, 56), + "KORD", "19990127", " 20:00:00", " 19:56:00", + 0.01, 2.21, 7.2, 0.0, 260.0], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 20, 56), + "KORD", "19990127", " 21:00:00", " 20:56:00", + -0.59, 2.21, 5.7, 0.0, 280.0], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 21, 18), + "KORD", "19990127", " 21:00:00", " 21:18:00", + -0.99, 2.01, 3.6, 0.0, 270.0], + [datetime(1999, 1, 27, 22, 0), datetime(1999, 1, 27, 21, 56), + "KORD", "19990127", " 22:00:00", " 21:56:00", + -0.59, 1.71, 5.1, 0.0, 290.0], + [datetime(1999, 1, 27, 23, 0), datetime(1999, 1, 27, 22, 56), + "KORD", "19990127", " 23:00:00", " 22:56:00", + -0.59, 1.71, 4.6, 0.0, 280.0], + ], columns=["actual", "nominal", "X0", "X1", "X2", + "X3", "X4", "X5", "X6", "X7", "X8"]) + + if not keep_date_col: + expected = expected.drop(["X1", "X2", "X3"], axis=1) + elif parser.engine == "python": + expected["X1"] = expected["X1"].astype(np.int64) + + # Python can sometimes be flaky about how + # the aggregated columns are entered, so + # this standardizes the order. + result = result[expected.columns] + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("keep_date_col", [True, False]) +def test_multiple_date_col(all_parsers, keep_date_col): + data = """\ +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), header=None, + prefix="X", parse_dates=[[1, 2], [1, 3]], + keep_date_col=keep_date_col) + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), datetime(1999, 1, 27, 18, 56), + "KORD", "19990127", " 19:00:00", " 18:56:00", + 0.81, 2.81, 7.2, 0.0, 280.0], + [datetime(1999, 1, 27, 20, 0), datetime(1999, 1, 27, 19, 56), + "KORD", "19990127", " 20:00:00", " 19:56:00", + 0.01, 2.21, 7.2, 0.0, 260.0], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 20, 56), + "KORD", "19990127", " 21:00:00", " 20:56:00", + -0.59, 2.21, 5.7, 0.0, 280.0], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 21, 18), + "KORD", "19990127", " 21:00:00", " 21:18:00", + -0.99, 2.01, 3.6, 0.0, 270.0], + [datetime(1999, 1, 27, 22, 0), datetime(1999, 1, 27, 21, 56), + "KORD", "19990127", " 22:00:00", " 21:56:00", + -0.59, 1.71, 5.1, 0.0, 290.0], + [datetime(1999, 1, 27, 23, 0), datetime(1999, 1, 27, 22, 56), + "KORD", "19990127", " 23:00:00", " 22:56:00", + -0.59, 1.71, 4.6, 0.0, 280.0], + ], columns=["X1_X2", "X1_X3", "X0", "X1", "X2", + "X3", "X4", "X5", "X6", "X7", "X8"]) + + if not keep_date_col: + expected = expected.drop(["X1", "X2", "X3"], axis=1) + elif parser.engine == "python": + expected["X1"] = expected["X1"].astype(np.int64) + + tm.assert_frame_equal(result, expected) + + +def test_date_col_as_index_col(all_parsers): + data = """\ +KORD,19990127 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), header=None, prefix="X", + parse_dates=[1], index_col=1) + + index = Index([datetime(1999, 1, 27, 19, 0), datetime(1999, 1, 27, 20, 0), + datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 21, 0), + datetime(1999, 1, 27, 22, 0)], name="X1") + expected = DataFrame([ + ["KORD", " 18:56:00", 0.81, 2.81, 7.2, 0.0, 280.0], + ["KORD", " 19:56:00", 0.01, 2.21, 7.2, 0.0, 260.0], + ["KORD", " 20:56:00", -0.59, 2.21, 5.7, 0.0, 280.0], + ["KORD", " 21:18:00", -0.99, 2.01, 3.6, 0.0, 270.0], + ["KORD", " 21:56:00", -0.59, 1.71, 5.1, 0.0, 290.0], + ], columns=["X0", "X2", "X3", "X4", "X5", "X6", "X7"], index=index) + tm.assert_frame_equal(result, expected) + + +def test_multiple_date_cols_int_cast(all_parsers): + data = ("KORD,19990127, 19:00:00, 18:56:00, 0.8100\n" + "KORD,19990127, 20:00:00, 19:56:00, 0.0100\n" + "KORD,19990127, 21:00:00, 20:56:00, -0.5900\n" + "KORD,19990127, 21:00:00, 21:18:00, -0.9900\n" + "KORD,19990127, 22:00:00, 21:56:00, -0.5900\n" + "KORD,19990127, 23:00:00, 22:56:00, -0.5900") + parse_dates = {"actual": [1, 2], "nominal": [1, 3]} + parser = all_parsers + + result = parser.read_csv(StringIO(data), header=None, + date_parser=conv.parse_date_time, + parse_dates=parse_dates, prefix="X") + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), datetime(1999, 1, 27, 18, 56), + "KORD", 0.81], + [datetime(1999, 1, 27, 20, 0), datetime(1999, 1, 27, 19, 56), + "KORD", 0.01], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 20, 56), + "KORD", -0.59], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 21, 18), + "KORD", -0.99], + [datetime(1999, 1, 27, 22, 0), datetime(1999, 1, 27, 21, 56), + "KORD", -0.59], + [datetime(1999, 1, 27, 23, 0), datetime(1999, 1, 27, 22, 56), + "KORD", -0.59], + ], columns=["actual", "nominal", "X0", "X4"]) + + # Python can sometimes be flaky about how + # the aggregated columns are entered, so + # this standardizes the order. + result = result[expected.columns] + tm.assert_frame_equal(result, expected) + + +def test_multiple_date_col_timestamp_parse(all_parsers): + parser = all_parsers + data = """05/31/2012,15:30:00.029,1306.25,1,E,0,,1306.25 +05/31/2012,15:30:00.029,1306.25,8,E,0,,1306.25""" + + result = parser.read_csv(StringIO(data), parse_dates=[[0, 1]], + header=None, date_parser=Timestamp) + expected = DataFrame([ + [Timestamp("05/31/2012, 15:30:00.029"), + 1306.25, 1, "E", 0, np.nan, 1306.25], + [Timestamp("05/31/2012, 15:30:00.029"), + 1306.25, 8, "E", 0, np.nan, 1306.25] + ], columns=["0_1", 2, 3, 4, 5, 6, 7]) + tm.assert_frame_equal(result, expected) + + +def test_multiple_date_cols_with_header(all_parsers): + parser = all_parsers + data = """\ +ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000""" + + result = parser.read_csv(StringIO(data), parse_dates={"nominal": [1, 2]}) + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), "KORD", " 18:56:00", + 0.81, 2.81, 7.2, 0.0, 280.0], + [datetime(1999, 1, 27, 20, 0), "KORD", " 19:56:00", + 0.01, 2.21, 7.2, 0.0, 260.0], + [datetime(1999, 1, 27, 21, 0), "KORD", " 20:56:00", + -0.59, 2.21, 5.7, 0.0, 280.0], + [datetime(1999, 1, 27, 21, 0), "KORD", " 21:18:00", + -0.99, 2.01, 3.6, 0.0, 270.0], + [datetime(1999, 1, 27, 22, 0), "KORD", " 21:56:00", + -0.59, 1.71, 5.1, 0.0, 290.0], + [datetime(1999, 1, 27, 23, 0), "KORD", " 22:56:00", + -0.59, 1.71, 4.6, 0.0, 280.0], + ], columns=["nominal", "ID", "ActualTime", "TDew", + "TAir", "Windspeed", "Precip", "WindDir"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,parse_dates,msg", [ + ("""\ +date_NominalTime,date,NominalTime +KORD1,19990127, 19:00:00 +KORD2,19990127, 20:00:00""", [[1, 2]], ("New date column already " + "in dict date_NominalTime")), + ("""\ +ID,date,nominalTime +KORD,19990127, 19:00:00 +KORD,19990127, 20:00:00""", dict(ID=[1, 2]), "Date column ID already in dict") +]) +def test_multiple_date_col_name_collision(all_parsers, data, parse_dates, msg): + parser = all_parsers + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), parse_dates=parse_dates) + + +def test_date_parser_int_bug(all_parsers): + # see gh-3071 + parser = all_parsers + data = ("posix_timestamp,elapsed,sys,user,queries,query_time,rows," + "accountid,userid,contactid,level,silo,method\n" + "1343103150,0.062353,0,4,6,0.01690,3," + "12345,1,-1,3,invoice_InvoiceResource,search\n") + + result = parser.read_csv( + StringIO(data), index_col=0, parse_dates=[0], + date_parser=lambda x: datetime.utcfromtimestamp(int(x))) + expected = DataFrame([[0.062353, 0, 4, 6, 0.01690, 3, 12345, 1, -1, + 3, "invoice_InvoiceResource", "search"]], + columns=["elapsed", "sys", "user", "queries", + "query_time", "rows", "accountid", + "userid", "contactid", "level", + "silo", "method"], + index=Index([Timestamp("2012-07-24 04:12:30")], + name="posix_timestamp")) + tm.assert_frame_equal(result, expected) + + +def test_nat_parse(all_parsers): + # see gh-3062 + parser = all_parsers + df = DataFrame(dict({"A": np.asarray(lrange(10), dtype="float64"), + "B": pd.Timestamp("20010101")})) + df.iloc[3:6, :] = np.nan + + with tm.ensure_clean("__nat_parse_.csv") as path: + df.to_csv(path) + + result = parser.read_csv(path, index_col=0, parse_dates=["B"]) + tm.assert_frame_equal(result, df) + + +def test_csv_custom_parser(all_parsers): + data = """A,B,C +20090101,a,1,2 +20090102,b,3,4 +20090103,c,4,5 +""" + parser = all_parsers + result = parser.read_csv( + StringIO(data), + date_parser=lambda x: datetime.strptime(x, "%Y%m%d")) + expected = parser.read_csv(StringIO(data), parse_dates=True) + tm.assert_frame_equal(result, expected) + + +def test_parse_dates_implicit_first_col(all_parsers): + data = """A,B,C +20090101,a,1,2 +20090102,b,3,4 +20090103,c,4,5 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), parse_dates=True) + + expected = parser.read_csv(StringIO(data), index_col=0, + parse_dates=True) + tm.assert_frame_equal(result, expected) + + +def test_parse_dates_string(all_parsers): + data = """date,A,B,C +20090101,a,1,2 +20090102,b,3,4 +20090103,c,4,5 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col="date", + parse_dates=["date"]) + index = date_range("1/1/2009", periods=3) + index.name = "date" + + expected = DataFrame({"A": ["a", "b", "c"], "B": [1, 3, 4], + "C": [2, 4, 5]}, index=index) + tm.assert_frame_equal(result, expected) + + +# Bug in https://github.com/dateutil/dateutil/issues/217 +# has been addressed, but we just don't pass in the `yearfirst` +@pytest.mark.xfail(reason="yearfirst is not surfaced in read_*") +@pytest.mark.parametrize("parse_dates", [ + [["date", "time"]], + [[0, 1]] +]) +def test_yy_format_with_year_first(all_parsers, parse_dates): + data = """date,time,B,C +090131,0010,1,2 +090228,1020,3,4 +090331,0830,5,6 +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), index_col=0, + parse_dates=parse_dates) + index = DatetimeIndex([datetime(2009, 1, 31, 0, 10, 0), + datetime(2009, 2, 28, 10, 20, 0), + datetime(2009, 3, 31, 8, 30, 0)], + dtype=object, name="date_time") + expected = DataFrame({"B": [1, 3, 5], "C": [2, 4, 6]}, index=index) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("parse_dates", [[0, 2], ["a", "c"]]) +def test_parse_dates_column_list(all_parsers, parse_dates): + data = "a,b,c\n01/01/2010,1,15/02/2010" + parser = all_parsers + + expected = DataFrame({"a": [datetime(2010, 1, 1)], "b": [1], + "c": [datetime(2010, 2, 15)]}) + expected = expected.set_index(["a", "b"]) + + result = parser.read_csv(StringIO(data), index_col=[0, 1], + parse_dates=parse_dates, dayfirst=True) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_col", [[0, 1], [1, 0]]) +def test_multi_index_parse_dates(all_parsers, index_col): + data = """index1,index2,A,B,C +20090101,one,a,1,2 +20090101,two,b,3,4 +20090101,three,c,4,5 +20090102,one,a,1,2 +20090102,two,b,3,4 +20090102,three,c,4,5 +20090103,one,a,1,2 +20090103,two,b,3,4 +20090103,three,c,4,5 +""" + parser = all_parsers + index = MultiIndex.from_product([ + (datetime(2009, 1, 1), datetime(2009, 1, 2), + datetime(2009, 1, 3)), ("one", "two", "three")], + names=["index1", "index2"]) + + # Out of order. + if index_col == [1, 0]: + index = index.swaplevel(0, 1) + + expected = DataFrame([["a", 1, 2], ["b", 3, 4], ["c", 4, 5], + ["a", 1, 2], ["b", 3, 4], ["c", 4, 5], + ["a", 1, 2], ["b", 3, 4], ["c", 4, 5]], + columns=["A", "B", "C"], index=index) + result = parser.read_csv(StringIO(data), index_col=index_col, + parse_dates=True) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(dayfirst=True), dict(day_first=True) +]) +def test_parse_dates_custom_euro_format(all_parsers, kwargs): + parser = all_parsers + data = """foo,bar,baz +31/01/2010,1,2 +01/02/2010,1,NA +02/02/2010,1,2 +""" + if "dayfirst" in kwargs: + df = parser.read_csv(StringIO(data), names=["time", "Q", "NTU"], + date_parser=lambda d: parse_date(d, **kwargs), + header=0, index_col=0, parse_dates=True, + na_values=["NA"]) + exp_index = Index([datetime(2010, 1, 31), datetime(2010, 2, 1), + datetime(2010, 2, 2)], name="time") + expected = DataFrame({"Q": [1, 1, 1], "NTU": [2, np.nan, 2]}, + index=exp_index, columns=["Q", "NTU"]) + tm.assert_frame_equal(df, expected) + else: + msg = "got an unexpected keyword argument 'day_first'" + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), names=["time", "Q", "NTU"], + date_parser=lambda d: parse_date(d, **kwargs), + skiprows=[0], index_col=0, parse_dates=True, + na_values=["NA"]) + + +def test_parse_tz_aware(all_parsers): + # See gh-1693 + parser = all_parsers + data = "Date,x\n2012-06-13T01:39:00Z,0.5" + + result = parser.read_csv(StringIO(data), index_col=0, + parse_dates=True) + expected = DataFrame({"x": [0.5]}, index=Index([Timestamp( + "2012-06-13 01:39:00+00:00")], name="Date")) + tm.assert_frame_equal(result, expected) + assert result.index.tz is pytz.utc + + +@pytest.mark.parametrize("parse_dates,index_col", [ + ({"nominal": [1, 2]}, "nominal"), + ({"nominal": [1, 2]}, 0), + ([[1, 2]], 0), +]) +def test_multiple_date_cols_index(all_parsers, parse_dates, index_col): + parser = all_parsers + data = """ +ID,date,NominalTime,ActualTime,TDew,TAir,Windspeed,Precip,WindDir +KORD1,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD2,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD3,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD4,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD5,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD6,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), "KORD1", " 18:56:00", + 0.81, 2.81, 7.2, 0.0, 280.0], + [datetime(1999, 1, 27, 20, 0), "KORD2", " 19:56:00", + 0.01, 2.21, 7.2, 0.0, 260.0], + [datetime(1999, 1, 27, 21, 0), "KORD3", " 20:56:00", + -0.59, 2.21, 5.7, 0.0, 280.0], + [datetime(1999, 1, 27, 21, 0), "KORD4", " 21:18:00", + -0.99, 2.01, 3.6, 0.0, 270.0], + [datetime(1999, 1, 27, 22, 0), "KORD5", " 21:56:00", + -0.59, 1.71, 5.1, 0.0, 290.0], + [datetime(1999, 1, 27, 23, 0), "KORD6", " 22:56:00", + -0.59, 1.71, 4.6, 0.0, 280.0], + ], columns=["nominal", "ID", "ActualTime", "TDew", + "TAir", "Windspeed", "Precip", "WindDir"]) + expected = expected.set_index("nominal") + + if not isinstance(parse_dates, dict): + expected.index.name = "date_NominalTime" + + result = parser.read_csv(StringIO(data), parse_dates=parse_dates, + index_col=index_col) + tm.assert_frame_equal(result, expected) + + +def test_multiple_date_cols_chunked(all_parsers): + parser = all_parsers + data = """\ +ID,date,nominalTime,actualTime,A,B,C,D,E +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + + expected = DataFrame([ + [datetime(1999, 1, 27, 19, 0), "KORD", " 18:56:00", + 0.81, 2.81, 7.2, 0.0, 280.0], + [datetime(1999, 1, 27, 20, 0), "KORD", " 19:56:00", + 0.01, 2.21, 7.2, 0.0, 260.0], + [datetime(1999, 1, 27, 21, 0), "KORD", " 20:56:00", + -0.59, 2.21, 5.7, 0.0, 280.0], + [datetime(1999, 1, 27, 21, 0), "KORD", " 21:18:00", + -0.99, 2.01, 3.6, 0.0, 270.0], + [datetime(1999, 1, 27, 22, 0), "KORD", " 21:56:00", + -0.59, 1.71, 5.1, 0.0, 290.0], + [datetime(1999, 1, 27, 23, 0), "KORD", " 22:56:00", + -0.59, 1.71, 4.6, 0.0, 280.0], + ], columns=["nominal", "ID", "actualTime", "A", "B", "C", "D", "E"]) + expected = expected.set_index("nominal") + + reader = parser.read_csv(StringIO(data), parse_dates={"nominal": [1, 2]}, + index_col="nominal", chunksize=2) + chunks = list(reader) + + tm.assert_frame_equal(chunks[0], expected[:2]) + tm.assert_frame_equal(chunks[1], expected[2:4]) + tm.assert_frame_equal(chunks[2], expected[4:]) + + +def test_multiple_date_col_named_index_compat(all_parsers): + parser = all_parsers + data = """\ +ID,date,nominalTime,actualTime,A,B,C,D,E +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + + with_indices = parser.read_csv(StringIO(data), + parse_dates={"nominal": [1, 2]}, + index_col="nominal") + with_names = parser.read_csv(StringIO(data), index_col="nominal", + parse_dates={"nominal": [ + "date", "nominalTime"]}) + tm.assert_frame_equal(with_indices, with_names) + + +def test_multiple_date_col_multiple_index_compat(all_parsers): + parser = all_parsers + data = """\ +ID,date,nominalTime,actualTime,A,B,C,D,E +KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 +KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 +KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 +KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 +KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 +KORD,19990127, 23:00:00, 22:56:00, -0.5900, 1.7100, 4.6000, 0.0000, 280.0000 +""" + result = parser.read_csv(StringIO(data), index_col=["nominal", "ID"], + parse_dates={"nominal": [1, 2]}) + expected = parser.read_csv(StringIO(data), + parse_dates={"nominal": [1, 2]}) + + expected = expected.set_index(["nominal", "ID"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [dict(), dict(index_col="C")]) +def test_read_with_parse_dates_scalar_non_bool(all_parsers, kwargs): + # see gh-5636 + parser = all_parsers + msg = ("Only booleans, lists, and dictionaries " + "are accepted for the 'parse_dates' parameter") + data = """A,B,C + 1,2,2003-11-1""" + + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), parse_dates="C", **kwargs) + + +@pytest.mark.parametrize("parse_dates", [ + (1,), np.array([4, 5]), {1, 3, 3} +]) +def test_read_with_parse_dates_invalid_type(all_parsers, parse_dates): + parser = all_parsers + msg = ("Only booleans, lists, and dictionaries " + "are accepted for the 'parse_dates' parameter") + data = """A,B,C + 1,2,2003-11-1""" + + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), parse_dates=(1,)) + + +def test_parse_dates_empty_string(all_parsers): + # see gh-2263 + parser = all_parsers + data = "Date,test\n2012-01-01,1\n,2" + result = parser.read_csv(StringIO(data), parse_dates=["Date"], + na_filter=False) + + expected = DataFrame([[datetime(2012, 1, 1), 1], [pd.NaT, 2]], + columns=["Date", "test"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + ("a\n04.15.2016", dict(parse_dates=["a"]), + DataFrame([datetime(2016, 4, 15)], columns=["a"])), + ("a\n04.15.2016", dict(parse_dates=True, index_col=0), + DataFrame(index=DatetimeIndex(["2016-04-15"], name="a"))), + ("a,b\n04.15.2016,09.16.2013", dict(parse_dates=["a", "b"]), + DataFrame([[datetime(2016, 4, 15), datetime(2013, 9, 16)]], + columns=["a", "b"])), + ("a,b\n04.15.2016,09.16.2013", dict(parse_dates=True, index_col=[0, 1]), + DataFrame(index=MultiIndex.from_tuples( + [(datetime(2016, 4, 15), datetime(2013, 9, 16))], names=["a", "b"]))), +]) +def test_parse_dates_no_convert_thousands(all_parsers, data, kwargs, expected): + # see gh-14066 + parser = all_parsers + + result = parser.read_csv(StringIO(data), thousands=".", **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_parse_date_time_multi_level_column_name(all_parsers): + data = """\ +D,T,A,B +date, time,a,b +2001-01-05, 09:00:00, 0.0, 10. +2001-01-06, 00:00:00, 1.0, 11. +""" + parser = all_parsers + result = parser.read_csv(StringIO(data), header=[0, 1], + parse_dates={"date_time": [0, 1]}, + date_parser=conv.parse_date_time) + + expected_data = [[datetime(2001, 1, 5, 9, 0, 0), 0., 10.], + [datetime(2001, 1, 6, 0, 0, 0), 1., 11.]] + expected = DataFrame(expected_data, + columns=["date_time", ("A", "a"), ("B", "b")]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + ("""\ +date,time,a,b +2001-01-05, 10:00:00, 0.0, 10. +2001-01-05, 00:00:00, 1., 11. +""", dict(header=0, parse_dates={"date_time": [0, 1]}), + DataFrame([[datetime(2001, 1, 5, 10, 0, 0), 0.0, 10], + [datetime(2001, 1, 5, 0, 0, 0), 1.0, 11.0]], + columns=["date_time", "a", "b"])), + (("KORD,19990127, 19:00:00, 18:56:00, 0.8100\n" + "KORD,19990127, 20:00:00, 19:56:00, 0.0100\n" + "KORD,19990127, 21:00:00, 20:56:00, -0.5900\n" + "KORD,19990127, 21:00:00, 21:18:00, -0.9900\n" + "KORD,19990127, 22:00:00, 21:56:00, -0.5900\n" + "KORD,19990127, 23:00:00, 22:56:00, -0.5900"), + dict(header=None, parse_dates={"actual": [1, 2], "nominal": [1, 3]}), + DataFrame([ + [datetime(1999, 1, 27, 19, 0), datetime(1999, 1, 27, 18, 56), + "KORD", 0.81], + [datetime(1999, 1, 27, 20, 0), datetime(1999, 1, 27, 19, 56), + "KORD", 0.01], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 20, 56), + "KORD", -0.59], + [datetime(1999, 1, 27, 21, 0), datetime(1999, 1, 27, 21, 18), + "KORD", -0.99], + [datetime(1999, 1, 27, 22, 0), datetime(1999, 1, 27, 21, 56), + "KORD", -0.59], + [datetime(1999, 1, 27, 23, 0), datetime(1999, 1, 27, 22, 56), + "KORD", -0.59]], columns=["actual", "nominal", 0, 4])), +]) +def test_parse_date_time(all_parsers, data, kwargs, expected): + parser = all_parsers + result = parser.read_csv(StringIO(data), date_parser=conv.parse_date_time, + **kwargs) + + # Python can sometimes be flaky about how + # the aggregated columns are entered, so + # this standardizes the order. + result = result[expected.columns] + tm.assert_frame_equal(result, expected) + + +def test_parse_date_fields(all_parsers): + parser = all_parsers + data = ("year,month,day,a\n2001,01,10,10.\n" + "2001,02,1,11.") + result = parser.read_csv(StringIO(data), header=0, + parse_dates={"ymd": [0, 1, 2]}, + date_parser=conv.parse_date_fields) + + expected = DataFrame([[datetime(2001, 1, 10), 10.], + [datetime(2001, 2, 1), 11.]], columns=["ymd", "a"]) + tm.assert_frame_equal(result, expected) + + +def test_parse_date_all_fields(all_parsers): + parser = all_parsers + data = """\ +year,month,day,hour,minute,second,a,b +2001,01,05,10,00,0,0.0,10. +2001,01,5,10,0,00,1.,11. +""" + result = parser.read_csv(StringIO(data), header=0, + date_parser=conv.parse_all_fields, + parse_dates={"ymdHMS": [0, 1, 2, 3, 4, 5]}) + expected = DataFrame([[datetime(2001, 1, 5, 10, 0, 0), 0.0, 10.0], + [datetime(2001, 1, 5, 10, 0, 0), 1.0, 11.0]], + columns=["ymdHMS", "a", "b"]) + tm.assert_frame_equal(result, expected) + + +def test_datetime_fractional_seconds(all_parsers): + parser = all_parsers + data = """\ +year,month,day,hour,minute,second,a,b +2001,01,05,10,00,0.123456,0.0,10. +2001,01,5,10,0,0.500000,1.,11. +""" + result = parser.read_csv(StringIO(data), header=0, + date_parser=conv.parse_all_fields, + parse_dates={"ymdHMS": [0, 1, 2, 3, 4, 5]}) + expected = DataFrame([[datetime(2001, 1, 5, 10, 0, 0, + microsecond=123456), 0.0, 10.0], + [datetime(2001, 1, 5, 10, 0, 0, + microsecond=500000), 1.0, 11.0]], + columns=["ymdHMS", "a", "b"]) + tm.assert_frame_equal(result, expected) + + +def test_generic(all_parsers): + parser = all_parsers + data = "year,month,day,a\n2001,01,10,10.\n2001,02,1,11." + + result = parser.read_csv(StringIO(data), header=0, + parse_dates={"ym": [0, 1]}, + date_parser=lambda y, m: date(year=int(y), + month=int(m), + day=1)) + expected = DataFrame([[date(2001, 1, 1), 10, 10.], + [date(2001, 2, 1), 1, 11.]], + columns=["ym", "day", "a"]) + tm.assert_frame_equal(result, expected) + + +def test_date_parser_resolution_if_not_ns(all_parsers): + # see gh-10245 + parser = all_parsers + data = """\ +date,time,prn,rxstatus +2013-11-03,19:00:00,126,00E80000 +2013-11-03,19:00:00,23,00E80000 +2013-11-03,19:00:00,13,00E80000 +""" + + def date_parser(dt, time): + return np_array_datetime64_compat(dt + "T" + time + "Z", + dtype="datetime64[s]") + + result = parser.read_csv(StringIO(data), date_parser=date_parser, + parse_dates={"datetime": ["date", "time"]}, + index_col=["datetime", "prn"]) + + datetimes = np_array_datetime64_compat(["2013-11-03T19:00:00Z"] * 3, + dtype="datetime64[s]") + expected = DataFrame(data={"rxstatus": ["00E80000"] * 3}, + index=MultiIndex.from_tuples( + [(datetimes[0], 126), (datetimes[1], 23), + (datetimes[2], 13)], names=["datetime", "prn"])) + tm.assert_frame_equal(result, expected) + + +def test_parse_date_column_with_empty_string(all_parsers): + # see gh-6428 + parser = all_parsers + data = "case,opdate\n7,10/18/2006\n7,10/18/2008\n621, " + result = parser.read_csv(StringIO(data), parse_dates=["opdate"]) + + expected_data = [[7, "10/18/2006"], + [7, "10/18/2008"], + [621, " "]] + expected = DataFrame(expected_data, columns=["case", "opdate"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,expected", [ + ("a\n135217135789158401\n1352171357E+5", + DataFrame({"a": [135217135789158401, + 135217135700000]}, dtype="float64")), + ("a\n99999999999\n123456789012345\n1234E+0", + DataFrame({"a": [99999999999, + 123456789012345, + 1234]}, dtype="float64")) +]) +@pytest.mark.parametrize("parse_dates", [True, False]) +def test_parse_date_float(all_parsers, data, expected, parse_dates): + # see gh-2697 + # + # Date parsing should fail, so we leave the data untouched + # (i.e. float precision should remain unchanged). + parser = all_parsers + + result = parser.read_csv(StringIO(data), parse_dates=parse_dates) + tm.assert_frame_equal(result, expected) + + +def test_parse_timezone(all_parsers): + # see gh-22256 + parser = all_parsers + data = """dt,val + 2018-01-04 09:01:00+09:00,23350 + 2018-01-04 09:02:00+09:00,23400 + 2018-01-04 09:03:00+09:00,23400 + 2018-01-04 09:04:00+09:00,23400 + 2018-01-04 09:05:00+09:00,23400""" + result = parser.read_csv(StringIO(data), parse_dates=["dt"]) + + dti = pd.date_range(start="2018-01-04 09:01:00", + end="2018-01-04 09:05:00", freq="1min", + tz=pytz.FixedOffset(540)) + expected_data = {"dt": dti, "val": [23350, 23400, 23400, 23400, 23400]} + + expected = DataFrame(expected_data) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_parsers.py b/pandas/tests/io/parser/test_parsers.py deleted file mode 100644 index 2ae557a7d57db..0000000000000 --- a/pandas/tests/io/parser/test_parsers.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- - -import os - -import pandas.util.testing as tm - -from pandas import read_csv, read_table -from pandas.core.common import AbstractMethodError - -from .common import ParserTests -from .header import HeaderTests -from .comment import CommentTests -from .dialect import DialectTests -from .quoting import QuotingTests -from .usecols import UsecolsTests -from .skiprows import SkipRowsTests -from .index_col import IndexColTests -from .na_values import NAvaluesTests -from .converters import ConverterTests -from .c_parser_only import CParserTests -from .parse_dates import ParseDatesTests -from .compression import CompressionTests -from .multithread import MultithreadTests -from .python_parser_only import PythonParserTests -from .dtypes import DtypeTests - - -class BaseParser(CommentTests, CompressionTests, - ConverterTests, DialectTests, - HeaderTests, IndexColTests, - MultithreadTests, NAvaluesTests, - ParseDatesTests, ParserTests, - SkipRowsTests, UsecolsTests, - QuotingTests, DtypeTests): - - def read_csv(self, *args, **kwargs): - raise NotImplementedError - - def read_table(self, *args, **kwargs): - raise NotImplementedError - - def float_precision_choices(self): - raise AbstractMethodError(self) - - def setUp(self): - self.dirpath = tm.get_data_path() - self.csv1 = os.path.join(self.dirpath, 'test1.csv') - self.csv2 = os.path.join(self.dirpath, 'test2.csv') - self.xls1 = os.path.join(self.dirpath, 'test.xls') - self.csv_shiftjs = os.path.join(self.dirpath, 'sauron.SHIFT_JIS.csv') - - -class TestCParserHighMemory(BaseParser, CParserTests, tm.TestCase): - engine = 'c' - low_memory = False - float_precision_choices = [None, 'high', 'round_trip'] - - def read_csv(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - kwds['low_memory'] = self.low_memory - return read_csv(*args, **kwds) - - def read_table(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - kwds['low_memory'] = self.low_memory - return read_table(*args, **kwds) - - -class TestCParserLowMemory(BaseParser, CParserTests, tm.TestCase): - engine = 'c' - low_memory = True - float_precision_choices = [None, 'high', 'round_trip'] - - def read_csv(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - kwds['low_memory'] = self.low_memory - return read_csv(*args, **kwds) - - def read_table(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - kwds['low_memory'] = True - return read_table(*args, **kwds) - - -class TestPythonParser(BaseParser, PythonParserTests, tm.TestCase): - engine = 'python' - float_precision_choices = [None] - - def read_csv(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - return read_csv(*args, **kwds) - - def read_table(self, *args, **kwds): - kwds = kwds.copy() - kwds['engine'] = self.engine - return read_table(*args, **kwds) diff --git a/pandas/tests/io/parser/test_python_parser_only.py b/pandas/tests/io/parser/test_python_parser_only.py new file mode 100644 index 0000000000000..c2edff258f1b5 --- /dev/null +++ b/pandas/tests/io/parser/test_python_parser_only.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- + +""" +Tests that apply specifically to the Python parser. Unless specifically +stated as a Python-specific issue, the goal is to eventually move as many of +these tests out of this module as soon as the C parser can accept further +arguments when parsing. +""" + +import csv + +import pytest + +import pandas.compat as compat +from pandas.compat import BytesIO, StringIO, u +from pandas.errors import ParserError + +from pandas import DataFrame, Index, MultiIndex +import pandas.util.testing as tm + + +def test_default_separator(python_parser_only): + # see gh-17333 + # + # csv.Sniffer in Python treats "o" as separator. + data = "aob\n1o2\n3o4" + parser = python_parser_only + expected = DataFrame({"a": [1, 3], "b": [2, 4]}) + + result = parser.read_csv(StringIO(data), sep=None) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("skipfooter", ["foo", 1.5, True]) +def test_invalid_skipfooter_non_int(python_parser_only, skipfooter): + # see gh-15925 (comment) + data = "a\n1\n2" + parser = python_parser_only + msg = "skipfooter must be an integer" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), skipfooter=skipfooter) + + +def test_invalid_skipfooter_negative(python_parser_only): + # see gh-15925 (comment) + data = "a\n1\n2" + parser = python_parser_only + msg = "skipfooter cannot be negative" + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), skipfooter=-1) + + +@pytest.mark.parametrize("kwargs", [ + dict(sep=None), + dict(delimiter="|") +]) +def test_sniff_delimiter(python_parser_only, kwargs): + data = """index|A|B|C +foo|1|2|3 +bar|4|5|6 +baz|7|8|9 +""" + parser = python_parser_only + result = parser.read_csv(StringIO(data), index_col=0, **kwargs) + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=["A", "B", "C"], + index=Index(["foo", "bar", "baz"], name="index")) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("encoding", [None, "utf-8"]) +def test_sniff_delimiter_encoding(python_parser_only, encoding): + parser = python_parser_only + data = """ignore this +ignore this too +index|A|B|C +foo|1|2|3 +bar|4|5|6 +baz|7|8|9 +""" + + if encoding is not None: + data = u(data).encode(encoding) + data = BytesIO(data) + + if compat.PY3: + from io import TextIOWrapper + data = TextIOWrapper(data, encoding=encoding) + else: + data = StringIO(data) + + result = parser.read_csv(data, index_col=0, sep=None, + skiprows=2, encoding=encoding) + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=["A", "B", "C"], + index=Index(["foo", "bar", "baz"], name="index")) + tm.assert_frame_equal(result, expected) + + +def test_single_line(python_parser_only): + # see gh-6607: sniff separator + parser = python_parser_only + result = parser.read_csv(StringIO("1,2"), names=["a", "b"], + header=None, sep=None) + + expected = DataFrame({"a": [1], "b": [2]}) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [dict(skipfooter=2), dict(nrows=3)]) +def test_skipfooter(python_parser_only, kwargs): + # see gh-6607 + data = """A,B,C +1,2,3 +4,5,6 +7,8,9 +want to skip this +also also skip this +""" + parser = python_parser_only + result = parser.read_csv(StringIO(data), **kwargs) + + expected = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=["A", "B", "C"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("compression,klass", [ + ("gzip", "GzipFile"), + ("bz2", "BZ2File"), +]) +def test_decompression_regex_sep(python_parser_only, csv1, compression, klass): + # see gh-6607 + parser = python_parser_only + + with open(csv1, "rb") as f: + data = f.read() + + data = data.replace(b",", b"::") + expected = parser.read_csv(csv1) + + module = pytest.importorskip(compression) + klass = getattr(module, klass) + + with tm.ensure_clean() as path: + tmp = klass(path, mode="wb") + tmp.write(data) + tmp.close() + + result = parser.read_csv(path, sep="::", + compression=compression) + tm.assert_frame_equal(result, expected) + + +def test_read_csv_buglet_4x_multi_index(python_parser_only): + # see gh-6607 + data = """ A B C D E +one two three four +a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 +a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 +x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" + parser = python_parser_only + + expected = DataFrame([[-0.5109, -2.3358, -0.4645, 0.05076, 0.3640], + [0.4473, 1.4152, 0.2834, 1.00661, 0.1744], + [-0.6662, -0.5243, -0.3580, 0.89145, 2.5838]], + columns=["A", "B", "C", "D", "E"], + index=MultiIndex.from_tuples([ + ("a", "b", 10.0032, 5), + ("a", "q", 20, 4), + ("x", "q", 30, 3), + ], names=["one", "two", "three", "four"])) + result = parser.read_csv(StringIO(data), sep=r"\s+") + tm.assert_frame_equal(result, expected) + + +def test_read_csv_buglet_4x_multi_index2(python_parser_only): + # see gh-6893 + data = " A B C\na b c\n1 3 7 0 3 6\n3 1 4 1 5 9" + parser = python_parser_only + + expected = DataFrame.from_records( + [(1, 3, 7, 0, 3, 6), (3, 1, 4, 1, 5, 9)], + columns=list("abcABC"), index=list("abc")) + result = parser.read_csv(StringIO(data), sep=r"\s+") + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("add_footer", [True, False]) +def test_skipfooter_with_decimal(python_parser_only, add_footer): + # see gh-6971 + data = "1#2\n3#4" + parser = python_parser_only + expected = DataFrame({"a": [1.2, 3.4]}) + + if add_footer: + # The stray footer line should not mess with the + # casting of the first two lines if we skip it. + kwargs = dict(skipfooter=1) + data += "\nFooter" + else: + kwargs = dict() + + result = parser.read_csv(StringIO(data), names=["a"], + decimal="#", **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("sep", ["::", "#####", "!!!", "123", "#1!c5", + "%!c!d", "@@#4:2", "_!pd#_"]) +@pytest.mark.parametrize("encoding", ["utf-16", "utf-16-be", "utf-16-le", + "utf-32", "cp037"]) +def test_encoding_non_utf8_multichar_sep(python_parser_only, sep, encoding): + # see gh-3404 + expected = DataFrame({"a": [1], "b": [2]}) + parser = python_parser_only + + data = "1" + sep + "2" + encoded_data = data.encode(encoding) + + result = parser.read_csv(BytesIO(encoded_data), sep=sep, + names=["a", "b"], encoding=encoding) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("quoting", [csv.QUOTE_MINIMAL, csv.QUOTE_NONE]) +def test_multi_char_sep_quotes(python_parser_only, quoting): + # see gh-13374 + kwargs = dict(sep=",,") + parser = python_parser_only + + data = 'a,,b\n1,,a\n2,,"2,,b"' + msg = "ignored when a multi-char delimiter is used" + + def fail_read(): + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), quoting=quoting, **kwargs) + + if quoting == csv.QUOTE_NONE: + # We expect no match, so there should be an assertion + # error out of the inner context manager. + with pytest.raises(AssertionError): + fail_read() + else: + fail_read() + + +def test_none_delimiter(python_parser_only, capsys): + # see gh-13374 and gh-17465 + parser = python_parser_only + data = "a,b,c\n0,1,2\n3,4,5,6\n7,8,9" + expected = DataFrame({"a": [0, 7], "b": [1, 8], "c": [2, 9]}) + + # We expect the third line in the data to be + # skipped because it is malformed, but we do + # not expect any errors to occur. + result = parser.read_csv(StringIO(data), header=0, + sep=None, warn_bad_lines=True, + error_bad_lines=False) + tm.assert_frame_equal(result, expected) + + captured = capsys.readouterr() + assert "Skipping line 3" in captured.err + + +@pytest.mark.parametrize("data", [ + 'a\n1\n"b"a', 'a,b,c\ncat,foo,bar\ndog,foo,"baz']) +@pytest.mark.parametrize("skipfooter", [0, 1]) +def test_skipfooter_bad_row(python_parser_only, data, skipfooter): + # see gh-13879 and gh-15910 + msg = "parsing errors in the skipped footer rows" + parser = python_parser_only + + def fail_read(): + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), skipfooter=skipfooter) + + if skipfooter: + fail_read() + else: + # We expect no match, so there should be an assertion + # error out of the inner context manager. + with pytest.raises(AssertionError): + fail_read() + + +def test_malformed_skipfooter(python_parser_only): + parser = python_parser_only + data = """ignore +A,B,C +1,2,3 # comment +1,2,3,4,5 +2,3,4 +footer +""" + msg = "Expected 3 fields in line 4, saw 5" + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data), header=1, + comment="#", skipfooter=1) diff --git a/pandas/tests/io/parser/test_quoting.py b/pandas/tests/io/parser/test_quoting.py new file mode 100644 index 0000000000000..b33a1b8448bea --- /dev/null +++ b/pandas/tests/io/parser/test_quoting.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- + +""" +Tests that quoting specifications are properly handled +during parsing for all of the parsers defined in parsers.py +""" + +import csv + +import pytest + +from pandas.compat import PY2, StringIO, u +from pandas.errors import ParserError + +from pandas import DataFrame +import pandas.util.testing as tm + + +@pytest.mark.parametrize("kwargs,msg", [ + (dict(quotechar="foo"), '"quotechar" must be a(n)? 1-character string'), + (dict(quotechar=None, quoting=csv.QUOTE_MINIMAL), + "quotechar must be set if quoting enabled"), + (dict(quotechar=2), '"quotechar" must be string, not int') +]) +def test_bad_quote_char(all_parsers, kwargs, msg): + data = "1,2,3" + parser = all_parsers + + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + + +@pytest.mark.parametrize("quoting,msg", [ + ("foo", '"quoting" must be an integer'), + (5, 'bad "quoting" value'), # quoting must be in the range [0, 3] +]) +def test_bad_quoting(all_parsers, quoting, msg): + data = "1,2,3" + parser = all_parsers + + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), quoting=quoting) + + +def test_quote_char_basic(all_parsers): + parser = all_parsers + data = 'a,b,c\n1,2,"cat"' + expected = DataFrame([[1, 2, "cat"]], + columns=["a", "b", "c"]) + + result = parser.read_csv(StringIO(data), quotechar='"') + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("quote_char", ["~", "*", "%", "$", "@", "P"]) +def test_quote_char_various(all_parsers, quote_char): + parser = all_parsers + expected = DataFrame([[1, 2, "cat"]], + columns=["a", "b", "c"]) + + data = 'a,b,c\n1,2,"cat"' + new_data = data.replace('"', quote_char) + + result = parser.read_csv(StringIO(new_data), quotechar=quote_char) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("quoting", [csv.QUOTE_MINIMAL, csv.QUOTE_NONE]) +@pytest.mark.parametrize("quote_char", ["", None]) +def test_null_quote_char(all_parsers, quoting, quote_char): + kwargs = dict(quotechar=quote_char, quoting=quoting) + data = "a,b,c\n1,2,3" + parser = all_parsers + + if quoting != csv.QUOTE_NONE: + # Sanity checking. + msg = "quotechar must be set if quoting enabled" + + with pytest.raises(TypeError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + else: + expected = DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,exp_data", [ + (dict(), [[1, 2, "foo"]]), # Test default. + + # QUOTE_MINIMAL only applies to CSV writing, so no effect on reading. + (dict(quotechar='"', quoting=csv.QUOTE_MINIMAL), [[1, 2, "foo"]]), + + # QUOTE_MINIMAL only applies to CSV writing, so no effect on reading. + (dict(quotechar='"', quoting=csv.QUOTE_ALL), [[1, 2, "foo"]]), + + # QUOTE_NONE tells the reader to do no special handling + # of quote characters and leave them alone. + (dict(quotechar='"', quoting=csv.QUOTE_NONE), [[1, 2, '"foo"']]), + + # QUOTE_NONNUMERIC tells the reader to cast + # all non-quoted fields to float + (dict(quotechar='"', quoting=csv.QUOTE_NONNUMERIC), [[1.0, 2.0, "foo"]]) +]) +def test_quoting_various(all_parsers, kwargs, exp_data): + data = '1,2,"foo"' + parser = all_parsers + columns = ["a", "b", "c"] + + result = parser.read_csv(StringIO(data), names=columns, **kwargs) + expected = DataFrame(exp_data, columns=columns) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("doublequote,exp_data", [ + (True, [[3, '4 " 5']]), + (False, [[3, '4 " 5"']]), +]) +def test_double_quote(all_parsers, doublequote, exp_data): + parser = all_parsers + data = 'a,b\n3,"4 "" 5"' + + result = parser.read_csv(StringIO(data), quotechar='"', + doublequote=doublequote) + expected = DataFrame(exp_data, columns=["a", "b"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("quotechar", [ + u('"'), + pytest.param(u('\u0001'), marks=pytest.mark.skipif( + PY2, reason="Python 2.x does not handle unicode well."))]) +def test_quotechar_unicode(all_parsers, quotechar): + # see gh-14477 + data = "a\n1" + parser = all_parsers + expected = DataFrame({"a": [1]}) + + result = parser.read_csv(StringIO(data), quotechar=quotechar) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("balanced", [True, False]) +def test_unbalanced_quoting(all_parsers, balanced): + # see gh-22789. + parser = all_parsers + data = "a,b,c\n1,2,\"3" + + if balanced: + # Re-balance the quoting and read in without errors. + expected = DataFrame([[1, 2, 3]], columns=["a", "b", "c"]) + result = parser.read_csv(StringIO(data + '"')) + tm.assert_frame_equal(result, expected) + else: + msg = ("EOF inside string starting at row 1" if parser.engine == "c" + else "unexpected end of data") + + with pytest.raises(ParserError, match=msg): + parser.read_csv(StringIO(data)) diff --git a/pandas/tests/io/parser/test_read_fwf.py b/pandas/tests/io/parser/test_read_fwf.py index dccae06afe4d1..172bbe0bad4c7 100644 --- a/pandas/tests/io/parser/test_read_fwf.py +++ b/pandas/tests/io/parser/test_read_fwf.py @@ -8,83 +8,170 @@ from datetime import datetime -import pytest import numpy as np +import pytest + +import pandas.compat as compat +from pandas.compat import BytesIO, StringIO + import pandas as pd +from pandas import DataFrame, DatetimeIndex import pandas.util.testing as tm -from pandas import DataFrame -from pandas import compat -from pandas.compat import StringIO, BytesIO -from pandas.io.parsers import read_csv, read_fwf, EmptyDataError +from pandas.io.parsers import EmptyDataError, read_csv, read_fwf -class TestFwfParsing(tm.TestCase): - - def test_fwf(self): - data_expected = """\ -2011,58,360.242940,149.910199,11950.7 -2011,59,444.953632,166.985655,11788.4 -2011,60,364.136849,183.628767,11806.2 -2011,61,413.836124,184.375703,11916.8 -2011,62,502.953953,173.237159,12468.3 +def test_basic(): + data = """\ +A B C D +201158 360.242940 149.910199 11950.7 +201159 444.953632 166.985655 11788.4 +201160 364.136849 183.628767 11806.2 +201161 413.836124 184.375703 11916.8 +201162 502.953953 173.237159 12468.3 """ - expected = read_csv(StringIO(data_expected), - engine='python', header=None) - - data1 = """\ + result = read_fwf(StringIO(data)) + expected = DataFrame([[201158, 360.242940, 149.910199, 11950.7], + [201159, 444.953632, 166.985655, 11788.4], + [201160, 364.136849, 183.628767, 11806.2], + [201161, 413.836124, 184.375703, 11916.8], + [201162, 502.953953, 173.237159, 12468.3]], + columns=["A", "B", "C", "D"]) + tm.assert_frame_equal(result, expected) + + +def test_colspecs(): + data = """\ +A B C D E 201158 360.242940 149.910199 11950.7 201159 444.953632 166.985655 11788.4 201160 364.136849 183.628767 11806.2 201161 413.836124 184.375703 11916.8 201162 502.953953 173.237159 12468.3 """ - colspecs = [(0, 4), (4, 8), (8, 20), (21, 33), (34, 43)] - df = read_fwf(StringIO(data1), colspecs=colspecs, header=None) - tm.assert_frame_equal(df, expected) + colspecs = [(0, 4), (4, 8), (8, 20), (21, 33), (34, 43)] + result = read_fwf(StringIO(data), colspecs=colspecs) + + expected = DataFrame([[2011, 58, 360.242940, 149.910199, 11950.7], + [2011, 59, 444.953632, 166.985655, 11788.4], + [2011, 60, 364.136849, 183.628767, 11806.2], + [2011, 61, 413.836124, 184.375703, 11916.8], + [2011, 62, 502.953953, 173.237159, 12468.3]], + columns=["A", "B", "C", "D", "E"]) + tm.assert_frame_equal(result, expected) - data2 = """\ + +def test_widths(): + data = """\ +A B C D E 2011 58 360.242940 149.910199 11950.7 2011 59 444.953632 166.985655 11788.4 2011 60 364.136849 183.628767 11806.2 2011 61 413.836124 184.375703 11916.8 2011 62 502.953953 173.237159 12468.3 """ - df = read_fwf(StringIO(data2), widths=[5, 5, 13, 13, 7], header=None) - tm.assert_frame_equal(df, expected) - - # From Thomas Kluyver: apparently some non-space filler characters can - # be seen, this is supported by specifying the 'delimiter' character: - # http://publib.boulder.ibm.com/infocenter/dmndhelp/v6r1mx/index.jsp?topic=/com.ibm.wbit.612.help.config.doc/topics/rfixwidth.html - data3 = """\ + result = read_fwf(StringIO(data), widths=[5, 5, 13, 13, 7]) + + expected = DataFrame([[2011, 58, 360.242940, 149.910199, 11950.7], + [2011, 59, 444.953632, 166.985655, 11788.4], + [2011, 60, 364.136849, 183.628767, 11806.2], + [2011, 61, 413.836124, 184.375703, 11916.8], + [2011, 62, 502.953953, 173.237159, 12468.3]], + columns=["A", "B", "C", "D", "E"]) + tm.assert_frame_equal(result, expected) + + +def test_non_space_filler(): + # From Thomas Kluyver: + # + # Apparently, some non-space filler characters can be seen, this is + # supported by specifying the 'delimiter' character: + # + # http://publib.boulder.ibm.com/infocenter/dmndhelp/v6r1mx/index.jsp?topic=/com.ibm.wbit.612.help.config.doc/topics/rfixwidth.html + data = """\ +A~~~~B~~~~C~~~~~~~~~~~~D~~~~~~~~~~~~E 201158~~~~360.242940~~~149.910199~~~11950.7 201159~~~~444.953632~~~166.985655~~~11788.4 201160~~~~364.136849~~~183.628767~~~11806.2 201161~~~~413.836124~~~184.375703~~~11916.8 201162~~~~502.953953~~~173.237159~~~12468.3 """ - df = read_fwf( - StringIO(data3), colspecs=colspecs, delimiter='~', header=None) - tm.assert_frame_equal(df, expected) + colspecs = [(0, 4), (4, 8), (8, 20), (21, 33), (34, 43)] + result = read_fwf(StringIO(data), colspecs=colspecs, delimiter="~") - with tm.assertRaisesRegexp(ValueError, "must specify only one of"): - read_fwf(StringIO(data3), colspecs=colspecs, widths=[6, 10, 10, 7]) + expected = DataFrame([[2011, 58, 360.242940, 149.910199, 11950.7], + [2011, 59, 444.953632, 166.985655, 11788.4], + [2011, 60, 364.136849, 183.628767, 11806.2], + [2011, 61, 413.836124, 184.375703, 11916.8], + [2011, 62, 502.953953, 173.237159, 12468.3]], + columns=["A", "B", "C", "D", "E"]) + tm.assert_frame_equal(result, expected) + + +def test_over_specified(): + data = """\ +A B C D E +201158 360.242940 149.910199 11950.7 +201159 444.953632 166.985655 11788.4 +201160 364.136849 183.628767 11806.2 +201161 413.836124 184.375703 11916.8 +201162 502.953953 173.237159 12468.3 +""" + colspecs = [(0, 4), (4, 8), (8, 20), (21, 33), (34, 43)] - with tm.assertRaisesRegexp(ValueError, "Must specify either"): - read_fwf(StringIO(data3), colspecs=None, widths=None) + with pytest.raises(ValueError, match="must specify only one of"): + read_fwf(StringIO(data), colspecs=colspecs, widths=[6, 10, 10, 7]) - def test_BytesIO_input(self): - if not compat.PY3: - pytest.skip( - "Bytes-related test - only needs to work on Python 3") - result = read_fwf(BytesIO("שלום\nשלום".encode('utf8')), widths=[ - 2, 2], encoding='utf8') - expected = DataFrame([["של", "ום"]], columns=["של", "ום"]) - tm.assert_frame_equal(result, expected) +def test_under_specified(): + data = """\ +A B C D E +201158 360.242940 149.910199 11950.7 +201159 444.953632 166.985655 11788.4 +201160 364.136849 183.628767 11806.2 +201161 413.836124 184.375703 11916.8 +201162 502.953953 173.237159 12468.3 +""" + with pytest.raises(ValueError, match="Must specify either"): + read_fwf(StringIO(data), colspecs=None, widths=None) + - def test_fwf_colspecs_is_list_or_tuple(self): - data = """index,A,B,C,D +def test_read_csv_compat(): + csv_data = """\ +A,B,C,D,E +2011,58,360.242940,149.910199,11950.7 +2011,59,444.953632,166.985655,11788.4 +2011,60,364.136849,183.628767,11806.2 +2011,61,413.836124,184.375703,11916.8 +2011,62,502.953953,173.237159,12468.3 +""" + expected = read_csv(StringIO(csv_data), engine="python") + + fwf_data = """\ +A B C D E +201158 360.242940 149.910199 11950.7 +201159 444.953632 166.985655 11788.4 +201160 364.136849 183.628767 11806.2 +201161 413.836124 184.375703 11916.8 +201162 502.953953 173.237159 12468.3 +""" + colspecs = [(0, 4), (4, 8), (8, 20), (21, 33), (34, 43)] + result = read_fwf(StringIO(fwf_data), colspecs=colspecs) + tm.assert_frame_equal(result, expected) + + +def test_bytes_io_input(): + if not compat.PY3: + pytest.skip("Bytes-related test - only needs to work on Python 3") + + result = read_fwf(BytesIO("שלום\nשלום".encode('utf8')), + widths=[2, 2], encoding="utf8") + expected = DataFrame([["של", "ום"]], columns=["של", "ום"]) + tm.assert_frame_equal(result, expected) + + +def test_fwf_colspecs_is_list_or_tuple(): + data = """index,A,B,C,D foo,2,3,4,5 bar,7,8,9,10 baz,12,13,14,15 @@ -93,14 +180,14 @@ def test_fwf_colspecs_is_list_or_tuple(self): bar2,12,13,14,15 """ - with tm.assertRaisesRegexp(TypeError, - 'column specifications must be a list or ' - 'tuple.+'): - pd.io.parsers.FixedWidthReader(StringIO(data), - {'a': 1}, ',', '#') + msg = "column specifications must be a list or tuple.+" + + with pytest.raises(TypeError, match=msg): + read_fwf(StringIO(data), colspecs={"a": 1}, delimiter=",") + - def test_fwf_colspecs_is_list_or_tuple_of_two_element_tuples(self): - data = """index,A,B,C,D +def test_fwf_colspecs_is_list_or_tuple_of_two_element_tuples(): + data = """index,A,B,C,D foo,2,3,4,5 bar,7,8,9,10 baz,12,13,14,15 @@ -109,146 +196,151 @@ def test_fwf_colspecs_is_list_or_tuple_of_two_element_tuples(self): bar2,12,13,14,15 """ - with tm.assertRaisesRegexp(TypeError, - 'Each column specification must be.+'): - read_fwf(StringIO(data), [('a', 1)]) + msg = "Each column specification must be.+" - def test_fwf_colspecs_None(self): - # GH 7079 - data = """\ + with pytest.raises(TypeError, match=msg): + read_fwf(StringIO(data), [("a", 1)]) + + +@pytest.mark.parametrize("colspecs,exp_data", [ + ([(0, 3), (3, None)], [[123, 456], [456, 789]]), + ([(None, 3), (3, 6)], [[123, 456], [456, 789]]), + ([(0, None), (3, None)], [[123456, 456], [456789, 789]]), + ([(None, None), (3, 6)], [[123456, 456], [456789, 789]]), +]) +def test_fwf_colspecs_none(colspecs, exp_data): + # see gh-7079 + data = """\ 123456 456789 """ - colspecs = [(0, 3), (3, None)] - result = read_fwf(StringIO(data), colspecs=colspecs, header=None) - expected = DataFrame([[123, 456], [456, 789]]) - tm.assert_frame_equal(result, expected) + expected = DataFrame(exp_data) - colspecs = [(None, 3), (3, 6)] - result = read_fwf(StringIO(data), colspecs=colspecs, header=None) - expected = DataFrame([[123, 456], [456, 789]]) - tm.assert_frame_equal(result, expected) + result = read_fwf(StringIO(data), colspecs=colspecs, header=None) + tm.assert_frame_equal(result, expected) - colspecs = [(0, None), (3, None)] - result = read_fwf(StringIO(data), colspecs=colspecs, header=None) - expected = DataFrame([[123456, 456], [456789, 789]]) - tm.assert_frame_equal(result, expected) - colspecs = [(None, None), (3, 6)] - result = read_fwf(StringIO(data), colspecs=colspecs, header=None) - expected = DataFrame([[123456, 456], [456789, 789]]) - tm.assert_frame_equal(result, expected) +@pytest.mark.parametrize("infer_nrows,exp_data", [ + # infer_nrows --> colspec == [(2, 3), (5, 6)] + (1, [[1, 2], [3, 8]]), - def test_fwf_regression(self): - # GH 3594 - # turns out 'T060' is parsable as a datetime slice! - - tzlist = [1, 10, 20, 30, 60, 80, 100] - ntz = len(tzlist) - tcolspecs = [16] + [8] * ntz - tcolnames = ['SST'] + ["T%03d" % z for z in tzlist[1:]] - data = """ 2009164202000 9.5403 9.4105 8.6571 7.8372 6.0612 5.8843 5.5192 - 2009164203000 9.5435 9.2010 8.6167 7.8176 6.0804 5.8728 5.4869 - 2009164204000 9.5873 9.1326 8.4694 7.5889 6.0422 5.8526 5.4657 - 2009164205000 9.5810 9.0896 8.4009 7.4652 6.0322 5.8189 5.4379 - 2009164210000 9.6034 9.0897 8.3822 7.4905 6.0908 5.7904 5.4039 + # infer_nrows > number of rows + (10, [[1, 2], [123, 98]]), +]) +def test_fwf_colspecs_infer_nrows(infer_nrows, exp_data): + # see gh-15138 + data = """\ + 1 2 +123 98 """ + expected = DataFrame(exp_data) + + result = read_fwf(StringIO(data), infer_nrows=infer_nrows, header=None) + tm.assert_frame_equal(result, expected) - df = read_fwf(StringIO(data), - index_col=0, - header=None, - names=tcolnames, - widths=tcolspecs, - parse_dates=True, - date_parser=lambda s: datetime.strptime(s, '%Y%j%H%M%S')) - for c in df.columns: - res = df.loc[:, c] - self.assertTrue(len(res)) +def test_fwf_regression(): + # see gh-3594 + # + # Turns out "T060" is parsable as a datetime slice! + tz_list = [1, 10, 20, 30, 60, 80, 100] + widths = [16] + [8] * len(tz_list) + names = ["SST"] + ["T%03d" % z for z in tz_list[1:]] - def test_fwf_for_uint8(self): - data = """1421302965.213420 PRI=3 PGN=0xef00 DST=0x17 SRC=0x28 04 154 00 00 00 00 00 127 + data = """ 2009164202000 9.5403 9.4105 8.6571 7.8372 6.0612 5.8843 5.5192 +2009164203000 9.5435 9.2010 8.6167 7.8176 6.0804 5.8728 5.4869 +2009164204000 9.5873 9.1326 8.4694 7.5889 6.0422 5.8526 5.4657 +2009164205000 9.5810 9.0896 8.4009 7.4652 6.0322 5.8189 5.4379 +2009164210000 9.6034 9.0897 8.3822 7.4905 6.0908 5.7904 5.4039 +""" + + result = read_fwf(StringIO(data), index_col=0, header=None, names=names, + widths=widths, parse_dates=True, + date_parser=lambda s: datetime.strptime(s, "%Y%j%H%M%S")) + expected = DataFrame([ + [9.5403, 9.4105, 8.6571, 7.8372, 6.0612, 5.8843, 5.5192], + [9.5435, 9.2010, 8.6167, 7.8176, 6.0804, 5.8728, 5.4869], + [9.5873, 9.1326, 8.4694, 7.5889, 6.0422, 5.8526, 5.4657], + [9.5810, 9.0896, 8.4009, 7.4652, 6.0322, 5.8189, 5.4379], + [9.6034, 9.0897, 8.3822, 7.4905, 6.0908, 5.7904, 5.4039], + ], index=DatetimeIndex(["2009-06-13 20:20:00", "2009-06-13 20:30:00", + "2009-06-13 20:40:00", "2009-06-13 20:50:00", + "2009-06-13 21:00:00"]), + columns=["SST", "T010", "T020", "T030", "T060", "T080", "T100"]) + tm.assert_frame_equal(result, expected) + + +def test_fwf_for_uint8(): + data = """1421302965.213420 PRI=3 PGN=0xef00 DST=0x17 SRC=0x28 04 154 00 00 00 00 00 127 1421302964.226776 PRI=6 PGN=0xf002 SRC=0x47 243 00 00 255 247 00 00 71""" # noqa - df = read_fwf(StringIO(data), - colspecs=[(0, 17), (25, 26), (33, 37), - (49, 51), (58, 62), (63, 1000)], - names=['time', 'pri', 'pgn', 'dst', 'src', 'data'], - converters={ - 'pgn': lambda x: int(x, 16), - 'src': lambda x: int(x, 16), - 'dst': lambda x: int(x, 16), - 'data': lambda x: len(x.split(' '))}) - - expected = DataFrame([[1421302965.213420, 3, 61184, 23, 40, 8], - [1421302964.226776, 6, 61442, None, 71, 8]], - columns=["time", "pri", "pgn", - "dst", "src", "data"]) - expected["dst"] = expected["dst"].astype(object) - - tm.assert_frame_equal(df, expected) - - def test_fwf_compression(self): - try: - import gzip - import bz2 - except ImportError: - pytest.skip("Need gzip and bz2 to run this test") - - data = """1111111111 - 2222222222 - 3333333333""".strip() - widths = [5, 5] - names = ['one', 'two'] - expected = read_fwf(StringIO(data), widths=widths, names=names) - if compat.PY3: - data = bytes(data, encoding='utf-8') - comps = [('gzip', gzip.GzipFile), ('bz2', bz2.BZ2File)] - for comp_name, compresser in comps: - with tm.ensure_clean() as path: - tmp = compresser(path, mode='wb') - tmp.write(data) - tmp.close() - result = read_fwf(path, widths=widths, names=names, - compression=comp_name) - tm.assert_frame_equal(result, expected) - - def test_comment_fwf(self): - data = """ + df = read_fwf(StringIO(data), + colspecs=[(0, 17), (25, 26), (33, 37), + (49, 51), (58, 62), (63, 1000)], + names=["time", "pri", "pgn", "dst", "src", "data"], + converters={ + "pgn": lambda x: int(x, 16), + "src": lambda x: int(x, 16), + "dst": lambda x: int(x, 16), + "data": lambda x: len(x.split(" "))}) + + expected = DataFrame([[1421302965.213420, 3, 61184, 23, 40, 8], + [1421302964.226776, 6, 61442, None, 71, 8]], + columns=["time", "pri", "pgn", + "dst", "src", "data"]) + expected["dst"] = expected["dst"].astype(object) + tm.assert_frame_equal(df, expected) + + +@pytest.mark.parametrize("comment", ["#", "~", "!"]) +def test_fwf_comment(comment): + data = """\ 1 2. 4 #hello world 5 NaN 10.0 """ - expected = np.array([[1, 2., 4], - [5, np.nan, 10.]]) - df = read_fwf(StringIO(data), colspecs=[(0, 3), (4, 9), (9, 25)], - comment='#') - tm.assert_almost_equal(df.values, expected) - - def test_1000_fwf(self): - data = """ + data = data.replace("#", comment) + + colspecs = [(0, 3), (4, 9), (9, 25)] + expected = DataFrame([[1, 2., 4], [5, np.nan, 10.]]) + + result = read_fwf(StringIO(data), colspecs=colspecs, + header=None, comment=comment) + tm.assert_almost_equal(result, expected) + + +@pytest.mark.parametrize("thousands", [",", "#", "~"]) +def test_fwf_thousands(thousands): + data = """\ 1 2,334.0 5 10 13 10. """ - expected = np.array([[1, 2334., 5], - [10, 13, 10]]) - df = read_fwf(StringIO(data), colspecs=[(0, 3), (3, 11), (12, 16)], - thousands=',') - tm.assert_almost_equal(df.values, expected) - - def test_bool_header_arg(self): - # see gh-6114 - data = """\ + data = data.replace(",", thousands) + + colspecs = [(0, 3), (3, 11), (12, 16)] + expected = DataFrame([[1, 2334., 5], [10, 13, 10.]]) + + result = read_fwf(StringIO(data), header=None, + colspecs=colspecs, thousands=thousands) + tm.assert_almost_equal(result, expected) + + +@pytest.mark.parametrize("header", [True, False]) +def test_bool_header_arg(header): + # see gh-6114 + data = """\ MyColumn a b a b""" - for arg in [True, False]: - with tm.assertRaises(TypeError): - read_fwf(StringIO(data), header=arg) - def test_full_file(self): - # File with all values - test = """index A B C + msg = "Passing a bool to header is invalid" + with pytest.raises(TypeError, match=msg): + read_fwf(StringIO(data), header=header) + + +def test_full_file(): + # File with all values. + test = """index A B C 2000-01-03T00:00:00 0.980268513777 3 foo 2000-01-04T00:00:00 1.04791624281 -4 bar 2000-01-05T00:00:00 0.498580885705 73 baz @@ -256,13 +348,16 @@ def test_full_file(self): 2000-01-07T00:00:00 0.487094399463 0 bar 2000-01-10T00:00:00 0.836648671666 2 baz 2000-01-11T00:00:00 0.157160753327 34 foo""" - colspecs = ((0, 19), (21, 35), (38, 40), (42, 45)) - expected = read_fwf(StringIO(test), colspecs=colspecs) - tm.assert_frame_equal(expected, read_fwf(StringIO(test))) + colspecs = ((0, 19), (21, 35), (38, 40), (42, 45)) + expected = read_fwf(StringIO(test), colspecs=colspecs) + + result = read_fwf(StringIO(test)) + tm.assert_frame_equal(result, expected) - def test_full_file_with_missing(self): - # File with missing values - test = """index A B C + +def test_full_file_with_missing(): + # File with missing values. + test = """index A B C 2000-01-03T00:00:00 0.980268513777 3 foo 2000-01-04T00:00:00 1.04791624281 -4 bar 0.498580885705 73 baz @@ -270,136 +365,216 @@ def test_full_file_with_missing(self): 2000-01-07T00:00:00 0 bar 2000-01-10T00:00:00 0.836648671666 2 baz 34""" - colspecs = ((0, 19), (21, 35), (38, 40), (42, 45)) - expected = read_fwf(StringIO(test), colspecs=colspecs) - tm.assert_frame_equal(expected, read_fwf(StringIO(test))) + colspecs = ((0, 19), (21, 35), (38, 40), (42, 45)) + expected = read_fwf(StringIO(test), colspecs=colspecs) + + result = read_fwf(StringIO(test)) + tm.assert_frame_equal(result, expected) - def test_full_file_with_spaces(self): - # File with spaces in columns - test = """ + +def test_full_file_with_spaces(): + # File with spaces in columns. + test = """ Account Name Balance CreditLimit AccountCreated 101 Keanu Reeves 9315.45 10000.00 1/17/1998 312 Gerard Butler 90.00 1000.00 8/6/2003 868 Jennifer Love Hewitt 0 17000.00 5/25/1985 761 Jada Pinkett-Smith 49654.87 100000.00 12/5/2006 317 Bill Murray 789.65 5000.00 2/5/2007 -""".strip('\r\n') - colspecs = ((0, 7), (8, 28), (30, 38), (42, 53), (56, 70)) - expected = read_fwf(StringIO(test), colspecs=colspecs) - tm.assert_frame_equal(expected, read_fwf(StringIO(test))) - - def test_full_file_with_spaces_and_missing(self): - # File with spaces and missing values in columsn - test = """ +""".strip("\r\n") + colspecs = ((0, 7), (8, 28), (30, 38), (42, 53), (56, 70)) + expected = read_fwf(StringIO(test), colspecs=colspecs) + + result = read_fwf(StringIO(test)) + tm.assert_frame_equal(result, expected) + + +def test_full_file_with_spaces_and_missing(): + # File with spaces and missing values in columns. + test = """ Account Name Balance CreditLimit AccountCreated 101 10000.00 1/17/1998 312 Gerard Butler 90.00 1000.00 8/6/2003 868 5/25/1985 761 Jada Pinkett-Smith 49654.87 100000.00 12/5/2006 317 Bill Murray 789.65 -""".strip('\r\n') - colspecs = ((0, 7), (8, 28), (30, 38), (42, 53), (56, 70)) - expected = read_fwf(StringIO(test), colspecs=colspecs) - tm.assert_frame_equal(expected, read_fwf(StringIO(test))) - - def test_messed_up_data(self): - # Completely messed up file - test = """ +""".strip("\r\n") + colspecs = ((0, 7), (8, 28), (30, 38), (42, 53), (56, 70)) + expected = read_fwf(StringIO(test), colspecs=colspecs) + + result = read_fwf(StringIO(test)) + tm.assert_frame_equal(result, expected) + + +def test_messed_up_data(): + # Completely messed up file. + test = """ Account Name Balance Credit Limit Account Created 101 10000.00 1/17/1998 312 Gerard Butler 90.00 1000.00 761 Jada Pinkett-Smith 49654.87 100000.00 12/5/2006 317 Bill Murray 789.65 -""".strip('\r\n') - colspecs = ((2, 10), (15, 33), (37, 45), (49, 61), (64, 79)) - expected = read_fwf(StringIO(test), colspecs=colspecs) - tm.assert_frame_equal(expected, read_fwf(StringIO(test))) +""".strip("\r\n") + colspecs = ((2, 10), (15, 33), (37, 45), (49, 61), (64, 79)) + expected = read_fwf(StringIO(test), colspecs=colspecs) - def test_multiple_delimiters(self): - test = r""" + result = read_fwf(StringIO(test)) + tm.assert_frame_equal(result, expected) + + +def test_multiple_delimiters(): + test = r""" col1~~~~~col2 col3++++++++++++++++++col4 ~~22.....11.0+++foo~~~~~~~~~~Keanu Reeves 33+++122.33\\\bar.........Gerard Butler ++44~~~~12.01 baz~~Jennifer Love Hewitt ~~55 11+++foo++++Jada Pinkett-Smith ..66++++++.03~~~bar Bill Murray -""".strip('\r\n') - colspecs = ((0, 4), (7, 13), (15, 19), (21, 41)) - expected = read_fwf(StringIO(test), colspecs=colspecs, - delimiter=' +~.\\') - tm.assert_frame_equal(expected, read_fwf(StringIO(test), - delimiter=' +~.\\')) - - def test_variable_width_unicode(self): - if not compat.PY3: - pytest.skip( - 'Bytes-related test - only needs to work on Python 3') - test = """ +""".strip("\r\n") + delimiter = " +~.\\" + colspecs = ((0, 4), (7, 13), (15, 19), (21, 41)) + expected = read_fwf(StringIO(test), colspecs=colspecs, delimiter=delimiter) + + result = read_fwf(StringIO(test), delimiter=delimiter) + tm.assert_frame_equal(result, expected) + + +def test_variable_width_unicode(): + if not compat.PY3: + pytest.skip("Bytes-related test - only needs to work on Python 3") + + data = """ שלום שלום ום שלל של ום -""".strip('\r\n') - expected = read_fwf(BytesIO(test.encode('utf8')), - colspecs=[(0, 4), (5, 9)], - header=None, encoding='utf8') - tm.assert_frame_equal(expected, read_fwf( - BytesIO(test.encode('utf8')), header=None, encoding='utf8')) - - def test_dtype(self): - data = """ a b c +""".strip("\r\n") + encoding = "utf8" + kwargs = dict(header=None, encoding=encoding) + + expected = read_fwf(BytesIO(data.encode(encoding)), + colspecs=[(0, 4), (5, 9)], **kwargs) + result = read_fwf(BytesIO(data.encode(encoding)), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("dtype", [ + dict(), {"a": "float64", "b": str, "c": "int32"} +]) +def test_dtype(dtype): + data = """ a b c 1 2 3.2 3 4 5.2 """ - colspecs = [(0, 5), (5, 10), (10, None)] - result = pd.read_fwf(StringIO(data), colspecs=colspecs) - expected = pd.DataFrame({ - 'a': [1, 3], - 'b': [2, 4], - 'c': [3.2, 5.2]}, columns=['a', 'b', 'c']) - tm.assert_frame_equal(result, expected) + colspecs = [(0, 5), (5, 10), (10, None)] + result = read_fwf(StringIO(data), colspecs=colspecs, dtype=dtype) - expected['a'] = expected['a'].astype('float64') - expected['b'] = expected['b'].astype(str) - expected['c'] = expected['c'].astype('int32') - result = pd.read_fwf(StringIO(data), colspecs=colspecs, - dtype={'a': 'float64', 'b': str, 'c': 'int32'}) - tm.assert_frame_equal(result, expected) + expected = pd.DataFrame({ + "a": [1, 3], "b": [2, 4], + "c": [3.2, 5.2]}, columns=["a", "b", "c"]) - def test_skiprows_inference(self): - # GH11256 - test = """ + for col, dt in dtype.items(): + expected[col] = expected[col].astype(dt) + + tm.assert_frame_equal(result, expected) + + +def test_skiprows_inference(): + # see gh-11256 + data = """ Text contained in the file header DataCol1 DataCol2 0.0 1.0 101.6 956.1 """.strip() - expected = read_csv(StringIO(test), skiprows=2, - delim_whitespace=True) - tm.assert_frame_equal(expected, read_fwf( - StringIO(test), skiprows=2)) + skiprows = 2 + expected = read_csv(StringIO(data), skiprows=skiprows, + delim_whitespace=True) + + result = read_fwf(StringIO(data), skiprows=skiprows) + tm.assert_frame_equal(result, expected) - def test_skiprows_by_index_inference(self): - test = """ + +def test_skiprows_by_index_inference(): + data = """ To be skipped Not To Be Skipped Once more to be skipped 123 34 8 123 456 78 9 456 """.strip() + skiprows = [0, 2] + expected = read_csv(StringIO(data), skiprows=skiprows, + delim_whitespace=True) + + result = read_fwf(StringIO(data), skiprows=skiprows) + tm.assert_frame_equal(result, expected) - expected = read_csv(StringIO(test), skiprows=[0, 2], - delim_whitespace=True) - tm.assert_frame_equal(expected, read_fwf( - StringIO(test), skiprows=[0, 2])) - def test_skiprows_inference_empty(self): - test = """ +def test_skiprows_inference_empty(): + data = """ AA BBB C 12 345 6 78 901 2 """.strip() - with tm.assertRaises(EmptyDataError): - read_fwf(StringIO(test), skiprows=3) + msg = "No rows from which to infer column width" + with pytest.raises(EmptyDataError, match=msg): + read_fwf(StringIO(data), skiprows=3) + + +def test_whitespace_preservation(): + # see gh-16772 + header = None + csv_data = """ + a ,bbb + cc,dd """ + + fwf_data = """ + a bbb + ccdd """ + result = read_fwf(StringIO(fwf_data), widths=[3, 3], + header=header, skiprows=[0], delimiter="\n\t") + expected = read_csv(StringIO(csv_data), header=header) + tm.assert_frame_equal(result, expected) + + +def test_default_delimiter(): + header = None + csv_data = """ +a,bbb +cc,dd""" + + fwf_data = """ +a \tbbb +cc\tdd """ + result = read_fwf(StringIO(fwf_data), widths=[3, 3], + header=header, skiprows=[0]) + expected = read_csv(StringIO(csv_data), header=header) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("infer", [True, False, None]) +def test_fwf_compression(compression_only, infer): + data = """1111111111 + 2222222222 + 3333333333""".strip() + + compression = compression_only + extension = "gz" if compression == "gzip" else compression + + kwargs = dict(widths=[5, 5], names=["one", "two"]) + expected = read_fwf(StringIO(data), **kwargs) + + if compat.PY3: + data = bytes(data, encoding="utf-8") + + with tm.ensure_clean(filename="tmp." + extension) as path: + tm.write_to_compressed(compression, path, data) + + if infer is not None: + kwargs["compression"] = "infer" if infer else compression + + result = read_fwf(path, **kwargs) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/test_skiprows.py b/pandas/tests/io/parser/test_skiprows.py new file mode 100644 index 0000000000000..1df2ca4fad4d8 --- /dev/null +++ b/pandas/tests/io/parser/test_skiprows.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +""" +Tests that skipped rows are properly handled during +parsing for all of the parsers defined in parsers.py +""" + +from datetime import datetime + +import numpy as np +import pytest + +from pandas.compat import StringIO, lrange, range +from pandas.errors import EmptyDataError + +from pandas import DataFrame, Index +import pandas.util.testing as tm + + +@pytest.mark.parametrize("skiprows", [lrange(6), 6]) +def test_skip_rows_bug(all_parsers, skiprows): + # see gh-505 + parser = all_parsers + text = """#foo,a,b,c +#foo,a,b,c +#foo,a,b,c +#foo,a,b,c +#foo,a,b,c +#foo,a,b,c +1/1/2000,1.,2.,3. +1/2/2000,4,5,6 +1/3/2000,7,8,9 +""" + result = parser.read_csv(StringIO(text), skiprows=skiprows, header=None, + index_col=0, parse_dates=True) + index = Index([datetime(2000, 1, 1), datetime(2000, 1, 2), + datetime(2000, 1, 3)], name=0) + + expected = DataFrame(np.arange(1., 10.).reshape((3, 3)), + columns=[1, 2, 3], index=index) + tm.assert_frame_equal(result, expected) + + +def test_deep_skip_rows(all_parsers): + # see gh-4382 + parser = all_parsers + data = "a,b,c\n" + "\n".join([",".join([str(i), str(i + 1), str(i + 2)]) + for i in range(10)]) + condensed_data = "a,b,c\n" + "\n".join([ + ",".join([str(i), str(i + 1), str(i + 2)]) + for i in [0, 1, 2, 3, 4, 6, 8, 9]]) + + result = parser.read_csv(StringIO(data), skiprows=[6, 8]) + condensed_result = parser.read_csv(StringIO(condensed_data)) + tm.assert_frame_equal(result, condensed_result) + + +def test_skip_rows_blank(all_parsers): + # see gh-9832 + parser = all_parsers + text = """#foo,a,b,c +#foo,a,b,c + +#foo,a,b,c +#foo,a,b,c + +1/1/2000,1.,2.,3. +1/2/2000,4,5,6 +1/3/2000,7,8,9 +""" + data = parser.read_csv(StringIO(text), skiprows=6, header=None, + index_col=0, parse_dates=True) + index = Index([datetime(2000, 1, 1), datetime(2000, 1, 2), + datetime(2000, 1, 3)], name=0) + + expected = DataFrame(np.arange(1., 10.).reshape((3, 3)), + columns=[1, 2, 3], + index=index) + tm.assert_frame_equal(data, expected) + + +@pytest.mark.parametrize("data,kwargs,expected", [ + ("""id,text,num_lines +1,"line 11 +line 12",2 +2,"line 21 +line 22",2 +3,"line 31",1""", + dict(skiprows=[1]), + DataFrame([[2, "line 21\nline 22", 2], + [3, "line 31", 1]], columns=["id", "text", "num_lines"])), + ("a,b,c\n~a\n b~,~e\n d~,~f\n f~\n1,2,~12\n 13\n 14~", + dict(quotechar="~", skiprows=[2]), + DataFrame([["a\n b", "e\n d", "f\n f"]], columns=["a", "b", "c"])), + (("Text,url\n~example\n " + "sentence\n one~,url1\n~" + "example\n sentence\n two~,url2\n~" + "example\n sentence\n three~,url3"), + dict(quotechar="~", skiprows=[1, 3]), + DataFrame([['example\n sentence\n two', 'url2']], + columns=["Text", "url"])) +]) +def test_skip_row_with_newline(all_parsers, data, kwargs, expected): + # see gh-12775 and gh-10911 + parser = all_parsers + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_skip_row_with_quote(all_parsers): + # see gh-12775 and gh-10911 + parser = all_parsers + data = """id,text,num_lines +1,"line '11' line 12",2 +2,"line '21' line 22",2 +3,"line '31' line 32",1""" + + exp_data = [[2, "line '21' line 22", 2], + [3, "line '31' line 32", 1]] + expected = DataFrame(exp_data, columns=[ + "id", "text", "num_lines"]) + + result = parser.read_csv(StringIO(data), skiprows=[1]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,exp_data", [ + ("""id,text,num_lines +1,"line \n'11' line 12",2 +2,"line \n'21' line 22",2 +3,"line \n'31' line 32",1""", + [[2, "line \n'21' line 22", 2], + [3, "line \n'31' line 32", 1]]), + ("""id,text,num_lines +1,"line '11\n' line 12",2 +2,"line '21\n' line 22",2 +3,"line '31\n' line 32",1""", + [[2, "line '21\n' line 22", 2], + [3, "line '31\n' line 32", 1]]), + ("""id,text,num_lines +1,"line '11\n' \r\tline 12",2 +2,"line '21\n' \r\tline 22",2 +3,"line '31\n' \r\tline 32",1""", + [[2, "line '21\n' \r\tline 22", 2], + [3, "line '31\n' \r\tline 32", 1]]), +]) +def test_skip_row_with_newline_and_quote(all_parsers, data, exp_data): + # see gh-12775 and gh-10911 + parser = all_parsers + result = parser.read_csv(StringIO(data), skiprows=[1]) + + expected = DataFrame(exp_data, columns=["id", "text", "num_lines"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("line_terminator", [ + "\n", # "LF" + "\r\n", # "CRLF" + "\r" # "CR" +]) +def test_skiprows_lineterminator(all_parsers, line_terminator): + # see gh-9079 + parser = all_parsers + data = "\n".join(["SMOSMANIA ThetaProbe-ML2X ", + "2007/01/01 01:00 0.2140 U M ", + "2007/01/01 02:00 0.2141 M O ", + "2007/01/01 04:00 0.2142 D M "]) + expected = DataFrame([["2007/01/01", "01:00", 0.2140, "U", "M"], + ["2007/01/01", "02:00", 0.2141, "M", "O"], + ["2007/01/01", "04:00", 0.2142, "D", "M"]], + columns=["date", "time", "var", "flag", + "oflag"]) + + if parser.engine == "python" and line_terminator == "\r": + pytest.skip("'CR' not respect with the Python parser yet") + + data = data.replace("\n", line_terminator) + result = parser.read_csv(StringIO(data), skiprows=1, delim_whitespace=True, + names=["date", "time", "var", "flag", "oflag"]) + tm.assert_frame_equal(result, expected) + + +def test_skiprows_infield_quote(all_parsers): + # see gh-14459 + parser = all_parsers + data = "a\"\nb\"\na\n1" + expected = DataFrame({"a": [1]}) + + result = parser.read_csv(StringIO(data), skiprows=2) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,expected", [ + (dict(), DataFrame({"1": [3, 5]})), + (dict(header=0, names=["foo"]), DataFrame({"foo": [3, 5]})) +]) +def test_skip_rows_callable(all_parsers, kwargs, expected): + parser = all_parsers + data = "a\n1\n2\n3\n4\n5" + + result = parser.read_csv(StringIO(data), + skiprows=lambda x: x % 2 == 0, + **kwargs) + tm.assert_frame_equal(result, expected) + + +def test_skip_rows_skip_all(all_parsers): + parser = all_parsers + data = "a\n1\n2\n3\n4\n5" + msg = "No columns to parse from file" + + with pytest.raises(EmptyDataError, match=msg): + parser.read_csv(StringIO(data), skiprows=lambda x: True) + + +def test_skip_rows_bad_callable(all_parsers): + msg = "by zero" + parser = all_parsers + data = "a\n1\n2\n3\n4\n5" + + with pytest.raises(ZeroDivisionError, match=msg): + parser.read_csv(StringIO(data), skiprows=lambda x: 1 / 0) diff --git a/pandas/tests/io/parser/test_textreader.py b/pandas/tests/io/parser/test_textreader.py index b6a9900b0b087..8119de67890a5 100644 --- a/pandas/tests/io/parser/test_textreader.py +++ b/pandas/tests/io/parser/test_textreader.py @@ -5,52 +5,46 @@ is integral to the C engine in parsers.py """ -from pandas.compat import StringIO, BytesIO, map -from pandas import compat - import os -import sys -from numpy import nan import numpy as np +from numpy import nan +import pytest -from pandas import DataFrame -from pandas.io.parsers import (read_csv, TextFileReader) -from pandas.util.testing import assert_frame_equal +import pandas._libs.parsers as parser +from pandas._libs.parsers import TextReader +import pandas.compat as compat +from pandas.compat import BytesIO, StringIO, map +from pandas import DataFrame import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal -from pandas.io.libparsers import TextReader -import pandas.io.libparsers as parser +from pandas.io.parsers import TextFileReader, read_csv -class TestTextReader(tm.TestCase): +class TestTextReader(object): - def setUp(self): - self.dirpath = tm.get_data_path() + @pytest.fixture(autouse=True) + def setup_method(self, datapath): + self.dirpath = datapath('io', 'parser', 'data') self.csv1 = os.path.join(self.dirpath, 'test1.csv') self.csv2 = os.path.join(self.dirpath, 'test2.csv') self.xls1 = os.path.join(self.dirpath, 'test.xls') def test_file_handle(self): - try: - f = open(self.csv1, 'rb') + with open(self.csv1, 'rb') as f: reader = TextReader(f) - result = reader.read() # noqa - finally: - f.close() + reader.read() def test_string_filename(self): reader = TextReader(self.csv1, header=None) reader.read() def test_file_handle_mmap(self): - try: - f = open(self.csv1, 'rb') + with open(self.csv1, 'rb') as f: reader = TextReader(f, memory_map=True, header=None) reader.read() - finally: - f.close() def test_StringIO(self): with open(self.csv1, 'rb') as f: @@ -64,7 +58,7 @@ def test_string_factorize(self): data = 'a\nb\na\nb\na' reader = TextReader(StringIO(data), header=None) result = reader.read() - self.assertEqual(len(set(map(id, result[0]))), 2) + assert len(set(map(id, result[0]))) == 2 def test_skipinitialspace(self): data = ('a, b\n' @@ -76,12 +70,10 @@ def test_skipinitialspace(self): header=None) result = reader.read() - self.assert_numpy_array_equal(result[0], - np.array(['a', 'a', 'a', 'a'], - dtype=np.object_)) - self.assert_numpy_array_equal(result[1], - np.array(['b', 'b', 'b', 'b'], - dtype=np.object_)) + tm.assert_numpy_array_equal(result[0], np.array(['a', 'a', 'a', 'a'], + dtype=np.object_)) + tm.assert_numpy_array_equal(result[1], np.array(['b', 'b', 'b', 'b'], + dtype=np.object_)) def test_parse_booleans(self): data = 'True\nFalse\nTrue\nTrue' @@ -89,7 +81,7 @@ def test_parse_booleans(self): reader = TextReader(StringIO(data), header=None) result = reader.read() - self.assertEqual(result[0].dtype, np.bool_) + assert result[0].dtype == np.bool_ def test_delimit_whitespace(self): data = 'a b\na\t\t "b"\n"a"\t \t b' @@ -98,10 +90,10 @@ def test_delimit_whitespace(self): header=None) result = reader.read() - self.assert_numpy_array_equal(result[0], np.array(['a', 'a', 'a'], - dtype=np.object_)) - self.assert_numpy_array_equal(result[1], np.array(['b', 'b', 'b'], - dtype=np.object_)) + tm.assert_numpy_array_equal(result[0], np.array(['a', 'a', 'a'], + dtype=np.object_)) + tm.assert_numpy_array_equal(result[1], np.array(['b', 'b', 'b'], + dtype=np.object_)) def test_embedded_newline(self): data = 'a\n"hello\nthere"\nthis' @@ -110,7 +102,7 @@ def test_embedded_newline(self): result = reader.read() expected = np.array(['a', 'hello\nthere', 'this'], dtype=np.object_) - self.assert_numpy_array_equal(result[0], expected) + tm.assert_numpy_array_equal(result[0], expected) def test_euro_decimal(self): data = '12345,67\n345,678' @@ -142,7 +134,7 @@ def test_integer_thousands_alt(self): expected = DataFrame([123456, 12500]) tm.assert_frame_equal(result, expected) - def test_skip_bad_lines(self): + def test_skip_bad_lines(self, capsys): # too many lines, see #2430 for why data = ('a:b:c\n' 'd:e:f\n' @@ -153,31 +145,30 @@ def test_skip_bad_lines(self): reader = TextReader(StringIO(data), delimiter=':', header=None) - self.assertRaises(parser.ParserError, reader.read) + msg = (r"Error tokenizing data\. C error: Expected 3 fields in" + " line 4, saw 4") + with pytest.raises(parser.ParserError, match=msg): + reader.read() reader = TextReader(StringIO(data), delimiter=':', header=None, error_bad_lines=False, warn_bad_lines=False) result = reader.read() - expected = {0: ['a', 'd', 'g', 'l'], - 1: ['b', 'e', 'h', 'm'], - 2: ['c', 'f', 'i', 'n']} + expected = {0: np.array(['a', 'd', 'g', 'l'], dtype=object), + 1: np.array(['b', 'e', 'h', 'm'], dtype=object), + 2: np.array(['c', 'f', 'i', 'n'], dtype=object)} assert_array_dicts_equal(result, expected) - stderr = sys.stderr - sys.stderr = StringIO() - try: - reader = TextReader(StringIO(data), delimiter=':', - header=None, - error_bad_lines=False, - warn_bad_lines=True) - reader.read() - val = sys.stderr.getvalue() - self.assertTrue('Skipping line 4' in val) - self.assertTrue('Skipping line 6' in val) - finally: - sys.stderr = stderr + reader = TextReader(StringIO(data), delimiter=':', + header=None, + error_bad_lines=False, + warn_bad_lines=True) + reader.read() + captured = capsys.readouterr() + + assert 'Skipping line 4' in captured.err + assert 'Skipping line 6' in captured.err def test_header_not_enough_lines(self): data = ('skip this\n' @@ -189,36 +180,13 @@ def test_header_not_enough_lines(self): reader = TextReader(StringIO(data), delimiter=',', header=2) header = reader.header expected = [['a', 'b', 'c']] - self.assertEqual(header, expected) - - recs = reader.read() - expected = {0: [1, 4], 1: [2, 5], 2: [3, 6]} - assert_array_dicts_equal(expected, recs) - - # not enough rows - self.assertRaises(parser.ParserError, TextReader, StringIO(data), - delimiter=',', header=5, as_recarray=True) - - def test_header_not_enough_lines_as_recarray(self): - data = ('skip this\n' - 'skip this\n' - 'a,b,c\n' - '1,2,3\n' - '4,5,6') - - reader = TextReader(StringIO(data), delimiter=',', header=2, - as_recarray=True) - header = reader.header - expected = [['a', 'b', 'c']] - self.assertEqual(header, expected) + assert header == expected recs = reader.read() - expected = {'a': [1, 4], 'b': [2, 5], 'c': [3, 6]} - assert_array_dicts_equal(expected, recs) - - # not enough rows - self.assertRaises(parser.ParserError, TextReader, StringIO(data), - delimiter=',', header=5, as_recarray=True) + expected = {0: np.array([1, 4], dtype=np.int64), + 1: np.array([2, 5], dtype=np.int64), + 2: np.array([3, 6], dtype=np.int64)} + assert_array_dicts_equal(recs, expected) def test_escapechar(self): data = ('\\"hello world\"\n' @@ -228,7 +196,7 @@ def test_escapechar(self): reader = TextReader(StringIO(data), delimiter=',', header=None, escapechar='\\') result = reader.read() - expected = {0: ['"hello world"'] * 3} + expected = {0: np.array(['"hello world"'] * 3, dtype=object)} assert_array_dicts_equal(result, expected) def test_eof_has_eol(self): @@ -253,37 +221,18 @@ def _make_reader(**kwds): reader = _make_reader(dtype='S5,i4') result = reader.read() - self.assertEqual(result[0].dtype, 'S5') + assert result[0].dtype == 'S5' ex_values = np.array(['a', 'aa', 'aaa', 'aaaa', 'aaaaa'], dtype='S5') - self.assertTrue((result[0] == ex_values).all()) - self.assertEqual(result[1].dtype, 'i4') + assert (result[0] == ex_values).all() + assert result[1].dtype == 'i4' reader = _make_reader(dtype='S4') result = reader.read() - self.assertEqual(result[0].dtype, 'S4') - ex_values = np.array(['a', 'aa', 'aaa', 'aaaa', 'aaaa'], dtype='S4') - self.assertTrue((result[0] == ex_values).all()) - self.assertEqual(result[1].dtype, 'S4') - - def test_numpy_string_dtype_as_recarray(self): - data = """\ -a,1 -aa,2 -aaa,3 -aaaa,4 -aaaaa,5""" - - def _make_reader(**kwds): - return TextReader(StringIO(data), delimiter=',', header=None, - **kwds) - - reader = _make_reader(dtype='S4', as_recarray=True) - result = reader.read() - self.assertEqual(result['0'].dtype, 'S4') + assert result[0].dtype == 'S4' ex_values = np.array(['a', 'aa', 'aaa', 'aaaa', 'aaaa'], dtype='S4') - self.assertTrue((result['0'] == ex_values).all()) - self.assertEqual(result['1'].dtype, 'S4') + assert (result[0] == ex_values).all() + assert result[1].dtype == 'S4' def test_pass_dtype(self): data = """\ @@ -298,19 +247,19 @@ def _make_reader(**kwds): reader = _make_reader(dtype={'one': 'u1', 1: 'S1'}) result = reader.read() - self.assertEqual(result[0].dtype, 'u1') - self.assertEqual(result[1].dtype, 'S1') + assert result[0].dtype == 'u1' + assert result[1].dtype == 'S1' reader = _make_reader(dtype={'one': np.uint8, 1: object}) result = reader.read() - self.assertEqual(result[0].dtype, 'u1') - self.assertEqual(result[1].dtype, 'O') + assert result[0].dtype == 'u1' + assert result[1].dtype == 'O' reader = _make_reader(dtype={'one': np.dtype('u1'), 1: np.dtype('O')}) result = reader.read() - self.assertEqual(result[0].dtype, 'u1') - self.assertEqual(result[1].dtype, 'O') + assert result[0].dtype == 'u1' + assert result[1].dtype == 'O' def test_usecols(self): data = """\ @@ -327,9 +276,9 @@ def _make_reader(**kwds): result = reader.read() exp = _make_reader().read() - self.assertEqual(len(result), 2) - self.assertTrue((result[1] == exp[1]).all()) - self.assertTrue((result[2] == exp[2]).all()) + assert len(result) == 2 + assert (result[1] == exp[1]).all() + assert (result[2] == exp[2]).all() def test_cr_delimited(self): def _test(text, **kwargs): @@ -363,7 +312,7 @@ def test_empty_field_eof(self): result = TextReader(StringIO(data), delimiter=',').read() - expected = {0: np.array([1, 4]), + expected = {0: np.array([1, 4], dtype=np.int64), 1: np.array(['2', ''], dtype=object), 2: np.array(['3', ''], dtype=object)} assert_array_dicts_equal(result, expected) @@ -395,9 +344,10 @@ def test_empty_csv_input(self): # GH14867 df = read_csv(StringIO(), chunksize=20, header=None, names=['a', 'b', 'c']) - self.assertTrue(isinstance(df, TextFileReader)) + assert isinstance(df, TextFileReader) def assert_array_dicts_equal(left, right): for k, v in compat.iteritems(left): - assert(np.array_equal(v, right[k])) + assert tm.assert_numpy_array_equal(np.asarray(v), + np.asarray(right[k])) diff --git a/pandas/tests/io/parser/test_unsupported.py b/pandas/tests/io/parser/test_unsupported.py index 48dd5d4ba506b..8c6dbd64c785d 100644 --- a/pandas/tests/io/parser/test_unsupported.py +++ b/pandas/tests/io/parser/test_unsupported.py @@ -9,15 +9,23 @@ test suite as new feature support is added to the parsers. """ -import pandas.io.parsers as parsers -import pandas.util.testing as tm +import pytest from pandas.compat import StringIO -from pandas.io.common import ParserError -from pandas.io.parsers import read_csv, read_table +from pandas.errors import ParserError +import pandas.util.testing as tm + +import pandas.io.parsers as parsers +from pandas.io.parsers import read_csv -class TestUnsupportedFeatures(tm.TestCase): + +@pytest.fixture(params=["python", "python-fwf"], ids=lambda val: val) +def python_engine(request): + return request.param + + +class TestUnsupportedFeatures(object): def test_mangle_dupe_cols_false(self): # see gh-12935 @@ -25,7 +33,7 @@ def test_mangle_dupe_cols_false(self): msg = 'is not supported' for engine in ('c', 'python'): - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): read_csv(StringIO(data), engine=engine, mangle_dupe_cols=False) @@ -35,25 +43,25 @@ def test_c_engine(self): msg = 'does not support' # specify C engine with unsupported options (raise) - with tm.assertRaisesRegexp(ValueError, msg): - read_table(StringIO(data), engine='c', - sep=None, delim_whitespace=False) - with tm.assertRaisesRegexp(ValueError, msg): - read_table(StringIO(data), engine='c', sep=r'\s') - with tm.assertRaisesRegexp(ValueError, msg): - read_table(StringIO(data), engine='c', quotechar=chr(128)) - with tm.assertRaisesRegexp(ValueError, msg): - read_table(StringIO(data), engine='c', skipfooter=1) + with pytest.raises(ValueError, match=msg): + read_csv(StringIO(data), engine='c', + sep=None, delim_whitespace=False) + with pytest.raises(ValueError, match=msg): + read_csv(StringIO(data), engine='c', sep=r'\s') + with pytest.raises(ValueError, match=msg): + read_csv(StringIO(data), engine='c', sep='\t', quotechar=chr(128)) + with pytest.raises(ValueError, match=msg): + read_csv(StringIO(data), engine='c', skipfooter=1) # specify C-unsupported options without python-unsupported options with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), sep=None, delim_whitespace=False) + read_csv(StringIO(data), sep=None, delim_whitespace=False) with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), quotechar=chr(128)) + read_csv(StringIO(data), sep=r'\s') with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), sep=r'\s') + read_csv(StringIO(data), sep='\t', quotechar=chr(128)) with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), skipfooter=1) + read_csv(StringIO(data), skipfooter=1) text = """ A B C D E one two three four @@ -62,27 +70,27 @@ def test_c_engine(self): x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" msg = 'Error tokenizing data' - with tm.assertRaisesRegexp(ParserError, msg): - read_table(StringIO(text), sep='\\s+') - with tm.assertRaisesRegexp(ParserError, msg): - read_table(StringIO(text), engine='c', sep='\\s+') + with pytest.raises(ParserError, match=msg): + read_csv(StringIO(text), sep='\\s+') + with pytest.raises(ParserError, match=msg): + read_csv(StringIO(text), engine='c', sep='\\s+') msg = "Only length-1 thousands markers supported" data = """A|B|C 1|2,334|5 10|13|10. """ - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): read_csv(StringIO(data), thousands=',,') - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): read_csv(StringIO(data), thousands='') msg = "Only length-1 line terminators supported" data = 'a,b,c~~1,2,3~~4,5,6' - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): read_csv(StringIO(data), lineterminator='~~') - def test_python_engine(self): + def test_python_engine(self, python_engine): from pandas.io.parsers import _python_unsupported as py_unsupported data = """1,2,3,, @@ -90,46 +98,43 @@ def test_python_engine(self): 1,2,3,4,5 1,2,,, 1,2,3,4,""" - engines = 'python', 'python-fwf' - for engine in engines: - for default in py_unsupported: - msg = ('The %r option is not supported ' - 'with the %r engine' % (default, engine)) + for default in py_unsupported: + msg = ('The %r option is not supported ' + 'with the %r engine' % (default, python_engine)) + + kwargs = {default: object()} + with pytest.raises(ValueError, match=msg): + read_csv(StringIO(data), engine=python_engine, **kwargs) - kwargs = {default: object()} - with tm.assertRaisesRegexp(ValueError, msg): - read_csv(StringIO(data), engine=engine, **kwargs) + def test_python_engine_file_no_next(self, python_engine): + # see gh-16530 + class NoNextBuffer(object): + def __init__(self, csv_data): + self.data = csv_data + def __iter__(self): + return self -class TestDeprecatedFeatures(tm.TestCase): + def read(self): + return self.data - def test_deprecated_args(self): - data = '1,2,3' + data = "a\n1" + msg = "The 'python' engine cannot iterate" - # deprecated arguments with non-default values - deprecated = { - 'as_recarray': True, - 'buffer_lines': True, - 'compact_ints': True, - 'skip_footer': True, - 'use_unsigned': True, - } + with pytest.raises(ValueError, match=msg): + read_csv(NoNextBuffer(data), engine=python_engine) - engines = 'c', 'python' - for engine in engines: - for arg, non_default_val in deprecated.items(): - if engine == 'c' and arg == 'skip_footer': - # unsupported --> exception is raised - continue +class TestDeprecatedFeatures(object): - if engine == 'python' and arg == 'buffer_lines': - # unsupported --> exception is raised - continue + @pytest.mark.parametrize("engine", ["c", "python"]) + @pytest.mark.parametrize("kwargs", [{"tupleize_cols": True}, + {"tupleize_cols": False}]) + def test_deprecated_args(self, engine, kwargs): + data = "1,2,3" + arg, _ = list(kwargs.items())[0] - with tm.assert_produces_warning( - FutureWarning, check_stacklevel=False): - kwargs = {arg: non_default_val} - read_csv(StringIO(data), engine=engine, - **kwargs) + with tm.assert_produces_warning( + FutureWarning, check_stacklevel=False): + read_csv(StringIO(data), engine=engine, **kwargs) diff --git a/pandas/tests/io/parser/test_usecols.py b/pandas/tests/io/parser/test_usecols.py new file mode 100644 index 0000000000000..652f78d198ee8 --- /dev/null +++ b/pandas/tests/io/parser/test_usecols.py @@ -0,0 +1,534 @@ +# -*- coding: utf-8 -*- + +""" +Tests the usecols functionality during parsing +for all of the parsers defined in parsers.py +""" + +import numpy as np +import pytest + +from pandas._libs.tslib import Timestamp +from pandas.compat import StringIO + +from pandas import DataFrame, Index +import pandas.util.testing as tm + +_msg_validate_usecols_arg = ("'usecols' must either be list-like " + "of all strings, all unicode, all " + "integers or a callable.") +_msg_validate_usecols_names = ("Usecols do not match columns, columns " + "expected but not found: {0}") + + +def test_raise_on_mixed_dtype_usecols(all_parsers): + # See gh-12678 + data = """a,b,c + 1000,2000,3000 + 4000,5000,6000 + """ + usecols = [0, "b", 2] + parser = all_parsers + + with pytest.raises(ValueError, match=_msg_validate_usecols_arg): + parser.read_csv(StringIO(data), usecols=usecols) + + +@pytest.mark.parametrize("usecols", [(1, 2), ("b", "c")]) +def test_usecols(all_parsers, usecols): + data = """\ +a,b,c +1,2,3 +4,5,6 +7,8,9 +10,11,12""" + parser = all_parsers + result = parser.read_csv(StringIO(data), usecols=usecols) + + expected = DataFrame([[2, 3], [5, 6], [8, 9], + [11, 12]], columns=["b", "c"]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_names(all_parsers): + data = """\ +a,b,c +1,2,3 +4,5,6 +7,8,9 +10,11,12""" + parser = all_parsers + names = ["foo", "bar"] + result = parser.read_csv(StringIO(data), names=names, + usecols=[1, 2], header=0) + + expected = DataFrame([[2, 3], [5, 6], [8, 9], + [11, 12]], columns=names) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("names,usecols", [ + (["b", "c"], [1, 2]), + (["a", "b", "c"], ["b", "c"]) +]) +def test_usecols_relative_to_names(all_parsers, names, usecols): + data = """\ +1,2,3 +4,5,6 +7,8,9 +10,11,12""" + parser = all_parsers + result = parser.read_csv(StringIO(data), names=names, + header=None, usecols=usecols) + + expected = DataFrame([[2, 3], [5, 6], [8, 9], + [11, 12]], columns=["b", "c"]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_relative_to_names2(all_parsers): + # see gh-5766 + data = """\ +1,2,3 +4,5,6 +7,8,9 +10,11,12""" + parser = all_parsers + result = parser.read_csv(StringIO(data), names=["a", "b"], + header=None, usecols=[0, 1]) + + expected = DataFrame([[1, 2], [4, 5], [7, 8], + [10, 11]], columns=["a", "b"]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_name_length_conflict(all_parsers): + data = """\ +1,2,3 +4,5,6 +7,8,9 +10,11,12""" + parser = all_parsers + msg = ("Number of passed names did not " + "match number of header fields in the file" + if parser.engine == "python" else + "Passed header names mismatches usecols") + + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), names=["a", "b"], + header=None, usecols=[1]) + + +def test_usecols_single_string(all_parsers): + # see gh-20558 + parser = all_parsers + data = """foo, bar, baz +1000, 2000, 3000 +4000, 5000, 6000""" + + with pytest.raises(ValueError, match=_msg_validate_usecols_arg): + parser.read_csv(StringIO(data), usecols="foo") + + +@pytest.mark.parametrize("data", ["a,b,c,d\n1,2,3,4\n5,6,7,8", + "a,b,c,d\n1,2,3,4,\n5,6,7,8,"]) +def test_usecols_index_col_false(all_parsers, data): + # see gh-9082 + parser = all_parsers + usecols = ["a", "c", "d"] + expected = DataFrame({"a": [1, 5], "c": [3, 7], "d": [4, 8]}) + + result = parser.read_csv(StringIO(data), usecols=usecols, index_col=False) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("index_col", ["b", 0]) +@pytest.mark.parametrize("usecols", [["b", "c"], [1, 2]]) +def test_usecols_index_col_conflict(all_parsers, usecols, index_col): + # see gh-4201: test that index_col as integer reflects usecols + parser = all_parsers + data = "a,b,c,d\nA,a,1,one\nB,b,2,two" + expected = DataFrame({"c": [1, 2]}, index=Index(["a", "b"], name="b")) + + result = parser.read_csv(StringIO(data), usecols=usecols, + index_col=index_col) + tm.assert_frame_equal(result, expected) + + +def test_usecols_index_col_conflict2(all_parsers): + # see gh-4201: test that index_col as integer reflects usecols + parser = all_parsers + data = "a,b,c,d\nA,a,1,one\nB,b,2,two" + + expected = DataFrame({"b": ["a", "b"], "c": [1, 2], "d": ("one", "two")}) + expected = expected.set_index(["b", "c"]) + + result = parser.read_csv(StringIO(data), usecols=["b", "c", "d"], + index_col=["b", "c"]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_implicit_index_col(all_parsers): + # see gh-2654 + parser = all_parsers + data = "a,b,c\n4,apple,bat,5.7\n8,orange,cow,10" + + result = parser.read_csv(StringIO(data), usecols=["a", "b"]) + expected = DataFrame({"a": ["apple", "orange"], + "b": ["bat", "cow"]}, index=[4, 8]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_regex_sep(all_parsers): + # see gh-2733 + parser = all_parsers + data = "a b c\n4 apple bat 5.7\n8 orange cow 10" + result = parser.read_csv(StringIO(data), sep=r"\s+", usecols=("a", "b")) + + expected = DataFrame({"a": ["apple", "orange"], + "b": ["bat", "cow"]}, index=[4, 8]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_whitespace(all_parsers): + parser = all_parsers + data = "a b c\n4 apple bat 5.7\n8 orange cow 10" + + result = parser.read_csv(StringIO(data), delim_whitespace=True, + usecols=("a", "b")) + expected = DataFrame({"a": ["apple", "orange"], + "b": ["bat", "cow"]}, index=[4, 8]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols,expected", [ + # Column selection by index. + ([0, 1], DataFrame(data=[[1000, 2000], [4000, 5000]], + columns=["2", "0"])), + + # Column selection by name. + (["0", "1"], DataFrame(data=[[2000, 3000], [5000, 6000]], + columns=["0", "1"])), +]) +def test_usecols_with_integer_like_header(all_parsers, usecols, expected): + parser = all_parsers + data = """2,0,1 +1000,2000,3000 +4000,5000,6000""" + + result = parser.read_csv(StringIO(data), usecols=usecols) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols", [[0, 2, 3], [3, 0, 2]]) +def test_usecols_with_parse_dates(all_parsers, usecols): + # see gh-9755 + data = """a,b,c,d,e +0,1,20140101,0900,4 +0,1,20140102,1000,4""" + parser = all_parsers + parse_dates = [[1, 2]] + + cols = { + "a": [0, 0], + "c_d": [ + Timestamp("2014-01-01 09:00:00"), + Timestamp("2014-01-02 10:00:00") + ] + } + expected = DataFrame(cols, columns=["c_d", "a"]) + result = parser.read_csv(StringIO(data), usecols=usecols, + parse_dates=parse_dates) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_parse_dates2(all_parsers): + # see gh-13604 + parser = all_parsers + data = """2008-02-07 09:40,1032.43 +2008-02-07 09:50,1042.54 +2008-02-07 10:00,1051.65""" + + names = ["date", "values"] + usecols = names[:] + parse_dates = [0] + + index = Index([Timestamp("2008-02-07 09:40"), + Timestamp("2008-02-07 09:50"), + Timestamp("2008-02-07 10:00")], + name="date") + cols = {"values": [1032.43, 1042.54, 1051.65]} + expected = DataFrame(cols, index=index) + + result = parser.read_csv(StringIO(data), parse_dates=parse_dates, + index_col=0, usecols=usecols, + header=None, names=names) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_parse_dates3(all_parsers): + # see gh-14792 + parser = all_parsers + data = """a,b,c,d,e,f,g,h,i,j +2016/09/21,1,1,2,3,4,5,6,7,8""" + + usecols = list("abcdefghij") + parse_dates = [0] + + cols = {"a": Timestamp("2016-09-21"), + "b": [1], "c": [1], "d": [2], + "e": [3], "f": [4], "g": [5], + "h": [6], "i": [7], "j": [8]} + expected = DataFrame(cols, columns=usecols) + + result = parser.read_csv(StringIO(data), usecols=usecols, + parse_dates=parse_dates) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_parse_dates4(all_parsers): + data = "a,b,c,d,e,f,g,h,i,j\n2016/09/21,1,1,2,3,4,5,6,7,8" + usecols = list("abcdefghij") + parse_dates = [[0, 1]] + parser = all_parsers + + cols = {"a_b": "2016/09/21 1", + "c": [1], "d": [2], "e": [3], "f": [4], + "g": [5], "h": [6], "i": [7], "j": [8]} + expected = DataFrame(cols, columns=["a_b"] + list("cdefghij")) + + result = parser.read_csv(StringIO(data), usecols=usecols, + parse_dates=parse_dates) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols", [[0, 2, 3], [3, 0, 2]]) +@pytest.mark.parametrize("names", [ + list("abcde"), # Names span all columns in original data. + list("acd"), # Names span only the selected columns. +]) +def test_usecols_with_parse_dates_and_names(all_parsers, usecols, names): + # see gh-9755 + s = """0,1,20140101,0900,4 +0,1,20140102,1000,4""" + parse_dates = [[1, 2]] + parser = all_parsers + + cols = { + "a": [0, 0], + "c_d": [ + Timestamp("2014-01-01 09:00:00"), + Timestamp("2014-01-02 10:00:00") + ] + } + expected = DataFrame(cols, columns=["c_d", "a"]) + + result = parser.read_csv(StringIO(s), names=names, + parse_dates=parse_dates, + usecols=usecols) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_unicode_strings(all_parsers): + # see gh-13219 + data = """AAA,BBB,CCC,DDD +0.056674973,8,True,a +2.613230982,2,False,b +3.568935038,7,False,a""" + parser = all_parsers + + exp_data = { + "AAA": { + 0: 0.056674972999999997, + 1: 2.6132309819999997, + 2: 3.5689350380000002 + }, + "BBB": {0: 8, 1: 2, 2: 7} + } + expected = DataFrame(exp_data) + + result = parser.read_csv(StringIO(data), usecols=[u"AAA", u"BBB"]) + tm.assert_frame_equal(result, expected) + + +def test_usecols_with_single_byte_unicode_strings(all_parsers): + # see gh-13219 + data = """A,B,C,D +0.056674973,8,True,a +2.613230982,2,False,b +3.568935038,7,False,a""" + parser = all_parsers + + exp_data = { + "A": { + 0: 0.056674972999999997, + 1: 2.6132309819999997, + 2: 3.5689350380000002 + }, + "B": {0: 8, 1: 2, 2: 7} + } + expected = DataFrame(exp_data) + + result = parser.read_csv(StringIO(data), usecols=[u"A", u"B"]) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols", [[u"AAA", b"BBB"], [b"AAA", u"BBB"]]) +def test_usecols_with_mixed_encoding_strings(all_parsers, usecols): + data = """AAA,BBB,CCC,DDD +0.056674973,8,True,a +2.613230982,2,False,b +3.568935038,7,False,a""" + parser = all_parsers + + with pytest.raises(ValueError, match=_msg_validate_usecols_arg): + parser.read_csv(StringIO(data), usecols=usecols) + + +@pytest.mark.parametrize("usecols", [ + ["あああ", "いい"], + [u"あああ", u"いい"] +]) +def test_usecols_with_multi_byte_characters(all_parsers, usecols): + data = """あああ,いい,ううう,ええええ +0.056674973,8,True,a +2.613230982,2,False,b +3.568935038,7,False,a""" + parser = all_parsers + + exp_data = { + "あああ": { + 0: 0.056674972999999997, + 1: 2.6132309819999997, + 2: 3.5689350380000002 + }, + "いい": {0: 8, 1: 2, 2: 7} + } + expected = DataFrame(exp_data) + + result = parser.read_csv(StringIO(data), usecols=usecols) + tm.assert_frame_equal(result, expected) + + +def test_empty_usecols(all_parsers): + data = "a,b,c\n1,2,3\n4,5,6" + expected = DataFrame() + parser = all_parsers + + result = parser.read_csv(StringIO(data), usecols=set()) + tm.assert_frame_equal(result, expected) + + +def test_np_array_usecols(all_parsers): + # see gh-12546 + parser = all_parsers + data = "a,b,c\n1,2,3" + usecols = np.array(["a", "b"]) + + expected = DataFrame([[1, 2]], columns=usecols) + result = parser.read_csv(StringIO(data), usecols=usecols) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols,expected", [ + (lambda x: x.upper() in ["AAA", "BBB", "DDD"], + DataFrame({ + "AaA": { + 0: 0.056674972999999997, + 1: 2.6132309819999997, + 2: 3.5689350380000002 + }, + "bBb": {0: 8, 1: 2, 2: 7}, + "ddd": {0: "a", 1: "b", 2: "a"} + })), + (lambda x: False, DataFrame()), +]) +def test_callable_usecols(all_parsers, usecols, expected): + # see gh-14154 + data = """AaA,bBb,CCC,ddd +0.056674973,8,True,a +2.613230982,2,False,b +3.568935038,7,False,a""" + parser = all_parsers + + result = parser.read_csv(StringIO(data), usecols=usecols) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols", [["a", "c"], lambda x: x in ["a", "c"]]) +def test_incomplete_first_row(all_parsers, usecols): + # see gh-6710 + data = "1,2\n1,2,3" + parser = all_parsers + names = ["a", "b", "c"] + expected = DataFrame({"a": [1, 1], "c": [np.nan, 3]}) + + result = parser.read_csv(StringIO(data), names=names, usecols=usecols) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("data,usecols,kwargs,expected", [ + # see gh-8985 + ("19,29,39\n" * 2 + "10,20,30,40", [0, 1, 2], + dict(header=None), DataFrame([[19, 29, 39], [19, 29, 39], [10, 20, 30]])), + + # see gh-9549 + (("A,B,C\n1,2,3\n3,4,5\n1,2,4,5,1,6\n" + "1,2,3,,,1,\n1,2,3\n5,6,7"), ["A", "B", "C"], + dict(), DataFrame({"A": [1, 3, 1, 1, 1, 5], + "B": [2, 4, 2, 2, 2, 6], + "C": [3, 5, 4, 3, 3, 7]})), +]) +def test_uneven_length_cols(all_parsers, data, usecols, kwargs, expected): + # see gh-8985 + parser = all_parsers + result = parser.read_csv(StringIO(data), usecols=usecols, **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("usecols,kwargs,expected,msg", [ + (["a", "b", "c", "d"], dict(), + DataFrame({"a": [1, 5], "b": [2, 6], "c": [3, 7], "d": [4, 8]}), None), + (["a", "b", "c", "f"], dict(), None, + _msg_validate_usecols_names.format(r"\['f'\]")), + (["a", "b", "f"], dict(), None, + _msg_validate_usecols_names.format(r"\['f'\]")), + (["a", "b", "f", "g"], dict(), None, + _msg_validate_usecols_names.format(r"\[('f', 'g'|'g', 'f')\]")), + + # see gh-14671 + (None, dict(header=0, names=["A", "B", "C", "D"]), + DataFrame({"A": [1, 5], "B": [2, 6], "C": [3, 7], + "D": [4, 8]}), None), + (["A", "B", "C", "f"], dict(header=0, names=["A", "B", "C", "D"]), + None, _msg_validate_usecols_names.format(r"\['f'\]")), + (["A", "B", "f"], dict(names=["A", "B", "C", "D"]), + None, _msg_validate_usecols_names.format(r"\['f'\]")), +]) +def test_raises_on_usecols_names_mismatch(all_parsers, usecols, + kwargs, expected, msg): + data = "a,b,c,d\n1,2,3,4\n5,6,7,8" + kwargs.update(usecols=usecols) + parser = all_parsers + + if expected is None: + with pytest.raises(ValueError, match=msg): + parser.read_csv(StringIO(data), **kwargs) + else: + result = parser.read_csv(StringIO(data), **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.xfail( + reason="see gh-16469: works on the C engine but not the Python engine", + strict=False) +@pytest.mark.parametrize("usecols", [["A", "C"], [0, 2]]) +def test_usecols_subset_names_mismatch_orig_columns(all_parsers, usecols): + data = "a,b,c,d\n1,2,3,4\n5,6,7,8" + names = ["A", "B", "C", "D"] + parser = all_parsers + + result = parser.read_csv(StringIO(data), header=0, + names=names, usecols=usecols) + expected = DataFrame({"A": [1, 5], "C": [3, 7]}) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/io/parser/usecols.py b/pandas/tests/io/parser/usecols.py deleted file mode 100644 index 0cf642983e8d3..0000000000000 --- a/pandas/tests/io/parser/usecols.py +++ /dev/null @@ -1,477 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -Tests the usecols functionality during parsing -for all of the parsers defined in parsers.py -""" - -import pytest - -import numpy as np -import pandas.util.testing as tm - -from pandas import DataFrame, Index -from pandas._libs.lib import Timestamp -from pandas.compat import StringIO - - -class UsecolsTests(object): - - def test_raise_on_mixed_dtype_usecols(self): - # See gh-12678 - data = """a,b,c - 1000,2000,3000 - 4000,5000,6000 - """ - - msg = ("'usecols' must either be all strings, all unicode, " - "all integers or a callable") - usecols = [0, 'b', 2] - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(data), usecols=usecols) - - def test_usecols(self): - data = """\ -a,b,c -1,2,3 -4,5,6 -7,8,9 -10,11,12""" - - result = self.read_csv(StringIO(data), usecols=(1, 2)) - result2 = self.read_csv(StringIO(data), usecols=('b', 'c')) - exp = self.read_csv(StringIO(data)) - - self.assertEqual(len(result.columns), 2) - self.assertTrue((result['b'] == exp['b']).all()) - self.assertTrue((result['c'] == exp['c']).all()) - - tm.assert_frame_equal(result, result2) - - result = self.read_csv(StringIO(data), usecols=[1, 2], header=0, - names=['foo', 'bar']) - expected = self.read_csv(StringIO(data), usecols=[1, 2]) - expected.columns = ['foo', 'bar'] - tm.assert_frame_equal(result, expected) - - data = """\ -1,2,3 -4,5,6 -7,8,9 -10,11,12""" - result = self.read_csv(StringIO(data), names=['b', 'c'], - header=None, usecols=[1, 2]) - - expected = self.read_csv(StringIO(data), names=['a', 'b', 'c'], - header=None) - expected = expected[['b', 'c']] - tm.assert_frame_equal(result, expected) - - result2 = self.read_csv(StringIO(data), names=['a', 'b', 'c'], - header=None, usecols=['b', 'c']) - tm.assert_frame_equal(result2, result) - - # see gh-5766 - result = self.read_csv(StringIO(data), names=['a', 'b'], - header=None, usecols=[0, 1]) - - expected = self.read_csv(StringIO(data), names=['a', 'b', 'c'], - header=None) - expected = expected[['a', 'b']] - tm.assert_frame_equal(result, expected) - - # length conflict, passed names and usecols disagree - self.assertRaises(ValueError, self.read_csv, StringIO(data), - names=['a', 'b'], usecols=[1], header=None) - - def test_usecols_index_col_False(self): - # see gh-9082 - s = "a,b,c,d\n1,2,3,4\n5,6,7,8" - s_malformed = "a,b,c,d\n1,2,3,4,\n5,6,7,8," - cols = ['a', 'c', 'd'] - expected = DataFrame({'a': [1, 5], 'c': [3, 7], 'd': [4, 8]}) - df = self.read_csv(StringIO(s), usecols=cols, index_col=False) - tm.assert_frame_equal(expected, df) - df = self.read_csv(StringIO(s_malformed), - usecols=cols, index_col=False) - tm.assert_frame_equal(expected, df) - - def test_usecols_index_col_conflict(self): - # see gh-4201: test that index_col as integer reflects usecols - data = 'a,b,c,d\nA,a,1,one\nB,b,2,two' - expected = DataFrame({'c': [1, 2]}, index=Index( - ['a', 'b'], name='b')) - - df = self.read_csv(StringIO(data), usecols=['b', 'c'], - index_col=0) - tm.assert_frame_equal(expected, df) - - df = self.read_csv(StringIO(data), usecols=['b', 'c'], - index_col='b') - tm.assert_frame_equal(expected, df) - - df = self.read_csv(StringIO(data), usecols=[1, 2], - index_col='b') - tm.assert_frame_equal(expected, df) - - df = self.read_csv(StringIO(data), usecols=[1, 2], - index_col=0) - tm.assert_frame_equal(expected, df) - - expected = DataFrame( - {'b': ['a', 'b'], 'c': [1, 2], 'd': ('one', 'two')}) - expected = expected.set_index(['b', 'c']) - df = self.read_csv(StringIO(data), usecols=['b', 'c', 'd'], - index_col=['b', 'c']) - tm.assert_frame_equal(expected, df) - - def test_usecols_implicit_index_col(self): - # see gh-2654 - data = 'a,b,c\n4,apple,bat,5.7\n8,orange,cow,10' - - result = self.read_csv(StringIO(data), usecols=['a', 'b']) - expected = DataFrame({'a': ['apple', 'orange'], - 'b': ['bat', 'cow']}, index=[4, 8]) - - tm.assert_frame_equal(result, expected) - - def test_usecols_regex_sep(self): - # see gh-2733 - data = 'a b c\n4 apple bat 5.7\n8 orange cow 10' - - df = self.read_csv(StringIO(data), sep=r'\s+', usecols=('a', 'b')) - - expected = DataFrame({'a': ['apple', 'orange'], - 'b': ['bat', 'cow']}, index=[4, 8]) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_whitespace(self): - data = 'a b c\n4 apple bat 5.7\n8 orange cow 10' - - result = self.read_csv(StringIO(data), delim_whitespace=True, - usecols=('a', 'b')) - expected = DataFrame({'a': ['apple', 'orange'], - 'b': ['bat', 'cow']}, index=[4, 8]) - - tm.assert_frame_equal(result, expected) - - def test_usecols_with_integer_like_header(self): - data = """2,0,1 - 1000,2000,3000 - 4000,5000,6000 - """ - - usecols = [0, 1] # column selection by index - expected = DataFrame(data=[[1000, 2000], - [4000, 5000]], - columns=['2', '0']) - df = self.read_csv(StringIO(data), usecols=usecols) - tm.assert_frame_equal(df, expected) - - usecols = ['0', '1'] # column selection by name - expected = DataFrame(data=[[2000, 3000], - [5000, 6000]], - columns=['0', '1']) - df = self.read_csv(StringIO(data), usecols=usecols) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_parse_dates(self): - # See gh-9755 - s = """a,b,c,d,e - 0,1,20140101,0900,4 - 0,1,20140102,1000,4""" - parse_dates = [[1, 2]] - - cols = { - 'a': [0, 0], - 'c_d': [ - Timestamp('2014-01-01 09:00:00'), - Timestamp('2014-01-02 10:00:00') - ] - } - expected = DataFrame(cols, columns=['c_d', 'a']) - - df = self.read_csv(StringIO(s), usecols=[0, 2, 3], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(s), usecols=[3, 0, 2], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - # See gh-13604 - s = """2008-02-07 09:40,1032.43 - 2008-02-07 09:50,1042.54 - 2008-02-07 10:00,1051.65 - """ - parse_dates = [0] - names = ['date', 'values'] - usecols = names[:] - - index = Index([Timestamp('2008-02-07 09:40'), - Timestamp('2008-02-07 09:50'), - Timestamp('2008-02-07 10:00')], - name='date') - cols = {'values': [1032.43, 1042.54, 1051.65]} - expected = DataFrame(cols, index=index) - - df = self.read_csv(StringIO(s), parse_dates=parse_dates, index_col=0, - usecols=usecols, header=None, names=names) - tm.assert_frame_equal(df, expected) - - # See gh-14792 - s = """a,b,c,d,e,f,g,h,i,j - 2016/09/21,1,1,2,3,4,5,6,7,8""" - parse_dates = [0] - usecols = list('abcdefghij') - cols = {'a': Timestamp('2016-09-21'), - 'b': [1], 'c': [1], 'd': [2], - 'e': [3], 'f': [4], 'g': [5], - 'h': [6], 'i': [7], 'j': [8]} - expected = DataFrame(cols, columns=usecols) - df = self.read_csv(StringIO(s), usecols=usecols, - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - s = """a,b,c,d,e,f,g,h,i,j\n2016/09/21,1,1,2,3,4,5,6,7,8""" - parse_dates = [[0, 1]] - usecols = list('abcdefghij') - cols = {'a_b': '2016/09/21 1', - 'c': [1], 'd': [2], 'e': [3], 'f': [4], - 'g': [5], 'h': [6], 'i': [7], 'j': [8]} - expected = DataFrame(cols, columns=['a_b'] + list('cdefghij')) - df = self.read_csv(StringIO(s), usecols=usecols, - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_parse_dates_and_full_names(self): - # See gh-9755 - s = """0,1,20140101,0900,4 - 0,1,20140102,1000,4""" - parse_dates = [[1, 2]] - names = list('abcde') - - cols = { - 'a': [0, 0], - 'c_d': [ - Timestamp('2014-01-01 09:00:00'), - Timestamp('2014-01-02 10:00:00') - ] - } - expected = DataFrame(cols, columns=['c_d', 'a']) - - df = self.read_csv(StringIO(s), names=names, - usecols=[0, 2, 3], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(s), names=names, - usecols=[3, 0, 2], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_parse_dates_and_usecol_names(self): - # See gh-9755 - s = """0,1,20140101,0900,4 - 0,1,20140102,1000,4""" - parse_dates = [[1, 2]] - names = list('acd') - - cols = { - 'a': [0, 0], - 'c_d': [ - Timestamp('2014-01-01 09:00:00'), - Timestamp('2014-01-02 10:00:00') - ] - } - expected = DataFrame(cols, columns=['c_d', 'a']) - - df = self.read_csv(StringIO(s), names=names, - usecols=[0, 2, 3], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - df = self.read_csv(StringIO(s), names=names, - usecols=[3, 0, 2], - parse_dates=parse_dates) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_unicode_strings(self): - # see gh-13219 - - s = '''AAA,BBB,CCC,DDD - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - - data = { - 'AAA': { - 0: 0.056674972999999997, - 1: 2.6132309819999997, - 2: 3.5689350380000002 - }, - 'BBB': {0: 8, 1: 2, 2: 7} - } - expected = DataFrame(data) - - df = self.read_csv(StringIO(s), usecols=[u'AAA', u'BBB']) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_single_byte_unicode_strings(self): - # see gh-13219 - - s = '''A,B,C,D - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - - data = { - 'A': { - 0: 0.056674972999999997, - 1: 2.6132309819999997, - 2: 3.5689350380000002 - }, - 'B': {0: 8, 1: 2, 2: 7} - } - expected = DataFrame(data) - - df = self.read_csv(StringIO(s), usecols=[u'A', u'B']) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_mixed_encoding_strings(self): - s = '''AAA,BBB,CCC,DDD - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - - msg = ("'usecols' must either be all strings, all unicode, " - "all integers or a callable") - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(s), usecols=[u'AAA', b'BBB']) - - with tm.assertRaisesRegexp(ValueError, msg): - self.read_csv(StringIO(s), usecols=[b'AAA', u'BBB']) - - def test_usecols_with_multibyte_characters(self): - s = '''あああ,いい,ううう,ええええ - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - data = { - 'あああ': { - 0: 0.056674972999999997, - 1: 2.6132309819999997, - 2: 3.5689350380000002 - }, - 'いい': {0: 8, 1: 2, 2: 7} - } - expected = DataFrame(data) - - df = self.read_csv(StringIO(s), usecols=['あああ', 'いい']) - tm.assert_frame_equal(df, expected) - - def test_usecols_with_multibyte_unicode_characters(self): - pytest.skip('TODO: see gh-13253') - - s = '''あああ,いい,ううう,ええええ - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - data = { - 'あああ': { - 0: 0.056674972999999997, - 1: 2.6132309819999997, - 2: 3.5689350380000002 - }, - 'いい': {0: 8, 1: 2, 2: 7} - } - expected = DataFrame(data) - - df = self.read_csv(StringIO(s), usecols=[u'あああ', u'いい']) - tm.assert_frame_equal(df, expected) - - def test_empty_usecols(self): - # should not raise - data = 'a,b,c\n1,2,3\n4,5,6' - expected = DataFrame() - result = self.read_csv(StringIO(data), usecols=set([])) - tm.assert_frame_equal(result, expected) - - def test_np_array_usecols(self): - # See gh-12546 - data = 'a,b,c\n1,2,3' - usecols = np.array(['a', 'b']) - - expected = DataFrame([[1, 2]], columns=usecols) - result = self.read_csv(StringIO(data), usecols=usecols) - tm.assert_frame_equal(result, expected) - - def test_callable_usecols(self): - # See gh-14154 - s = '''AaA,bBb,CCC,ddd - 0.056674973,8,True,a - 2.613230982,2,False,b - 3.568935038,7,False,a - ''' - - data = { - 'AaA': { - 0: 0.056674972999999997, - 1: 2.6132309819999997, - 2: 3.5689350380000002 - }, - 'bBb': {0: 8, 1: 2, 2: 7}, - 'ddd': {0: 'a', 1: 'b', 2: 'a'} - } - expected = DataFrame(data) - df = self.read_csv(StringIO(s), usecols=lambda x: - x.upper() in ['AAA', 'BBB', 'DDD']) - tm.assert_frame_equal(df, expected) - - # Check that a callable returning only False returns - # an empty DataFrame - expected = DataFrame() - df = self.read_csv(StringIO(s), usecols=lambda x: False) - tm.assert_frame_equal(df, expected) - - def test_incomplete_first_row(self): - # see gh-6710 - data = '1,2\n1,2,3' - names = ['a', 'b', 'c'] - expected = DataFrame({'a': [1, 1], - 'c': [np.nan, 3]}) - - usecols = ['a', 'c'] - df = self.read_csv(StringIO(data), names=names, usecols=usecols) - tm.assert_frame_equal(df, expected) - - usecols = lambda x: x in ['a', 'c'] - df = self.read_csv(StringIO(data), names=names, usecols=usecols) - tm.assert_frame_equal(df, expected) - - def test_uneven_length_cols(self): - # see gh-8985 - usecols = [0, 1, 2] - data = '19,29,39\n' * 2 + '10,20,30,40' - expected = DataFrame([[19, 29, 39], - [19, 29, 39], - [10, 20, 30]]) - df = self.read_csv(StringIO(data), header=None, usecols=usecols) - tm.assert_frame_equal(df, expected) - - # see gh-9549 - usecols = ['A', 'B', 'C'] - data = ('A,B,C\n1,2,3\n3,4,5\n1,2,4,5,1,6\n' - '1,2,3,,,1,\n1,2,3\n5,6,7') - expected = DataFrame({'A': [1, 3, 1, 1, 1, 5], - 'B': [2, 4, 2, 2, 2, 6], - 'C': [3, 5, 4, 3, 3, 7]}) - df = self.read_csv(StringIO(data), usecols=usecols) - tm.assert_frame_equal(df, expected) diff --git a/pandas/tests/io/sas/data/cars.sas7bdat b/pandas/tests/io/sas/data/cars.sas7bdat new file mode 100644 index 0000000000000000000000000000000000000000..ca5d3474c36ad4532d1b87d1f872491c9bee8f1f GIT binary patch literal 13312 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 literal 0 HcmV?d00001 diff --git a/pandas/tests/io/sas/data/datetime.csv b/pandas/tests/io/sas/data/datetime.csv new file mode 100644 index 0000000000000..6126f6d04eaf0 --- /dev/null +++ b/pandas/tests/io/sas/data/datetime.csv @@ -0,0 +1,5 @@ +Date1,Date2,DateTime,DateTimeHi,Taiw +1677-09-22,1677-09-22,1677-09-21 00:12:44,1677-09-21 00:12:43.145226,1912-01-01 +1960-01-01,1960-01-01,1960-01-01 00:00:00,1960-01-01 00:00:00.000000,1960-01-01 +2016-02-29,2016-02-29,2016-02-29 23:59:59,2016-02-29 23:59:59.123456,2016-02-29 +2262-04-11,2262-04-11,2262-04-11 23:47:16,2262-04-11 23:47:16.854774,2262-04-11 diff --git a/pandas/tests/io/data/legacy_hdf/legacy_0.10.h5 b/pandas/tests/io/sas/data/datetime.sas7bdat similarity index 54% rename from pandas/tests/io/data/legacy_hdf/legacy_0.10.h5 rename to pandas/tests/io/sas/data/datetime.sas7bdat index b1439ef16361abbc0756fbf7d344fd65d8a1a473..6469dbf29f8eeeafb4703f01657eae4d0872ef10 100644 GIT binary patch literal 131072 zcmeIy!E0Pa7y$6uG_|QjNibjzl`cUf(556Ypaq4jB(VY0M!VHk1REh>Al?e~Ac){W zXbvg`59Z)O`~zAqf?_Du9z_sAk>Ei*cn)gQHtWpnd)dueN>l`e--MU%n|a^Ny!pMC zgwSeNetF~U<$?aMK6|$(?7ud)GI+4Be`qL-4^_IxySK~sRCbJyS7OQIA@uF3lqDx? zC#y5FPfbtN>eKbvx#{uh-)nJxEzNx3`He%LeED7N<#$ef^l6NFpTRFCTQO~2i9fA& z^vLMM=-AxYa}STd_{jV-Ct7#@T9)>Hdf@%z<<&X+^6JQX8Y`c%^S(X-0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1pY4qmsUsC%V%BwJ!W0~;hq?(TOs^dY2F!#d2dnl)6Eb@i}CrG zUrgh%KM?cBiu~%05ax^g?U+v#`3q$p`@?Pd#%56y$Jg5Om5#hvCWYKSICOn2CCykp zyz*YSa`D@Ta?Aw>zs~n_zKdUzJ>N{GvSwWSd}8VQ+Q&a%%ds^c?{922>vF7qc;Ra0 ziMVf$xvsdMa*Y510t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oU&Z0$Vp4jdF#%W71Q0a(pvXVtg&!wPUn> z&VjH$+#j!2JD2AycdXPt=e}5+u2wsptLN`9XJ0Jd`D{~ppUKeOS|(=g`~3Ml6M4?z z&hs=Hjp6Xa>uI5Cy!M8Dv2Jhb#&)Zbc1XQS*xI(b8Rtx;+eORI)?LZZSmu54UYQS; zc~2$pQI41M<8qDBvJ)OGc7L>p$BMI`4e3lFyt?Dmx8mHlw&%t(bhKxe7SGogURs`7 zIJY&~2+o12OcKNIsbHlAXwK3(?fORp@ZetmXsdOOCtG=6NUS)1;q`uf7s`MKHS zPo6r_TrUnaTLenal3e~GQSne?gV*8ZM~q4IU*?jLC&^^5=j literal 238321 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 literal 211111 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 diff --git a/pandas/tests/io/sas/test_sas.py b/pandas/tests/io/sas/test_sas.py index 237e3676c3b3d..34bca1e5b74a1 100644 --- a/pandas/tests/io/sas/test_sas.py +++ b/pandas/tests/io/sas/test_sas.py @@ -1,13 +1,25 @@ -import pandas.util.testing as tm +import pytest + from pandas.compat import StringIO + from pandas import read_sas +import pandas.util.testing as tm -class TestSas(tm.TestCase): +class TestSas(object): def test_sas_buffer_format(self): - - # GH14947 + # see gh-14947 b = StringIO("") - with self.assertRaises(ValueError): + + msg = ("If this is a buffer object rather than a string " + "name, you must specify a format string") + with pytest.raises(ValueError, match=msg): read_sas(b) + + def test_sas_read_no_format_or_extension(self): + # see gh-24548 + msg = ("unable to infer format of SAS file") + with tm.ensure_clean('test_file_no_extension') as path: + with pytest.raises(ValueError, match=msg): + read_sas(path) diff --git a/pandas/tests/io/sas/test_sas7bdat.py b/pandas/tests/io/sas/test_sas7bdat.py index 69073a90e9669..3dd8d0449ef5f 100644 --- a/pandas/tests/io/sas/test_sas7bdat.py +++ b/pandas/tests/io/sas/test_sas7bdat.py @@ -1,19 +1,29 @@ -import pandas as pd -from pandas.compat import PY2 -import pandas.util.testing as tm -import os import io +import os + import numpy as np +import pytest + +from pandas.compat import PY2 +from pandas.errors import EmptyDataError +import pandas.util._test_decorators as td + +import pandas as pd +import pandas.util.testing as tm -class TestSAS7BDAT(tm.TestCase): +# https://github.com/cython/cython/issues/1720 +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") +class TestSAS7BDAT(object): - def setUp(self): - self.dirpath = tm.get_data_path() + @pytest.fixture(autouse=True) + def setup_method(self, datapath): + self.dirpath = datapath("io", "sas", "data") self.data = [] self.test_ix = [list(range(1, 16)), [16]] for j in 1, 2: - fname = os.path.join(self.dirpath, "test_sas7bdat_%d.csv" % j) + fname = os.path.join( + self.dirpath, "test_sas7bdat_{j}.csv".format(j=j)) df = pd.read_csv(fname) epoch = pd.datetime(1960, 1, 1) t1 = pd.to_timedelta(df["Column4"], unit='d') @@ -35,7 +45,8 @@ def test_from_file(self): for j in 0, 1: df0 = self.data[j] for k in self.test_ix[j]: - fname = os.path.join(self.dirpath, "test%d.sas7bdat" % k) + fname = os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k)) df = pd.read_sas(fname, encoding='utf-8') tm.assert_frame_equal(df, df0) @@ -43,7 +54,8 @@ def test_from_buffer(self): for j in 0, 1: df0 = self.data[j] for k in self.test_ix[j]: - fname = os.path.join(self.dirpath, "test%d.sas7bdat" % k) + fname = os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k)) with open(fname, 'rb') as f: byts = f.read() buf = io.BytesIO(byts) @@ -57,7 +69,8 @@ def test_from_iterator(self): for j in 0, 1: df0 = self.data[j] for k in self.test_ix[j]: - fname = os.path.join(self.dirpath, "test%d.sas7bdat" % k) + fname = os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k)) rdr = pd.read_sas(fname, iterator=True, encoding='utf-8') df = rdr.read(2) tm.assert_frame_equal(df, df0.iloc[0:2, :]) @@ -65,23 +78,46 @@ def test_from_iterator(self): tm.assert_frame_equal(df, df0.iloc[2:5, :]) rdr.close() + @td.skip_if_no('pathlib') + def test_path_pathlib(self): + from pathlib import Path + for j in 0, 1: + df0 = self.data[j] + for k in self.test_ix[j]: + fname = Path(os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k))) + df = pd.read_sas(fname, encoding='utf-8') + tm.assert_frame_equal(df, df0) + + @td.skip_if_no('py.path') + def test_path_localpath(self): + from py.path import local as LocalPath + for j in 0, 1: + df0 = self.data[j] + for k in self.test_ix[j]: + fname = LocalPath(os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k))) + df = pd.read_sas(fname, encoding='utf-8') + tm.assert_frame_equal(df, df0) + def test_iterator_loop(self): # github #13654 for j in 0, 1: for k in self.test_ix[j]: for chunksize in 3, 5, 10, 11: - fname = os.path.join(self.dirpath, "test%d.sas7bdat" % k) + fname = os.path.join( + self.dirpath, "test{k}.sas7bdat".format(k=k)) rdr = pd.read_sas(fname, chunksize=10, encoding='utf-8') y = 0 for x in rdr: y += x.shape[0] - self.assertTrue(y == rdr.row_count) + assert y == rdr.row_count rdr.close() def test_iterator_read_too_much(self): # github #14734 k = self.test_ix[0][0] - fname = os.path.join(self.dirpath, "test%d.sas7bdat" % k) + fname = os.path.join(self.dirpath, "test{k}.sas7bdat".format(k=k)) rdr = pd.read_sas(fname, format="sas7bdat", iterator=True, encoding='utf-8') d1 = rdr.read(rdr.row_count + 20) @@ -93,9 +129,8 @@ def test_iterator_read_too_much(self): rdr.close() -def test_encoding_options(): - dirpath = tm.get_data_path() - fname = os.path.join(dirpath, "test1.sas7bdat") +def test_encoding_options(datapath): + fname = datapath("io", "sas", "data", "test1.sas7bdat") df1 = pd.read_sas(fname) df2 = pd.read_sas(fname, encoding='utf-8') for col in df1.columns: @@ -113,32 +148,80 @@ def test_encoding_options(): assert(x == y.decode()) -def test_productsales(): - dirpath = tm.get_data_path() - fname = os.path.join(dirpath, "productsales.sas7bdat") +def test_productsales(datapath): + fname = datapath("io", "sas", "data", "productsales.sas7bdat") df = pd.read_sas(fname, encoding='utf-8') - fname = os.path.join(dirpath, "productsales.csv") - df0 = pd.read_csv(fname) - vn = ["ACTUAL", "PREDICT", "QUARTER", "YEAR", "MONTH"] + fname = datapath("io", "sas", "data", "productsales.csv") + df0 = pd.read_csv(fname, parse_dates=['MONTH']) + vn = ["ACTUAL", "PREDICT", "QUARTER", "YEAR"] df0[vn] = df0[vn].astype(np.float64) tm.assert_frame_equal(df, df0) -def test_12659(): - dirpath = tm.get_data_path() - fname = os.path.join(dirpath, "test_12659.sas7bdat") +def test_12659(datapath): + fname = datapath("io", "sas", "data", "test_12659.sas7bdat") df = pd.read_sas(fname) - fname = os.path.join(dirpath, "test_12659.csv") + fname = datapath("io", "sas", "data", "test_12659.csv") df0 = pd.read_csv(fname) df0 = df0.astype(np.float64) tm.assert_frame_equal(df, df0) -def test_airline(): - dirpath = tm.get_data_path() - fname = os.path.join(dirpath, "airline.sas7bdat") +def test_airline(datapath): + fname = datapath("io", "sas", "data", "airline.sas7bdat") df = pd.read_sas(fname) - fname = os.path.join(dirpath, "airline.csv") + fname = datapath("io", "sas", "data", "airline.csv") df0 = pd.read_csv(fname) df0 = df0.astype(np.float64) tm.assert_frame_equal(df, df0, check_exact=False) + + +def test_date_time(datapath): + # Support of different SAS date/datetime formats (PR #15871) + fname = datapath("io", "sas", "data", "datetime.sas7bdat") + df = pd.read_sas(fname) + fname = datapath("io", "sas", "data", "datetime.csv") + df0 = pd.read_csv(fname, parse_dates=['Date1', 'Date2', 'DateTime', + 'DateTimeHi', 'Taiw']) + # GH 19732: Timestamps imported from sas will incur floating point errors + df.iloc[:, 3] = df.iloc[:, 3].dt.round('us') + tm.assert_frame_equal(df, df0) + + +def test_compact_numerical_values(datapath): + # Regression test for #21616 + fname = datapath("io", "sas", "data", "cars.sas7bdat") + df = pd.read_sas(fname, encoding='latin-1') + # The two columns CYL and WGT in cars.sas7bdat have column + # width < 8 and only contain integral values. + # Test that pandas doesn't corrupt the numbers by adding + # decimals. + result = df['WGT'] + expected = df['WGT'].round() + tm.assert_series_equal(result, expected, check_exact=True) + result = df['CYL'] + expected = df['CYL'].round() + tm.assert_series_equal(result, expected, check_exact=True) + + +def test_many_columns(datapath): + # Test for looking for column information in more places (PR #22628) + fname = datapath("io", "sas", "data", "many_columns.sas7bdat") + df = pd.read_sas(fname, encoding='latin-1') + fname = datapath("io", "sas", "data", "many_columns.csv") + df0 = pd.read_csv(fname, encoding='latin-1') + tm.assert_frame_equal(df, df0) + + +def test_inconsistent_number_of_rows(datapath): + # Regression test for issue #16615. (PR #22628) + fname = datapath("io", "sas", "data", "load_log.sas7bdat") + df = pd.read_sas(fname, encoding='latin-1') + assert len(df) == 2097 + + +def test_zero_variables(datapath): + # Check if the SAS file has zero variables (PR #18184) + fname = datapath("io", "sas", "data", "zero_variables.sas7bdat") + with pytest.raises(EmptyDataError): + pd.read_sas(fname) diff --git a/pandas/tests/io/sas/test_xport.py b/pandas/tests/io/sas/test_xport.py index fe2f7cb4bf4be..1b086daf51c41 100644 --- a/pandas/tests/io/sas/test_xport.py +++ b/pandas/tests/io/sas/test_xport.py @@ -1,8 +1,12 @@ +import os + +import numpy as np +import pytest + import pandas as pd import pandas.util.testing as tm + from pandas.io.sas.sasreader import read_sas -import numpy as np -import os # CSV versions of test xpt files were obtained using the R foreign library @@ -16,10 +20,11 @@ def numeric_as_float(data): data[v] = data[v].astype(np.float64) -class TestXport(tm.TestCase): +class TestXport(object): - def setUp(self): - self.dirpath = tm.get_data_path() + @pytest.fixture(autouse=True) + def setup_method(self, datapath): + self.dirpath = datapath("io", "sas", "data") self.file01 = os.path.join(self.dirpath, "DEMO_G.xpt") self.file02 = os.path.join(self.dirpath, "SSHSV1_A.xpt") self.file03 = os.path.join(self.dirpath, "DRXFCD_G.xpt") @@ -40,7 +45,7 @@ def test1_basic(self): # Test reading beyond end of file reader = read_sas(self.file01, format="xport", iterator=True) data = reader.read(num_rows + 100) - self.assertTrue(data.shape[0] == num_rows) + assert data.shape[0] == num_rows reader.close() # Test incremental read with `read` method. @@ -61,7 +66,7 @@ def test1_basic(self): for x in reader: m += x.shape[0] reader.close() - self.assertTrue(m == num_rows) + assert m == num_rows # Read full file with `read_sas` method data = read_sas(self.file01) diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 2e701143357e3..565db92210b0a 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -1,109 +1,205 @@ # -*- coding: utf-8 -*- +from textwrap import dedent + import numpy as np from numpy.random import randint - import pytest -import pandas as pd -from pandas import DataFrame -from pandas import read_clipboard -from pandas import get_option +from pandas.compat import PY2 + +import pandas as pd +from pandas import DataFrame, get_option, read_clipboard from pandas.util import testing as tm from pandas.util.testing import makeCustomDataframe as mkdf -from pandas.util.clipboard.exceptions import PyperclipException +from pandas.io.clipboard import clipboard_get, clipboard_set +from pandas.io.clipboard.exceptions import PyperclipException try: DataFrame({'A': [1, 2]}).to_clipboard() _DEPS_INSTALLED = 1 -except PyperclipException: +except (PyperclipException, RuntimeError): _DEPS_INSTALLED = 0 +def build_kwargs(sep, excel): + kwargs = {} + if excel != 'default': + kwargs['excel'] = excel + if sep != 'default': + kwargs['sep'] = sep + return kwargs + + +@pytest.fixture(params=['delims', 'utf8', 'utf16', 'string', 'long', + 'nonascii', 'colwidth', 'mixed', 'float', 'int']) +def df(request): + data_type = request.param + + if data_type == 'delims': + return pd.DataFrame({'a': ['"a,\t"b|c', 'd\tef´'], + 'b': ['hi\'j', 'k\'\'lm']}) + elif data_type == 'utf8': + return pd.DataFrame({'a': ['µasd', 'Ωœ∑´'], + 'b': ['øπ∆˚¬', 'œ∑´®']}) + elif data_type == 'utf16': + return pd.DataFrame({'a': ['\U0001f44d\U0001f44d', + '\U0001f44d\U0001f44d'], + 'b': ['abc', 'def']}) + elif data_type == 'string': + return mkdf(5, 3, c_idx_type='s', r_idx_type='i', + c_idx_names=[None], r_idx_names=[None]) + elif data_type == 'long': + max_rows = get_option('display.max_rows') + return mkdf(max_rows + 1, 3, + data_gen_f=lambda *args: randint(2), + c_idx_type='s', r_idx_type='i', + c_idx_names=[None], r_idx_names=[None]) + elif data_type == 'nonascii': + return pd.DataFrame({'en': 'in English'.split(), + 'es': 'en español'.split()}) + elif data_type == 'colwidth': + _cw = get_option('display.max_colwidth') + 1 + return mkdf(5, 3, data_gen_f=lambda *args: 'x' * _cw, + c_idx_type='s', r_idx_type='i', + c_idx_names=[None], r_idx_names=[None]) + elif data_type == 'mixed': + return DataFrame({'a': np.arange(1.0, 6.0) + 0.01, + 'b': np.arange(1, 6), + 'c': list('abcde')}) + elif data_type == 'float': + return mkdf(5, 3, data_gen_f=lambda r, c: float(r) + 0.01, + c_idx_type='s', r_idx_type='i', + c_idx_names=[None], r_idx_names=[None]) + elif data_type == 'int': + return mkdf(5, 3, data_gen_f=lambda *args: randint(2), + c_idx_type='s', r_idx_type='i', + c_idx_names=[None], r_idx_names=[None]) + else: + raise ValueError + + +@pytest.fixture +def mock_clipboard(monkeypatch, request): + """Fixture mocking clipboard IO. + + This mocks pandas.io.clipboard.clipboard_get and + pandas.io.clipboard.clipboard_set. + + This uses a local dict for storing data. The dictionary + key used is the test ID, available with ``request.node.name``. + + This returns the local dictionary, for direct manipulation by + tests. + """ + + # our local clipboard for tests + _mock_data = {} + + def _mock_set(data): + _mock_data[request.node.name] = data + + def _mock_get(): + return _mock_data[request.node.name] + + monkeypatch.setattr("pandas.io.clipboard.clipboard_set", _mock_set) + monkeypatch.setattr("pandas.io.clipboard.clipboard_get", _mock_get) + + yield _mock_data + + +@pytest.mark.clipboard +def test_mock_clipboard(mock_clipboard): + import pandas.io.clipboard + pandas.io.clipboard.clipboard_set("abc") + assert "abc" in set(mock_clipboard.values()) + result = pandas.io.clipboard.clipboard_get() + assert result == "abc" + + @pytest.mark.single +@pytest.mark.clipboard @pytest.mark.skipif(not _DEPS_INSTALLED, reason="clipboard primitives not installed") -class TestClipboard(tm.TestCase): - - @classmethod - def setUpClass(cls): - super(TestClipboard, cls).setUpClass() - cls.data = {} - cls.data['string'] = mkdf(5, 3, c_idx_type='s', r_idx_type='i', - c_idx_names=[None], r_idx_names=[None]) - cls.data['int'] = mkdf(5, 3, data_gen_f=lambda *args: randint(2), - c_idx_type='s', r_idx_type='i', - c_idx_names=[None], r_idx_names=[None]) - cls.data['float'] = mkdf(5, 3, - data_gen_f=lambda r, c: float(r) + 0.01, - c_idx_type='s', r_idx_type='i', - c_idx_names=[None], r_idx_names=[None]) - cls.data['mixed'] = DataFrame({'a': np.arange(1.0, 6.0) + 0.01, - 'b': np.arange(1, 6), - 'c': list('abcde')}) - - # Test columns exceeding "max_colwidth" (GH8305) - _cw = get_option('display.max_colwidth') + 1 - cls.data['colwidth'] = mkdf(5, 3, data_gen_f=lambda *args: 'x' * _cw, - c_idx_type='s', r_idx_type='i', - c_idx_names=[None], r_idx_names=[None]) - # Test GH-5346 - max_rows = get_option('display.max_rows') - cls.data['longdf'] = mkdf(max_rows + 1, 3, - data_gen_f=lambda *args: randint(2), - c_idx_type='s', r_idx_type='i', - c_idx_names=[None], r_idx_names=[None]) - # Test for non-ascii text: GH9263 - cls.data['nonascii'] = pd.DataFrame({'en': 'in English'.split(), - 'es': 'en español'.split()}) - # unicode round trip test for GH 13747, GH 12529 - cls.data['utf8'] = pd.DataFrame({'a': ['µasd', 'Ωœ∑´'], - 'b': ['øπ∆˚¬', 'œ∑´®']}) - cls.data_types = list(cls.data.keys()) - - @classmethod - def tearDownClass(cls): - super(TestClipboard, cls).tearDownClass() - del cls.data_types, cls.data - - def check_round_trip_frame(self, data_type, excel=None, sep=None, +@pytest.mark.usefixtures("mock_clipboard") +class TestClipboard(object): + + def check_round_trip_frame(self, data, excel=None, sep=None, encoding=None): - data = self.data[data_type] data.to_clipboard(excel=excel, sep=sep, encoding=encoding) - if sep is not None: - result = read_clipboard(sep=sep, index_col=0, encoding=encoding) - else: - result = read_clipboard(encoding=encoding) + result = read_clipboard(sep=sep or '\t', index_col=0, + encoding=encoding) tm.assert_frame_equal(data, result, check_dtype=False) - def test_round_trip_frame_sep(self): - for dt in self.data_types: - self.check_round_trip_frame(dt, sep=',') - self.check_round_trip_frame(dt, sep=r'\s+') - self.check_round_trip_frame(dt, sep='|') - - def test_round_trip_frame_string(self): - for dt in self.data_types: - self.check_round_trip_frame(dt, excel=False) - - def test_round_trip_frame(self): - for dt in self.data_types: - self.check_round_trip_frame(dt) - - def test_read_clipboard_infer_excel(self): - from textwrap import dedent - from pandas.util.clipboard import clipboard_set + # Test that default arguments copy as tab delimited + def test_round_trip_frame(self, df): + self.check_round_trip_frame(df) + + # Test that explicit delimiters are respected + @pytest.mark.parametrize('sep', ['\t', ',', '|']) + def test_round_trip_frame_sep(self, df, sep): + self.check_round_trip_frame(df, sep=sep) + + # Test white space separator + def test_round_trip_frame_string(self, df): + df.to_clipboard(excel=False, sep=None) + result = read_clipboard() + assert df.to_string() == result.to_string() + assert df.shape == result.shape + + # Two character separator is not supported in to_clipboard + # Test that multi-character separators are not silently passed + def test_excel_sep_warning(self, df): + with tm.assert_produces_warning(): + df.to_clipboard(excel=True, sep=r'\t') + + # Separator is ignored when excel=False and should produce a warning + def test_copy_delim_warning(self, df): + with tm.assert_produces_warning(): + df.to_clipboard(excel=False, sep='\t') + + # Tests that the default behavior of to_clipboard is tab + # delimited and excel="True" + @pytest.mark.parametrize('sep', ['\t', None, 'default']) + @pytest.mark.parametrize('excel', [True, None, 'default']) + def test_clipboard_copy_tabs_default(self, sep, excel, df, request, + mock_clipboard): + kwargs = build_kwargs(sep, excel) + df.to_clipboard(**kwargs) + if PY2: + # to_clipboard copies unicode, to_csv produces bytes. This is + # expected behavior + result = mock_clipboard[request.node.name].encode('utf-8') + expected = df.to_csv(sep='\t') + assert result == expected + else: + assert mock_clipboard[request.node.name] == df.to_csv(sep='\t') + + # Tests reading of white space separated tables + @pytest.mark.parametrize('sep', [None, 'default']) + @pytest.mark.parametrize('excel', [False]) + def test_clipboard_copy_strings(self, sep, excel, df): + kwargs = build_kwargs(sep, excel) + df.to_clipboard(**kwargs) + result = read_clipboard(sep=r'\s+') + assert result.to_string() == df.to_string() + assert df.shape == result.shape + + def test_read_clipboard_infer_excel(self, request, + mock_clipboard): + # gh-19010: avoid warnings + clip_kwargs = dict(engine="python") text = dedent(""" John James Charlie Mingus 1 2 4 Harry Carney """.strip()) - clipboard_set(text) - df = pd.read_clipboard() + mock_clipboard[request.node.name] = text + df = pd.read_clipboard(**clip_kwargs) # excel data is parsed correctly - self.assertEqual(df.iloc[1][1], 'Harry Carney') + assert df.iloc[1][1] == 'Harry Carney' # having diff tab counts doesn't trigger it text = dedent(""" @@ -111,28 +207,37 @@ def test_read_clipboard_infer_excel(self): 1 2 3 4 """.strip()) - clipboard_set(text) - res = pd.read_clipboard() + mock_clipboard[request.node.name] = text + res = pd.read_clipboard(**clip_kwargs) text = dedent(""" a b 1 2 3 4 """.strip()) - clipboard_set(text) - exp = pd.read_clipboard() + mock_clipboard[request.node.name] = text + exp = pd.read_clipboard(**clip_kwargs) tm.assert_frame_equal(res, exp) - def test_invalid_encoding(self): + def test_invalid_encoding(self, df): # test case for testing invalid encoding - data = self.data['string'] - with tm.assertRaises(ValueError): - data.to_clipboard(encoding='ascii') - with tm.assertRaises(NotImplementedError): + with pytest.raises(ValueError): + df.to_clipboard(encoding='ascii') + with pytest.raises(NotImplementedError): pd.read_clipboard(encoding='ascii') - def test_round_trip_valid_encodings(self): - for enc in ['UTF-8', 'utf-8', 'utf8']: - for dt in self.data_types: - self.check_round_trip_frame(dt, encoding=enc) + @pytest.mark.parametrize('enc', ['UTF-8', 'utf-8', 'utf8']) + def test_round_trip_valid_encodings(self, enc, df): + self.check_round_trip_frame(df, encoding=enc) + + +@pytest.mark.single +@pytest.mark.clipboard +@pytest.mark.skipif(not _DEPS_INSTALLED, + reason="clipboard primitives not installed") +@pytest.mark.parametrize('data', [u'\U0001f44d...', u'Ωœ∑´...', 'abcd...']) +def test_raw_roundtrip(data): + # PR #25040 wide unicode wasn't copied correctly on PY3 on windows + clipboard_set(data) + assert data == clipboard_get() diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 3c980cae3351a..3354bca63be92 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -1,30 +1,50 @@ """ - Tests for the pandas.io.common functionalities +Tests for the pandas.io.common functionalities """ import mmap import os -from os.path import isabs -import pandas.util.testing as tm +import pytest -from pandas.io import common -from pandas.compat import is_platform_windows, StringIO +from pandas.compat import FileNotFoundError, StringIO, is_platform_windows +import pandas.util._test_decorators as td -from pandas import read_csv, concat import pandas as pd +import pandas.util.testing as tm + +import pandas.io.common as icom + + +class CustomFSPath(object): + """For testing fspath on unknown objects""" + def __init__(self, path): + self.path = path + + def __fspath__(self): + return self.path + + +# Functions that consume a string path and return a string or path-like object +path_types = [str, CustomFSPath] try: from pathlib import Path + path_types.append(Path) except ImportError: pass try: from py.path import local as LocalPath + path_types.append(LocalPath) except ImportError: pass +HERE = os.path.abspath(os.path.dirname(__file__)) + -class TestCommonIOCapabilities(tm.TestCase): +# https://github.com/cython/cython/issues/1720 +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") +class TestCommonIOCapabilities(object): data1 = """index,A,B,C,D foo,2,3,4,5 bar,7,8,9,10 @@ -36,84 +56,252 @@ class TestCommonIOCapabilities(tm.TestCase): def test_expand_user(self): filename = '~/sometest' - expanded_name = common._expand_user(filename) + expanded_name = icom._expand_user(filename) - self.assertNotEqual(expanded_name, filename) - self.assertTrue(isabs(expanded_name)) - self.assertEqual(os.path.expanduser(filename), expanded_name) + assert expanded_name != filename + assert os.path.isabs(expanded_name) + assert os.path.expanduser(filename) == expanded_name def test_expand_user_normal_path(self): filename = '/somefolder/sometest' - expanded_name = common._expand_user(filename) + expanded_name = icom._expand_user(filename) - self.assertEqual(expanded_name, filename) - self.assertEqual(os.path.expanduser(filename), expanded_name) + assert expanded_name == filename + assert os.path.expanduser(filename) == expanded_name + @td.skip_if_no('pathlib') def test_stringify_path_pathlib(self): - tm._skip_if_no_pathlib() - - rel_path = common._stringify_path(Path('.')) - self.assertEqual(rel_path, '.') - redundant_path = common._stringify_path(Path('foo//bar')) - self.assertEqual(redundant_path, os.path.join('foo', 'bar')) + rel_path = icom._stringify_path(Path('.')) + assert rel_path == '.' + redundant_path = icom._stringify_path(Path('foo//bar')) + assert redundant_path == os.path.join('foo', 'bar') + @td.skip_if_no('py.path') def test_stringify_path_localpath(self): - tm._skip_if_no_localpath() - path = os.path.join('foo', 'bar') abs_path = os.path.abspath(path) lpath = LocalPath(path) - self.assertEqual(common._stringify_path(lpath), abs_path) + assert icom._stringify_path(lpath) == abs_path + + def test_stringify_path_fspath(self): + p = CustomFSPath('foo/bar.csv') + result = icom._stringify_path(p) + assert result == 'foo/bar.csv' + + @pytest.mark.parametrize('extension,expected', [ + ('', None), + ('.gz', 'gzip'), + ('.bz2', 'bz2'), + ('.zip', 'zip'), + ('.xz', 'xz'), + ]) + @pytest.mark.parametrize('path_type', path_types) + def test_infer_compression_from_path(self, extension, expected, path_type): + path = path_type('foo/bar.csv' + extension) + compression = icom._infer_compression(path, compression='infer') + assert compression == expected def test_get_filepath_or_buffer_with_path(self): filename = '~/sometest' - filepath_or_buffer, _, _ = common.get_filepath_or_buffer(filename) - self.assertNotEqual(filepath_or_buffer, filename) - self.assertTrue(isabs(filepath_or_buffer)) - self.assertEqual(os.path.expanduser(filename), filepath_or_buffer) + filepath_or_buffer, _, _, should_close = icom.get_filepath_or_buffer( + filename) + assert filepath_or_buffer != filename + assert os.path.isabs(filepath_or_buffer) + assert os.path.expanduser(filename) == filepath_or_buffer + assert not should_close def test_get_filepath_or_buffer_with_buffer(self): input_buffer = StringIO() - filepath_or_buffer, _, _ = common.get_filepath_or_buffer(input_buffer) - self.assertEqual(filepath_or_buffer, input_buffer) + filepath_or_buffer, _, _, should_close = icom.get_filepath_or_buffer( + input_buffer) + assert filepath_or_buffer == input_buffer + assert not should_close def test_iterator(self): - reader = read_csv(StringIO(self.data1), chunksize=1) - result = concat(reader, ignore_index=True) - expected = read_csv(StringIO(self.data1)) + reader = pd.read_csv(StringIO(self.data1), chunksize=1) + result = pd.concat(reader, ignore_index=True) + expected = pd.read_csv(StringIO(self.data1)) tm.assert_frame_equal(result, expected) # GH12153 - it = read_csv(StringIO(self.data1), chunksize=1) + it = pd.read_csv(StringIO(self.data1), chunksize=1) first = next(it) tm.assert_frame_equal(first, expected.iloc[[0]]) - tm.assert_frame_equal(concat(it), expected.iloc[1:]) + tm.assert_frame_equal(pd.concat(it), expected.iloc[1:]) + + @pytest.mark.parametrize('reader, module, error_class, fn_ext', [ + (pd.read_csv, 'os', FileNotFoundError, 'csv'), + (pd.read_fwf, 'os', FileNotFoundError, 'txt'), + (pd.read_excel, 'xlrd', FileNotFoundError, 'xlsx'), + (pd.read_feather, 'feather', Exception, 'feather'), + (pd.read_hdf, 'tables', FileNotFoundError, 'h5'), + (pd.read_stata, 'os', FileNotFoundError, 'dta'), + (pd.read_sas, 'os', FileNotFoundError, 'sas7bdat'), + (pd.read_json, 'os', ValueError, 'json'), + (pd.read_msgpack, 'os', ValueError, 'mp'), + (pd.read_pickle, 'os', FileNotFoundError, 'pickle'), + ]) + def test_read_non_existant(self, reader, module, error_class, fn_ext): + pytest.importorskip(module) + + path = os.path.join(HERE, 'data', 'does_not_exist.' + fn_ext) + msg1 = (r"File (b')?.+does_not_exist\.{}'? does not exist" + .format(fn_ext)) + msg2 = (r"\[Errno 2\] No such file or directory: '.+does_not_exist" + r"\.{}'").format(fn_ext) + msg3 = "Expected object or value" + msg4 = "path_or_buf needs to be a string file path or file-like" + msg5 = (r"\[Errno 2\] File .+does_not_exist\.{} does not exist:" + r" '.+does_not_exist\.{}'").format(fn_ext, fn_ext) + with pytest.raises(error_class, match=r"({}|{}|{}|{}|{})".format( + msg1, msg2, msg3, msg4, msg5)): + reader(path) + + @pytest.mark.parametrize('reader, module, error_class, fn_ext', [ + (pd.read_csv, 'os', FileNotFoundError, 'csv'), + (pd.read_fwf, 'os', FileNotFoundError, 'txt'), + (pd.read_excel, 'xlrd', FileNotFoundError, 'xlsx'), + (pd.read_feather, 'feather', Exception, 'feather'), + (pd.read_hdf, 'tables', FileNotFoundError, 'h5'), + (pd.read_stata, 'os', FileNotFoundError, 'dta'), + (pd.read_sas, 'os', FileNotFoundError, 'sas7bdat'), + (pd.read_json, 'os', ValueError, 'json'), + (pd.read_msgpack, 'os', ValueError, 'mp'), + (pd.read_pickle, 'os', FileNotFoundError, 'pickle'), + ]) + def test_read_expands_user_home_dir(self, reader, module, + error_class, fn_ext, monkeypatch): + pytest.importorskip(module) + + path = os.path.join('~', 'does_not_exist.' + fn_ext) + monkeypatch.setattr(icom, '_expand_user', + lambda x: os.path.join('foo', x)) + + msg1 = (r"File (b')?.+does_not_exist\.{}'? does not exist" + .format(fn_ext)) + msg2 = (r"\[Errno 2\] No such file or directory:" + r" '.+does_not_exist\.{}'").format(fn_ext) + msg3 = "Unexpected character found when decoding 'false'" + msg4 = "path_or_buf needs to be a string file path or file-like" + msg5 = (r"\[Errno 2\] File .+does_not_exist\.{} does not exist:" + r" '.+does_not_exist\.{}'").format(fn_ext, fn_ext) + + with pytest.raises(error_class, match=r"({}|{}|{}|{}|{})".format( + msg1, msg2, msg3, msg4, msg5)): + reader(path) + + def test_read_non_existant_read_table(self): + path = os.path.join(HERE, 'data', 'does_not_exist.' + 'csv') + msg1 = r"File b'.+does_not_exist\.csv' does not exist" + msg2 = (r"\[Errno 2\] File .+does_not_exist\.csv does not exist:" + r" '.+does_not_exist\.csv'") + with pytest.raises(FileNotFoundError, match=r"({}|{})".format( + msg1, msg2)): + with tm.assert_produces_warning(FutureWarning): + pd.read_table(path) + + @pytest.mark.parametrize('reader, module, path', [ + (pd.read_csv, 'os', ('io', 'data', 'iris.csv')), + (pd.read_fwf, 'os', ('io', 'data', 'fixed_width_format.txt')), + (pd.read_excel, 'xlrd', ('io', 'data', 'test1.xlsx')), + (pd.read_feather, 'feather', ('io', 'data', 'feather-0_3_1.feather')), + (pd.read_hdf, 'tables', ('io', 'data', 'legacy_hdf', + 'datetimetz_object.h5')), + (pd.read_stata, 'os', ('io', 'data', 'stata10_115.dta')), + (pd.read_sas, 'os', ('io', 'sas', 'data', 'test1.sas7bdat')), + (pd.read_json, 'os', ('io', 'json', 'data', 'tsframe_v012.json')), + (pd.read_msgpack, 'os', ('io', 'msgpack', 'data', 'frame.mp')), + (pd.read_pickle, 'os', ('io', 'data', 'categorical_0_14_1.pickle')), + ]) + def test_read_fspath_all(self, reader, module, path, datapath): + pytest.importorskip(module) + path = datapath(*path) + + mypath = CustomFSPath(path) + result = reader(mypath) + expected = reader(path) + + if path.endswith('.pickle'): + # categorical + tm.assert_categorical_equal(result, expected) + else: + tm.assert_frame_equal(result, expected) + + def test_read_fspath_all_read_table(self, datapath): + path = datapath('io', 'data', 'iris.csv') - def test_error_rename(self): - # see gh-12665 - try: - raise common.CParserError() - except common.ParserError: - pass + mypath = CustomFSPath(path) + with tm.assert_produces_warning(FutureWarning): + result = pd.read_table(mypath) + with tm.assert_produces_warning(FutureWarning): + expected = pd.read_table(path) - try: - raise common.ParserError() - except common.CParserError: - pass + if path.endswith('.pickle'): + # categorical + tm.assert_categorical_equal(result, expected) + else: + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('writer_name, writer_kwargs, module', [ + ('to_csv', {}, 'os'), + ('to_excel', {'engine': 'xlwt'}, 'xlwt'), + ('to_feather', {}, 'feather'), + ('to_html', {}, 'os'), + ('to_json', {}, 'os'), + ('to_latex', {}, 'os'), + ('to_msgpack', {}, 'os'), + ('to_pickle', {}, 'os'), + ('to_stata', {}, 'os'), + ]) + def test_write_fspath_all(self, writer_name, writer_kwargs, module): + p1 = tm.ensure_clean('string') + p2 = tm.ensure_clean('fspath') + df = pd.DataFrame({"A": [1, 2]}) + + with p1 as string, p2 as fspath: + pytest.importorskip(module) + mypath = CustomFSPath(fspath) + writer = getattr(df, writer_name) + + writer(string, **writer_kwargs) + with open(string, 'rb') as f: + expected = f.read() + + writer(mypath, **writer_kwargs) + with open(fspath, 'rb') as f: + result = f.read() + + assert result == expected + + def test_write_fspath_hdf5(self): + # Same test as write_fspath_all, except HDF5 files aren't + # necessarily byte-for-byte identical for a given dataframe, so we'll + # have to read and compare equality + pytest.importorskip('tables') + + df = pd.DataFrame({"A": [1, 2]}) + p1 = tm.ensure_clean('string') + p2 = tm.ensure_clean('fspath') + + with p1 as string, p2 as fspath: + mypath = CustomFSPath(fspath) + df.to_hdf(mypath, key='bar') + df.to_hdf(string, key='bar') + + result = pd.read_hdf(fspath, key='bar') + expected = pd.read_hdf(string, key='bar') + + tm.assert_frame_equal(result, expected) - try: - raise common.ParserError() - except pd.parser.CParserError: - pass +@pytest.fixture +def mmap_file(datapath): + return datapath('io', 'data', 'test_mmap.csv') -class TestMMapWrapper(tm.TestCase): - def setUp(self): - self.mmap_file = os.path.join(tm.get_data_path(), - 'test_mmap.csv') +class TestMMapWrapper(object): - def test_constructor_bad_file(self): + def test_constructor_bad_file(self, mmap_file): non_file = StringIO('I am not a file') non_file.fileno = lambda: -1 @@ -125,17 +313,19 @@ def test_constructor_bad_file(self): msg = "[Errno 22]" err = mmap.error - tm.assertRaisesRegexp(err, msg, common.MMapWrapper, non_file) + with pytest.raises(err, match=msg): + icom.MMapWrapper(non_file) - target = open(self.mmap_file, 'r') + target = open(mmap_file, 'r') target.close() msg = "I/O operation on closed file" - tm.assertRaisesRegexp(ValueError, msg, common.MMapWrapper, target) + with pytest.raises(ValueError, match=msg): + icom.MMapWrapper(target) - def test_get_attr(self): - with open(self.mmap_file, 'r') as target: - wrapper = common.MMapWrapper(target) + def test_get_attr(self, mmap_file): + with open(mmap_file, 'r') as target: + wrapper = icom.MMapWrapper(target) attrs = dir(wrapper.mmap) attrs = [attr for attr in attrs @@ -143,17 +333,25 @@ def test_get_attr(self): attrs.append('__next__') for attr in attrs: - self.assertTrue(hasattr(wrapper, attr)) + assert hasattr(wrapper, attr) - self.assertFalse(hasattr(wrapper, 'foo')) + assert not hasattr(wrapper, 'foo') - def test_next(self): - with open(self.mmap_file, 'r') as target: - wrapper = common.MMapWrapper(target) + def test_next(self, mmap_file): + with open(mmap_file, 'r') as target: + wrapper = icom.MMapWrapper(target) lines = target.readlines() for line in lines: next_line = next(wrapper) - self.assertEqual(next_line.strip(), line.strip()) + assert next_line.strip() == line.strip() + + with pytest.raises(StopIteration, match=r'^$'): + next(wrapper) - self.assertRaises(StopIteration, next, wrapper) + def test_unknown_engine(self): + with tm.ensure_clean() as path: + df = tm.makeDataFrame() + df.to_csv(path) + with pytest.raises(ValueError, match='Unknown engine'): + pd.read_csv(path, engine='pyt') diff --git a/pandas/tests/io/test_compression.py b/pandas/tests/io/test_compression.py new file mode 100644 index 0000000000000..a3fb35f9f01f2 --- /dev/null +++ b/pandas/tests/io/test_compression.py @@ -0,0 +1,116 @@ +import contextlib +import os +import warnings + +import pytest + +import pandas as pd +import pandas.util.testing as tm + +import pandas.io.common as icom + + +@contextlib.contextmanager +def catch_to_csv_depr(): + # Catching warnings because Series.to_csv has + # been deprecated. Remove this context when + # Series.to_csv has been aligned. + + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) + yield + + +@pytest.mark.parametrize('obj', [ + pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']), + pd.Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) +@pytest.mark.parametrize('method', ['to_pickle', 'to_json', 'to_csv']) +def test_compression_size(obj, method, compression_only): + with tm.ensure_clean() as path: + with catch_to_csv_depr(): + getattr(obj, method)(path, compression=compression_only) + compressed_size = os.path.getsize(path) + getattr(obj, method)(path, compression=None) + uncompressed_size = os.path.getsize(path) + assert uncompressed_size > compressed_size + + +@pytest.mark.parametrize('obj', [ + pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']), + pd.Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) +@pytest.mark.parametrize('method', ['to_csv', 'to_json']) +def test_compression_size_fh(obj, method, compression_only): + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=compression_only) + with catch_to_csv_depr(): + with f: + getattr(obj, method)(f) + assert not f.closed + assert f.closed + compressed_size = os.path.getsize(path) + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=None) + with catch_to_csv_depr(): + with f: + getattr(obj, method)(f) + assert not f.closed + assert f.closed + uncompressed_size = os.path.getsize(path) + assert uncompressed_size > compressed_size + + +@pytest.mark.parametrize('write_method, write_kwargs, read_method', [ + ('to_csv', {'index': False}, pd.read_csv), + ('to_json', {}, pd.read_json), + ('to_pickle', {}, pd.read_pickle), +]) +def test_dataframe_compression_defaults_to_infer( + write_method, write_kwargs, read_method, compression_only): + # GH22004 + input = pd.DataFrame([[1.0, 0, -4], [3.4, 5, 2]], columns=['X', 'Y', 'Z']) + extension = icom._compression_to_extension[compression_only] + with tm.ensure_clean('compressed' + extension) as path: + getattr(input, write_method)(path, **write_kwargs) + output = read_method(path, compression=compression_only) + tm.assert_frame_equal(output, input) + + +@pytest.mark.parametrize('write_method,write_kwargs,read_method,read_kwargs', [ + ('to_csv', {'index': False, 'header': True}, + pd.read_csv, {'squeeze': True}), + ('to_json', {}, pd.read_json, {'typ': 'series'}), + ('to_pickle', {}, pd.read_pickle, {}), +]) +def test_series_compression_defaults_to_infer( + write_method, write_kwargs, read_method, read_kwargs, + compression_only): + # GH22004 + input = pd.Series([0, 5, -2, 10], name='X') + extension = icom._compression_to_extension[compression_only] + with tm.ensure_clean('compressed' + extension) as path: + getattr(input, write_method)(path, **write_kwargs) + output = read_method(path, compression=compression_only, **read_kwargs) + tm.assert_series_equal(output, input, check_names=False) + + +def test_compression_warning(compression_only): + # Assert that passing a file object to to_csv while explicitly specifying a + # compression protocol triggers a RuntimeWarning, as per GH21227. + # Note that pytest has an issue that causes assert_produces_warning to fail + # in Python 2 if the warning has occurred in previous tests + # (see https://git.io/fNEBm & https://git.io/fNEBC). Hence, should this + # test fail in just Python 2 builds, it likely indicates that other tests + # are producing RuntimeWarnings, thereby triggering the pytest bug. + df = pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']) + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=compression_only) + with tm.assert_produces_warning(RuntimeWarning, + check_stacklevel=False): + with f: + df.to_csv(f, compression=compression_only) diff --git a/pandas/tests/io/test_date_converters.py b/pandas/tests/io/test_date_converters.py new file mode 100644 index 0000000000000..c5a94883aa609 --- /dev/null +++ b/pandas/tests/io/test_date_converters.py @@ -0,0 +1,43 @@ +from datetime import datetime + +import numpy as np + +import pandas.util.testing as tm + +import pandas.io.date_converters as conv + + +def test_parse_date_time(): + dates = np.array(['2007/1/3', '2008/2/4'], dtype=object) + times = np.array(['05:07:09', '06:08:00'], dtype=object) + expected = np.array([datetime(2007, 1, 3, 5, 7, 9), + datetime(2008, 2, 4, 6, 8, 0)]) + + result = conv.parse_date_time(dates, times) + tm.assert_numpy_array_equal(result, expected) + + +def test_parse_date_fields(): + days = np.array([3, 4]) + months = np.array([1, 2]) + years = np.array([2007, 2008]) + result = conv.parse_date_fields(years, months, days) + + expected = np.array([datetime(2007, 1, 3), datetime(2008, 2, 4)]) + tm.assert_numpy_array_equal(result, expected) + + +def test_parse_all_fields(): + hours = np.array([5, 6]) + minutes = np.array([7, 8]) + seconds = np.array([9, 0]) + + days = np.array([3, 4]) + years = np.array([2007, 2008]) + months = np.array([1, 2]) + + result = conv.parse_all_fields(years, months, days, + hours, minutes, seconds) + expected = np.array([datetime(2007, 1, 3, 5, 7, 9), + datetime(2008, 2, 4, 6, 8, 0)]) + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index 256a37e922177..04c9c58a326a4 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -1,75 +1,30 @@ -# pylint: disable=E1101 - -from pandas.compat import u, range, map, openpyxl_compat, BytesIO, iteritems -from datetime import datetime, date, time -import sys -import os +from collections import OrderedDict +import contextlib +from datetime import date, datetime, time, timedelta from distutils.version import LooseVersion - +from functools import partial +import os import warnings -import operator -import functools -import pytest -from numpy import nan import numpy as np +from numpy import nan +import pytest + +from pandas.compat import PY36, BytesIO, iteritems, map, range, u +import pandas.util._test_decorators as td import pandas as pd -from pandas import DataFrame, Index, MultiIndex -from pandas.io.parsers import read_csv -from pandas.io.excel import ( - ExcelFile, ExcelWriter, read_excel, _XlwtWriter, _Openpyxl1Writer, - _Openpyxl20Writer, _Openpyxl22Writer, register_writer, _XlsxWriter -) -from pandas.io.common import URLError -from pandas.util.testing import ensure_clean, makeCustomDataframe as mkdf -from pandas.core.config import set_option, get_option +from pandas import DataFrame, Index, MultiIndex, Series +from pandas.core.config import get_option, set_option import pandas.util.testing as tm +from pandas.util.testing import ensure_clean, makeCustomDataframe as mkdf - -def _skip_if_no_xlrd(): - try: - import xlrd - ver = tuple(map(int, xlrd.__VERSION__.split(".")[:2])) - if ver < (0, 9): - pytest.skip('xlrd < 0.9, skipping') - except ImportError: - pytest.skip('xlrd not installed, skipping') - - -def _skip_if_no_xlwt(): - try: - import xlwt # NOQA - except ImportError: - pytest.skip('xlwt not installed, skipping') - - -def _skip_if_no_openpyxl(): - try: - import openpyxl # NOQA - except ImportError: - pytest.skip('openpyxl not installed, skipping') - - -def _skip_if_no_xlsxwriter(): - try: - import xlsxwriter # NOQA - except ImportError: - pytest.skip('xlsxwriter not installed, skipping') - - -def _skip_if_no_excelsuite(): - _skip_if_no_xlrd() - _skip_if_no_xlwt() - _skip_if_no_openpyxl() - - -def _skip_if_no_s3fs(): - try: - import s3fs # noqa - except ImportError: - pytest.skip('s3fs not installed, skipping') - +from pandas.io.common import URLError +from pandas.io.excel import ( + ExcelFile, ExcelWriter, _OpenpyxlWriter, _XlsxWriter, _XlwtWriter, + read_excel, register_writer) +from pandas.io.formats.excel import ExcelFormatter +from pandas.io.parsers import read_csv _seriesd = tm.getSeriesData() _tsd = tm.getTimeSeriesData() @@ -80,10 +35,26 @@ def _skip_if_no_s3fs(): _mixed_frame['foo'] = 'bar' +@contextlib.contextmanager +def ignore_xlrd_time_clock_warning(): + """ + Context manager to ignore warnings raised by the xlrd library, + regarding the deprecation of `time.clock` in Python 3.7. + """ + with warnings.catch_warnings(): + warnings.filterwarnings( + action='ignore', + message='time.clock has been deprecated', + category=DeprecationWarning) + yield + + +@td.skip_if_no('xlrd', '1.0.0') class SharedItems(object): - def setUp(self): - self.dirpath = tm.get_data_path() + @pytest.fixture(autouse=True) + def setup_method(self, datapath): + self.dirpath = datapath("io", "data") self.frame = _frame.copy() self.frame2 = _frame2.copy() self.tsframe = _tsframe.copy() @@ -92,7 +63,6 @@ def setUp(self): def get_csv_refdf(self, basename): """ Obtain the reference data from read_csv with the Python engine. - Test data path is defined by pandas.util.testing.get_data_path() Parameters ---------- @@ -109,10 +79,9 @@ def get_csv_refdf(self, basename): dfref = read_csv(pref, index_col=0, parse_dates=True, engine='python') return dfref - def get_excelfile(self, basename): + def get_excelfile(self, basename, ext): """ - Return test data ExcelFile instance. Test data path is defined by - pandas.util.testing.get_data_path() + Return test data ExcelFile instance. Parameters ---------- @@ -125,12 +94,11 @@ def get_excelfile(self, basename): excel : io.excel.ExcelFile """ - return ExcelFile(os.path.join(self.dirpath, basename + self.ext)) + return ExcelFile(os.path.join(self.dirpath, basename + ext)) - def get_exceldf(self, basename, *args, **kwds): + def get_exceldf(self, basename, ext, *args, **kwds): """ - Return test data DataFrame. Test data path is defined by - pandas.util.testing.get_data_path() + Return test data DataFrame. Parameters ---------- @@ -143,94 +111,208 @@ def get_exceldf(self, basename, *args, **kwds): df : DataFrame """ - pth = os.path.join(self.dirpath, basename + self.ext) + pth = os.path.join(self.dirpath, basename + ext) return read_excel(pth, *args, **kwds) class ReadingTestsBase(SharedItems): # This is based on ExcelWriterBase - # - # Base class for test cases to run with different Excel readers. - # To add a reader test, define the following: - # 1. A check_skip function that skips your tests if your reader isn't - # installed. - # 2. Add a property ext, which is the file extension that your reader - # reades from. (needs to start with '.' so it's a valid path) - # 3. Add a property engine_name, which is the name of the reader class. - # For the reader this is not used for anything at the moment. - - def setUp(self): - self.check_skip() - super(ReadingTestsBase, self).setUp() - - def test_parse_cols_int(self): - dfref = self.get_csv_refdf('test1') - dfref = dfref.reindex(columns=['A', 'B', 'C']) - df1 = self.get_exceldf('test1', 'Sheet1', index_col=0, parse_cols=3) - df2 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0, - parse_cols=3) + @pytest.fixture(autouse=True, params=['xlrd', None]) + def set_engine(self, request): + func_name = "get_exceldf" + old_func = getattr(self, func_name) + new_func = partial(old_func, engine=request.param) + setattr(self, func_name, new_func) + yield + setattr(self, func_name, old_func) + + @td.skip_if_no("xlrd", "1.0.1") # see gh-22682 + def test_usecols_int(self, ext): + + df_ref = self.get_csv_refdf("test1") + df_ref = df_ref.reindex(columns=["A", "B", "C"]) + + # usecols as int + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + with ignore_xlrd_time_clock_warning(): + df1 = self.get_exceldf("test1", ext, "Sheet1", + index_col=0, usecols=3) + + # usecols as int + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + with ignore_xlrd_time_clock_warning(): + df2 = self.get_exceldf("test1", ext, "Sheet2", skiprows=[1], + index_col=0, usecols=3) + + # parse_cols instead of usecols, usecols as int + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + with ignore_xlrd_time_clock_warning(): + df3 = self.get_exceldf("test1", ext, "Sheet2", skiprows=[1], + index_col=0, parse_cols=3) + # TODO add index to xls file) - tm.assert_frame_equal(df1, dfref, check_names=False) - tm.assert_frame_equal(df2, dfref, check_names=False) + tm.assert_frame_equal(df1, df_ref, check_names=False) + tm.assert_frame_equal(df2, df_ref, check_names=False) + tm.assert_frame_equal(df3, df_ref, check_names=False) - def test_parse_cols_list(self): + @td.skip_if_no('xlrd', '1.0.1') # GH-22682 + def test_usecols_list(self, ext): dfref = self.get_csv_refdf('test1') dfref = dfref.reindex(columns=['B', 'C']) - df1 = self.get_exceldf('test1', 'Sheet1', index_col=0, - parse_cols=[0, 2, 3]) - df2 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0, - parse_cols=[0, 2, 3]) + df1 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0, + usecols=[0, 2, 3]) + df2 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, usecols=[0, 2, 3]) + + with tm.assert_produces_warning(FutureWarning): + with ignore_xlrd_time_clock_warning(): + df3 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, parse_cols=[0, 2, 3]) + # TODO add index to xls file) tm.assert_frame_equal(df1, dfref, check_names=False) tm.assert_frame_equal(df2, dfref, check_names=False) + tm.assert_frame_equal(df3, dfref, check_names=False) - def test_parse_cols_str(self): + @td.skip_if_no('xlrd', '1.0.1') # GH-22682 + def test_usecols_str(self, ext): dfref = self.get_csv_refdf('test1') df1 = dfref.reindex(columns=['A', 'B', 'C']) - df2 = self.get_exceldf('test1', 'Sheet1', index_col=0, - parse_cols='A:D') - df3 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0, - parse_cols='A:D') + df2 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0, + usecols='A:D') + df3 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, usecols='A:D') + + with tm.assert_produces_warning(FutureWarning): + with ignore_xlrd_time_clock_warning(): + df4 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, parse_cols='A:D') + # TODO add index to xls, read xls ignores index name ? tm.assert_frame_equal(df2, df1, check_names=False) tm.assert_frame_equal(df3, df1, check_names=False) + tm.assert_frame_equal(df4, df1, check_names=False) df1 = dfref.reindex(columns=['B', 'C']) - df2 = self.get_exceldf('test1', 'Sheet1', index_col=0, - parse_cols='A,C,D') - df3 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0, - parse_cols='A,C,D') + df2 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0, + usecols='A,C,D') + df3 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, usecols='A,C,D') # TODO add index to xls file tm.assert_frame_equal(df2, df1, check_names=False) tm.assert_frame_equal(df3, df1, check_names=False) df1 = dfref.reindex(columns=['B', 'C']) - df2 = self.get_exceldf('test1', 'Sheet1', index_col=0, - parse_cols='A,C:D') - df3 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0, - parse_cols='A,C:D') + df2 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0, + usecols='A,C:D') + df3 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0, usecols='A,C:D') tm.assert_frame_equal(df2, df1, check_names=False) tm.assert_frame_equal(df3, df1, check_names=False) - def test_excel_stop_iterator(self): + @pytest.mark.parametrize("usecols", [ + [0, 1, 3], [0, 3, 1], + [1, 0, 3], [1, 3, 0], + [3, 0, 1], [3, 1, 0], + ]) + def test_usecols_diff_positional_int_columns_order(self, ext, usecols): + expected = self.get_csv_refdf("test1")[["A", "C"]] + result = self.get_exceldf("test1", ext, "Sheet1", + index_col=0, usecols=usecols) + tm.assert_frame_equal(result, expected, check_names=False) + + @pytest.mark.parametrize("usecols", [ + ["B", "D"], ["D", "B"] + ]) + def test_usecols_diff_positional_str_columns_order(self, ext, usecols): + expected = self.get_csv_refdf("test1")[["B", "D"]] + expected.index = range(len(expected)) + + result = self.get_exceldf("test1", ext, "Sheet1", usecols=usecols) + tm.assert_frame_equal(result, expected, check_names=False) + + def test_read_excel_without_slicing(self, ext): + expected = self.get_csv_refdf("test1") + result = self.get_exceldf("test1", ext, "Sheet1", index_col=0) + tm.assert_frame_equal(result, expected, check_names=False) + + def test_usecols_excel_range_str(self, ext): + expected = self.get_csv_refdf("test1")[["C", "D"]] + result = self.get_exceldf("test1", ext, "Sheet1", + index_col=0, usecols="A,D:E") + tm.assert_frame_equal(result, expected, check_names=False) + + def test_usecols_excel_range_str_invalid(self, ext): + msg = "Invalid column name: E1" + + with pytest.raises(ValueError, match=msg): + self.get_exceldf("test1", ext, "Sheet1", usecols="D:E1") + + def test_index_col_label_error(self, ext): + msg = "list indices must be integers.*, not str" + + with pytest.raises(TypeError, match=msg): + self.get_exceldf("test1", ext, "Sheet1", index_col=["A"], + usecols=["A", "C"]) + + def test_index_col_empty(self, ext): + # see gh-9208 + result = self.get_exceldf("test1", ext, "Sheet3", + index_col=["A", "B", "C"]) + expected = DataFrame(columns=["D", "E", "F"], + index=MultiIndex(levels=[[]] * 3, + codes=[[]] * 3, + names=["A", "B", "C"])) + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize("index_col", [None, 2]) + def test_index_col_with_unnamed(self, ext, index_col): + # see gh-18792 + result = self.get_exceldf("test1", ext, "Sheet4", + index_col=index_col) + expected = DataFrame([["i1", "a", "x"], ["i2", "b", "y"]], + columns=["Unnamed: 0", "col1", "col2"]) + if index_col: + expected = expected.set_index(expected.columns[index_col]) + + tm.assert_frame_equal(result, expected) + + def test_usecols_pass_non_existent_column(self, ext): + msg = ("Usecols do not match columns, " + "columns expected but not found: " + r"\['E'\]") + + with pytest.raises(ValueError, match=msg): + self.get_exceldf("test1", ext, usecols=["E"]) + + def test_usecols_wrong_type(self, ext): + msg = ("'usecols' must either be list-like of " + "all strings, all unicode, all integers or a callable.") + + with pytest.raises(ValueError, match=msg): + self.get_exceldf("test1", ext, usecols=["E1", 0]) + + def test_excel_stop_iterator(self, ext): - parsed = self.get_exceldf('test2', 'Sheet1') + parsed = self.get_exceldf('test2', ext, 'Sheet1') expected = DataFrame([['aaaa', 'bbbbb']], columns=['Test', 'Test1']) tm.assert_frame_equal(parsed, expected) - def test_excel_cell_error_na(self): + def test_excel_cell_error_na(self, ext): - parsed = self.get_exceldf('test3', 'Sheet1') + parsed = self.get_exceldf('test3', ext, 'Sheet1') expected = DataFrame([[np.nan]], columns=['Test']) tm.assert_frame_equal(parsed, expected) - def test_excel_passes_na(self): + def test_excel_passes_na(self, ext): - excel = self.get_excelfile('test4') + excel = self.get_excelfile('test4', ext) parsed = read_excel(excel, 'Sheet1', keep_default_na=False, na_values=['apple']) @@ -245,7 +327,7 @@ def test_excel_passes_na(self): tm.assert_frame_equal(parsed, expected) # 13967 - excel = self.get_excelfile('test5') + excel = self.get_excelfile('test5', ext) parsed = read_excel(excel, 'Sheet1', keep_default_na=False, na_values=['apple']) @@ -259,9 +341,21 @@ def test_excel_passes_na(self): columns=['Test']) tm.assert_frame_equal(parsed, expected) - def test_excel_table_sheet_by_index(self): + @td.skip_if_no('xlrd', '1.0.1') # GH-22682 + def test_deprecated_sheetname(self, ext): + # gh-17964 + excel = self.get_excelfile('test1', ext) - excel = self.get_excelfile('test1') + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + read_excel(excel, sheetname='Sheet1') + + with pytest.raises(TypeError): + read_excel(excel, sheet='Sheet1') + + @td.skip_if_no('xlrd', '1.0.1') # GH-22682 + def test_excel_table_sheet_by_index(self, ext): + + excel = self.get_excelfile('test1', ext) dfref = self.get_csv_refdf('test1') df1 = read_excel(excel, 0, index_col=0) @@ -275,39 +369,37 @@ def test_excel_table_sheet_by_index(self): tm.assert_frame_equal(df2, dfref, check_names=False) df3 = read_excel(excel, 0, index_col=0, skipfooter=1) - df4 = read_excel(excel, 0, index_col=0, skip_footer=1) tm.assert_frame_equal(df3, df1.iloc[:-1]) - tm.assert_frame_equal(df3, df4) + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + df4 = read_excel(excel, 0, index_col=0, skip_footer=1) + tm.assert_frame_equal(df3, df4) df3 = excel.parse(0, index_col=0, skipfooter=1) - df4 = excel.parse(0, index_col=0, skip_footer=1) tm.assert_frame_equal(df3, df1.iloc[:-1]) - tm.assert_frame_equal(df3, df4) import xlrd - with tm.assertRaises(xlrd.XLRDError): + with pytest.raises(xlrd.XLRDError): read_excel(excel, 'asdf') - def test_excel_table(self): + def test_excel_table(self, ext): dfref = self.get_csv_refdf('test1') - df1 = self.get_exceldf('test1', 'Sheet1', index_col=0) - df2 = self.get_exceldf('test1', 'Sheet2', skiprows=[1], index_col=0) + df1 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0) + df2 = self.get_exceldf('test1', ext, 'Sheet2', skiprows=[1], + index_col=0) # TODO add index to file tm.assert_frame_equal(df1, dfref, check_names=False) tm.assert_frame_equal(df2, dfref, check_names=False) - df3 = self.get_exceldf('test1', 'Sheet1', index_col=0, + df3 = self.get_exceldf('test1', ext, 'Sheet1', index_col=0, skipfooter=1) - df4 = self.get_exceldf('test1', 'Sheet1', index_col=0, - skip_footer=1) tm.assert_frame_equal(df3, df1.iloc[:-1]) - tm.assert_frame_equal(df3, df4) - def test_reader_special_dtypes(self): + def test_reader_special_dtypes(self, ext): - expected = DataFrame.from_items([ + expected = DataFrame.from_dict(OrderedDict([ ("IntCol", [1, 2, -3, 4, 0]), ("FloatCol", [1.25, 2.25, 1.83, 1.92, 0.0000000005]), ("BoolCol", [True, False, True, True, False]), @@ -317,50 +409,49 @@ def test_reader_special_dtypes(self): ("DateCol", [datetime(2013, 10, 30), datetime(2013, 10, 31), datetime(1905, 1, 1), datetime(2013, 12, 14), datetime(2015, 3, 14)]) - ]) - + ])) basename = 'test_types' # should read in correctly and infer types - actual = self.get_exceldf(basename, 'Sheet1') + actual = self.get_exceldf(basename, ext, 'Sheet1') tm.assert_frame_equal(actual, expected) # if not coercing number, then int comes in as float float_expected = expected.copy() float_expected["IntCol"] = float_expected["IntCol"].astype(float) float_expected.loc[float_expected.index[1], "Str2Col"] = 3.0 - actual = self.get_exceldf(basename, 'Sheet1', convert_float=False) + actual = self.get_exceldf(basename, ext, 'Sheet1', convert_float=False) tm.assert_frame_equal(actual, float_expected) # check setting Index (assuming xls and xlsx are the same here) for icol, name in enumerate(expected.columns): - actual = self.get_exceldf(basename, 'Sheet1', index_col=icol) + actual = self.get_exceldf(basename, ext, 'Sheet1', index_col=icol) exp = expected.set_index(name) tm.assert_frame_equal(actual, exp) # convert_float and converters should be different but both accepted expected["StrCol"] = expected["StrCol"].apply(str) actual = self.get_exceldf( - basename, 'Sheet1', converters={"StrCol": str}) + basename, ext, 'Sheet1', converters={"StrCol": str}) tm.assert_frame_equal(actual, expected) no_convert_float = float_expected.copy() no_convert_float["StrCol"] = no_convert_float["StrCol"].apply(str) - actual = self.get_exceldf(basename, 'Sheet1', convert_float=False, + actual = self.get_exceldf(basename, ext, 'Sheet1', convert_float=False, converters={"StrCol": str}) tm.assert_frame_equal(actual, no_convert_float) # GH8212 - support for converters and missing values - def test_reader_converters(self): + def test_reader_converters(self, ext): basename = 'test_converters' - expected = DataFrame.from_items([ + expected = DataFrame.from_dict(OrderedDict([ ("IntCol", [1, 2, -3, -1000, 0]), ("FloatCol", [12.5, np.nan, 18.3, 19.2, 0.000000005]), ("BoolCol", ['Found', 'Found', 'Found', 'Not found', 'Found']), ("StrCol", ['1', np.nan, '3', '4', '5']), - ]) + ])) converters = {'IntCol': lambda x: int(x) if x != '' else -1000, 'FloatCol': lambda x: 10 * x if x else np.nan, @@ -370,13 +461,14 @@ def test_reader_converters(self): # should read in correctly and set types of single cells (not array # dtypes) - actual = self.get_exceldf(basename, 'Sheet1', converters=converters) + actual = self.get_exceldf(basename, ext, 'Sheet1', + converters=converters) tm.assert_frame_equal(actual, expected) - def test_reader_dtype(self): + def test_reader_dtype(self, ext): # GH 8212 basename = 'testdtype' - actual = self.get_exceldf(basename) + actual = self.get_exceldf(basename, ext) expected = DataFrame({ 'a': [1, 2, 3, 4], @@ -387,7 +479,7 @@ def test_reader_dtype(self): tm.assert_frame_equal(actual, expected) - actual = self.get_exceldf(basename, + actual = self.get_exceldf(basename, ext, dtype={'a': 'float64', 'b': 'float32', 'c': str}) @@ -397,23 +489,50 @@ def test_reader_dtype(self): expected['c'] = ['001', '002', '003', '004'] tm.assert_frame_equal(actual, expected) - with tm.assertRaises(ValueError): - actual = self.get_exceldf(basename, dtype={'d': 'int64'}) + with pytest.raises(ValueError): + self.get_exceldf(basename, ext, dtype={'d': 'int64'}) + + @pytest.mark.parametrize("dtype,expected", [ + (None, + DataFrame({ + "a": [1, 2, 3, 4], + "b": [2.5, 3.5, 4.5, 5.5], + "c": [1, 2, 3, 4], + "d": [1.0, 2.0, np.nan, 4.0] + })), + ({"a": "float64", + "b": "float32", + "c": str, + "d": str + }, + DataFrame({ + "a": Series([1, 2, 3, 4], dtype="float64"), + "b": Series([2.5, 3.5, 4.5, 5.5], dtype="float32"), + "c": ["001", "002", "003", "004"], + "d": ["1", "2", np.nan, "4"] + })), + ]) + def test_reader_dtype_str(self, ext, dtype, expected): + # see gh-20377 + basename = "testdtype" + + actual = self.get_exceldf(basename, ext, dtype=dtype) + tm.assert_frame_equal(actual, expected) - def test_reading_all_sheets(self): + def test_reading_all_sheets(self, ext): # Test reading all sheetnames by setting sheetname to None, # Ensure a dict is returned. # See PR #9450 basename = 'test_multisheet' - dfs = self.get_exceldf(basename, sheetname=None) + dfs = self.get_exceldf(basename, ext, sheet_name=None) # ensure this is not alphabetical to test order preservation expected_keys = ['Charlie', 'Alpha', 'Beta'] tm.assert_contains_all(expected_keys, dfs.keys()) # Issue 9930 # Ensure sheet order is preserved - tm.assert_equal(expected_keys, list(dfs.keys())) + assert expected_keys == list(dfs.keys()) - def test_reading_multiple_specific_sheets(self): + def test_reading_multiple_specific_sheets(self, ext): # Test reading specific sheetnames by specifying a mixed list # of integers and strings, and confirm that duplicated sheet # references (positions/names) are removed properly. @@ -422,100 +541,82 @@ def test_reading_multiple_specific_sheets(self): basename = 'test_multisheet' # Explicitly request duplicates. Only the set should be returned. expected_keys = [2, 'Charlie', 'Charlie'] - dfs = self.get_exceldf(basename, sheetname=expected_keys) + dfs = self.get_exceldf(basename, ext, sheet_name=expected_keys) expected_keys = list(set(expected_keys)) tm.assert_contains_all(expected_keys, dfs.keys()) assert len(expected_keys) == len(dfs.keys()) - def test_reading_all_sheets_with_blank(self): + def test_reading_all_sheets_with_blank(self, ext): # Test reading all sheetnames by setting sheetname to None, # In the case where some sheets are blank. # Issue #11711 basename = 'blank_with_header' - dfs = self.get_exceldf(basename, sheetname=None) + dfs = self.get_exceldf(basename, ext, sheet_name=None) expected_keys = ['Sheet1', 'Sheet2', 'Sheet3'] tm.assert_contains_all(expected_keys, dfs.keys()) # GH6403 - def test_read_excel_blank(self): - actual = self.get_exceldf('blank', 'Sheet1') + def test_read_excel_blank(self, ext): + actual = self.get_exceldf('blank', ext, 'Sheet1') tm.assert_frame_equal(actual, DataFrame()) - def test_read_excel_blank_with_header(self): + def test_read_excel_blank_with_header(self, ext): expected = DataFrame(columns=['col_1', 'col_2']) - actual = self.get_exceldf('blank_with_header', 'Sheet1') + actual = self.get_exceldf('blank_with_header', ext, 'Sheet1') tm.assert_frame_equal(actual, expected) - # GH 12292 : error when read one empty column from excel file - def test_read_one_empty_col_no_header(self): - _skip_if_no_xlwt() - _skip_if_no_openpyxl() - + @td.skip_if_no("xlwt") + @td.skip_if_no("openpyxl") + @pytest.mark.parametrize("header,expected", [ + (None, DataFrame([np.nan] * 4)), + (0, DataFrame({"Unnamed: 0": [np.nan] * 3})) + ]) + def test_read_one_empty_col_no_header(self, ext, header, expected): + # xref gh-12292 + filename = "no_header" df = pd.DataFrame( [["", 1, 100], ["", 2, 200], ["", 3, 300], ["", 4, 400]] ) - with ensure_clean(self.ext) as path: - df.to_excel(path, 'no_header', index=False, header=False) - actual_header_none = read_excel( - path, - 'no_header', - parse_cols=[0], - header=None - ) - - actual_header_zero = read_excel( - path, - 'no_header', - parse_cols=[0], - header=0 - ) - expected = DataFrame() - tm.assert_frame_equal(actual_header_none, expected) - tm.assert_frame_equal(actual_header_zero, expected) - - def test_read_one_empty_col_with_header(self): - _skip_if_no_xlwt() - _skip_if_no_openpyxl() + with ensure_clean(ext) as path: + df.to_excel(path, filename, index=False, header=False) + result = read_excel(path, filename, usecols=[0], header=header) + + tm.assert_frame_equal(result, expected) + + @td.skip_if_no("xlwt") + @td.skip_if_no("openpyxl") + @pytest.mark.parametrize("header,expected", [ + (None, DataFrame([0] + [np.nan] * 4)), + (0, DataFrame([np.nan] * 4)) + ]) + def test_read_one_empty_col_with_header(self, ext, header, expected): + filename = "with_header" df = pd.DataFrame( [["", 1, 100], ["", 2, 200], ["", 3, 300], ["", 4, 400]] ) - with ensure_clean(self.ext) as path: + + with ensure_clean(ext) as path: df.to_excel(path, 'with_header', index=False, header=True) - actual_header_none = read_excel( - path, - 'with_header', - parse_cols=[0], - header=None - ) - - actual_header_zero = read_excel( - path, - 'with_header', - parse_cols=[0], - header=0 - ) - expected_header_none = DataFrame(pd.Series([0], dtype='int64')) - tm.assert_frame_equal(actual_header_none, expected_header_none) - expected_header_zero = DataFrame(columns=[0], dtype='int64') - tm.assert_frame_equal(actual_header_zero, expected_header_zero) - - def test_set_column_names_in_parameter(self): - _skip_if_no_xlwt() - _skip_if_no_openpyxl() + result = read_excel(path, filename, usecols=[0], header=header) + + tm.assert_frame_equal(result, expected) + @td.skip_if_no('openpyxl') + @td.skip_if_no('xlwt') + def test_set_column_names_in_parameter(self, ext): # GH 12870 : pass down column names associated with # keyword argument names refdf = pd.DataFrame([[1, 'foo'], [2, 'bar'], [3, 'baz']], columns=['a', 'b']) - with ensure_clean(self.ext) as pth: + with ensure_clean(ext) as pth: with ExcelWriter(pth) as writer: refdf.to_excel(writer, 'Data_no_head', header=False, index=False) @@ -532,26 +633,58 @@ def test_set_column_names_in_parameter(self): tm.assert_frame_equal(xlsdf_no_head, refdf) tm.assert_frame_equal(xlsdf_with_head, refdf) - def test_date_conversion_overflow(self): + def test_date_conversion_overflow(self, ext): # GH 10001 : pandas.ExcelFile ignore parse_dates=False expected = pd.DataFrame([[pd.Timestamp('2016-03-12'), 'Marc Johnson'], [pd.Timestamp('2016-03-16'), 'Jack Black'], [1e+20, 'Timothy Brown']], columns=['DateColWithBigInt', 'StringCol']) - result = self.get_exceldf('testdateoverflow') + result = self.get_exceldf('testdateoverflow', ext) tm.assert_frame_equal(result, expected) - -class XlrdTests(ReadingTestsBase): - """ - This is the base class for the xlrd tests, and 3 different file formats - are supported: xls, xlsx, xlsm - """ - - def test_excel_read_buffer(self): - - pth = os.path.join(self.dirpath, 'test1' + self.ext) + @td.skip_if_no("xlrd", "1.0.1") # see gh-22682 + def test_sheet_name_and_sheetname(self, ext): + # gh-10559: Minor improvement: Change "sheet_name" to "sheetname" + # gh-10969: DOC: Consistent var names (sheetname vs sheet_name) + # gh-12604: CLN GH10559 Rename sheetname variable to sheet_name + # gh-20920: ExcelFile.parse() and pd.read_xlsx() have different + # behavior for "sheetname" argument + filename = "test1" + sheet_name = "Sheet1" + + df_ref = self.get_csv_refdf(filename) + df1 = self.get_exceldf(filename, ext, + sheet_name=sheet_name, index_col=0) # doc + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + with ignore_xlrd_time_clock_warning(): + df2 = self.get_exceldf(filename, ext, index_col=0, + sheetname=sheet_name) # backward compat + + excel = self.get_excelfile(filename, ext) + df1_parse = excel.parse(sheet_name=sheet_name, index_col=0) # doc + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + df2_parse = excel.parse(index_col=0, + sheetname=sheet_name) # backward compat + + tm.assert_frame_equal(df1, df_ref, check_names=False) + tm.assert_frame_equal(df2, df_ref, check_names=False) + tm.assert_frame_equal(df1_parse, df_ref, check_names=False) + tm.assert_frame_equal(df2_parse, df_ref, check_names=False) + + def test_sheet_name_both_raises(self, ext): + with pytest.raises(TypeError, match="Cannot specify both"): + self.get_exceldf('test1', ext, sheetname='Sheet1', + sheet_name='Sheet1') + + excel = self.get_excelfile('test1', ext) + with pytest.raises(TypeError, match="Cannot specify both"): + excel.parse(sheetname='Sheet1', + sheet_name='Sheet1') + + def test_excel_read_buffer(self, ext): + + pth = os.path.join(self.dirpath, 'test1' + ext) expected = read_excel(pth, 'Sheet1', index_col=0) with open(pth, 'rb') as f: actual = read_excel(f, 'Sheet1', index_col=0) @@ -562,47 +695,40 @@ def test_excel_read_buffer(self): actual = read_excel(xls, 'Sheet1', index_col=0) tm.assert_frame_equal(expected, actual) - def test_read_xlrd_Book(self): - _skip_if_no_xlwt() - - import xlrd - df = self.frame - with ensure_clean('.xls') as pth: - df.to_excel(pth, "SheetA") - book = xlrd.open_workbook(pth) - - with ExcelFile(book, engine="xlrd") as xl: - result = read_excel(xl, "SheetA") - tm.assert_frame_equal(df, result) - - result = read_excel(book, sheetname="SheetA", engine="xlrd") - tm.assert_frame_equal(df, result) + def test_bad_engine_raises(self, ext): + bad_engine = 'foo' + with pytest.raises(ValueError, match="Unknown engine: foo"): + read_excel('', engine=bad_engine) @tm.network - def test_read_from_http_url(self): + def test_read_from_http_url(self, ext): url = ('https://raw.github.com/pandas-dev/pandas/master/' - 'pandas/tests/io/data/test1' + self.ext) + 'pandas/tests/io/data/test1' + ext) url_table = read_excel(url) - local_table = self.get_exceldf('test1') + local_table = self.get_exceldf('test1', ext) tm.assert_frame_equal(url_table, local_table) - @tm.network(check_before_test=True) - def test_read_from_s3_url(self): - _skip_if_no_s3fs() + @td.skip_if_not_us_locale + def test_read_from_s3_url(self, ext, s3_resource): + # Bucket "pandas-test" created in tests/io/conftest.py + file_name = os.path.join(self.dirpath, 'test1' + ext) + + with open(file_name, "rb") as f: + s3_resource.Bucket("pandas-test").put_object(Key="test1" + ext, + Body=f) - url = ('s3://pandas-test/test1' + self.ext) + url = ('s3://pandas-test/test1' + ext) url_table = read_excel(url) - local_table = self.get_exceldf('test1') + local_table = self.get_exceldf('test1', ext) tm.assert_frame_equal(url_table, local_table) - @tm.slow - def test_read_from_file_url(self): + @pytest.mark.slow + # ignore warning from old xlrd + @pytest.mark.filterwarnings("ignore:This metho:PendingDeprecationWarning") + def test_read_from_file_url(self, ext): # FILE - if sys.version_info[:2] < (2, 6): - pytest.skip("file:// not supported with Python < 2.6") - - localtable = os.path.join(self.dirpath, 'test1' + self.ext) + localtable = os.path.join(self.dirpath, 'test1' + ext) local_table = read_excel(localtable) try: @@ -615,346 +741,347 @@ def test_read_from_file_url(self): tm.assert_frame_equal(url_table, local_table) - def test_read_from_pathlib_path(self): + @td.skip_if_no('pathlib') + def test_read_from_pathlib_path(self, ext): # GH12655 - tm._skip_if_no_pathlib() - from pathlib import Path - str_path = os.path.join(self.dirpath, 'test1' + self.ext) + str_path = os.path.join(self.dirpath, 'test1' + ext) expected = read_excel(str_path, 'Sheet1', index_col=0) - path_obj = Path(self.dirpath, 'test1' + self.ext) + path_obj = Path(self.dirpath, 'test1' + ext) actual = read_excel(path_obj, 'Sheet1', index_col=0) tm.assert_frame_equal(expected, actual) - def test_read_from_py_localpath(self): + @td.skip_if_no('py.path') + def test_read_from_py_localpath(self, ext): # GH12655 - tm._skip_if_no_localpath() - from py.path import local as LocalPath - str_path = os.path.join(self.dirpath, 'test1' + self.ext) + str_path = os.path.join(self.dirpath, 'test1' + ext) expected = read_excel(str_path, 'Sheet1', index_col=0) abs_dir = os.path.abspath(self.dirpath) - path_obj = LocalPath(abs_dir).join('test1' + self.ext) + path_obj = LocalPath(abs_dir).join('test1' + ext) actual = read_excel(path_obj, 'Sheet1', index_col=0) tm.assert_frame_equal(expected, actual) - def test_reader_closes_file(self): + def test_reader_closes_file(self, ext): - pth = os.path.join(self.dirpath, 'test1' + self.ext) + pth = os.path.join(self.dirpath, 'test1' + ext) f = open(pth, 'rb') with ExcelFile(f) as xlsx: # parses okay read_excel(xlsx, 'Sheet1', index_col=0) - self.assertTrue(f.closed) - - def test_creating_and_reading_multiple_sheets(self): - # Test reading multiple sheets, from a runtime created excel file - # with multiple sheets. - # See PR #9450 - - _skip_if_no_xlwt() - _skip_if_no_openpyxl() + assert f.closed - def tdf(sheetname): + @td.skip_if_no("xlwt") + @td.skip_if_no("openpyxl") + def test_creating_and_reading_multiple_sheets(self, ext): + # see gh-9450 + # + # Test reading multiple sheets, from a runtime + # created Excel file with multiple sheets. + def tdf(col_sheet_name): d, i = [11, 22, 33], [1, 2, 3] - return DataFrame(d, i, columns=[sheetname]) + return DataFrame(d, i, columns=[col_sheet_name]) - sheets = ['AAA', 'BBB', 'CCC'] + sheets = ["AAA", "BBB", "CCC"] dfs = [tdf(s) for s in sheets] dfs = dict(zip(sheets, dfs)) - with ensure_clean(self.ext) as pth: + with ensure_clean(ext) as pth: with ExcelWriter(pth) as ew: for sheetname, df in iteritems(dfs): df.to_excel(ew, sheetname) - dfs_returned = read_excel(pth, sheetname=sheets) + + dfs_returned = read_excel(pth, sheet_name=sheets, index_col=0) + for s in sheets: tm.assert_frame_equal(dfs[s], dfs_returned[s]) - def test_reader_seconds(self): - # Test reading times with and without milliseconds. GH5945. - import xlrd + def test_reader_seconds(self, ext): - if LooseVersion(xlrd.__VERSION__) >= LooseVersion("0.9.3"): - # Xlrd >= 0.9.3 can handle Excel milliseconds. - expected = DataFrame.from_items([("Time", - [time(1, 2, 3), - time(2, 45, 56, 100000), - time(4, 29, 49, 200000), - time(6, 13, 42, 300000), - time(7, 57, 35, 400000), - time(9, 41, 28, 500000), - time(11, 25, 21, 600000), - time(13, 9, 14, 700000), - time(14, 53, 7, 800000), - time(16, 37, 0, 900000), - time(18, 20, 54)])]) - else: - # Xlrd < 0.9.3 rounds Excel milliseconds. - expected = DataFrame.from_items([("Time", - [time(1, 2, 3), - time(2, 45, 56), - time(4, 29, 49), - time(6, 13, 42), - time(7, 57, 35), - time(9, 41, 29), - time(11, 25, 22), - time(13, 9, 15), - time(14, 53, 8), - time(16, 37, 1), - time(18, 20, 54)])]) - - actual = self.get_exceldf('times_1900', 'Sheet1') + # Test reading times with and without milliseconds. GH5945. + expected = DataFrame.from_dict({"Time": [time(1, 2, 3), + time(2, 45, 56, 100000), + time(4, 29, 49, 200000), + time(6, 13, 42, 300000), + time(7, 57, 35, 400000), + time(9, 41, 28, 500000), + time(11, 25, 21, 600000), + time(13, 9, 14, 700000), + time(14, 53, 7, 800000), + time(16, 37, 0, 900000), + time(18, 20, 54)]}) + + actual = self.get_exceldf('times_1900', ext, 'Sheet1') tm.assert_frame_equal(actual, expected) - actual = self.get_exceldf('times_1904', 'Sheet1') + actual = self.get_exceldf('times_1904', ext, 'Sheet1') tm.assert_frame_equal(actual, expected) - def test_read_excel_multiindex(self): - # GH 4679 - mi = MultiIndex.from_product([['foo', 'bar'], ['a', 'b']]) - mi_file = os.path.join(self.dirpath, 'testmultiindex' + self.ext) + def test_read_excel_multiindex(self, ext): + # see gh-4679 + mi = MultiIndex.from_product([["foo", "bar"], ["a", "b"]]) + mi_file = os.path.join(self.dirpath, "testmultiindex" + ext) - expected = DataFrame([[1, 2.5, pd.Timestamp('2015-01-01'), True], - [2, 3.5, pd.Timestamp('2015-01-02'), False], - [3, 4.5, pd.Timestamp('2015-01-03'), False], - [4, 5.5, pd.Timestamp('2015-01-04'), True]], + # "mi_column" sheet + expected = DataFrame([[1, 2.5, pd.Timestamp("2015-01-01"), True], + [2, 3.5, pd.Timestamp("2015-01-02"), False], + [3, 4.5, pd.Timestamp("2015-01-03"), False], + [4, 5.5, pd.Timestamp("2015-01-04"), True]], columns=mi) - actual = read_excel(mi_file, 'mi_column', header=[0, 1]) - tm.assert_frame_equal(actual, expected) - actual = read_excel(mi_file, 'mi_column', header=[0, 1], index_col=0) + actual = read_excel(mi_file, "mi_column", header=[0, 1], index_col=0) tm.assert_frame_equal(actual, expected) - expected.columns = ['a', 'b', 'c', 'd'] + # "mi_index" sheet expected.index = mi - actual = read_excel(mi_file, 'mi_index', index_col=[0, 1]) + expected.columns = ["a", "b", "c", "d"] + + actual = read_excel(mi_file, "mi_index", index_col=[0, 1]) tm.assert_frame_equal(actual, expected, check_names=False) + # "both" sheet expected.columns = mi - actual = read_excel(mi_file, 'both', index_col=[0, 1], header=[0, 1]) + + actual = read_excel(mi_file, "both", index_col=[0, 1], header=[0, 1]) tm.assert_frame_equal(actual, expected, check_names=False) - expected.index = mi.set_names(['ilvl1', 'ilvl2']) - expected.columns = ['a', 'b', 'c', 'd'] - actual = read_excel(mi_file, 'mi_index_name', index_col=[0, 1]) + # "mi_index_name" sheet + expected.columns = ["a", "b", "c", "d"] + expected.index = mi.set_names(["ilvl1", "ilvl2"]) + + actual = read_excel(mi_file, "mi_index_name", index_col=[0, 1]) tm.assert_frame_equal(actual, expected) + # "mi_column_name" sheet expected.index = list(range(4)) - expected.columns = mi.set_names(['c1', 'c2']) - actual = read_excel(mi_file, 'mi_column_name', + expected.columns = mi.set_names(["c1", "c2"]) + actual = read_excel(mi_file, "mi_column_name", header=[0, 1], index_col=0) tm.assert_frame_equal(actual, expected) - # Issue #11317 + # see gh-11317 + # "name_with_int" sheet expected.columns = mi.set_levels( - [1, 2], level=1).set_names(['c1', 'c2']) - actual = read_excel(mi_file, 'name_with_int', + [1, 2], level=1).set_names(["c1", "c2"]) + + actual = read_excel(mi_file, "name_with_int", index_col=0, header=[0, 1]) tm.assert_frame_equal(actual, expected) - expected.columns = mi.set_names(['c1', 'c2']) - expected.index = mi.set_names(['ilvl1', 'ilvl2']) - actual = read_excel(mi_file, 'both_name', - index_col=[0, 1], header=[0, 1]) - tm.assert_frame_equal(actual, expected) + # "both_name" sheet + expected.columns = mi.set_names(["c1", "c2"]) + expected.index = mi.set_names(["ilvl1", "ilvl2"]) - actual = read_excel(mi_file, 'both_name', + actual = read_excel(mi_file, "both_name", index_col=[0, 1], header=[0, 1]) tm.assert_frame_equal(actual, expected) - actual = read_excel(mi_file, 'both_name_skiprows', index_col=[0, 1], + # "both_skiprows" sheet + actual = read_excel(mi_file, "both_name_skiprows", index_col=[0, 1], header=[0, 1], skiprows=2) tm.assert_frame_equal(actual, expected) - def test_read_excel_multiindex_empty_level(self): - # GH 12453 - _skip_if_no_xlsxwriter() - with ensure_clean('.xlsx') as path: + def test_read_excel_multiindex_header_only(self, ext): + # see gh-11733. + # + # Don't try to parse a header name if there isn't one. + mi_file = os.path.join(self.dirpath, "testmultiindex" + ext) + result = read_excel(mi_file, "index_col_none", header=[0, 1]) + + exp_columns = MultiIndex.from_product([("A", "B"), ("key", "val")]) + expected = DataFrame([[1, 2, 3, 4]] * 2, columns=exp_columns) + tm.assert_frame_equal(result, expected) + + @td.skip_if_no("xlsxwriter") + def test_read_excel_multiindex_empty_level(self, ext): + # see gh-12453 + with ensure_clean(ext) as path: df = DataFrame({ - ('Zero', ''): {0: 0}, - ('One', 'x'): {0: 1}, - ('Two', 'X'): {0: 3}, - ('Two', 'Y'): {0: 7} + ("One", "x"): {0: 1}, + ("Two", "X"): {0: 3}, + ("Two", "Y"): {0: 7}, + ("Zero", ""): {0: 0} }) expected = DataFrame({ - ('Zero', 'Unnamed: 3_level_1'): {0: 0}, - ('One', u'x'): {0: 1}, - ('Two', u'X'): {0: 3}, - ('Two', u'Y'): {0: 7} + ("One", "x"): {0: 1}, + ("Two", "X"): {0: 3}, + ("Two", "Y"): {0: 7}, + ("Zero", "Unnamed: 4_level_1"): {0: 0} }) df.to_excel(path) - actual = pd.read_excel(path, header=[0, 1]) + actual = pd.read_excel(path, header=[0, 1], index_col=0) tm.assert_frame_equal(actual, expected) df = pd.DataFrame({ - ('Beg', ''): {0: 0}, - ('Middle', 'x'): {0: 1}, - ('Tail', 'X'): {0: 3}, - ('Tail', 'Y'): {0: 7} + ("Beg", ""): {0: 0}, + ("Middle", "x"): {0: 1}, + ("Tail", "X"): {0: 3}, + ("Tail", "Y"): {0: 7} }) expected = pd.DataFrame({ - ('Beg', 'Unnamed: 0_level_1'): {0: 0}, - ('Middle', u'x'): {0: 1}, - ('Tail', u'X'): {0: 3}, - ('Tail', u'Y'): {0: 7} + ("Beg", "Unnamed: 1_level_1"): {0: 0}, + ("Middle", "x"): {0: 1}, + ("Tail", "X"): {0: 3}, + ("Tail", "Y"): {0: 7} }) df.to_excel(path) - actual = pd.read_excel(path, header=[0, 1]) + actual = pd.read_excel(path, header=[0, 1], index_col=0) tm.assert_frame_equal(actual, expected) - def test_excel_multindex_roundtrip(self): - # GH 4679 - _skip_if_no_xlsxwriter() - with ensure_clean('.xlsx') as pth: - for c_idx_names in [True, False]: - for r_idx_names in [True, False]: - for c_idx_levels in [1, 3]: - for r_idx_levels in [1, 3]: - # column index name can't be serialized unless - # MultiIndex - if (c_idx_levels == 1 and c_idx_names): - continue - - # empty name case current read in as unamed levels, - # not Nones - check_names = True - if not r_idx_names and r_idx_levels > 1: - check_names = False - - df = mkdf(5, 5, c_idx_names, - r_idx_names, c_idx_levels, - r_idx_levels) - df.to_excel(pth) - act = pd.read_excel( - pth, index_col=list(range(r_idx_levels)), + @td.skip_if_no("xlsxwriter") + @pytest.mark.parametrize("c_idx_names", [True, False]) + @pytest.mark.parametrize("r_idx_names", [True, False]) + @pytest.mark.parametrize("c_idx_levels", [1, 3]) + @pytest.mark.parametrize("r_idx_levels", [1, 3]) + def test_excel_multindex_roundtrip(self, ext, c_idx_names, r_idx_names, + c_idx_levels, r_idx_levels): + # see gh-4679 + with ensure_clean(ext) as pth: + if c_idx_levels == 1 and c_idx_names: + pytest.skip("Column index name cannot be " + "serialized unless it's a MultiIndex") + + # Empty name case current read in as + # unnamed levels, not Nones. + check_names = r_idx_names or r_idx_levels <= 1 + + df = mkdf(5, 5, c_idx_names, r_idx_names, + c_idx_levels, r_idx_levels) + df.to_excel(pth) + + act = pd.read_excel(pth, index_col=list(range(r_idx_levels)), header=list(range(c_idx_levels))) - tm.assert_frame_equal( - df, act, check_names=check_names) + tm.assert_frame_equal(df, act, check_names=check_names) - df.iloc[0, :] = np.nan - df.to_excel(pth) - act = pd.read_excel( - pth, index_col=list(range(r_idx_levels)), + df.iloc[0, :] = np.nan + df.to_excel(pth) + + act = pd.read_excel(pth, index_col=list(range(r_idx_levels)), header=list(range(c_idx_levels))) - tm.assert_frame_equal( - df, act, check_names=check_names) + tm.assert_frame_equal(df, act, check_names=check_names) - df.iloc[-1, :] = np.nan - df.to_excel(pth) - act = pd.read_excel( - pth, index_col=list(range(r_idx_levels)), + df.iloc[-1, :] = np.nan + df.to_excel(pth) + act = pd.read_excel(pth, index_col=list(range(r_idx_levels)), header=list(range(c_idx_levels))) - tm.assert_frame_equal( - df, act, check_names=check_names) - - def test_excel_oldindex_format(self): - # GH 4679 - data = np.array([['R0C0', 'R0C1', 'R0C2', 'R0C3', 'R0C4'], - ['R1C0', 'R1C1', 'R1C2', 'R1C3', 'R1C4'], - ['R2C0', 'R2C1', 'R2C2', 'R2C3', 'R2C4'], - ['R3C0', 'R3C1', 'R3C2', 'R3C3', 'R3C4'], - ['R4C0', 'R4C1', 'R4C2', 'R4C3', 'R4C4']]) - columns = ['C_l0_g0', 'C_l0_g1', 'C_l0_g2', 'C_l0_g3', 'C_l0_g4'] - mi = MultiIndex(levels=[['R_l0_g0', 'R_l0_g1', 'R_l0_g2', - 'R_l0_g3', 'R_l0_g4'], - ['R_l1_g0', 'R_l1_g1', 'R_l1_g2', - 'R_l1_g3', 'R_l1_g4']], - labels=[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], - names=['R0', 'R1']) - si = Index(['R_l0_g0', 'R_l0_g1', 'R_l0_g2', - 'R_l0_g3', 'R_l0_g4'], name='R0') - - in_file = os.path.join( - self.dirpath, 'test_index_name_pre17' + self.ext) + tm.assert_frame_equal(df, act, check_names=check_names) + + def test_excel_old_index_format(self, ext): + # see gh-4679 + filename = "test_index_name_pre17" + ext + in_file = os.path.join(self.dirpath, filename) + + # We detect headers to determine if index names exist, so + # that "index" name in the "names" version of the data will + # now be interpreted as rows that include null data. + data = np.array([[None, None, None, None, None], + ["R0C0", "R0C1", "R0C2", "R0C3", "R0C4"], + ["R1C0", "R1C1", "R1C2", "R1C3", "R1C4"], + ["R2C0", "R2C1", "R2C2", "R2C3", "R2C4"], + ["R3C0", "R3C1", "R3C2", "R3C3", "R3C4"], + ["R4C0", "R4C1", "R4C2", "R4C3", "R4C4"]]) + columns = ["C_l0_g0", "C_l0_g1", "C_l0_g2", "C_l0_g3", "C_l0_g4"] + mi = MultiIndex(levels=[["R0", "R_l0_g0", "R_l0_g1", + "R_l0_g2", "R_l0_g3", "R_l0_g4"], + ["R1", "R_l1_g0", "R_l1_g1", + "R_l1_g2", "R_l1_g3", "R_l1_g4"]], + codes=[[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]], + names=[None, None]) + si = Index(["R0", "R_l0_g0", "R_l0_g1", "R_l0_g2", + "R_l0_g3", "R_l0_g4"], name=None) expected = pd.DataFrame(data, index=si, columns=columns) - with tm.assert_produces_warning(FutureWarning): - actual = pd.read_excel( - in_file, 'single_names', has_index_names=True) + + actual = pd.read_excel(in_file, "single_names", index_col=0) tm.assert_frame_equal(actual, expected) - expected.index.name = None - actual = pd.read_excel(in_file, 'single_no_names') + expected.index = mi + + actual = pd.read_excel(in_file, "multi_names", index_col=[0, 1]) tm.assert_frame_equal(actual, expected) - with tm.assert_produces_warning(FutureWarning): - actual = pd.read_excel( - in_file, 'single_no_names', has_index_names=False) + + # The analogous versions of the "names" version data + # where there are explicitly no names for the indices. + data = np.array([["R0C0", "R0C1", "R0C2", "R0C3", "R0C4"], + ["R1C0", "R1C1", "R1C2", "R1C3", "R1C4"], + ["R2C0", "R2C1", "R2C2", "R2C3", "R2C4"], + ["R3C0", "R3C1", "R3C2", "R3C3", "R3C4"], + ["R4C0", "R4C1", "R4C2", "R4C3", "R4C4"]]) + columns = ["C_l0_g0", "C_l0_g1", "C_l0_g2", "C_l0_g3", "C_l0_g4"] + mi = MultiIndex(levels=[["R_l0_g0", "R_l0_g1", "R_l0_g2", + "R_l0_g3", "R_l0_g4"], + ["R_l1_g0", "R_l1_g1", "R_l1_g2", + "R_l1_g3", "R_l1_g4"]], + codes=[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], + names=[None, None]) + si = Index(["R_l0_g0", "R_l0_g1", "R_l0_g2", + "R_l0_g3", "R_l0_g4"], name=None) + + expected = pd.DataFrame(data, index=si, columns=columns) + + actual = pd.read_excel(in_file, "single_no_names", index_col=0) tm.assert_frame_equal(actual, expected) expected.index = mi - with tm.assert_produces_warning(FutureWarning): - actual = pd.read_excel( - in_file, 'multi_names', has_index_names=True) - tm.assert_frame_equal(actual, expected) - expected.index.names = [None, None] - actual = pd.read_excel(in_file, 'multi_no_names', index_col=[0, 1]) - tm.assert_frame_equal(actual, expected, check_names=False) - with tm.assert_produces_warning(FutureWarning): - actual = pd.read_excel(in_file, 'multi_no_names', index_col=[0, 1], - has_index_names=False) + actual = pd.read_excel(in_file, "multi_no_names", index_col=[0, 1]) tm.assert_frame_equal(actual, expected, check_names=False) - def test_read_excel_bool_header_arg(self): + def test_read_excel_bool_header_arg(self, ext): # GH 6114 for arg in [True, False]: - with tm.assertRaises(TypeError): - pd.read_excel(os.path.join(self.dirpath, 'test1' + self.ext), + with pytest.raises(TypeError): + pd.read_excel(os.path.join(self.dirpath, 'test1' + ext), header=arg) - def test_read_excel_chunksize(self): + def test_read_excel_chunksize(self, ext): # GH 8011 - with tm.assertRaises(NotImplementedError): - pd.read_excel(os.path.join(self.dirpath, 'test1' + self.ext), + with pytest.raises(NotImplementedError): + pd.read_excel(os.path.join(self.dirpath, 'test1' + ext), chunksize=100) - def test_read_excel_parse_dates(self): - # GH 11544, 12051 - + @td.skip_if_no("xlwt") + @td.skip_if_no("openpyxl") + def test_read_excel_parse_dates(self, ext): + # see gh-11544, gh-12051 df = DataFrame( - {'col': [1, 2, 3], - 'date_strings': pd.date_range('2012-01-01', periods=3)}) + {"col": [1, 2, 3], + "date_strings": pd.date_range("2012-01-01", periods=3)}) df2 = df.copy() - df2['date_strings'] = df2['date_strings'].dt.strftime('%m/%d/%Y') + df2["date_strings"] = df2["date_strings"].dt.strftime("%m/%d/%Y") - with ensure_clean(self.ext) as pth: + with ensure_clean(ext) as pth: df2.to_excel(pth) - res = read_excel(pth) + res = read_excel(pth, index_col=0) tm.assert_frame_equal(df2, res) - # no index_col specified when parse_dates is True - with tm.assert_produces_warning(): - res = read_excel(pth, parse_dates=True) - tm.assert_frame_equal(df2, res) - - res = read_excel(pth, parse_dates=['date_strings'], index_col=0) + res = read_excel(pth, parse_dates=["date_strings"], index_col=0) tm.assert_frame_equal(df, res) - dateparser = lambda x: pd.datetime.strptime(x, '%m/%d/%Y') - res = read_excel(pth, parse_dates=['date_strings'], - date_parser=dateparser, index_col=0) + date_parser = lambda x: pd.datetime.strptime(x, "%m/%d/%Y") + res = read_excel(pth, parse_dates=["date_strings"], + date_parser=date_parser, index_col=0) tm.assert_frame_equal(df, res) - def test_read_excel_skiprows_list(self): + def test_read_excel_skiprows_list(self, ext): # GH 4903 actual = pd.read_excel(os.path.join(self.dirpath, - 'testskiprows' + self.ext), + 'testskiprows' + ext), 'skiprows_list', skiprows=[0, 2]) expected = DataFrame([[1, 2.5, pd.Timestamp('2015-01-01'), True], [2, 3.5, pd.Timestamp('2015-01-02'), False], @@ -964,13 +1091,40 @@ def test_read_excel_skiprows_list(self): tm.assert_frame_equal(actual, expected) actual = pd.read_excel(os.path.join(self.dirpath, - 'testskiprows' + self.ext), + 'testskiprows' + ext), 'skiprows_list', skiprows=np.array([0, 2])) tm.assert_frame_equal(actual, expected) - def test_read_excel_squeeze(self): + def test_read_excel_nrows(self, ext): + # GH 16645 + num_rows_to_pull = 5 + actual = pd.read_excel(os.path.join(self.dirpath, 'test1' + ext), + nrows=num_rows_to_pull) + expected = pd.read_excel(os.path.join(self.dirpath, + 'test1' + ext)) + expected = expected[:num_rows_to_pull] + tm.assert_frame_equal(actual, expected) + + def test_read_excel_nrows_greater_than_nrows_in_file(self, ext): + # GH 16645 + expected = pd.read_excel(os.path.join(self.dirpath, + 'test1' + ext)) + num_records_in_file = len(expected) + num_rows_to_pull = num_records_in_file + 10 + actual = pd.read_excel(os.path.join(self.dirpath, 'test1' + ext), + nrows=num_rows_to_pull) + tm.assert_frame_equal(actual, expected) + + def test_read_excel_nrows_non_integer_parameter(self, ext): + # GH 16645 + msg = "'nrows' must be an integer >=0" + with pytest.raises(ValueError, match=msg): + pd.read_excel(os.path.join(self.dirpath, 'test1' + ext), + nrows='5') + + def test_read_excel_squeeze(self, ext): # GH 12157 - f = os.path.join(self.dirpath, 'test_squeeze' + self.ext) + f = os.path.join(self.dirpath, 'test_squeeze' + ext) actual = pd.read_excel(f, 'two_columns', index_col=0, squeeze=True) expected = pd.Series([2, 3, 4], [4, 5, 6], name='b') @@ -987,439 +1141,483 @@ def test_read_excel_squeeze(self): tm.assert_series_equal(actual, expected) -class XlsReaderTests(XlrdTests, tm.TestCase): - ext = '.xls' - engine_name = 'xlrd' - check_skip = staticmethod(_skip_if_no_xlrd) +@pytest.mark.parametrize("ext", ['.xls', '.xlsx', '.xlsm']) +class TestXlrdReader(ReadingTestsBase): + """ + This is the base class for the xlrd tests, and 3 different file formats + are supported: xls, xlsx, xlsm + """ + @td.skip_if_no("xlwt") + def test_read_xlrd_book(self, ext): + import xlrd + df = self.frame -class XlsxReaderTests(XlrdTests, tm.TestCase): - ext = '.xlsx' - engine_name = 'xlrd' - check_skip = staticmethod(_skip_if_no_xlrd) + engine = "xlrd" + sheet_name = "SheetA" + with ensure_clean(ext) as pth: + df.to_excel(pth, sheet_name) + book = xlrd.open_workbook(pth) -class XlsmReaderTests(XlrdTests, tm.TestCase): - ext = '.xlsm' - engine_name = 'xlrd' - check_skip = staticmethod(_skip_if_no_xlrd) + with ExcelFile(book, engine=engine) as xl: + result = read_excel(xl, sheet_name, index_col=0) + tm.assert_frame_equal(df, result) + result = read_excel(book, sheet_name=sheet_name, + engine=engine, index_col=0) + tm.assert_frame_equal(df, result) + + +class _WriterBase(SharedItems): + + @pytest.fixture(autouse=True) + def set_engine_and_path(self, request, merge_cells, engine, ext): + """Fixture to set engine and open file for use in each test case + + Rather than requiring `engine=...` to be provided explicitly as an + argument in each test, this fixture sets a global option to dictate + which engine should be used to write Excel files. After executing + the test it rolls back said change to the global option. -class ExcelWriterBase(SharedItems): + It also uses a context manager to open a temporary excel file for + the function to write to, accessible via `self.path` + + Notes + ----- + This fixture will run as part of each test method defined in the + class and any subclasses, on account of the `autouse=True` + argument + """ + option_name = 'io.excel.{ext}.writer'.format(ext=ext.strip('.')) + prev_engine = get_option(option_name) + set_option(option_name, engine) + with ensure_clean(ext) as path: + self.path = path + yield + set_option(option_name, prev_engine) # Roll back option change + + +@pytest.mark.parametrize("merge_cells", [True, False]) +@pytest.mark.parametrize("engine,ext", [ + pytest.param('openpyxl', '.xlsx', marks=pytest.mark.skipif( + not td.safe_import('openpyxl'), reason='No openpyxl')), + pytest.param('openpyxl', '.xlsm', marks=pytest.mark.skipif( + not td.safe_import('openpyxl'), reason='No openpyxl')), + pytest.param('xlwt', '.xls', marks=pytest.mark.skipif( + not td.safe_import('xlwt'), reason='No xlwt')), + pytest.param('xlsxwriter', '.xlsx', marks=pytest.mark.skipif( + not td.safe_import('xlsxwriter'), reason='No xlsxwriter')) +]) +class TestExcelWriter(_WriterBase): # Base class for test cases to run with different Excel writers. - # To add a writer test, define the following: - # 1. A check_skip function that skips your tests if your writer isn't - # installed. - # 2. Add a property ext, which is the file extension that your writer - # writes to. (needs to start with '.' so it's a valid path) - # 3. Add a property engine_name, which is the name of the writer class. - - # Test with MultiIndex and Hierarchical Rows as merged cells. - merge_cells = True - - def setUp(self): - self.check_skip() - super(ExcelWriterBase, self).setUp() - self.option_name = 'io.excel.%s.writer' % self.ext.strip('.') - self.prev_engine = get_option(self.option_name) - set_option(self.option_name, self.engine_name) - - def tearDown(self): - set_option(self.option_name, self.prev_engine) - - def test_excel_sheet_by_name_raise(self): - _skip_if_no_xlrd() + + def test_excel_sheet_by_name_raise(self, *_): import xlrd - with ensure_clean(self.ext) as pth: - gt = DataFrame(np.random.randn(10, 2)) - gt.to_excel(pth) - xl = ExcelFile(pth) - df = read_excel(xl, 0) - tm.assert_frame_equal(gt, df) + gt = DataFrame(np.random.randn(10, 2)) + gt.to_excel(self.path) - with tm.assertRaises(xlrd.XLRDError): - read_excel(xl, '0') + xl = ExcelFile(self.path) + df = read_excel(xl, 0, index_col=0) - def test_excelwriter_contextmanager(self): - _skip_if_no_xlrd() + tm.assert_frame_equal(gt, df) - with ensure_clean(self.ext) as pth: - with ExcelWriter(pth) as writer: - self.frame.to_excel(writer, 'Data1') - self.frame2.to_excel(writer, 'Data2') + with pytest.raises(xlrd.XLRDError): + read_excel(xl, "0") - with ExcelFile(pth) as reader: - found_df = read_excel(reader, 'Data1') - found_df2 = read_excel(reader, 'Data2') - tm.assert_frame_equal(found_df, self.frame) - tm.assert_frame_equal(found_df2, self.frame2) - - def test_roundtrip(self): - _skip_if_no_xlrd() - - with ensure_clean(self.ext) as path: - self.frame['A'][:5] = nan - - self.frame.to_excel(path, 'test1') - self.frame.to_excel(path, 'test1', columns=['A', 'B']) - self.frame.to_excel(path, 'test1', header=False) - self.frame.to_excel(path, 'test1', index=False) - - # test roundtrip - self.frame.to_excel(path, 'test1') - recons = read_excel(path, 'test1', index_col=0) - tm.assert_frame_equal(self.frame, recons) - - self.frame.to_excel(path, 'test1', index=False) - recons = read_excel(path, 'test1', index_col=None) - recons.index = self.frame.index - tm.assert_frame_equal(self.frame, recons) - - self.frame.to_excel(path, 'test1', na_rep='NA') - recons = read_excel(path, 'test1', index_col=0, na_values=['NA']) - tm.assert_frame_equal(self.frame, recons) - - # GH 3611 - self.frame.to_excel(path, 'test1', na_rep='88') - recons = read_excel(path, 'test1', index_col=0, na_values=['88']) - tm.assert_frame_equal(self.frame, recons) - - self.frame.to_excel(path, 'test1', na_rep='88') - recons = read_excel(path, 'test1', index_col=0, - na_values=[88, 88.0]) - tm.assert_frame_equal(self.frame, recons) - - # GH 6573 - self.frame.to_excel(path, 'Sheet1') - recons = read_excel(path, index_col=0) - tm.assert_frame_equal(self.frame, recons) - - self.frame.to_excel(path, '0') - recons = read_excel(path, index_col=0) - tm.assert_frame_equal(self.frame, recons) - - # GH 8825 Pandas Series should provide to_excel method - s = self.frame["A"] - s.to_excel(path) - recons = read_excel(path, index_col=0) - tm.assert_frame_equal(s.to_frame(), recons) - - def test_mixed(self): - _skip_if_no_xlrd() - - with ensure_clean(self.ext) as path: - self.mixed_frame.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', index_col=0) - tm.assert_frame_equal(self.mixed_frame, recons) - - def test_tsframe(self): - _skip_if_no_xlrd() + def test_excel_writer_context_manager(self, *_): + with ExcelWriter(self.path) as writer: + self.frame.to_excel(writer, "Data1") + self.frame2.to_excel(writer, "Data2") - df = tm.makeTimeDataFrame()[:5] + with ExcelFile(self.path) as reader: + found_df = read_excel(reader, "Data1", index_col=0) + found_df2 = read_excel(reader, "Data2", index_col=0) + + tm.assert_frame_equal(found_df, self.frame) + tm.assert_frame_equal(found_df2, self.frame2) + + def test_roundtrip(self, merge_cells, engine, ext): + self.frame['A'][:5] = nan + + self.frame.to_excel(self.path, 'test1') + self.frame.to_excel(self.path, 'test1', columns=['A', 'B']) + self.frame.to_excel(self.path, 'test1', header=False) + self.frame.to_excel(self.path, 'test1', index=False) + + # test roundtrip + self.frame.to_excel(self.path, 'test1') + recons = read_excel(self.path, 'test1', index_col=0) + tm.assert_frame_equal(self.frame, recons) + + self.frame.to_excel(self.path, 'test1', index=False) + recons = read_excel(self.path, 'test1', index_col=None) + recons.index = self.frame.index + tm.assert_frame_equal(self.frame, recons) + + self.frame.to_excel(self.path, 'test1', na_rep='NA') + recons = read_excel(self.path, 'test1', index_col=0, na_values=['NA']) + tm.assert_frame_equal(self.frame, recons) + + # GH 3611 + self.frame.to_excel(self.path, 'test1', na_rep='88') + recons = read_excel(self.path, 'test1', index_col=0, na_values=['88']) + tm.assert_frame_equal(self.frame, recons) + + self.frame.to_excel(self.path, 'test1', na_rep='88') + recons = read_excel(self.path, 'test1', index_col=0, + na_values=[88, 88.0]) + tm.assert_frame_equal(self.frame, recons) + + # GH 6573 + self.frame.to_excel(self.path, 'Sheet1') + recons = read_excel(self.path, index_col=0) + tm.assert_frame_equal(self.frame, recons) - with ensure_clean(self.ext) as path: - df.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1') - tm.assert_frame_equal(df, recons) - - def test_basics_with_nan(self): - _skip_if_no_xlrd() - with ensure_clean(self.ext) as path: - self.frame['A'][:5] = nan - self.frame.to_excel(path, 'test1') - self.frame.to_excel(path, 'test1', columns=['A', 'B']) - self.frame.to_excel(path, 'test1', header=False) - self.frame.to_excel(path, 'test1', index=False) - - def test_int_types(self): - _skip_if_no_xlrd() - - for np_type in (np.int8, np.int16, np.int32, np.int64): - - with ensure_clean(self.ext) as path: - # Test np.int values read come back as int (rather than float - # which is Excel's format). - frame = DataFrame(np.random.randint(-10, 10, size=(10, 2)), - dtype=np_type) - frame.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1') - int_frame = frame.astype(np.int64) - tm.assert_frame_equal(int_frame, recons) - recons2 = read_excel(path, 'test1') - tm.assert_frame_equal(int_frame, recons2) - - # test with convert_float=False comes back as float - float_frame = frame.astype(float) - recons = read_excel(path, 'test1', convert_float=False) - tm.assert_frame_equal(recons, float_frame, - check_index_type=False, - check_column_type=False) - - def test_float_types(self): - _skip_if_no_xlrd() - - for np_type in (np.float16, np.float32, np.float64): - with ensure_clean(self.ext) as path: - # Test np.float values read come back as float. - frame = DataFrame(np.random.random_sample(10), dtype=np_type) - frame.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1').astype(np_type) - tm.assert_frame_equal(frame, recons, check_dtype=False) - - def test_bool_types(self): - _skip_if_no_xlrd() - - for np_type in (np.bool8, np.bool_): - with ensure_clean(self.ext) as path: - # Test np.bool values read come back as float. - frame = (DataFrame([1, 0, True, False], dtype=np_type)) - frame.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1').astype(np_type) - tm.assert_frame_equal(frame, recons) - - def test_inf_roundtrip(self): - _skip_if_no_xlrd() + self.frame.to_excel(self.path, '0') + recons = read_excel(self.path, index_col=0) + tm.assert_frame_equal(self.frame, recons) + # GH 8825 Pandas Series should provide to_excel method + s = self.frame["A"] + s.to_excel(self.path) + recons = read_excel(self.path, index_col=0) + tm.assert_frame_equal(s.to_frame(), recons) + + def test_mixed(self, merge_cells, engine, ext): + self.mixed_frame.to_excel(self.path, 'test1') + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', index_col=0) + tm.assert_frame_equal(self.mixed_frame, recons) + + def test_ts_frame(self, *_): + df = tm.makeTimeDataFrame()[:5] + + df.to_excel(self.path, "test1") + reader = ExcelFile(self.path) + + recons = read_excel(reader, "test1", index_col=0) + tm.assert_frame_equal(df, recons) + + def test_basics_with_nan(self, merge_cells, engine, ext): + self.frame['A'][:5] = nan + self.frame.to_excel(self.path, 'test1') + self.frame.to_excel(self.path, 'test1', columns=['A', 'B']) + self.frame.to_excel(self.path, 'test1', header=False) + self.frame.to_excel(self.path, 'test1', index=False) + + @pytest.mark.parametrize("np_type", [ + np.int8, np.int16, np.int32, np.int64]) + def test_int_types(self, merge_cells, engine, ext, np_type): + # Test np.int values read come back as int + # (rather than float which is Excel's format). + frame = DataFrame(np.random.randint(-10, 10, size=(10, 2)), + dtype=np_type) + frame.to_excel(self.path, "test1") + + reader = ExcelFile(self.path) + recons = read_excel(reader, "test1", index_col=0) + + int_frame = frame.astype(np.int64) + tm.assert_frame_equal(int_frame, recons) + + recons2 = read_excel(self.path, "test1", index_col=0) + tm.assert_frame_equal(int_frame, recons2) + + # Test with convert_float=False comes back as float. + float_frame = frame.astype(float) + recons = read_excel(self.path, "test1", + convert_float=False, index_col=0) + tm.assert_frame_equal(recons, float_frame, + check_index_type=False, + check_column_type=False) + + @pytest.mark.parametrize("np_type", [ + np.float16, np.float32, np.float64]) + def test_float_types(self, merge_cells, engine, ext, np_type): + # Test np.float values read come back as float. + frame = DataFrame(np.random.random_sample(10), dtype=np_type) + frame.to_excel(self.path, "test1") + + reader = ExcelFile(self.path) + recons = read_excel(reader, "test1", index_col=0).astype(np_type) + + tm.assert_frame_equal(frame, recons, check_dtype=False) + + @pytest.mark.parametrize("np_type", [np.bool8, np.bool_]) + def test_bool_types(self, merge_cells, engine, ext, np_type): + # Test np.bool values read come back as float. + frame = (DataFrame([1, 0, True, False], dtype=np_type)) + frame.to_excel(self.path, "test1") + + reader = ExcelFile(self.path) + recons = read_excel(reader, "test1", index_col=0).astype(np_type) + + tm.assert_frame_equal(frame, recons) + + def test_inf_roundtrip(self, *_): frame = DataFrame([(1, np.inf), (2, 3), (5, -np.inf)]) - with ensure_clean(self.ext) as path: - frame.to_excel(path, 'test1') - reader = ExcelFile(path) - recons = read_excel(reader, 'test1') - tm.assert_frame_equal(frame, recons) + frame.to_excel(self.path, "test1") - def test_sheets(self): - _skip_if_no_xlrd() + reader = ExcelFile(self.path) + recons = read_excel(reader, "test1", index_col=0) - with ensure_clean(self.ext) as path: - self.frame['A'][:5] = nan + tm.assert_frame_equal(frame, recons) - self.frame.to_excel(path, 'test1') - self.frame.to_excel(path, 'test1', columns=['A', 'B']) - self.frame.to_excel(path, 'test1', header=False) - self.frame.to_excel(path, 'test1', index=False) + def test_sheets(self, merge_cells, engine, ext): + self.frame['A'][:5] = nan - # Test writing to separate sheets - writer = ExcelWriter(path) - self.frame.to_excel(writer, 'test1') - self.tsframe.to_excel(writer, 'test2') - writer.save() - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', index_col=0) - tm.assert_frame_equal(self.frame, recons) - recons = read_excel(reader, 'test2', index_col=0) - tm.assert_frame_equal(self.tsframe, recons) - self.assertEqual(2, len(reader.sheet_names)) - self.assertEqual('test1', reader.sheet_names[0]) - self.assertEqual('test2', reader.sheet_names[1]) - - def test_colaliases(self): - _skip_if_no_xlrd() - - with ensure_clean(self.ext) as path: - self.frame['A'][:5] = nan - - self.frame.to_excel(path, 'test1') - self.frame.to_excel(path, 'test1', columns=['A', 'B']) - self.frame.to_excel(path, 'test1', header=False) - self.frame.to_excel(path, 'test1', index=False) - - # column aliases - col_aliases = Index(['AA', 'X', 'Y', 'Z']) - self.frame2.to_excel(path, 'test1', header=col_aliases) - reader = ExcelFile(path) - rs = read_excel(reader, 'test1', index_col=0) - xp = self.frame2.copy() - xp.columns = col_aliases - tm.assert_frame_equal(xp, rs) - - def test_roundtrip_indexlabels(self): - _skip_if_no_xlrd() - - with ensure_clean(self.ext) as path: - - self.frame['A'][:5] = nan - - self.frame.to_excel(path, 'test1') - self.frame.to_excel(path, 'test1', columns=['A', 'B']) - self.frame.to_excel(path, 'test1', header=False) - self.frame.to_excel(path, 'test1', index=False) - - # test index_label - frame = (DataFrame(np.random.randn(10, 2)) >= 0) - frame.to_excel(path, 'test1', - index_label=['test'], - merge_cells=self.merge_cells) - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', - index_col=0, - ).astype(np.int64) - frame.index.names = ['test'] - self.assertEqual(frame.index.names, recons.index.names) - - frame = (DataFrame(np.random.randn(10, 2)) >= 0) - frame.to_excel(path, - 'test1', - index_label=['test', 'dummy', 'dummy2'], - merge_cells=self.merge_cells) - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', - index_col=0, - ).astype(np.int64) - frame.index.names = ['test'] - self.assertEqual(frame.index.names, recons.index.names) - - frame = (DataFrame(np.random.randn(10, 2)) >= 0) - frame.to_excel(path, - 'test1', - index_label='test', - merge_cells=self.merge_cells) - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', - index_col=0, - ).astype(np.int64) - frame.index.names = ['test'] - tm.assert_frame_equal(frame, recons.astype(bool)) - - with ensure_clean(self.ext) as path: - - self.frame.to_excel(path, - 'test1', - columns=['A', 'B', 'C', 'D'], - index=False, merge_cells=self.merge_cells) - # take 'A' and 'B' as indexes (same row as cols 'C', 'D') - df = self.frame.copy() - df = df.set_index(['A', 'B']) - - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', index_col=[0, 1]) - tm.assert_frame_equal(df, recons, check_less_precise=True) - - def test_excel_roundtrip_indexname(self): - _skip_if_no_xlrd() + self.frame.to_excel(self.path, 'test1') + self.frame.to_excel(self.path, 'test1', columns=['A', 'B']) + self.frame.to_excel(self.path, 'test1', header=False) + self.frame.to_excel(self.path, 'test1', index=False) + # Test writing to separate sheets + writer = ExcelWriter(self.path) + self.frame.to_excel(writer, 'test1') + self.tsframe.to_excel(writer, 'test2') + writer.save() + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', index_col=0) + tm.assert_frame_equal(self.frame, recons) + recons = read_excel(reader, 'test2', index_col=0) + tm.assert_frame_equal(self.tsframe, recons) + assert 2 == len(reader.sheet_names) + assert 'test1' == reader.sheet_names[0] + assert 'test2' == reader.sheet_names[1] + + def test_colaliases(self, merge_cells, engine, ext): + self.frame['A'][:5] = nan + + self.frame.to_excel(self.path, 'test1') + self.frame.to_excel(self.path, 'test1', columns=['A', 'B']) + self.frame.to_excel(self.path, 'test1', header=False) + self.frame.to_excel(self.path, 'test1', index=False) + + # column aliases + col_aliases = Index(['AA', 'X', 'Y', 'Z']) + self.frame2.to_excel(self.path, 'test1', header=col_aliases) + reader = ExcelFile(self.path) + rs = read_excel(reader, 'test1', index_col=0) + xp = self.frame2.copy() + xp.columns = col_aliases + tm.assert_frame_equal(xp, rs) + + def test_roundtrip_indexlabels(self, merge_cells, engine, ext): + self.frame['A'][:5] = nan + + self.frame.to_excel(self.path, 'test1') + self.frame.to_excel(self.path, 'test1', columns=['A', 'B']) + self.frame.to_excel(self.path, 'test1', header=False) + self.frame.to_excel(self.path, 'test1', index=False) + + # test index_label + frame = (DataFrame(np.random.randn(10, 2)) >= 0) + frame.to_excel(self.path, 'test1', + index_label=['test'], + merge_cells=merge_cells) + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', + index_col=0, + ).astype(np.int64) + frame.index.names = ['test'] + assert frame.index.names == recons.index.names + + frame = (DataFrame(np.random.randn(10, 2)) >= 0) + frame.to_excel(self.path, + 'test1', + index_label=['test', 'dummy', 'dummy2'], + merge_cells=merge_cells) + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', + index_col=0, + ).astype(np.int64) + frame.index.names = ['test'] + assert frame.index.names == recons.index.names + + frame = (DataFrame(np.random.randn(10, 2)) >= 0) + frame.to_excel(self.path, + 'test1', + index_label='test', + merge_cells=merge_cells) + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', + index_col=0, + ).astype(np.int64) + frame.index.names = ['test'] + tm.assert_frame_equal(frame, recons.astype(bool)) + + self.frame.to_excel(self.path, + 'test1', + columns=['A', 'B', 'C', 'D'], + index=False, merge_cells=merge_cells) + # take 'A' and 'B' as indexes (same row as cols 'C', 'D') + df = self.frame.copy() + df = df.set_index(['A', 'B']) + + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', index_col=[0, 1]) + tm.assert_frame_equal(df, recons, check_less_precise=True) + + def test_excel_roundtrip_indexname(self, merge_cells, engine, ext): df = DataFrame(np.random.randn(10, 4)) df.index.name = 'foo' - with ensure_clean(self.ext) as path: - df.to_excel(path, merge_cells=self.merge_cells) + df.to_excel(self.path, merge_cells=merge_cells) - xf = ExcelFile(path) - result = read_excel(xf, xf.sheet_names[0], - index_col=0) + xf = ExcelFile(self.path) + result = read_excel(xf, xf.sheet_names[0], + index_col=0) - tm.assert_frame_equal(result, df) - self.assertEqual(result.index.name, 'foo') - - def test_excel_roundtrip_datetime(self): - _skip_if_no_xlrd() + tm.assert_frame_equal(result, df) + assert result.index.name == 'foo' + def test_excel_roundtrip_datetime(self, merge_cells, *_): # datetime.date, not sure what to test here exactly tsf = self.tsframe.copy() - with ensure_clean(self.ext) as path: - tsf.index = [x.date() for x in self.tsframe.index] - tsf.to_excel(path, 'test1', merge_cells=self.merge_cells) - reader = ExcelFile(path) - recons = read_excel(reader, 'test1') - tm.assert_frame_equal(self.tsframe, recons) + tsf.index = [x.date() for x in self.tsframe.index] + tsf.to_excel(self.path, "test1", merge_cells=merge_cells) + + reader = ExcelFile(self.path) + recons = read_excel(reader, "test1", index_col=0) - # GH4133 - excel output format strings - def test_excel_date_datetime_format(self): - _skip_if_no_xlrd() + tm.assert_frame_equal(self.tsframe, recons) + + def test_excel_date_datetime_format(self, merge_cells, engine, ext): + # see gh-4133 + # + # Excel output format strings df = DataFrame([[date(2014, 1, 31), date(1999, 9, 24)], [datetime(1998, 5, 26, 23, 33, 4), datetime(2014, 2, 28, 13, 5, 13)]], - index=['DATE', 'DATETIME'], columns=['X', 'Y']) + index=["DATE", "DATETIME"], columns=["X", "Y"]) df_expected = DataFrame([[datetime(2014, 1, 31), datetime(1999, 9, 24)], [datetime(1998, 5, 26, 23, 33, 4), datetime(2014, 2, 28, 13, 5, 13)]], - index=['DATE', 'DATETIME'], columns=['X', 'Y']) - - with ensure_clean(self.ext) as filename1: - with ensure_clean(self.ext) as filename2: - writer1 = ExcelWriter(filename1) - writer2 = ExcelWriter(filename2, - date_format='DD.MM.YYYY', - datetime_format='DD.MM.YYYY HH-MM-SS') - - df.to_excel(writer1, 'test1') - df.to_excel(writer2, 'test1') - - writer1.close() - writer2.close() - - reader1 = ExcelFile(filename1) - reader2 = ExcelFile(filename2) - - rs1 = read_excel(reader1, 'test1', index_col=None) - rs2 = read_excel(reader2, 'test1', index_col=None) - - tm.assert_frame_equal(rs1, rs2) - - # since the reader returns a datetime object for dates, we need - # to use df_expected to check the result - tm.assert_frame_equal(rs2, df_expected) - - def test_to_excel_periodindex(self): - _skip_if_no_xlrd() - + index=["DATE", "DATETIME"], columns=["X", "Y"]) + + with ensure_clean(ext) as filename2: + writer1 = ExcelWriter(self.path) + writer2 = ExcelWriter(filename2, + date_format="DD.MM.YYYY", + datetime_format="DD.MM.YYYY HH-MM-SS") + + df.to_excel(writer1, "test1") + df.to_excel(writer2, "test1") + + writer1.close() + writer2.close() + + reader1 = ExcelFile(self.path) + reader2 = ExcelFile(filename2) + + rs1 = read_excel(reader1, "test1", index_col=0) + rs2 = read_excel(reader2, "test1", index_col=0) + + tm.assert_frame_equal(rs1, rs2) + + # Since the reader returns a datetime object for dates, + # we need to use df_expected to check the result. + tm.assert_frame_equal(rs2, df_expected) + + def test_to_excel_interval_no_labels(self, *_): + # see gh-19242 + # + # Test writing Interval without labels. + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + dtype=np.int64) + expected = frame.copy() + + frame["new"] = pd.cut(frame[0], 10) + expected["new"] = pd.cut(expected[0], 10).astype(str) + + frame.to_excel(self.path, "test1") + reader = ExcelFile(self.path) + + recons = read_excel(reader, "test1", index_col=0) + tm.assert_frame_equal(expected, recons) + + def test_to_excel_interval_labels(self, *_): + # see gh-19242 + # + # Test writing Interval with labels. + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + dtype=np.int64) + expected = frame.copy() + intervals = pd.cut(frame[0], 10, labels=["A", "B", "C", "D", "E", + "F", "G", "H", "I", "J"]) + frame["new"] = intervals + expected["new"] = pd.Series(list(intervals)) + + frame.to_excel(self.path, "test1") + reader = ExcelFile(self.path) + + recons = read_excel(reader, "test1", index_col=0) + tm.assert_frame_equal(expected, recons) + + def test_to_excel_timedelta(self, *_): + # see gh-19242, gh-9155 + # + # Test writing timedelta to xls. + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + columns=["A"], dtype=np.int64) + expected = frame.copy() + + frame["new"] = frame["A"].apply(lambda x: timedelta(seconds=x)) + expected["new"] = expected["A"].apply( + lambda x: timedelta(seconds=x).total_seconds() / float(86400)) + + frame.to_excel(self.path, "test1") + reader = ExcelFile(self.path) + + recons = read_excel(reader, "test1", index_col=0) + tm.assert_frame_equal(expected, recons) + + def test_to_excel_periodindex(self, merge_cells, engine, ext): frame = self.tsframe xp = frame.resample('M', kind='period').mean() - with ensure_clean(self.ext) as path: - xp.to_excel(path, 'sht1') - - reader = ExcelFile(path) - rs = read_excel(reader, 'sht1', index_col=0) - tm.assert_frame_equal(xp, rs.to_period('M')) + xp.to_excel(self.path, 'sht1') - def test_to_excel_multiindex(self): - _skip_if_no_xlrd() + reader = ExcelFile(self.path) + rs = read_excel(reader, 'sht1', index_col=0) + tm.assert_frame_equal(xp, rs.to_period('M')) + def test_to_excel_multiindex(self, merge_cells, engine, ext): frame = self.frame arrays = np.arange(len(frame.index) * 2).reshape(2, -1) new_index = MultiIndex.from_arrays(arrays, names=['first', 'second']) frame.index = new_index - with ensure_clean(self.ext) as path: - frame.to_excel(path, 'test1', header=False) - frame.to_excel(path, 'test1', columns=['A', 'B']) + frame.to_excel(self.path, 'test1', header=False) + frame.to_excel(self.path, 'test1', columns=['A', 'B']) - # round trip - frame.to_excel(path, 'test1', merge_cells=self.merge_cells) - reader = ExcelFile(path) - df = read_excel(reader, 'test1', index_col=[0, 1]) - tm.assert_frame_equal(frame, df) + # round trip + frame.to_excel(self.path, 'test1', merge_cells=merge_cells) + reader = ExcelFile(self.path) + df = read_excel(reader, 'test1', index_col=[0, 1]) + tm.assert_frame_equal(frame, df) # GH13511 - def test_to_excel_multiindex_nan_label(self): - _skip_if_no_xlrd() - + def test_to_excel_multiindex_nan_label(self, merge_cells, engine, ext): frame = pd.DataFrame({'A': [None, 2, 3], 'B': [10, 20, 30], 'C': np.random.sample(3)}) frame = frame.set_index(['A', 'B']) - with ensure_clean(self.ext) as path: - frame.to_excel(path, merge_cells=self.merge_cells) - df = read_excel(path, index_col=[0, 1]) - tm.assert_frame_equal(frame, df) + frame.to_excel(self.path, merge_cells=merge_cells) + df = read_excel(self.path, index_col=[0, 1]) + tm.assert_frame_equal(frame, df) # Test for Issue 11328. If column indices are integers, make # sure they are handled correctly for either setting of # merge_cells - def test_to_excel_multiindex_cols(self): - _skip_if_no_xlrd() - + def test_to_excel_multiindex_cols(self, merge_cells, engine, ext): frame = self.frame arrays = np.arange(len(frame.index) * 2).reshape(2, -1) new_index = MultiIndex.from_arrays(arrays, @@ -1430,42 +1628,37 @@ def test_to_excel_multiindex_cols(self): (50, 1), (50, 2)]) frame.columns = new_cols_index header = [0, 1] - if not self.merge_cells: + if not merge_cells: header = 0 - with ensure_clean(self.ext) as path: - # round trip - frame.to_excel(path, 'test1', merge_cells=self.merge_cells) - reader = ExcelFile(path) - df = read_excel(reader, 'test1', header=header, - index_col=[0, 1]) - if not self.merge_cells: - fm = frame.columns.format(sparsify=False, - adjoin=False, names=False) - frame.columns = [".".join(map(str, q)) for q in zip(*fm)] - tm.assert_frame_equal(frame, df) - - def test_to_excel_multiindex_dates(self): - _skip_if_no_xlrd() - + # round trip + frame.to_excel(self.path, 'test1', merge_cells=merge_cells) + reader = ExcelFile(self.path) + df = read_excel(reader, 'test1', header=header, + index_col=[0, 1]) + if not merge_cells: + fm = frame.columns.format(sparsify=False, + adjoin=False, names=False) + frame.columns = [".".join(map(str, q)) for q in zip(*fm)] + tm.assert_frame_equal(frame, df) + + def test_to_excel_multiindex_dates(self, merge_cells, engine, ext): # try multiindex with dates tsframe = self.tsframe.copy() new_index = [tsframe.index, np.arange(len(tsframe.index))] tsframe.index = MultiIndex.from_arrays(new_index) - with ensure_clean(self.ext) as path: - tsframe.index.names = ['time', 'foo'] - tsframe.to_excel(path, 'test1', merge_cells=self.merge_cells) - reader = ExcelFile(path) - recons = read_excel(reader, 'test1', - index_col=[0, 1]) - - tm.assert_frame_equal(tsframe, recons) - self.assertEqual(recons.index.names, ('time', 'foo')) + tsframe.index.names = ['time', 'foo'] + tsframe.to_excel(self.path, 'test1', merge_cells=merge_cells) + reader = ExcelFile(self.path) + recons = read_excel(reader, 'test1', + index_col=[0, 1]) - def test_to_excel_multiindex_no_write_index(self): - _skip_if_no_xlrd() + tm.assert_frame_equal(tsframe, recons) + assert recons.index.names == ('time', 'foo') + def test_to_excel_multiindex_no_write_index(self, merge_cells, engine, + ext): # Test writing and re-reading a MI witout the index. GH 5616. # Initial non-MI frame. @@ -1476,74 +1669,66 @@ def test_to_excel_multiindex_no_write_index(self): multi_index = MultiIndex.from_tuples([(70, 80), (90, 100)]) frame2.index = multi_index - with ensure_clean(self.ext) as path: - - # Write out to Excel without the index. - frame2.to_excel(path, 'test1', index=False) + # Write out to Excel without the index. + frame2.to_excel(self.path, 'test1', index=False) - # Read it back in. - reader = ExcelFile(path) - frame3 = read_excel(reader, 'test1') + # Read it back in. + reader = ExcelFile(self.path) + frame3 = read_excel(reader, 'test1') - # Test that it is the same as the initial frame. - tm.assert_frame_equal(frame1, frame3) - - def test_to_excel_float_format(self): - _skip_if_no_xlrd() + # Test that it is the same as the initial frame. + tm.assert_frame_equal(frame1, frame3) + def test_to_excel_float_format(self, *_): df = DataFrame([[0.123456, 0.234567, 0.567567], [12.32112, 123123.2, 321321.2]], - index=['A', 'B'], columns=['X', 'Y', 'Z']) + index=["A", "B"], columns=["X", "Y", "Z"]) + df.to_excel(self.path, "test1", float_format="%.2f") - with ensure_clean(self.ext) as filename: - df.to_excel(filename, 'test1', float_format='%.2f') + reader = ExcelFile(self.path) + result = read_excel(reader, "test1", index_col=0) - reader = ExcelFile(filename) - rs = read_excel(reader, 'test1', index_col=None) - xp = DataFrame([[0.12, 0.23, 0.57], - [12.32, 123123.20, 321321.20]], - index=['A', 'B'], columns=['X', 'Y', 'Z']) - tm.assert_frame_equal(rs, xp) - - def test_to_excel_output_encoding(self): - _skip_if_no_xlrd() - - # avoid mixed inferred_type - df = DataFrame([[u'\u0192', u'\u0193', u'\u0194'], - [u'\u0195', u'\u0196', u'\u0197']], - index=[u'A\u0192', u'B'], - columns=[u'X\u0193', u'Y', u'Z']) - - with ensure_clean('__tmp_to_excel_float_format__.' + self.ext)\ - as filename: - df.to_excel(filename, sheet_name='TestSheet', encoding='utf8') - result = read_excel(filename, 'TestSheet', encoding='utf8') + expected = DataFrame([[0.12, 0.23, 0.57], + [12.32, 123123.20, 321321.20]], + index=["A", "B"], columns=["X", "Y", "Z"]) + tm.assert_frame_equal(result, expected) + + def test_to_excel_output_encoding(self, merge_cells, engine, ext): + # Avoid mixed inferred_type. + df = DataFrame([[u"\u0192", u"\u0193", u"\u0194"], + [u"\u0195", u"\u0196", u"\u0197"]], + index=[u"A\u0192", u"B"], + columns=[u"X\u0193", u"Y", u"Z"]) + + with ensure_clean("__tmp_to_excel_float_format__." + ext) as filename: + df.to_excel(filename, sheet_name="TestSheet", encoding="utf8") + result = read_excel(filename, "TestSheet", + encoding="utf8", index_col=0) tm.assert_frame_equal(result, df) - def test_to_excel_unicode_filename(self): - _skip_if_no_xlrd() - with ensure_clean(u('\u0192u.') + self.ext) as filename: + def test_to_excel_unicode_filename(self, merge_cells, engine, ext): + with ensure_clean(u("\u0192u.") + ext) as filename: try: - f = open(filename, 'wb') + f = open(filename, "wb") except UnicodeEncodeError: - pytest.skip('no unicode file names on this system') + pytest.skip("No unicode file names on this system") else: f.close() df = DataFrame([[0.123456, 0.234567, 0.567567], [12.32112, 123123.2, 321321.2]], - index=['A', 'B'], columns=['X', 'Y', 'Z']) - - df.to_excel(filename, 'test1', float_format='%.2f') + index=["A", "B"], columns=["X", "Y", "Z"]) + df.to_excel(filename, "test1", float_format="%.2f") reader = ExcelFile(filename) - rs = read_excel(reader, 'test1', index_col=None) - xp = DataFrame([[0.12, 0.23, 0.57], - [12.32, 123123.20, 321321.20]], - index=['A', 'B'], columns=['X', 'Y', 'Z']) - tm.assert_frame_equal(rs, xp) + result = read_excel(reader, "test1", index_col=0) - # def test_to_excel_header_styling_xls(self): + expected = DataFrame([[0.12, 0.23, 0.57], + [12.32, 123123.20, 321321.20]], + index=["A", "B"], columns=["X", "Y", "Z"]) + tm.assert_frame_equal(result, expected) + + # def test_to_excel_header_styling_xls(self, merge_cells, engine, ext): # import StringIO # s = StringIO( @@ -1575,23 +1760,22 @@ def test_to_excel_unicode_filename(self): # wbk = xlrd.open_workbook(filename, # formatting_info=True) - # self.assertEqual(["test1"], wbk.sheet_names()) + # assert ["test1"] == wbk.sheet_names() # ws = wbk.sheet_by_name('test1') - # self.assertEqual([(0, 1, 5, 7), (0, 1, 3, 5), (0, 1, 1, 3)], - # ws.merged_cells) + # assert [(0, 1, 5, 7), (0, 1, 3, 5), (0, 1, 1, 3)] == ws.merged_cells # for i in range(0, 2): # for j in range(0, 7): # xfx = ws.cell_xf_index(0, 0) # cell_xf = wbk.xf_list[xfx] # font = wbk.font_list - # self.assertEqual(1, font[cell_xf.font_index].bold) - # self.assertEqual(1, cell_xf.border.top_line_style) - # self.assertEqual(1, cell_xf.border.right_line_style) - # self.assertEqual(1, cell_xf.border.bottom_line_style) - # self.assertEqual(1, cell_xf.border.left_line_style) - # self.assertEqual(2, cell_xf.alignment.hor_align) + # assert 1 == font[cell_xf.font_index].bold + # assert 1 == cell_xf.border.top_line_style + # assert 1 == cell_xf.border.right_line_style + # assert 1 == cell_xf.border.bottom_line_style + # assert 1 == cell_xf.border.left_line_style + # assert 2 == cell_xf.alignment.hor_align # os.remove(filename) - # def test_to_excel_header_styling_xlsx(self): + # def test_to_excel_header_styling_xlsx(self, merge_cells, engine, ext): # import StringIO # s = StringIO( # """Date,ticker,type,value @@ -1621,175 +1805,209 @@ def test_to_excel_unicode_filename(self): # filename = '__tmp_to_excel_header_styling_xlsx__.xlsx' # pdf.to_excel(filename, 'test1') # wbk = openpyxl.load_workbook(filename) - # self.assertEqual(["test1"], wbk.get_sheet_names()) + # assert ["test1"] == wbk.get_sheet_names() # ws = wbk.get_sheet_by_name('test1') # xlsaddrs = ["%s2" % chr(i) for i in range(ord('A'), ord('H'))] # xlsaddrs += ["A%s" % i for i in range(1, 6)] # xlsaddrs += ["B1", "D1", "F1"] # for xlsaddr in xlsaddrs: # cell = ws.cell(xlsaddr) - # self.assertTrue(cell.style.font.bold) - # self.assertEqual(openpyxl.style.Border.BORDER_THIN, - # cell.style.borders.top.border_style) - # self.assertEqual(openpyxl.style.Border.BORDER_THIN, - # cell.style.borders.right.border_style) - # self.assertEqual(openpyxl.style.Border.BORDER_THIN, - # cell.style.borders.bottom.border_style) - # self.assertEqual(openpyxl.style.Border.BORDER_THIN, - # cell.style.borders.left.border_style) - # self.assertEqual(openpyxl.style.Alignment.HORIZONTAL_CENTER, - # cell.style.alignment.horizontal) + # assert cell.style.font.bold + # assert (openpyxl.style.Border.BORDER_THIN == + # cell.style.borders.top.border_style) + # assert (openpyxl.style.Border.BORDER_THIN == + # cell.style.borders.right.border_style) + # assert (openpyxl.style.Border.BORDER_THIN == + # cell.style.borders.bottom.border_style) + # assert (openpyxl.style.Border.BORDER_THIN == + # cell.style.borders.left.border_style) + # assert (openpyxl.style.Alignment.HORIZONTAL_CENTER == + # cell.style.alignment.horizontal) # mergedcells_addrs = ["C1", "E1", "G1"] # for maddr in mergedcells_addrs: - # self.assertTrue(ws.cell(maddr).merged) + # assert ws.cell(maddr).merged # os.remove(filename) - def test_excel_010_hemstring(self): - _skip_if_no_xlrd() + @pytest.mark.parametrize("use_headers", [True, False]) + @pytest.mark.parametrize("r_idx_nlevels", [1, 2, 3]) + @pytest.mark.parametrize("c_idx_nlevels", [1, 2, 3]) + def test_excel_010_hemstring(self, merge_cells, engine, ext, + c_idx_nlevels, r_idx_nlevels, use_headers): - if self.merge_cells: - pytest.skip('Skip tests for merged MI format.') + def roundtrip(data, header=True, parser_hdr=0, index=True): + data.to_excel(self.path, header=header, + merge_cells=merge_cells, index=index) - from pandas.util.testing import makeCustomDataframe as mkdf - # ensure limited functionality in 0.10 - # override of #2370 until sorted out in 0.11 + xf = ExcelFile(self.path) + return read_excel(xf, xf.sheet_names[0], header=parser_hdr) - def roundtrip(df, header=True, parser_hdr=0, index=True): + # Basic test. + parser_header = 0 if use_headers else None + res = roundtrip(DataFrame([0]), use_headers, parser_header) - with ensure_clean(self.ext) as path: - df.to_excel(path, header=header, - merge_cells=self.merge_cells, index=index) - xf = ExcelFile(path) - res = read_excel(xf, xf.sheet_names[0], header=parser_hdr) - return res + assert res.shape == (1, 2) + assert res.iloc[0, 0] is not np.nan + # More complex tests with multi-index. nrows = 5 ncols = 3 - for use_headers in (True, False): - for i in range(1, 4): # row multindex upto nlevel=3 - for j in range(1, 4): # col "" - df = mkdf(nrows, ncols, r_idx_nlevels=i, c_idx_nlevels=j) - - # this if will be removed once multi column excel writing - # is implemented for now fixing #9794 - if j > 1: - with tm.assertRaises(NotImplementedError): - res = roundtrip(df, use_headers, index=False) - else: - res = roundtrip(df, use_headers) - - if use_headers: - self.assertEqual(res.shape, (nrows, ncols + i)) - else: - # first row taken as columns - self.assertEqual(res.shape, (nrows - 1, ncols + i)) - - # no nans - for r in range(len(res.index)): - for c in range(len(res.columns)): - self.assertTrue(res.iloc[r, c] is not np.nan) - - res = roundtrip(DataFrame([0])) - self.assertEqual(res.shape, (1, 1)) - self.assertTrue(res.iloc[0, 0] is not np.nan) - res = roundtrip(DataFrame([0]), False, None) - self.assertEqual(res.shape, (1, 2)) - self.assertTrue(res.iloc[0, 0] is not np.nan) + from pandas.util.testing import makeCustomDataframe as mkdf + # ensure limited functionality in 0.10 + # override of gh-2370 until sorted out in 0.11 - def test_excel_010_hemstring_raises_NotImplementedError(self): - # This test was failing only for j>1 and header=False, - # So I reproduced a simple test. - _skip_if_no_xlrd() + df = mkdf(nrows, ncols, r_idx_nlevels=r_idx_nlevels, + c_idx_nlevels=c_idx_nlevels) - if self.merge_cells: - pytest.skip('Skip tests for merged MI format.') + # This if will be removed once multi-column Excel writing + # is implemented. For now fixing gh-9794. + if c_idx_nlevels > 1: + with pytest.raises(NotImplementedError): + roundtrip(df, use_headers, index=False) + else: + res = roundtrip(df, use_headers) - from pandas.util.testing import makeCustomDataframe as mkdf - # ensure limited functionality in 0.10 - # override of #2370 until sorted out in 0.11 + if use_headers: + assert res.shape == (nrows, ncols + r_idx_nlevels) + else: + # First row taken as columns. + assert res.shape == (nrows - 1, ncols + r_idx_nlevels) + + # No NaNs. + for r in range(len(res.index)): + for c in range(len(res.columns)): + assert res.iloc[r, c] is not np.nan + + def test_duplicated_columns(self, *_): + # see gh-5235 + df = DataFrame([[1, 2, 3], [1, 2, 3], [1, 2, 3]], + columns=["A", "B", "B"]) + df.to_excel(self.path, "test1") + expected = DataFrame([[1, 2, 3], [1, 2, 3], [1, 2, 3]], + columns=["A", "B", "B.1"]) + + # By default, we mangle. + result = read_excel(self.path, "test1", index_col=0) + tm.assert_frame_equal(result, expected) - def roundtrip2(df, header=True, parser_hdr=0, index=True): + # Explicitly, we pass in the parameter. + result = read_excel(self.path, "test1", index_col=0, + mangle_dupe_cols=True) + tm.assert_frame_equal(result, expected) - with ensure_clean(self.ext) as path: - df.to_excel(path, header=header, - merge_cells=self.merge_cells, index=index) - xf = ExcelFile(path) - res = read_excel(xf, xf.sheet_names[0], header=parser_hdr) - return res + # see gh-11007, gh-10970 + df = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]], + columns=["A", "B", "A", "B"]) + df.to_excel(self.path, "test1") - nrows = 5 - ncols = 3 - j = 2 - i = 1 - df = mkdf(nrows, ncols, r_idx_nlevels=i, c_idx_nlevels=j) - with tm.assertRaises(NotImplementedError): - roundtrip2(df, header=False, index=False) - - def test_duplicated_columns(self): - # Test for issue #5235 - _skip_if_no_xlrd() - - with ensure_clean(self.ext) as path: - write_frame = DataFrame([[1, 2, 3], [1, 2, 3], [1, 2, 3]]) - colnames = ['A', 'B', 'B'] - - write_frame.columns = colnames - write_frame.to_excel(path, 'test1') - - read_frame = read_excel(path, 'test1') - read_frame.columns = colnames - tm.assert_frame_equal(write_frame, read_frame) - - # 11007 / #10970 - write_frame = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]], - columns=['A', 'B', 'A', 'B']) - write_frame.to_excel(path, 'test1') - read_frame = read_excel(path, 'test1') - read_frame.columns = ['A', 'B', 'A', 'B'] - tm.assert_frame_equal(write_frame, read_frame) - - # 10982 - write_frame.to_excel(path, 'test1', index=False, header=False) - read_frame = read_excel(path, 'test1', header=None) - write_frame.columns = [0, 1, 2, 3] - tm.assert_frame_equal(write_frame, read_frame) - - def test_swapped_columns(self): - # Test for issue #5427. - _skip_if_no_xlrd() + result = read_excel(self.path, "test1", index_col=0) + expected = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]], + columns=["A", "B", "A.1", "B.1"]) + tm.assert_frame_equal(result, expected) - with ensure_clean(self.ext) as path: - write_frame = DataFrame({'A': [1, 1, 1], - 'B': [2, 2, 2]}) - write_frame.to_excel(path, 'test1', columns=['B', 'A']) + # see gh-10982 + df.to_excel(self.path, "test1", index=False, header=False) + result = read_excel(self.path, "test1", header=None) - read_frame = read_excel(path, 'test1', header=0) + expected = DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]]) + tm.assert_frame_equal(result, expected) - tm.assert_series_equal(write_frame['A'], read_frame['A']) - tm.assert_series_equal(write_frame['B'], read_frame['B']) + msg = "Setting mangle_dupe_cols=False is not supported yet" + with pytest.raises(ValueError, match=msg): + read_excel(self.path, "test1", header=None, mangle_dupe_cols=False) - def test_invalid_columns(self): - # 10982 - _skip_if_no_xlrd() + def test_swapped_columns(self, merge_cells, engine, ext): + # Test for issue #5427. + write_frame = DataFrame({'A': [1, 1, 1], + 'B': [2, 2, 2]}) + write_frame.to_excel(self.path, 'test1', columns=['B', 'A']) + + read_frame = read_excel(self.path, 'test1', header=0) + + tm.assert_series_equal(write_frame['A'], read_frame['A']) + tm.assert_series_equal(write_frame['B'], read_frame['B']) + + def test_invalid_columns(self, *_): + # see gh-10982 + write_frame = DataFrame({"A": [1, 1, 1], + "B": [2, 2, 2]}) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + write_frame.to_excel(self.path, "test1", columns=["B", "C"]) + + expected = write_frame.reindex(columns=["B", "C"]) + read_frame = read_excel(self.path, "test1", index_col=0) + tm.assert_frame_equal(expected, read_frame) + + with pytest.raises(KeyError): + write_frame.to_excel(self.path, "test1", columns=["C", "D"]) + + def test_comment_arg(self, *_): + # see gh-18735 + # + # Test the comment argument functionality to read_excel. + + # Create file to read in. + df = DataFrame({"A": ["one", "#one", "one"], + "B": ["two", "two", "#two"]}) + df.to_excel(self.path, "test_c") + + # Read file without comment arg. + result1 = read_excel(self.path, "test_c", index_col=0) + + result1.iloc[1, 0] = None + result1.iloc[1, 1] = None + result1.iloc[2, 1] = None + + result2 = read_excel(self.path, "test_c", comment="#", index_col=0) + tm.assert_frame_equal(result1, result2) + + def test_comment_default(self, merge_cells, engine, ext): + # Re issue #18735 + # Test the comment argument default to read_excel + + # Create file to read in + df = DataFrame({'A': ['one', '#one', 'one'], + 'B': ['two', 'two', '#two']}) + df.to_excel(self.path, 'test_c') + + # Read file with default and explicit comment=None + result1 = read_excel(self.path, 'test_c') + result2 = read_excel(self.path, 'test_c', comment=None) + tm.assert_frame_equal(result1, result2) + + def test_comment_used(self, *_): + # see gh-18735 + # + # Test the comment argument is working as expected when used. + + # Create file to read in. + df = DataFrame({"A": ["one", "#one", "one"], + "B": ["two", "two", "#two"]}) + df.to_excel(self.path, "test_c") + + # Test read_frame_comment against manually produced expected output. + expected = DataFrame({"A": ["one", None, "one"], + "B": ["two", None, None]}) + result = read_excel(self.path, "test_c", comment="#", index_col=0) + tm.assert_frame_equal(result, expected) - with ensure_clean(self.ext) as path: - write_frame = DataFrame({'A': [1, 1, 1], - 'B': [2, 2, 2]}) + def test_comment_empty_line(self, merge_cells, engine, ext): + # Re issue #18735 + # Test that read_excel ignores commented lines at the end of file - write_frame.to_excel(path, 'test1', columns=['B', 'C']) - expected = write_frame.loc[:, ['B', 'C']] - read_frame = read_excel(path, 'test1') - tm.assert_frame_equal(expected, read_frame) + df = DataFrame({'a': ['1', '#2'], 'b': ['2', '3']}) + df.to_excel(self.path, index=False) - with tm.assertRaises(KeyError): - write_frame.to_excel(path, 'test1', columns=['C', 'D']) + # Test that all-comment lines at EoF are ignored + expected = DataFrame({'a': [1], 'b': [2]}) + result = read_excel(self.path, comment='#') + tm.assert_frame_equal(result, expected) - def test_datetimes(self): + def test_datetimes(self, merge_cells, engine, ext): # Test writing and reading datetimes. For issue #9139. (xref #9185) - _skip_if_no_xlrd() - datetimes = [datetime(2013, 1, 13, 1, 2, 3), datetime(2013, 1, 13, 2, 45, 56), datetime(2013, 1, 13, 4, 29, 49), @@ -1802,265 +2020,83 @@ def test_datetimes(self): datetime(2013, 1, 13, 16, 37, 0), datetime(2013, 1, 13, 18, 20, 52)] - with ensure_clean(self.ext) as path: - write_frame = DataFrame.from_items([('A', datetimes)]) - write_frame.to_excel(path, 'Sheet1') - read_frame = read_excel(path, 'Sheet1', header=0) - - tm.assert_series_equal(write_frame['A'], read_frame['A']) + write_frame = DataFrame({'A': datetimes}) + write_frame.to_excel(self.path, 'Sheet1') + read_frame = read_excel(self.path, 'Sheet1', header=0) - # GH7074 - def test_bytes_io(self): - _skip_if_no_xlrd() + tm.assert_series_equal(write_frame['A'], read_frame['A']) + def test_bytes_io(self, merge_cells, engine, ext): + # see gh-7074 bio = BytesIO() df = DataFrame(np.random.randn(10, 2)) - # pass engine explicitly as there is no file path to infer from - writer = ExcelWriter(bio, engine=self.engine_name) + + # Pass engine explicitly, as there is no file path to infer from. + writer = ExcelWriter(bio, engine=engine) df.to_excel(writer) writer.save() + bio.seek(0) - reread_df = read_excel(bio) + reread_df = read_excel(bio, index_col=0) tm.assert_frame_equal(df, reread_df) - # GH8188 - def test_write_lists_dict(self): - _skip_if_no_xlrd() + def test_write_lists_dict(self, *_): + # see gh-8188. + df = DataFrame({"mixed": ["a", ["b", "c"], {"d": "e", "f": 2}], + "numeric": [1, 2, 3.0], + "str": ["apple", "banana", "cherry"]}) + df.to_excel(self.path, "Sheet1") + read = read_excel(self.path, "Sheet1", header=0, index_col=0) - df = DataFrame({'mixed': ['a', ['b', 'c'], {'d': 'e', 'f': 2}], - 'numeric': [1, 2, 3.0], - 'str': ['apple', 'banana', 'cherry']}) expected = df.copy() expected.mixed = expected.mixed.apply(str) - expected.numeric = expected.numeric.astype('int64') - with ensure_clean(self.ext) as path: - df.to_excel(path, 'Sheet1') - read = read_excel(path, 'Sheet1', header=0) - tm.assert_frame_equal(read, expected) - - # GH13347 - def test_true_and_false_value_options(self): - df = pd.DataFrame([['foo', 'bar']], columns=['col1', 'col2']) - expected = df.replace({'foo': True, - 'bar': False}) - with ensure_clean(self.ext) as path: - df.to_excel(path) - read_frame = read_excel(path, true_values=['foo'], - false_values=['bar']) - tm.assert_frame_equal(read_frame, expected) - - def test_freeze_panes(self): - # GH15160 - expected = DataFrame([[1, 2], [3, 4]], columns=['col1', 'col2']) - with ensure_clean(self.ext) as path: - expected.to_excel(path, "Sheet1", freeze_panes=(1, 1)) - result = read_excel(path) - tm.assert_frame_equal(expected, result) - - -def raise_wrapper(major_ver): - def versioned_raise_wrapper(orig_method): - @functools.wraps(orig_method) - def wrapped(self, *args, **kwargs): - _skip_if_no_openpyxl() - if openpyxl_compat.is_compat(major_ver=major_ver): - orig_method(self, *args, **kwargs) - else: - msg = (r'Installed openpyxl is not supported at this ' - r'time\. Use.+') - with tm.assertRaisesRegexp(ValueError, msg): - orig_method(self, *args, **kwargs) - return wrapped - return versioned_raise_wrapper - - -def raise_on_incompat_version(major_ver): - def versioned_raise_on_incompat_version(cls): - methods = filter(operator.methodcaller( - 'startswith', 'test_'), dir(cls)) - for method in methods: - setattr(cls, method, raise_wrapper( - major_ver)(getattr(cls, method))) - return cls - return versioned_raise_on_incompat_version - - -@raise_on_incompat_version(1) -class OpenpyxlTests(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'openpyxl1' - check_skip = staticmethod(lambda *args, **kwargs: None) - - def test_to_excel_styleconverter(self): - _skip_if_no_openpyxl() - if not openpyxl_compat.is_compat(major_ver=1): - pytest.skip('incompatible openpyxl version') - - import openpyxl - - hstyle = {"font": {"bold": True}, - "borders": {"top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin"}, - "alignment": {"horizontal": "center", "vertical": "top"}} - - xlsx_style = _Openpyxl1Writer._convert_to_style(hstyle) - self.assertTrue(xlsx_style.font.bold) - self.assertEqual(openpyxl.style.Border.BORDER_THIN, - xlsx_style.borders.top.border_style) - self.assertEqual(openpyxl.style.Border.BORDER_THIN, - xlsx_style.borders.right.border_style) - self.assertEqual(openpyxl.style.Border.BORDER_THIN, - xlsx_style.borders.bottom.border_style) - self.assertEqual(openpyxl.style.Border.BORDER_THIN, - xlsx_style.borders.left.border_style) - self.assertEqual(openpyxl.style.Alignment.HORIZONTAL_CENTER, - xlsx_style.alignment.horizontal) - self.assertEqual(openpyxl.style.Alignment.VERTICAL_TOP, - xlsx_style.alignment.vertical) - - -def skip_openpyxl_gt21(cls): - """Skip a TestCase instance if openpyxl >= 2.2""" - - @classmethod - def setUpClass(cls): - _skip_if_no_openpyxl() - import openpyxl - ver = openpyxl.__version__ - if (not (LooseVersion(ver) >= LooseVersion('2.0.0') and - LooseVersion(ver) < LooseVersion('2.2.0'))): - pytest.skip("openpyxl %s >= 2.2" % str(ver)) - - cls.setUpClass = setUpClass - return cls - - -@raise_on_incompat_version(2) -@skip_openpyxl_gt21 -class Openpyxl20Tests(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'openpyxl20' - check_skip = staticmethod(lambda *args, **kwargs: None) - - def test_to_excel_styleconverter(self): - import openpyxl - from openpyxl import styles - - hstyle = { - "font": { - "color": '00FF0000', - "bold": True, - }, - "borders": { - "top": "thin", - "right": "thin", - "bottom": "thin", - "left": "thin", - }, - "alignment": { - "horizontal": "center", - "vertical": "top", - }, - "fill": { - "patternType": 'solid', - 'fgColor': { - 'rgb': '006666FF', - 'tint': 0.3, - }, - }, - "number_format": { - "format_code": "0.00" - }, - "protection": { - "locked": True, - "hidden": False, - }, - } - - font_color = styles.Color('00FF0000') - font = styles.Font(bold=True, color=font_color) - side = styles.Side(style=styles.borders.BORDER_THIN) - border = styles.Border(top=side, right=side, bottom=side, left=side) - alignment = styles.Alignment(horizontal='center', vertical='top') - fill_color = styles.Color(rgb='006666FF', tint=0.3) - fill = styles.PatternFill(patternType='solid', fgColor=fill_color) + expected.numeric = expected.numeric.astype("int64") - # ahh openpyxl API changes - ver = openpyxl.__version__ - if ver >= LooseVersion('2.0.0') and ver < LooseVersion('2.1.0'): - number_format = styles.NumberFormat(format_code='0.00') - else: - number_format = '0.00' # XXX: Only works with openpyxl-2.1.0 + tm.assert_frame_equal(read, expected) - protection = styles.Protection(locked=True, hidden=False) + def test_true_and_false_value_options(self, *_): + # see gh-13347 + df = pd.DataFrame([["foo", "bar"]], columns=["col1", "col2"]) + expected = df.replace({"foo": True, "bar": False}) - kw = _Openpyxl20Writer._convert_to_style_kwargs(hstyle) - self.assertEqual(kw['font'], font) - self.assertEqual(kw['border'], border) - self.assertEqual(kw['alignment'], alignment) - self.assertEqual(kw['fill'], fill) - self.assertEqual(kw['number_format'], number_format) - self.assertEqual(kw['protection'], protection) + df.to_excel(self.path) + read_frame = read_excel(self.path, true_values=["foo"], + false_values=["bar"], index_col=0) + tm.assert_frame_equal(read_frame, expected) - def test_write_cells_merge_styled(self): - from pandas.formats.format import ExcelCell - from openpyxl import styles - - sheet_name = 'merge_styled' + def test_freeze_panes(self, *_): + # see gh-15160 + expected = DataFrame([[1, 2], [3, 4]], columns=["col1", "col2"]) + expected.to_excel(self.path, "Sheet1", freeze_panes=(1, 1)) - sty_b1 = {'font': {'color': '00FF0000'}} - sty_a2 = {'font': {'color': '0000FF00'}} - - initial_cells = [ - ExcelCell(col=1, row=0, val=42, style=sty_b1), - ExcelCell(col=0, row=1, val=99, style=sty_a2), - ] - - sty_merged = {'font': {'color': '000000FF', 'bold': True}} - sty_kwargs = _Openpyxl20Writer._convert_to_style_kwargs(sty_merged) - openpyxl_sty_merged = styles.Style(**sty_kwargs) - merge_cells = [ - ExcelCell(col=0, row=0, val='pandas', - mergestart=1, mergeend=1, style=sty_merged), - ] - - with ensure_clean('.xlsx') as path: - writer = _Openpyxl20Writer(path) - writer.write_cells(initial_cells, sheet_name=sheet_name) - writer.write_cells(merge_cells, sheet_name=sheet_name) + result = read_excel(self.path, index_col=0) + tm.assert_frame_equal(result, expected) - wks = writer.sheets[sheet_name] - xcell_b1 = wks['B1'] - xcell_a2 = wks['A2'] - self.assertEqual(xcell_b1.style, openpyxl_sty_merged) - self.assertEqual(xcell_a2.style, openpyxl_sty_merged) + def test_path_path_lib(self, merge_cells, engine, ext): + df = tm.makeDataFrame() + writer = partial(df.to_excel, engine=engine) + reader = partial(pd.read_excel, index_col=0) + result = tm.round_trip_pathlib(writer, reader, + path="foo.{ext}".format(ext=ext)) + tm.assert_frame_equal(result, df) -def skip_openpyxl_lt22(cls): - """Skip a TestCase instance if openpyxl < 2.2""" + def test_path_local_path(self, merge_cells, engine, ext): + df = tm.makeDataFrame() + writer = partial(df.to_excel, engine=engine) - @classmethod - def setUpClass(cls): - _skip_if_no_openpyxl() - import openpyxl - ver = openpyxl.__version__ - if LooseVersion(ver) < LooseVersion('2.2.0'): - pytest.skip("openpyxl %s < 2.2" % str(ver)) + reader = partial(pd.read_excel, index_col=0) + result = tm.round_trip_pathlib(writer, reader, + path="foo.{ext}".format(ext=ext)) + tm.assert_frame_equal(result, df) - cls.setUpClass = setUpClass - return cls +@td.skip_if_no('openpyxl') +@pytest.mark.parametrize("merge_cells,ext,engine", [ + (None, '.xlsx', 'openpyxl')]) +class TestOpenpyxlTests(_WriterBase): -@raise_on_incompat_version(2) -@skip_openpyxl_lt22 -class Openpyxl22Tests(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'openpyxl22' - check_skip = staticmethod(lambda *args, **kwargs: None) - - def test_to_excel_styleconverter(self): + def test_to_excel_styleconverter(self, merge_cells, ext, engine): from openpyxl import styles hstyle = { @@ -2106,19 +2142,16 @@ def test_to_excel_styleconverter(self): protection = styles.Protection(locked=True, hidden=False) - kw = _Openpyxl22Writer._convert_to_style_kwargs(hstyle) - self.assertEqual(kw['font'], font) - self.assertEqual(kw['border'], border) - self.assertEqual(kw['alignment'], alignment) - self.assertEqual(kw['fill'], fill) - self.assertEqual(kw['number_format'], number_format) - self.assertEqual(kw['protection'], protection) - - def test_write_cells_merge_styled(self): - if not openpyxl_compat.is_compat(major_ver=2): - pytest.skip('incompatible openpyxl version') + kw = _OpenpyxlWriter._convert_to_style_kwargs(hstyle) + assert kw['font'] == font + assert kw['border'] == border + assert kw['alignment'] == alignment + assert kw['fill'] == fill + assert kw['number_format'] == number_format + assert kw['protection'] == protection - from pandas.formats.format import ExcelCell + def test_write_cells_merge_styled(self, merge_cells, ext, engine): + from pandas.io.formats.excel import ExcelCell sheet_name = 'merge_styled' @@ -2131,63 +2164,85 @@ def test_write_cells_merge_styled(self): ] sty_merged = {'font': {'color': '000000FF', 'bold': True}} - sty_kwargs = _Openpyxl22Writer._convert_to_style_kwargs(sty_merged) + sty_kwargs = _OpenpyxlWriter._convert_to_style_kwargs(sty_merged) openpyxl_sty_merged = sty_kwargs['font'] merge_cells = [ ExcelCell(col=0, row=0, val='pandas', mergestart=1, mergeend=1, style=sty_merged), ] - with ensure_clean('.xlsx') as path: - writer = _Openpyxl22Writer(path) + with ensure_clean(ext) as path: + writer = _OpenpyxlWriter(path) writer.write_cells(initial_cells, sheet_name=sheet_name) writer.write_cells(merge_cells, sheet_name=sheet_name) wks = writer.sheets[sheet_name] xcell_b1 = wks['B1'] xcell_a2 = wks['A2'] - self.assertEqual(xcell_b1.font, openpyxl_sty_merged) - self.assertEqual(xcell_a2.font, openpyxl_sty_merged) + assert xcell_b1.font == openpyxl_sty_merged + assert xcell_a2.font == openpyxl_sty_merged + + @pytest.mark.parametrize("mode,expected", [ + ('w', ['baz']), ('a', ['foo', 'bar', 'baz'])]) + def test_write_append_mode(self, merge_cells, ext, engine, mode, expected): + import openpyxl + df = DataFrame([1], columns=['baz']) + + with ensure_clean(ext) as f: + wb = openpyxl.Workbook() + wb.worksheets[0].title = 'foo' + wb.worksheets[0]['A1'].value = 'foo' + wb.create_sheet('bar') + wb.worksheets[1]['A1'].value = 'bar' + wb.save(f) + + writer = ExcelWriter(f, engine=engine, mode=mode) + df.to_excel(writer, sheet_name='baz', index=False) + writer.save() + wb2 = openpyxl.load_workbook(f) + result = [sheet.title for sheet in wb2.worksheets] + assert result == expected -class XlwtTests(ExcelWriterBase, tm.TestCase): - ext = '.xls' - engine_name = 'xlwt' - check_skip = staticmethod(_skip_if_no_xlwt) + for index, cell_value in enumerate(expected): + assert wb2.worksheets[index]['A1'].value == cell_value - def test_excel_raise_error_on_multiindex_columns_and_no_index(self): - _skip_if_no_xlwt() + +@td.skip_if_no('xlwt') +@pytest.mark.parametrize("merge_cells,ext,engine", [ + (None, '.xls', 'xlwt')]) +class TestXlwtTests(_WriterBase): + + def test_excel_raise_error_on_multiindex_columns_and_no_index( + self, merge_cells, ext, engine): # MultiIndex as columns is not yet implemented 9794 cols = MultiIndex.from_tuples([('site', ''), ('2014', 'height'), ('2014', 'weight')]) df = DataFrame(np.random.randn(10, 3), columns=cols) - with tm.assertRaises(NotImplementedError): - with ensure_clean(self.ext) as path: + with pytest.raises(NotImplementedError): + with ensure_clean(ext) as path: df.to_excel(path, index=False) - def test_excel_multiindex_columns_and_index_true(self): - _skip_if_no_xlwt() + def test_excel_multiindex_columns_and_index_true(self, merge_cells, ext, + engine): cols = MultiIndex.from_tuples([('site', ''), ('2014', 'height'), ('2014', 'weight')]) df = pd.DataFrame(np.random.randn(10, 3), columns=cols) - with ensure_clean(self.ext) as path: + with ensure_clean(ext) as path: df.to_excel(path, index=True) - def test_excel_multiindex_index(self): - _skip_if_no_xlwt() + def test_excel_multiindex_index(self, merge_cells, ext, engine): # MultiIndex as index works so assert no error #9794 cols = MultiIndex.from_tuples([('site', ''), ('2014', 'height'), ('2014', 'weight')]) df = DataFrame(np.random.randn(3, 10), index=cols) - with ensure_clean(self.ext) as path: + with ensure_clean(ext) as path: df.to_excel(path, index=False) - def test_to_excel_styleconverter(self): - _skip_if_no_xlwt() - + def test_to_excel_styleconverter(self, merge_cells, ext, engine): import xlwt hstyle = {"font": {"bold": True}, @@ -2198,32 +2253,37 @@ def test_to_excel_styleconverter(self): "alignment": {"horizontal": "center", "vertical": "top"}} xls_style = _XlwtWriter._convert_to_style(hstyle) - self.assertTrue(xls_style.font.bold) - self.assertEqual(xlwt.Borders.THIN, xls_style.borders.top) - self.assertEqual(xlwt.Borders.THIN, xls_style.borders.right) - self.assertEqual(xlwt.Borders.THIN, xls_style.borders.bottom) - self.assertEqual(xlwt.Borders.THIN, xls_style.borders.left) - self.assertEqual(xlwt.Alignment.HORZ_CENTER, xls_style.alignment.horz) - self.assertEqual(xlwt.Alignment.VERT_TOP, xls_style.alignment.vert) + assert xls_style.font.bold + assert xlwt.Borders.THIN == xls_style.borders.top + assert xlwt.Borders.THIN == xls_style.borders.right + assert xlwt.Borders.THIN == xls_style.borders.bottom + assert xlwt.Borders.THIN == xls_style.borders.left + assert xlwt.Alignment.HORZ_CENTER == xls_style.alignment.horz + assert xlwt.Alignment.VERT_TOP == xls_style.alignment.vert + + def test_write_append_mode_raises(self, merge_cells, ext, engine): + msg = "Append mode is not supported with xlwt!" + + with ensure_clean(ext) as f: + with pytest.raises(ValueError, match=msg): + ExcelWriter(f, engine=engine, mode='a') -class XlsxWriterTests(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'xlsxwriter' - check_skip = staticmethod(_skip_if_no_xlsxwriter) +@td.skip_if_no('xlsxwriter') +@pytest.mark.parametrize("merge_cells,ext,engine", [ + (None, '.xlsx', 'xlsxwriter')]) +class TestXlsxWriterTests(_WriterBase): - def test_column_format(self): + @td.skip_if_no('openpyxl') + def test_column_format(self, merge_cells, ext, engine): # Test that column formats are applied to cells. Test for issue #9167. # Applicable to xlsxwriter only. - _skip_if_no_xlsxwriter() - with warnings.catch_warnings(): # Ignore the openpyxl lxml warning. warnings.simplefilter("ignore") - _skip_if_no_openpyxl() import openpyxl - with ensure_clean(self.ext) as path: + with ensure_clean(ext) as path: frame = DataFrame({'A': [123456, 123456], 'B': [123456, 123456]}) @@ -2254,63 +2314,43 @@ def test_column_format(self): try: read_num_format = cell.number_format - except: + except Exception: read_num_format = cell.style.number_format._format_code - self.assertEqual(read_num_format, num_format) - - -class OpenpyxlTests_NoMerge(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'openpyxl' - check_skip = staticmethod(_skip_if_no_openpyxl) - - # Test < 0.13 non-merge behaviour for MultiIndex and Hierarchical Rows. - merge_cells = False - + assert read_num_format == num_format -class XlwtTests_NoMerge(ExcelWriterBase, tm.TestCase): - ext = '.xls' - engine_name = 'xlwt' - check_skip = staticmethod(_skip_if_no_xlwt) + def test_write_append_mode_raises(self, merge_cells, ext, engine): + msg = "Append mode is not supported with xlsxwriter!" - # Test < 0.13 non-merge behaviour for MultiIndex and Hierarchical Rows. - merge_cells = False + with ensure_clean(ext) as f: + with pytest.raises(ValueError, match=msg): + ExcelWriter(f, engine=engine, mode='a') -class XlsxWriterTests_NoMerge(ExcelWriterBase, tm.TestCase): - ext = '.xlsx' - engine_name = 'xlsxwriter' - check_skip = staticmethod(_skip_if_no_xlsxwriter) - - # Test < 0.13 non-merge behaviour for MultiIndex and Hierarchical Rows. - merge_cells = False - - -class ExcelWriterEngineTests(tm.TestCase): - - def test_ExcelWriter_dispatch(self): - with tm.assertRaisesRegexp(ValueError, 'No engine'): - ExcelWriter('nothing') +class TestExcelWriterEngineTests(object): - try: - import xlsxwriter # noqa - writer_klass = _XlsxWriter - except ImportError: - _skip_if_no_openpyxl() - if not openpyxl_compat.is_compat(major_ver=1): - pytest.skip('incompatible openpyxl version') - writer_klass = _Openpyxl1Writer - - with ensure_clean('.xlsx') as path: + @pytest.mark.parametrize('klass,ext', [ + pytest.param(_XlsxWriter, '.xlsx', marks=pytest.mark.skipif( + not td.safe_import('xlsxwriter'), reason='No xlsxwriter')), + pytest.param(_OpenpyxlWriter, '.xlsx', marks=pytest.mark.skipif( + not td.safe_import('openpyxl'), reason='No openpyxl')), + pytest.param(_XlwtWriter, '.xls', marks=pytest.mark.skipif( + not td.safe_import('xlwt'), reason='No xlwt')) + ]) + def test_ExcelWriter_dispatch(self, klass, ext): + with ensure_clean(ext) as path: writer = ExcelWriter(path) - tm.assertIsInstance(writer, writer_klass) + if ext == '.xlsx' and td.safe_import('xlsxwriter'): + # xlsxwriter has preference over openpyxl if both installed + assert isinstance(writer, _XlsxWriter) + else: + assert isinstance(writer, klass) - _skip_if_no_xlwt() - with ensure_clean('.xls') as path: - writer = ExcelWriter(path) - tm.assertIsInstance(writer, _XlwtWriter) + def test_ExcelWriter_dispatch_raises(self): + with pytest.raises(ValueError, match='No engine'): + ExcelWriter('nothing') + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_register_writer(self): # some awkward mocking to test out dispatch and such actually works called_save = [] @@ -2319,7 +2359,7 @@ def test_register_writer(self): class DummyClass(ExcelWriter): called_save = False called_write_cells = False - supported_extensions = ['test', 'xlsx', 'xls'] + supported_extensions = ['xlsx', 'xls'] engine = 'dummy' def save(self): @@ -2330,19 +2370,190 @@ def write_cells(self, *args, **kwargs): def check_called(func): func() - self.assertTrue(len(called_save) >= 1) - self.assertTrue(len(called_write_cells) >= 1) + assert len(called_save) >= 1 + assert len(called_write_cells) >= 1 del called_save[:] del called_write_cells[:] with pd.option_context('io.excel.xlsx.writer', 'dummy'): register_writer(DummyClass) - writer = ExcelWriter('something.test') - tm.assertIsInstance(writer, DummyClass) + writer = ExcelWriter('something.xlsx') + assert isinstance(writer, DummyClass) df = tm.makeCustomDataframe(1, 1) - panel = tm.makePanel() - func = lambda: df.to_excel('something.test') - check_called(func) - check_called(lambda: panel.to_excel('something.test')) check_called(lambda: df.to_excel('something.xlsx')) - check_called(lambda: df.to_excel('something.xls', engine='dummy')) + check_called( + lambda: df.to_excel( + 'something.xls', engine='dummy')) + + +@pytest.mark.parametrize('engine', [ + pytest.param('xlwt', + marks=pytest.mark.xfail(reason='xlwt does not support ' + 'openpyxl-compatible ' + 'style dicts')), + 'xlsxwriter', + 'openpyxl', +]) +def test_styler_to_excel(engine): + def style(df): + # XXX: RGB colors not supported in xlwt + return DataFrame([['font-weight: bold', '', ''], + ['', 'color: blue', ''], + ['', '', 'text-decoration: underline'], + ['border-style: solid', '', ''], + ['', 'font-style: italic', ''], + ['', '', 'text-align: right'], + ['background-color: red', '', ''], + ['number-format: 0%', '', ''], + ['', '', ''], + ['', '', ''], + ['', '', '']], + index=df.index, columns=df.columns) + + def assert_equal_style(cell1, cell2, engine): + if engine in ['xlsxwriter', 'openpyxl']: + pytest.xfail(reason=("GH25351: failing on some attribute " + "comparisons in {}".format(engine))) + # XXX: should find a better way to check equality + assert cell1.alignment.__dict__ == cell2.alignment.__dict__ + assert cell1.border.__dict__ == cell2.border.__dict__ + assert cell1.fill.__dict__ == cell2.fill.__dict__ + assert cell1.font.__dict__ == cell2.font.__dict__ + assert cell1.number_format == cell2.number_format + assert cell1.protection.__dict__ == cell2.protection.__dict__ + + def custom_converter(css): + # use bold iff there is custom style attached to the cell + if css.strip(' \n;'): + return {'font': {'bold': True}} + return {} + + pytest.importorskip('jinja2') + pytest.importorskip(engine) + + # Prepare spreadsheets + + df = DataFrame(np.random.randn(11, 3)) + with ensure_clean('.xlsx' if engine != 'xlwt' else '.xls') as path: + writer = ExcelWriter(path, engine=engine) + df.to_excel(writer, sheet_name='frame') + df.style.to_excel(writer, sheet_name='unstyled') + styled = df.style.apply(style, axis=None) + styled.to_excel(writer, sheet_name='styled') + ExcelFormatter(styled, style_converter=custom_converter).write( + writer, sheet_name='custom') + writer.save() + + if engine not in ('openpyxl', 'xlsxwriter'): + # For other engines, we only smoke test + return + openpyxl = pytest.importorskip('openpyxl') + wb = openpyxl.load_workbook(path) + + # (1) compare DataFrame.to_excel and Styler.to_excel when unstyled + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['unstyled'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + assert cell1.value == cell2.value + assert_equal_style(cell1, cell2, engine) + n_cells += 1 + + # ensure iteration actually happened: + assert n_cells == (11 + 1) * (3 + 1) + + # (2) check styling with default converter + + # XXX: openpyxl (as at 2.4) prefixes colors with 00, xlsxwriter with FF + alpha = '00' if engine == 'openpyxl' else 'FF' + + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['styled'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + ref = '%s%d' % (cell2.column, cell2.row) + # XXX: this isn't as strong a test as ideal; we should + # confirm that differences are exclusive + if ref == 'B2': + assert not cell1.font.bold + assert cell2.font.bold + elif ref == 'C3': + assert cell1.font.color.rgb != cell2.font.color.rgb + assert cell2.font.color.rgb == alpha + '0000FF' + elif ref == 'D4': + # This fails with engine=xlsxwriter due to + # https://bitbucket.org/openpyxl/openpyxl/issues/800 + if engine == 'xlsxwriter' \ + and (LooseVersion(openpyxl.__version__) < + LooseVersion('2.4.6')): + pass + else: + assert cell1.font.underline != cell2.font.underline + assert cell2.font.underline == 'single' + elif ref == 'B5': + assert not cell1.border.left.style + assert (cell2.border.top.style == + cell2.border.right.style == + cell2.border.bottom.style == + cell2.border.left.style == + 'medium') + elif ref == 'C6': + assert not cell1.font.italic + assert cell2.font.italic + elif ref == 'D7': + assert (cell1.alignment.horizontal != + cell2.alignment.horizontal) + assert cell2.alignment.horizontal == 'right' + elif ref == 'B8': + assert cell1.fill.fgColor.rgb != cell2.fill.fgColor.rgb + assert cell1.fill.patternType != cell2.fill.patternType + assert cell2.fill.fgColor.rgb == alpha + 'FF0000' + assert cell2.fill.patternType == 'solid' + elif ref == 'B9': + assert cell1.number_format == 'General' + assert cell2.number_format == '0%' + else: + assert_equal_style(cell1, cell2, engine) + + assert cell1.value == cell2.value + n_cells += 1 + + assert n_cells == (11 + 1) * (3 + 1) + + # (3) check styling with custom converter + n_cells = 0 + for col1, col2 in zip(wb['frame'].columns, + wb['custom'].columns): + assert len(col1) == len(col2) + for cell1, cell2 in zip(col1, col2): + ref = '%s%d' % (cell2.column, cell2.row) + if ref in ('B2', 'C3', 'D4', 'B5', 'C6', 'D7', 'B8', 'B9'): + assert not cell1.font.bold + assert cell2.font.bold + else: + assert_equal_style(cell1, cell2, engine) + + assert cell1.value == cell2.value + n_cells += 1 + + assert n_cells == (11 + 1) * (3 + 1) + + +@td.skip_if_no('openpyxl') +@pytest.mark.skipif(not PY36, reason='requires fspath') +class TestFSPath(object): + + def test_excelfile_fspath(self): + with tm.ensure_clean('foo.xlsx') as path: + df = DataFrame({"A": [1, 2]}) + df.to_excel(path) + xl = ExcelFile(path) + result = os.fspath(xl) + assert result == path + + def test_excelwriter_fspath(self): + with tm.ensure_clean('foo.xlsx') as path: + writer = ExcelWriter(path) + assert os.fspath(writer) == str(path) diff --git a/pandas/tests/io/test_feather.py b/pandas/tests/io/test_feather.py index 6e2c28a0f68de..d170e4c43feb3 100644 --- a/pandas/tests/io/test_feather.py +++ b/pandas/tests/io/test_feather.py @@ -1,37 +1,42 @@ """ test feather-format compat """ +from distutils.version import LooseVersion +import numpy as np import pytest -feather = pytest.importorskip('feather') -import numpy as np import pandas as pd -from pandas.io.feather_format import to_feather, read_feather - -from feather import FeatherError import pandas.util.testing as tm from pandas.util.testing import assert_frame_equal, ensure_clean +from pandas.io.feather_format import read_feather, to_feather # noqa:E402 + +pyarrow = pytest.importorskip('pyarrow') + -class TestFeather(tm.TestCase): +pyarrow_version = LooseVersion(pyarrow.__version__) - def setUp(self): - pass + +@pytest.mark.single +class TestFeather(object): def check_error_on_write(self, df, exc): # check that we are raising the exception # on writing - def f(): + with pytest.raises(exc): with ensure_clean() as path: to_feather(df, path) - self.assertRaises(exc, f) - def check_round_trip(self, df): + def check_round_trip(self, df, expected=None, **kwargs): + + if expected is None: + expected = df with ensure_clean() as path: to_feather(df, path) - result = read_feather(path) - assert_frame_equal(result, df) + + result = read_feather(path, **kwargs) + assert_frame_equal(result, expected) def test_error(self): @@ -41,26 +46,25 @@ def test_error(self): def test_basic(self): - df = pd.DataFrame({'a': list('abc'), - 'b': list(range(1, 4)), - 'c': np.arange(3, 6).astype('u1'), - 'd': np.arange(4.0, 7.0, dtype='float64'), - 'e': [True, False, True], - 'f': pd.Categorical(list('abc')), - 'g': pd.date_range('20130101', periods=3), - 'h': pd.date_range('20130101', periods=3, - tz='US/Eastern'), - 'i': pd.date_range('20130101', periods=3, - freq='ns')}) - + df = pd.DataFrame({'string': list('abc'), + 'int': list(range(1, 4)), + 'uint': np.arange(3, 6).astype('u1'), + 'float': np.arange(4.0, 7.0, dtype='float64'), + 'float_with_null': [1., np.nan, 3], + 'bool': [True, False, True], + 'bool_with_null': [True, np.nan, False], + 'cat': pd.Categorical(list('abc')), + 'dt': pd.date_range('20130101', periods=3), + 'dttz': pd.date_range('20130101', periods=3, + tz='US/Eastern'), + 'dt_with_null': [pd.Timestamp('20130101'), pd.NaT, + pd.Timestamp('20130103')], + 'dtns': pd.date_range('20130101', periods=3, + freq='ns')}) + + assert df.dttz.dtype.tz.zone == 'US/Eastern' self.check_round_trip(df) - def test_strided_data_issues(self): - - # strided data issuehttps://github.com/wesm/feather/issues/97 - df = pd.DataFrame(np.arange(12).reshape(4, 3), columns=list('abc')) - self.check_error_on_write(df, FeatherError) - def test_duplicate_columns(self): # https://github.com/wesm/feather/issues/53 @@ -74,15 +78,47 @@ def test_stringify_columns(self): df = pd.DataFrame(np.arange(12).reshape(4, 3)).copy() self.check_error_on_write(df, ValueError) - def test_unsupported(self): + def test_read_columns(self): + # GH 24025 + df = pd.DataFrame({'col1': list('abc'), + 'col2': list(range(1, 4)), + 'col3': list('xyz'), + 'col4': list(range(4, 7))}) + columns = ['col1', 'col3'] + self.check_round_trip(df, expected=df[columns], + columns=columns) + + def test_unsupported_other(self): # period df = pd.DataFrame({'a': pd.period_range('2013', freq='M', periods=3)}) - self.check_error_on_write(df, ValueError) - - # non-strings - df = pd.DataFrame({'a': ['a', 1, 2.0]}) - self.check_error_on_write(df, ValueError) + # Some versions raise ValueError, others raise ArrowInvalid. + self.check_error_on_write(df, Exception) + + def test_rw_nthreads(self): + df = pd.DataFrame({'A': np.arange(100000)}) + expected_warning = ( + "the 'nthreads' keyword is deprecated, " + "use 'use_threads' instead" + ) + # TODO: make the warning work with check_stacklevel=True + with tm.assert_produces_warning( + FutureWarning, check_stacklevel=False) as w: + self.check_round_trip(df, nthreads=2) + # we have an extra FutureWarning because of #GH23752 + assert any(expected_warning in str(x) for x in w) + + # TODO: make the warning work with check_stacklevel=True + with tm.assert_produces_warning( + FutureWarning, check_stacklevel=False) as w: + self.check_round_trip(df, nthreads=1) + # we have an extra FutureWarnings because of #GH23752 + assert any(expected_warning in str(x) for x in w) + + def test_rw_use_threads(self): + df = pd.DataFrame({'A': np.arange(100000)}) + self.check_round_trip(df, use_threads=True) + self.check_round_trip(df, use_threads=False) def test_write_with_index(self): @@ -110,3 +146,13 @@ def test_write_with_index(self): df.index = [0, 1, 2] df.columns = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)]), self.check_error_on_write(df, ValueError) + + def test_path_pathlib(self): + df = tm.makeDataFrame().reset_index() + result = tm.round_trip_pathlib(df.to_feather, pd.read_feather) + tm.assert_frame_equal(df, result) + + def test_path_localpath(self): + df = tm.makeDataFrame().reset_index() + result = tm.round_trip_localpath(df.to_feather, pd.read_feather) + tm.assert_frame_equal(df, result) diff --git a/pandas/tests/io/test_gbq.py b/pandas/tests/io/test_gbq.py index 13529e7b54714..d3569af8d7786 100644 --- a/pandas/tests/io/test_gbq.py +++ b/pandas/tests/io/test_gbq.py @@ -1,18 +1,21 @@ -import pytest from datetime import datetime -import pytz -import platform -from time import sleep import os +import platform import numpy as np -import pandas as pd -from pandas import compat, DataFrame +import pytest +import pytz from pandas.compat import range + +import pandas as pd +from pandas import DataFrame, compat import pandas.util.testing as tm -pandas_gbq = pytest.importorskip('pandas_gbq') +api_exceptions = pytest.importorskip("google.api_core.exceptions") +bigquery = pytest.importorskip("google.cloud.bigquery") +service_account = pytest.importorskip("google.oauth2.service_account") +pandas_gbq = pytest.importorskip("pandas_gbq") PROJECT_ID = None PRIVATE_KEY_JSON_PATH = None @@ -49,32 +52,31 @@ def _in_travis_environment(): def _get_project_id(): if _in_travis_environment(): return os.environ.get('GBQ_PROJECT_ID') - else: - return PROJECT_ID + return PROJECT_ID or os.environ.get('GBQ_PROJECT_ID') def _get_private_key_path(): if _in_travis_environment(): return os.path.join(*[os.environ.get('TRAVIS_BUILD_DIR'), 'ci', 'travis_gbq.json']) - else: - return PRIVATE_KEY_JSON_PATH + private_key_path = PRIVATE_KEY_JSON_PATH + if not private_key_path: + private_key_path = os.environ.get('GBQ_GOOGLE_APPLICATION_CREDENTIALS') + return private_key_path -def clean_gbq_environment(private_key=None): - dataset = pandas_gbq.gbq._Dataset(_get_project_id(), - private_key=private_key) - for i in range(1, 10): - if DATASET_ID + str(i) in dataset.datasets(): - dataset_id = DATASET_ID + str(i) - table = pandas_gbq.gbq._Table(_get_project_id(), dataset_id, - private_key=private_key) - for j in range(1, 20): - if TABLE_ID + str(j) in dataset.tables(dataset_id): - table.delete(TABLE_ID + str(j)) +def _get_credentials(): + private_key_path = _get_private_key_path() + if private_key_path: + return service_account.Credentials.from_service_account_file( + private_key_path) - dataset.delete(dataset_id) + +def _get_client(): + project_id = _get_project_id() + credentials = _get_credentials() + return bigquery.Client(project=project_id, credentials=credentials) def make_mixed_dataframe_v2(test_size): @@ -93,11 +95,23 @@ def make_mixed_dataframe_v2(test_size): index=range(test_size)) +def test_read_gbq_without_dialect_warns_future_change(monkeypatch): + # Default dialect is changing to standard SQL. See: + # https://github.com/pydata/pandas-gbq/issues/195 + + def mock_read_gbq(*args, **kwargs): + return DataFrame([[1.0]]) + + monkeypatch.setattr(pandas_gbq, 'read_gbq', mock_read_gbq) + with tm.assert_produces_warning(FutureWarning): + pd.read_gbq("SELECT 1") + + @pytest.mark.single -class TestToGBQIntegrationWithServiceAccountKeyPath(tm.TestCase): +class TestToGBQIntegrationWithServiceAccountKeyPath(object): @classmethod - def setUpClass(cls): + def setup_class(cls): # - GLOBAL CLASS FIXTURES - # put here any instruction you want to execute only *ONCE* *BEFORE* # executing *ALL* tests described below. @@ -105,18 +119,22 @@ def setUpClass(cls): _skip_if_no_project_id() _skip_if_no_private_key_path() - clean_gbq_environment(_get_private_key_path()) - pandas_gbq.gbq._Dataset(_get_project_id(), - private_key=_get_private_key_path() - ).create(DATASET_ID + "1") + cls.client = _get_client() + cls.dataset = cls.client.dataset(DATASET_ID + "1") + try: + # Clean-up previous test runs. + cls.client.delete_dataset(cls.dataset, delete_contents=True) + except api_exceptions.NotFound: + pass # It's OK if the dataset doesn't already exist. + + cls.client.create_dataset(bigquery.Dataset(cls.dataset)) @classmethod - def tearDownClass(cls): + def teardown_class(cls): # - GLOBAL CLASS FIXTURES - # put here any instruction you want to execute only *ONCE* *AFTER* # executing all tests. - - clean_gbq_environment(_get_private_key_path()) + cls.client.delete_dataset(cls.dataset, delete_contents=True) def test_roundtrip(self): destination_table = DESTINATION_TABLE + "1" @@ -124,13 +142,12 @@ def test_roundtrip(self): test_size = 20001 df = make_mixed_dataframe_v2(test_size) - df.to_gbq(destination_table, _get_project_id(), chunksize=10000, - private_key=_get_private_key_path()) - - sleep(30) # <- Curses Google!!! + df.to_gbq(destination_table, _get_project_id(), chunksize=None, + credentials=_get_credentials()) result = pd.read_gbq("SELECT COUNT(*) AS num_rows FROM {0}" .format(destination_table), project_id=_get_project_id(), - private_key=_get_private_key_path()) - self.assertEqual(result['num_rows'][0], test_size) + credentials=_get_credentials(), + dialect="standard") + assert result['num_rows'][0] == test_size diff --git a/pandas/tests/io/test_gcs.py b/pandas/tests/io/test_gcs.py new file mode 100644 index 0000000000000..ec0631e748dfc --- /dev/null +++ b/pandas/tests/io/test_gcs.py @@ -0,0 +1,72 @@ +import numpy as np +import pytest + +from pandas.compat import StringIO + +from pandas import DataFrame, date_range, read_csv +from pandas.util import _test_decorators as td +from pandas.util.testing import assert_frame_equal + +from pandas.io.common import is_gcs_url + + +def test_is_gcs_url(): + assert is_gcs_url("gcs://pandas/somethingelse.com") + assert is_gcs_url("gs://pandas/somethingelse.com") + assert not is_gcs_url("s3://pandas/somethingelse.com") + + +@td.skip_if_no('gcsfs') +def test_read_csv_gcs(monkeypatch): + df1 = DataFrame({'int': [1, 3], 'float': [2.0, np.nan], 'str': ['t', 's'], + 'dt': date_range('2018-06-18', periods=2)}) + + class MockGCSFileSystem(): + def open(*args): + return StringIO(df1.to_csv(index=False)) + + monkeypatch.setattr('gcsfs.GCSFileSystem', MockGCSFileSystem) + df2 = read_csv('gs://test/test.csv', parse_dates=['dt']) + + assert_frame_equal(df1, df2) + + +@td.skip_if_no('gcsfs') +def test_to_csv_gcs(monkeypatch): + df1 = DataFrame({'int': [1, 3], 'float': [2.0, np.nan], 'str': ['t', 's'], + 'dt': date_range('2018-06-18', periods=2)}) + s = StringIO() + + class MockGCSFileSystem(): + def open(*args): + return s + + monkeypatch.setattr('gcsfs.GCSFileSystem', MockGCSFileSystem) + df1.to_csv('gs://test/test.csv', index=True) + df2 = read_csv(StringIO(s.getvalue()), parse_dates=['dt'], index_col=0) + + assert_frame_equal(df1, df2) + + +@td.skip_if_no('gcsfs') +def test_gcs_get_filepath_or_buffer(monkeypatch): + df1 = DataFrame({'int': [1, 3], 'float': [2.0, np.nan], 'str': ['t', 's'], + 'dt': date_range('2018-06-18', periods=2)}) + + def mock_get_filepath_or_buffer(*args, **kwargs): + return (StringIO(df1.to_csv(index=False)), + None, None, False) + + monkeypatch.setattr('pandas.io.gcs.get_filepath_or_buffer', + mock_get_filepath_or_buffer) + df2 = read_csv('gs://test/test.csv', parse_dates=['dt']) + + assert_frame_equal(df1, df2) + + +@pytest.mark.skipif(td.safe_import('gcsfs'), + reason='Only check when gcsfs not installed') +def test_gcs_not_present_exception(): + with pytest.raises(ImportError) as e: + read_csv('gs://test/test.csv') + assert 'gcsfs library is required' in str(e.value) diff --git a/pandas/tests/io/test_html.py b/pandas/tests/io/test_html.py index 4aa85c0f63a68..b2b0c21c81263 100644 --- a/pandas/tests/io/test_html.py +++ b/pandas/tests/io/test_html.py @@ -1,66 +1,40 @@ from __future__ import print_function -import glob +from functools import partial import os import re -import warnings - -try: - from importlib import import_module -except ImportError: - import_module = __import__ - -from distutils.version import LooseVersion - -import pytest +import threading import numpy as np from numpy.random import rand +import pytest -from pandas import (DataFrame, MultiIndex, read_csv, Timestamp, Index, - date_range, Series) -from pandas.compat import (map, zip, StringIO, string_types, BytesIO, - is_platform_windows) -from pandas.io.common import URLError, urlopen, file_path_to_url -from pandas.io.html import read_html -from pandas.io.libparsers import ParserError +from pandas.compat import ( + PY3, BytesIO, StringIO, is_platform_windows, map, reload, zip) +from pandas.errors import ParserError +import pandas.util._test_decorators as td +from pandas import ( + DataFrame, Index, MultiIndex, Series, Timestamp, date_range, read_csv) import pandas.util.testing as tm from pandas.util.testing import makeCustomDataframe as mkdf, network +from pandas.io.common import URLError, file_path_to_url +import pandas.io.html +from pandas.io.html import read_html -def _have_module(module_name): - try: - import_module(module_name) - return True - except ImportError: - return False - - -def _skip_if_no(module_name): - if not _have_module(module_name): - pytest.skip("{0!r} not found".format(module_name)) - - -def _skip_if_none_of(module_names): - if isinstance(module_names, string_types): - _skip_if_no(module_names) - if module_names == 'bs4': - import bs4 - if bs4.__version__ == LooseVersion('4.2.0'): - pytest.skip("Bad version of bs4: 4.2.0") - else: - not_found = [module_name for module_name in module_names if not - _have_module(module_name)] - if set(not_found) & set(module_names): - pytest.skip("{0!r} not found".format(not_found)) - if 'bs4' in module_names: - import bs4 - if bs4.__version__ == LooseVersion('4.2.0'): - pytest.skip("Bad version of bs4: 4.2.0") +HERE = os.path.dirname(__file__) -DATA_PATH = tm.get_data_path() +@pytest.fixture(params=[ + 'chinese_utf-16.html', + 'chinese_utf-32.html', + 'chinese_utf-8.html', + 'letz_latin1.html', +]) +def html_encoding_file(request, datapath): + """Parametrized fixture for HTML encoding test filenames.""" + return datapath('io', 'data', 'html_encoding', request.param) def assert_framelist_equal(list1, list2, *args, **kwargs): @@ -77,31 +51,51 @@ def assert_framelist_equal(list1, list2, *args, **kwargs): assert not frame_i.empty, 'frames are both empty' -def test_bs4_version_fails(): - _skip_if_none_of(('bs4', 'html5lib')) +@td.skip_if_no('bs4') +def test_bs4_version_fails(monkeypatch, datapath): import bs4 - if bs4.__version__ == LooseVersion('4.2.0'): - tm.assert_raises(AssertionError, read_html, os.path.join(DATA_PATH, - "spam.html"), - flavor='bs4') + monkeypatch.setattr(bs4, '__version__', '4.2') + with pytest.raises(ValueError, match="minimum version"): + read_html(datapath("io", "data", "spam.html"), flavor='bs4') + + +def test_invalid_flavor(): + url = "google.com" + flavor = "invalid flavor" + msg = r"\{" + flavor + r"\} is not a valid set of flavors" + with pytest.raises(ValueError, match=msg): + read_html(url, "google", flavor=flavor) -class ReadHtmlMixin(object): - def read_html(self, *args, **kwargs): - kwargs.setdefault('flavor', self.flavor) - return read_html(*args, **kwargs) +@td.skip_if_no('bs4') +@td.skip_if_no('lxml') +def test_same_ordering(datapath): + filename = datapath('io', 'data', 'valid_markup.html') + dfs_lxml = read_html(filename, index_col=0, flavor=['lxml']) + dfs_bs4 = read_html(filename, index_col=0, flavor=['bs4']) + assert_framelist_equal(dfs_lxml, dfs_bs4) + +@pytest.mark.parametrize("flavor", [ + pytest.param('bs4', marks=pytest.mark.skipif( + not td.safe_import('lxml'), reason='No bs4')), + pytest.param('lxml', marks=pytest.mark.skipif( + not td.safe_import('lxml'), reason='No lxml'))], scope="class") +class TestReadHtml(object): -class TestReadHtml(tm.TestCase, ReadHtmlMixin): - flavor = 'bs4' - spam_data = os.path.join(DATA_PATH, 'spam.html') - banklist_data = os.path.join(DATA_PATH, 'banklist.html') + @pytest.fixture(autouse=True) + def set_files(self, datapath): + self.spam_data = datapath('io', 'data', 'spam.html') + self.spam_data_kwargs = {} + if PY3: + self.spam_data_kwargs['encoding'] = 'UTF-8' + self.banklist_data = datapath("io", "data", "banklist.html") - @classmethod - def setUpClass(cls): - super(TestReadHtml, cls).setUpClass() - _skip_if_none_of(('bs4', 'html5lib')) + @pytest.fixture(autouse=True, scope="function") + def set_defaults(self, flavor, request): + self.read_html = partial(read_html, flavor=flavor) + yield def test_to_html_compat(self): df = mkdf(4, 3, data_gen_f=lambda *args: rand(), c_idx_names=False, @@ -121,14 +115,14 @@ def test_banklist_url(self): @network def test_spam_url(self): - url = ('http://ndb.nal.usda.gov/ndb/foods/show/1732?fg=&man=&' + url = ('http://ndb.nal.usda.gov/ndb/foods/show/300772?fg=&man=&' 'lfacet=&format=&count=&max=25&offset=&sort=&qlookup=spam') df1 = self.read_html(url, '.*Water.*') df2 = self.read_html(url, 'Unit') assert_framelist_equal(df1, df2) - @tm.slow + @pytest.mark.slow def test_banklist(self): df1 = self.read_html(self.banklist_data, '.*Florida.*', attrs={'id': 'table'}) @@ -137,38 +131,28 @@ def test_banklist(self): assert_framelist_equal(df1, df2) - def test_spam_no_types(self): - - # infer_types removed in #10892 + def test_spam(self): df1 = self.read_html(self.spam_data, '.*Water.*') df2 = self.read_html(self.spam_data, 'Unit') assert_framelist_equal(df1, df2) - self.assertEqual(df1[0].iloc[0, 0], 'Proximates') - self.assertEqual(df1[0].columns[0], 'Nutrient') - - def test_spam_with_types(self): - df1 = self.read_html(self.spam_data, '.*Water.*') - df2 = self.read_html(self.spam_data, 'Unit') - assert_framelist_equal(df1, df2) - - self.assertEqual(df1[0].iloc[0, 0], 'Proximates') - self.assertEqual(df1[0].columns[0], 'Nutrient') + assert df1[0].iloc[0, 0] == 'Proximates' + assert df1[0].columns[0] == 'Nutrient' def test_spam_no_match(self): dfs = self.read_html(self.spam_data) for df in dfs: - tm.assertIsInstance(df, DataFrame) + assert isinstance(df, DataFrame) def test_banklist_no_match(self): dfs = self.read_html(self.banklist_data, attrs={'id': 'table'}) for df in dfs: - tm.assertIsInstance(df, DataFrame) + assert isinstance(df, DataFrame) def test_spam_header(self): - df = self.read_html(self.spam_data, '.*Water.*', header=1)[0] - self.assertEqual(df.columns[0], 'Proximates') - self.assertFalse(df.empty) + df = self.read_html(self.spam_data, '.*Water.*', header=2)[0] + assert df.columns[0] == 'Proximates' + assert not df.empty def test_skiprows_int(self): df1 = self.read_html(self.spam_data, '.*Water.*', skiprows=1) @@ -188,8 +172,8 @@ def test_skiprows_list(self): assert_framelist_equal(df1, df2) def test_skiprows_set(self): - df1 = self.read_html(self.spam_data, '.*Water.*', skiprows=set([1, 2])) - df2 = self.read_html(self.spam_data, 'Unit', skiprows=set([2, 1])) + df1 = self.read_html(self.spam_data, '.*Water.*', skiprows={1, 2}) + df2 = self.read_html(self.spam_data, 'Unit', skiprows={2, 1}) assert_framelist_equal(df1, df2) @@ -219,8 +203,8 @@ def test_skiprows_ndarray(self): assert_framelist_equal(df1, df2) def test_skiprows_invalid(self): - with tm.assertRaisesRegexp(TypeError, - 'is not a valid type for skipping rows'): + with pytest.raises(TypeError, match=('is not a valid type ' + 'for skipping rows')): self.read_html(self.spam_data, '.*Water.*', skiprows='asdf') def test_index(self): @@ -248,10 +232,10 @@ def test_infer_types(self): assert_framelist_equal(df1, df2) def test_string_io(self): - with open(self.spam_data) as f: + with open(self.spam_data, **self.spam_data_kwargs) as f: data1 = StringIO(f.read()) - with open(self.spam_data) as f: + with open(self.spam_data, **self.spam_data_kwargs) as f: data2 = StringIO(f.read()) df1 = self.read_html(data1, '.*Water.*') @@ -259,7 +243,7 @@ def test_string_io(self): assert_framelist_equal(df1, df2) def test_string(self): - with open(self.spam_data) as f: + with open(self.spam_data, **self.spam_data_kwargs) as f: data = f.read() df1 = self.read_html(data, '.*Water.*') @@ -268,41 +252,42 @@ def test_string(self): assert_framelist_equal(df1, df2) def test_file_like(self): - with open(self.spam_data) as f: + with open(self.spam_data, **self.spam_data_kwargs) as f: df1 = self.read_html(f, '.*Water.*') - with open(self.spam_data) as f: + with open(self.spam_data, **self.spam_data_kwargs) as f: df2 = self.read_html(f, 'Unit') assert_framelist_equal(df1, df2) @network def test_bad_url_protocol(self): - with tm.assertRaises(URLError): + with pytest.raises(URLError): self.read_html('git://github.com', match='.*Water.*') @network def test_invalid_url(self): try: - with tm.assertRaises(URLError): + with pytest.raises(URLError): self.read_html('http://www.a23950sdfa908sd.com', match='.*Water.*') except ValueError as e: - self.assertEqual(str(e), 'No tables found') + assert 'No tables found' in str(e) - @tm.slow + @pytest.mark.slow def test_file_url(self): url = self.banklist_data - dfs = self.read_html(file_path_to_url(url), 'First', + dfs = self.read_html(file_path_to_url(os.path.abspath(url)), + 'First', attrs={'id': 'table'}) - tm.assertIsInstance(dfs, list) + assert isinstance(dfs, list) for df in dfs: - tm.assertIsInstance(df, DataFrame) + assert isinstance(df, DataFrame) - @tm.slow + @pytest.mark.slow def test_invalid_table_attrs(self): url = self.banklist_data - with tm.assertRaisesRegexp(ValueError, 'No tables found'): + with pytest.raises(ValueError, match='No tables found'): self.read_html(url, 'First Federal Bank of Florida', attrs={'id': 'tasdfable'}) @@ -310,90 +295,118 @@ def _bank_data(self, *args, **kwargs): return self.read_html(self.banklist_data, 'Metcalf', attrs={'id': 'table'}, *args, **kwargs) - @tm.slow + @pytest.mark.slow def test_multiindex_header(self): df = self._bank_data(header=[0, 1])[0] - tm.assertIsInstance(df.columns, MultiIndex) + assert isinstance(df.columns, MultiIndex) - @tm.slow + @pytest.mark.slow def test_multiindex_index(self): df = self._bank_data(index_col=[0, 1])[0] - tm.assertIsInstance(df.index, MultiIndex) + assert isinstance(df.index, MultiIndex) - @tm.slow + @pytest.mark.slow def test_multiindex_header_index(self): df = self._bank_data(header=[0, 1], index_col=[0, 1])[0] - tm.assertIsInstance(df.columns, MultiIndex) - tm.assertIsInstance(df.index, MultiIndex) + assert isinstance(df.columns, MultiIndex) + assert isinstance(df.index, MultiIndex) - @tm.slow + @pytest.mark.slow def test_multiindex_header_skiprows_tuples(self): - df = self._bank_data(header=[0, 1], skiprows=1, tupleize_cols=True)[0] - tm.assertIsInstance(df.columns, Index) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + df = self._bank_data(header=[0, 1], skiprows=1, + tupleize_cols=True)[0] + assert isinstance(df.columns, Index) - @tm.slow + @pytest.mark.slow def test_multiindex_header_skiprows(self): df = self._bank_data(header=[0, 1], skiprows=1)[0] - tm.assertIsInstance(df.columns, MultiIndex) + assert isinstance(df.columns, MultiIndex) - @tm.slow + @pytest.mark.slow def test_multiindex_header_index_skiprows(self): df = self._bank_data(header=[0, 1], index_col=[0, 1], skiprows=1)[0] - tm.assertIsInstance(df.index, MultiIndex) - tm.assertIsInstance(df.columns, MultiIndex) + assert isinstance(df.index, MultiIndex) + assert isinstance(df.columns, MultiIndex) - @tm.slow + @pytest.mark.slow def test_regex_idempotency(self): url = self.banklist_data - dfs = self.read_html(file_path_to_url(url), + dfs = self.read_html(file_path_to_url(os.path.abspath(url)), match=re.compile(re.compile('Florida')), attrs={'id': 'table'}) - tm.assertIsInstance(dfs, list) + assert isinstance(dfs, list) for df in dfs: - tm.assertIsInstance(df, DataFrame) + assert isinstance(df, DataFrame) def test_negative_skiprows(self): - with tm.assertRaisesRegexp(ValueError, - r'\(you passed a negative value\)'): + msg = r'\(you passed a negative value\)' + with pytest.raises(ValueError, match=msg): self.read_html(self.spam_data, 'Water', skiprows=-1) @network def test_multiple_matches(self): url = 'https://docs.python.org/2/' dfs = self.read_html(url, match='Python') - self.assertTrue(len(dfs) > 1) + assert len(dfs) > 1 @network def test_python_docs_table(self): url = 'https://docs.python.org/2/' dfs = self.read_html(url, match='Python') zz = [df.iloc[0, 0][0:4] for df in dfs] - self.assertEqual(sorted(zz), sorted(['Repo', 'What'])) + assert sorted(zz) == sorted(['Repo', 'What']) - @tm.slow - def test_thousands_macau_stats(self): + @pytest.mark.slow + def test_thousands_macau_stats(self, datapath): all_non_nan_table_index = -2 - macau_data = os.path.join(DATA_PATH, 'macau.html') + macau_data = datapath("io", "data", "macau.html") dfs = self.read_html(macau_data, index_col=0, attrs={'class': 'style1'}) df = dfs[all_non_nan_table_index] - self.assertFalse(any(s.isnull().any() for _, s in df.iteritems())) + assert not any(s.isna().any() for _, s in df.iteritems()) - @tm.slow - def test_thousands_macau_index_col(self): + @pytest.mark.slow + def test_thousands_macau_index_col(self, datapath): all_non_nan_table_index = -2 - macau_data = os.path.join(DATA_PATH, 'macau.html') + macau_data = datapath('io', 'data', 'macau.html') dfs = self.read_html(macau_data, index_col=0, header=0) df = dfs[all_non_nan_table_index] - self.assertFalse(any(s.isnull().any() for _, s in df.iteritems())) + assert not any(s.isna().any() for _, s in df.iteritems()) def test_empty_tables(self): """ Make sure that read_html ignores empty tables. """ - data1 = '''
    Help
    Help
    Nutrient Unit
    Value per 100.0g

    - + oz 1 NLEA serving
    56g .xz + # bz2 --> .bz2 + filename += compression + + df = DataFrame({"A": [1]}) + + to_compression = "infer" if to_infer else compression + read_compression = "infer" if read_infer else compression + + with tm.ensure_clean(filename) as path: + df.to_csv(path, compression=to_compression) + result = pd.read_csv(path, index_col=0, + compression=read_compression) + tm.assert_frame_equal(result, df) diff --git a/pandas/tests/io/formats/test_to_excel.py b/pandas/tests/io/formats/test_to_excel.py new file mode 100644 index 0000000000000..13eb517fcab6a --- /dev/null +++ b/pandas/tests/io/formats/test_to_excel.py @@ -0,0 +1,278 @@ +"""Tests formatting as writer-agnostic ExcelCells + +ExcelFormatter is tested implicitly in pandas/tests/io/test_excel.py +""" + +import pytest + +import pandas.util.testing as tm + +from pandas.io.formats.css import CSSWarning +from pandas.io.formats.excel import CSSToExcelConverter + + +@pytest.mark.parametrize('css,expected', [ + # FONT + # - name + ('font-family: foo,bar', {'font': {'name': 'foo'}}), + ('font-family: "foo bar",baz', {'font': {'name': 'foo bar'}}), + ('font-family: foo,\nbar', {'font': {'name': 'foo'}}), + ('font-family: foo, bar, baz', {'font': {'name': 'foo'}}), + ('font-family: bar, foo', {'font': {'name': 'bar'}}), + ('font-family: \'foo bar\', baz', {'font': {'name': 'foo bar'}}), + ('font-family: \'foo \\\'bar\', baz', {'font': {'name': 'foo \'bar'}}), + ('font-family: "foo \\"bar", baz', {'font': {'name': 'foo "bar'}}), + ('font-family: "foo ,bar", baz', {'font': {'name': 'foo ,bar'}}), + # - family + ('font-family: serif', {'font': {'name': 'serif', 'family': 1}}), + ('font-family: Serif', {'font': {'name': 'serif', 'family': 1}}), + ('font-family: roman, serif', {'font': {'name': 'roman', 'family': 1}}), + ('font-family: roman, sans-serif', {'font': {'name': 'roman', + 'family': 2}}), + ('font-family: roman, sans serif', {'font': {'name': 'roman'}}), + ('font-family: roman, sansserif', {'font': {'name': 'roman'}}), + ('font-family: roman, cursive', {'font': {'name': 'roman', 'family': 4}}), + ('font-family: roman, fantasy', {'font': {'name': 'roman', 'family': 5}}), + # - size + ('font-size: 1em', {'font': {'size': 12}}), + ('font-size: xx-small', {'font': {'size': 6}}), + ('font-size: x-small', {'font': {'size': 7.5}}), + ('font-size: small', {'font': {'size': 9.6}}), + ('font-size: medium', {'font': {'size': 12}}), + ('font-size: large', {'font': {'size': 13.5}}), + ('font-size: x-large', {'font': {'size': 18}}), + ('font-size: xx-large', {'font': {'size': 24}}), + ('font-size: 50%', {'font': {'size': 6}}), + # - bold + ('font-weight: 100', {'font': {'bold': False}}), + ('font-weight: 200', {'font': {'bold': False}}), + ('font-weight: 300', {'font': {'bold': False}}), + ('font-weight: 400', {'font': {'bold': False}}), + ('font-weight: normal', {'font': {'bold': False}}), + ('font-weight: lighter', {'font': {'bold': False}}), + ('font-weight: bold', {'font': {'bold': True}}), + ('font-weight: bolder', {'font': {'bold': True}}), + ('font-weight: 700', {'font': {'bold': True}}), + ('font-weight: 800', {'font': {'bold': True}}), + ('font-weight: 900', {'font': {'bold': True}}), + # - italic + ('font-style: italic', {'font': {'italic': True}}), + ('font-style: oblique', {'font': {'italic': True}}), + # - underline + ('text-decoration: underline', + {'font': {'underline': 'single'}}), + ('text-decoration: overline', + {}), + ('text-decoration: none', + {}), + # - strike + ('text-decoration: line-through', + {'font': {'strike': True}}), + ('text-decoration: underline line-through', + {'font': {'strike': True, 'underline': 'single'}}), + ('text-decoration: underline; text-decoration: line-through', + {'font': {'strike': True}}), + # - color + ('color: red', {'font': {'color': 'FF0000'}}), + ('color: #ff0000', {'font': {'color': 'FF0000'}}), + ('color: #f0a', {'font': {'color': 'FF00AA'}}), + # - shadow + ('text-shadow: none', {'font': {'shadow': False}}), + ('text-shadow: 0px -0em 0px #CCC', {'font': {'shadow': False}}), + ('text-shadow: 0px -0em 0px #999', {'font': {'shadow': False}}), + ('text-shadow: 0px -0em 0px', {'font': {'shadow': False}}), + ('text-shadow: 2px -0em 0px #CCC', {'font': {'shadow': True}}), + ('text-shadow: 0px -2em 0px #CCC', {'font': {'shadow': True}}), + ('text-shadow: 0px -0em 2px #CCC', {'font': {'shadow': True}}), + ('text-shadow: 0px -0em 2px', {'font': {'shadow': True}}), + ('text-shadow: 0px -2em', {'font': {'shadow': True}}), + + # FILL + # - color, fillType + ('background-color: red', {'fill': {'fgColor': 'FF0000', + 'patternType': 'solid'}}), + ('background-color: #ff0000', {'fill': {'fgColor': 'FF0000', + 'patternType': 'solid'}}), + ('background-color: #f0a', {'fill': {'fgColor': 'FF00AA', + 'patternType': 'solid'}}), + # BORDER + # - style + ('border-style: solid', + {'border': {'top': {'style': 'medium'}, + 'bottom': {'style': 'medium'}, + 'left': {'style': 'medium'}, + 'right': {'style': 'medium'}}}), + ('border-style: solid; border-width: thin', + {'border': {'top': {'style': 'thin'}, + 'bottom': {'style': 'thin'}, + 'left': {'style': 'thin'}, + 'right': {'style': 'thin'}}}), + + ('border-top-style: solid; border-top-width: thin', + {'border': {'top': {'style': 'thin'}}}), + ('border-top-style: solid; border-top-width: 1pt', + {'border': {'top': {'style': 'thin'}}}), + ('border-top-style: solid', + {'border': {'top': {'style': 'medium'}}}), + ('border-top-style: solid; border-top-width: medium', + {'border': {'top': {'style': 'medium'}}}), + ('border-top-style: solid; border-top-width: 2pt', + {'border': {'top': {'style': 'medium'}}}), + ('border-top-style: solid; border-top-width: thick', + {'border': {'top': {'style': 'thick'}}}), + ('border-top-style: solid; border-top-width: 4pt', + {'border': {'top': {'style': 'thick'}}}), + + ('border-top-style: dotted', + {'border': {'top': {'style': 'mediumDashDotDot'}}}), + ('border-top-style: dotted; border-top-width: thin', + {'border': {'top': {'style': 'dotted'}}}), + ('border-top-style: dashed', + {'border': {'top': {'style': 'mediumDashed'}}}), + ('border-top-style: dashed; border-top-width: thin', + {'border': {'top': {'style': 'dashed'}}}), + ('border-top-style: double', + {'border': {'top': {'style': 'double'}}}), + # - color + ('border-style: solid; border-color: #0000ff', + {'border': {'top': {'style': 'medium', 'color': '0000FF'}, + 'right': {'style': 'medium', 'color': '0000FF'}, + 'bottom': {'style': 'medium', 'color': '0000FF'}, + 'left': {'style': 'medium', 'color': '0000FF'}}}), + ('border-top-style: double; border-top-color: blue', + {'border': {'top': {'style': 'double', 'color': '0000FF'}}}), + ('border-top-style: solid; border-top-color: #06c', + {'border': {'top': {'style': 'medium', 'color': '0066CC'}}}), + # ALIGNMENT + # - horizontal + ('text-align: center', + {'alignment': {'horizontal': 'center'}}), + ('text-align: left', + {'alignment': {'horizontal': 'left'}}), + ('text-align: right', + {'alignment': {'horizontal': 'right'}}), + ('text-align: justify', + {'alignment': {'horizontal': 'justify'}}), + # - vertical + ('vertical-align: top', + {'alignment': {'vertical': 'top'}}), + ('vertical-align: text-top', + {'alignment': {'vertical': 'top'}}), + ('vertical-align: middle', + {'alignment': {'vertical': 'center'}}), + ('vertical-align: bottom', + {'alignment': {'vertical': 'bottom'}}), + ('vertical-align: text-bottom', + {'alignment': {'vertical': 'bottom'}}), + # - wrap_text + ('white-space: nowrap', + {'alignment': {'wrap_text': False}}), + ('white-space: pre', + {'alignment': {'wrap_text': False}}), + ('white-space: pre-line', + {'alignment': {'wrap_text': False}}), + ('white-space: normal', + {'alignment': {'wrap_text': True}}), + # NUMBER FORMAT + ('number-format: 0%', + {'number_format': {'format_code': '0%'}}), +]) +def test_css_to_excel(css, expected): + convert = CSSToExcelConverter() + assert expected == convert(css) + + +def test_css_to_excel_multiple(): + convert = CSSToExcelConverter() + actual = convert(''' + font-weight: bold; + text-decoration: underline; + color: red; + border-width: thin; + text-align: center; + vertical-align: top; + unused: something; + ''') + assert {"font": {"bold": True, "underline": "single", "color": "FF0000"}, + "border": {"top": {"style": "thin"}, + "right": {"style": "thin"}, + "bottom": {"style": "thin"}, + "left": {"style": "thin"}}, + "alignment": {"horizontal": "center", + "vertical": "top"}} == actual + + +@pytest.mark.parametrize('css,inherited,expected', [ + ('font-weight: bold', '', + {'font': {'bold': True}}), + ('', 'font-weight: bold', + {'font': {'bold': True}}), + ('font-weight: bold', 'font-style: italic', + {'font': {'bold': True, 'italic': True}}), + ('font-style: normal', 'font-style: italic', + {'font': {'italic': False}}), + ('font-style: inherit', '', {}), + ('font-style: normal; font-style: inherit', 'font-style: italic', + {'font': {'italic': True}}), +]) +def test_css_to_excel_inherited(css, inherited, expected): + convert = CSSToExcelConverter(inherited) + assert expected == convert(css) + + +@pytest.mark.parametrize("input_color,output_color", ( + [(name, rgb) for name, rgb in CSSToExcelConverter.NAMED_COLORS.items()] + + [("#" + rgb, rgb) for rgb in CSSToExcelConverter.NAMED_COLORS.values()] + + [("#F0F", "FF00FF"), ("#ABC", "AABBCC")]) +) +def test_css_to_excel_good_colors(input_color, output_color): + # see gh-18392 + css = ("border-top-color: {color}; " + "border-right-color: {color}; " + "border-bottom-color: {color}; " + "border-left-color: {color}; " + "background-color: {color}; " + "color: {color}").format(color=input_color) + + expected = dict() + + expected["fill"] = { + "patternType": "solid", + "fgColor": output_color + } + + expected["font"] = { + "color": output_color + } + + expected["border"] = { + k: { + "color": output_color, + } for k in ("top", "right", "bottom", "left") + } + + with tm.assert_produces_warning(None): + convert = CSSToExcelConverter() + assert expected == convert(css) + + +@pytest.mark.parametrize("input_color", [None, "not-a-color"]) +def test_css_to_excel_bad_colors(input_color): + # see gh-18392 + css = ("border-top-color: {color}; " + "border-right-color: {color}; " + "border-bottom-color: {color}; " + "border-left-color: {color}; " + "background-color: {color}; " + "color: {color}").format(color=input_color) + + expected = dict() + + if input_color is not None: + expected["fill"] = { + "patternType": "solid" + } + + with tm.assert_produces_warning(CSSWarning): + convert = CSSToExcelConverter() + assert expected == convert(css) diff --git a/pandas/tests/io/formats/test_to_html.py b/pandas/tests/io/formats/test_to_html.py new file mode 100644 index 0000000000000..428f1411a10a6 --- /dev/null +++ b/pandas/tests/io/formats/test_to_html.py @@ -0,0 +1,625 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime +from io import open +import re + +import numpy as np +import pytest + +from pandas.compat import StringIO, lrange, u + +import pandas as pd +from pandas import DataFrame, Index, MultiIndex, compat, option_context +from pandas.util import testing as tm + +import pandas.io.formats.format as fmt + +lorem_ipsum = ( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" + " tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim" + " veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex" + " ea commodo consequat. Duis aute irure dolor in reprehenderit in" + " voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur" + " sint occaecat cupidatat non proident, sunt in culpa qui officia" + " deserunt mollit anim id est laborum.") + + +def expected_html(datapath, name): + """ + Read HTML file from formats data directory. + + Parameters + ---------- + datapath : pytest fixture + The datapath fixture injected into a test by pytest. + name : str + The name of the HTML file without the suffix. + + Returns + ------- + str : contents of HTML file. + """ + filename = '.'.join([name, 'html']) + filepath = datapath('io', 'formats', 'data', 'html', filename) + with open(filepath, encoding='utf-8') as f: + html = f.read() + return html.rstrip() + + +@pytest.fixture(params=['mixed', 'empty']) +def biggie_df_fixture(request): + """Fixture for a big mixed Dataframe and an empty Dataframe""" + if request.param == 'mixed': + df = DataFrame({'A': np.random.randn(200), + 'B': tm.makeStringIndex(200)}, + index=lrange(200)) + df.loc[:20, 'A'] = np.nan + df.loc[:20, 'B'] = np.nan + return df + elif request.param == 'empty': + df = DataFrame(index=np.arange(200)) + return df + + +@pytest.fixture(params=fmt._VALID_JUSTIFY_PARAMETERS) +def justify(request): + return request.param + + +@pytest.mark.parametrize('col_space', [30, 50]) +def test_to_html_with_col_space(col_space): + df = DataFrame(np.random.random(size=(1, 3))) + # check that col_space affects HTML generation + # and be very brittle about it. + result = df.to_html(col_space=col_space) + hdrs = [x for x in result.split(r"\n") if re.search(r"\s]", x)] + assert len(hdrs) > 0 + for h in hdrs: + assert "min-width" in h + assert str(col_space) in h + + +def test_to_html_with_empty_string_label(): + # GH 3547, to_html regards empty string labels as repeated labels + data = {'c1': ['a', 'b'], 'c2': ['a', ''], 'data': [1, 2]} + df = DataFrame(data).set_index(['c1', 'c2']) + result = df.to_html() + assert "rowspan" not in result + + +@pytest.mark.parametrize('df,expected', [ + (DataFrame({u('\u03c3'): np.arange(10.)}), 'unicode_1'), + (DataFrame({'A': [u('\u03c3')]}), 'unicode_2') +]) +def test_to_html_unicode(df, expected, datapath): + expected = expected_html(datapath, expected) + result = df.to_html() + assert result == expected + + +def test_to_html_decimal(datapath): + # GH 12031 + df = DataFrame({'A': [6.0, 3.1, 2.2]}) + result = df.to_html(decimal=',') + expected = expected_html(datapath, 'gh12031_expected_output') + assert result == expected + + +@pytest.mark.parametrize('kwargs,string,expected', [ + (dict(), "", 'escaped'), + (dict(escape=False), "bold", 'escape_disabled') +]) +def test_to_html_escaped(kwargs, string, expected, datapath): + a = 'strl2': {a: string, + b: string}} + result = DataFrame(test_dict).to_html(**kwargs) + expected = expected_html(datapath, expected) + assert result == expected + + +@pytest.mark.parametrize('index_is_named', [True, False]) +def test_to_html_multiindex_index_false(index_is_named, datapath): + # GH 8452 + df = DataFrame({ + 'a': range(2), + 'b': range(3, 5), + 'c': range(5, 7), + 'd': range(3, 5) + }) + df.columns = MultiIndex.from_product([['a', 'b'], ['c', 'd']]) + if index_is_named: + df.index = Index(df.index.values, name='idx') + result = df.to_html(index=False) + expected = expected_html(datapath, 'gh8452_expected_output') + assert result == expected + + +@pytest.mark.parametrize('multi_sparse,expected', [ + (False, 'multiindex_sparsify_false_multi_sparse_1'), + (False, 'multiindex_sparsify_false_multi_sparse_2'), + (True, 'multiindex_sparsify_1'), + (True, 'multiindex_sparsify_2') +]) +def test_to_html_multiindex_sparsify(multi_sparse, expected, datapath): + index = MultiIndex.from_arrays([[0, 0, 1, 1], [0, 1, 0, 1]], + names=['foo', None]) + df = DataFrame([[0, 1], [2, 3], [4, 5], [6, 7]], index=index) + if expected.endswith('2'): + df.columns = index[::2] + with option_context('display.multi_sparse', multi_sparse): + result = df.to_html() + expected = expected_html(datapath, expected) + assert result == expected + + +@pytest.mark.parametrize('max_rows,expected', [ + (60, 'gh14882_expected_output_1'), + + # Test that ... appears in a middle level + (56, 'gh14882_expected_output_2') +]) +def test_to_html_multiindex_odd_even_truncate(max_rows, expected, datapath): + # GH 14882 - Issue on truncation with odd length DataFrame + index = MultiIndex.from_product([[100, 200, 300], + [10, 20, 30], + [1, 2, 3, 4, 5, 6, 7]], + names=['a', 'b', 'c']) + df = DataFrame({'n': range(len(index))}, index=index) + result = df.to_html(max_rows=max_rows) + expected = expected_html(datapath, expected) + assert result == expected + + +@pytest.mark.parametrize('df,formatters,expected', [ + (DataFrame( + [[0, 1], [2, 3], [4, 5], [6, 7]], + columns=['foo', None], index=lrange(4)), + {'__index__': lambda x: 'abcd' [x]}, + 'index_formatter'), + + (DataFrame( + {'months': [datetime(2016, 1, 1), datetime(2016, 2, 2)]}), + {'months': lambda x: x.strftime('%Y-%m')}, + 'datetime64_monthformatter'), + + (DataFrame({'hod': pd.to_datetime(['10:10:10.100', '12:12:12.120'], + format='%H:%M:%S.%f')}), + {'hod': lambda x: x.strftime('%H:%M')}, + 'datetime64_hourformatter') +]) +def test_to_html_formatters(df, formatters, expected, datapath): + expected = expected_html(datapath, expected) + result = df.to_html(formatters=formatters) + assert result == expected + + +def test_to_html_regression_GH6098(): + df = DataFrame({ + u('clé1'): [u('a'), u('a'), u('b'), u('b'), u('a')], + u('clé2'): [u('1er'), u('2ème'), u('1er'), u('2ème'), u('1er')], + 'données1': np.random.randn(5), + 'données2': np.random.randn(5)}) + + # it works + df.pivot_table(index=[u('clé1')], columns=[u('clé2')])._repr_html_() + + +def test_to_html_truncate(datapath): + index = pd.date_range(start='20010101', freq='D', periods=20) + df = DataFrame(index=index, columns=range(20)) + result = df.to_html(max_rows=8, max_cols=4) + expected = expected_html(datapath, 'truncate') + assert result == expected + + +@pytest.mark.parametrize('sparsify,expected', [ + (True, 'truncate_multi_index'), + (False, 'truncate_multi_index_sparse_off') +]) +def test_to_html_truncate_multi_index(sparsify, expected, datapath): + arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], + ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']] + df = DataFrame(index=arrays, columns=arrays) + result = df.to_html(max_rows=7, max_cols=7, sparsify=sparsify) + expected = expected_html(datapath, expected) + assert result == expected + + +@pytest.mark.parametrize('option,result,expected', [ + (None, lambda df: df.to_html(), '1'), + (None, lambda df: df.to_html(border=0), '0'), + (0, lambda df: df.to_html(), '0'), + (0, lambda df: df._repr_html_(), '0'), +]) +def test_to_html_border(option, result, expected): + df = DataFrame({'A': [1, 2]}) + if option is None: + result = result(df) + else: + with option_context('display.html.border', option): + result = result(df) + expected = 'border="{}"'.format(expected) + assert expected in result + + +def test_display_option_warning(): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + pd.options.html.border + + +@pytest.mark.parametrize('biggie_df_fixture', ['mixed'], indirect=True) +def test_to_html(biggie_df_fixture): + # TODO: split this test + df = biggie_df_fixture + s = df.to_html() + + buf = StringIO() + retval = df.to_html(buf=buf) + assert retval is None + assert buf.getvalue() == s + + assert isinstance(s, compat.string_types) + + df.to_html(columns=['B', 'A'], col_space=17) + df.to_html(columns=['B', 'A'], + formatters={'A': lambda x: '{x:.1f}'.format(x=x)}) + + df.to_html(columns=['B', 'A'], float_format=str) + df.to_html(columns=['B', 'A'], col_space=12, float_format=str) + + +@pytest.mark.parametrize('biggie_df_fixture', ['empty'], indirect=True) +def test_to_html_empty_dataframe(biggie_df_fixture): + df = biggie_df_fixture + df.to_html() + + +def test_to_html_filename(biggie_df_fixture, tmpdir): + df = biggie_df_fixture + expected = df.to_html() + path = tmpdir.join('test.html') + df.to_html(path) + result = path.read() + assert result == expected + + +def test_to_html_with_no_bold(): + df = DataFrame({'x': np.random.randn(5)}) + html = df.to_html(bold_rows=False) + result = html[html.find("
    + result = self.read_html(''' +
    + + + + + + + + + + + + +
    AB
    12
    + + + +
    + ''') + + assert len(result) == 1 + + def test_multiple_tbody(self): + # GH-20690 + # Read all tbody tags within a single table. + result = self.read_html(''' @@ -406,23 +419,24 @@ def test_empty_tables(self): -
    A2
    ''' - data2 = data1 + ''' + + + + -
    34
    ''' - res1 = self.read_html(StringIO(data1)) - res2 = self.read_html(StringIO(data2)) - assert_framelist_equal(res1, res2) + ''')[0] + + expected = DataFrame(data=[[1, 2], [3, 4]], columns=['A', 'B']) + + tm.assert_frame_equal(result, expected) def test_header_and_one_column(self): """ Don't fail with bs4 when there is a header and only one column as described in issue #9178 """ - data = StringIO(''' - - + result = self.read_html('''
    @@ -433,11 +447,36 @@ def test_header_and_one_column(self): -
    Headerfirst
    - - ''') + ''')[0] + expected = DataFrame(data={'Header': 'first'}, index=[0]) - result = self.read_html(data)[0] + + tm.assert_frame_equal(result, expected) + + def test_thead_without_tr(self): + """ + Ensure parser adds within on malformed HTML. + """ + result = self.read_html(''' + + + + + + + + + + + + + + +
    CountryMunicipalityYear
    UkraineOdessa1944
    ''')[0] + + expected = DataFrame(data=[['Ukraine', 'Odessa', 1944]], + columns=['Country', 'Municipality', 'Year']) + tm.assert_frame_equal(result, expected) def test_tfoot_read(self): @@ -463,66 +502,54 @@ def test_tfoot_read(self): ''' + expected1 = DataFrame(data=[['bodyA', 'bodyB']], columns=['A', 'B']) + + expected2 = DataFrame(data=[['bodyA', 'bodyB'], ['footA', 'footB']], + columns=['A', 'B']) + data1 = data_template.format(footer="") data2 = data_template.format( footer="footAfootB") - d1 = {'A': ['bodyA'], 'B': ['bodyB']} - d2 = {'A': ['bodyA', 'footA'], 'B': ['bodyB', 'footB']} + result1 = self.read_html(data1)[0] + result2 = self.read_html(data2)[0] - tm.assert_frame_equal(self.read_html(data1)[0], DataFrame(d1)) - tm.assert_frame_equal(self.read_html(data2)[0], DataFrame(d2)) + tm.assert_frame_equal(result1, expected1) + tm.assert_frame_equal(result2, expected2) - def test_countries_municipalities(self): - # GH5048 - data1 = StringIO(''' - - - - - - - - + def test_parse_header_of_non_string_column(self): + # GH5048: if header is specified explicitly, an int column should be + # parsed as int while its header is parsed as str + result = self.read_html(''' +
    CountryMunicipalityYear
    - - - - - -
    UkraineOdessa1944
    ''') - data2 = StringIO(''' - - - - - - + + - - + - -
    CountryMunicipalityYearSI
    UkraineOdessatext 1944
    ''') - res1 = self.read_html(data1) - res2 = self.read_html(data2, header=0) - assert_framelist_equal(res1, res2) + + ''', header=0)[0] - def test_nyse_wsj_commas_table(self): - data = os.path.join(DATA_PATH, 'nyse_wsj.html') + expected = DataFrame([['text', 1944]], columns=('S', 'I')) + + tm.assert_frame_equal(result, expected) + + def test_nyse_wsj_commas_table(self, datapath): + data = datapath('io', 'data', 'nyse_wsj.html') df = self.read_html(data, index_col=0, header=0, attrs={'class': 'mdcTable'})[0] - columns = Index(['Issue(Roll over for charts and headlines)', - 'Volume', 'Price', 'Chg', '% Chg']) + expected = Index(['Issue(Roll over for charts and headlines)', + 'Volume', 'Price', 'Chg', '% Chg']) nrows = 100 - self.assertEqual(df.shape[0], nrows) - self.assert_index_equal(df.columns, columns) + assert df.shape[0] == nrows + tm.assert_index_equal(df.columns, expected) - @tm.slow - def test_banklist_header(self): + @pytest.mark.slow + def test_banklist_header(self, datapath): from pandas.io.html import _remove_whitespace def try_remove_ws(x): @@ -533,10 +560,10 @@ def try_remove_ws(x): df = self.read_html(self.banklist_data, 'Metcalf', attrs={'id': 'table'})[0] - ground_truth = read_csv(os.path.join(DATA_PATH, 'banklist.csv'), + ground_truth = read_csv(datapath('io', 'data', 'banklist.csv'), converters={'Updated Date': Timestamp, 'Closing Date': Timestamp}) - self.assertEqual(df.shape, ground_truth.shape) + assert df.shape == ground_truth.shape old = ['First Vietnamese American BankIn Vietnamese', 'Westernbank Puerto RicoEn Espanol', 'R-G Premier Bank of Puerto RicoEn Espanol', @@ -560,19 +587,19 @@ def try_remove_ws(x): coerce=True) tm.assert_frame_equal(converted, gtnew) - @tm.slow + @pytest.mark.slow def test_gold_canyon(self): gc = 'Gold Canyon' with open(self.banklist_data, 'r') as f: raw_text = f.read() - self.assertIn(gc, raw_text) + assert gc in raw_text df = self.read_html(self.banklist_data, 'Gold Canyon', attrs={'id': 'table'})[0] - self.assertIn(gc, df.to_string()) + assert gc in df.to_string() - def test_different_number_of_rows(self): - expected = """ + def test_different_number_of_cols(self): + expected = self.read_html("""
    @@ -601,8 +628,9 @@ def test_different_number_of_rows(self): -
    0.222
    """ - out = """ +
    """, index_col=0)[0] + + result = self.read_html(""" @@ -628,10 +656,151 @@ def test_different_number_of_rows(self): -
    0.222
    """ - expected = self.read_html(expected, index_col=0)[0] - res = self.read_html(out, index_col=0)[0] - tm.assert_frame_equal(expected, res) + """, index_col=0)[0] + + tm.assert_frame_equal(result, expected) + + def test_colspan_rowspan_1(self): + # GH17054 + result = self.read_html(""" + + + + + + + + + + + +
    ABC
    abc
    + """)[0] + + expected = DataFrame([['a', 'b', 'c']], columns=['A', 'B', 'C']) + + tm.assert_frame_equal(result, expected) + + def test_colspan_rowspan_copy_values(self): + # GH17054 + + # In ASCII, with lowercase letters being copies: + # + # X x Y Z W + # A B b z C + + result = self.read_html(""" + + + + + + + + + + + + +
    XYZW
    ABC
    + """, header=0)[0] + + expected = DataFrame(data=[['A', 'B', 'B', 'Z', 'C']], + columns=['X', 'X.1', 'Y', 'Z', 'W']) + + tm.assert_frame_equal(result, expected) + + def test_colspan_rowspan_both_not_1(self): + # GH17054 + + # In ASCII, with lowercase letters being copies: + # + # A B b b C + # a b b b D + + result = self.read_html(""" + + + + + + + + + +
    ABC
    D
    + """, header=0)[0] + + expected = DataFrame(data=[['A', 'B', 'B', 'B', 'D']], + columns=['A', 'B', 'B.1', 'B.2', 'C']) + + tm.assert_frame_equal(result, expected) + + def test_rowspan_at_end_of_row(self): + # GH17054 + + # In ASCII, with lowercase letters being copies: + # + # A B + # C b + + result = self.read_html(""" + + + + + + + + +
    AB
    C
    + """, header=0)[0] + + expected = DataFrame(data=[['C', 'B']], columns=['A', 'B']) + + tm.assert_frame_equal(result, expected) + + def test_rowspan_only_rows(self): + # GH17054 + + result = self.read_html(""" + + + + + +
    AB
    + """, header=0)[0] + + expected = DataFrame(data=[['A', 'B'], ['A', 'B']], + columns=['A', 'B']) + + tm.assert_frame_equal(result, expected) + + def test_header_inferred_from_rows_with_only_th(self): + # GH17054 + result = self.read_html(""" + + + + + + + + + + + + + +
    AB
    ab
    12
    + """)[0] + + columns = MultiIndex(levels=[['A', 'B'], ['a', 'b']], + codes=[[0, 1], [0, 1]]) + expected = DataFrame(data=[[1, 2]], columns=columns) + + tm.assert_frame_equal(result, expected) def test_parse_dates_list(self): df = DataFrame({'date': date_range('1/1/2001', periods=10)}) @@ -650,24 +819,42 @@ def test_parse_dates_combine(self): newdf = DataFrame({'datetime': raw_dates}) tm.assert_frame_equal(newdf, res[0]) - def test_computer_sales_page(self): - data = os.path.join(DATA_PATH, 'computer_sales_page.html') - with tm.assertRaisesRegexp(ParserError, r"Passed header=\[0,1\] are " - "too many rows for this multi_index " - "of columns"): + def test_computer_sales_page(self, datapath): + data = datapath('io', 'data', 'computer_sales_page.html') + msg = (r"Passed header=\[0,1\] are too many " + r"rows for this multi_index of columns") + with pytest.raises(ParserError, match=msg): self.read_html(data, header=[0, 1]) - def test_wikipedia_states_table(self): - data = os.path.join(DATA_PATH, 'wikipedia_states.html') + data = datapath('io', 'data', 'computer_sales_page.html') + assert self.read_html(data, header=[1, 2]) + + def test_wikipedia_states_table(self, datapath): + data = datapath('io', 'data', 'wikipedia_states.html') assert os.path.isfile(data), '%r is not a file' % data assert os.path.getsize(data), '%r is an empty file' % data result = self.read_html(data, 'Arizona', header=1)[0] - self.assertEqual(result['sq mi'].dtype, np.dtype('float64')) + assert result['sq mi'].dtype == np.dtype('float64') + + def test_parser_error_on_empty_header_row(self): + msg = (r"Passed header=\[0,1\] are too many " + r"rows for this multi_index of columns") + with pytest.raises(ParserError, match=msg): + self.read_html(""" + + + + + + + + +
    AB
    ab
    + """, header=[0, 1]) def test_decimal_rows(self): - # GH 12907 - data = StringIO(''' + result = self.read_html(''' @@ -682,63 +869,72 @@ def test_decimal_rows(self):
    - ''') + ''', decimal='#')[0] + expected = DataFrame(data={'Header': 1100.101}, index=[0]) - result = self.read_html(data, decimal='#')[0] + assert result['Header'].dtype == np.dtype('float64') tm.assert_frame_equal(result, expected) def test_bool_header_arg(self): # GH 6114 for arg in [True, False]: - with tm.assertRaises(TypeError): - read_html(self.spam_data, header=arg) + with pytest.raises(TypeError): + self.read_html(self.spam_data, header=arg) def test_converters(self): # GH 13461 - html_data = """ - - - - - - - - - - - - -
    a
    0.763
    0.244
    """ + result = self.read_html( + """ + + + + + + + + + + + + + +
    a
    0.763
    0.244
    """, + converters={'a': str} + )[0] + + expected = DataFrame({'a': ['0.763', '0.244']}) - expected_df = DataFrame({'a': ['0.763', '0.244']}) - html_df = read_html(html_data, converters={'a': str})[0] - tm.assert_frame_equal(expected_df, html_df) + tm.assert_frame_equal(result, expected) def test_na_values(self): # GH 13461 - html_data = """ - - - - - - - - - - - - -
    a
    0.763
    0.244
    """ + result = self.read_html( + """ + + + + + + + + + + + + + +
    a
    0.763
    0.244
    """, + na_values=[0.244])[0] + + expected = DataFrame({'a': [0.763, np.nan]}) - expected_df = DataFrame({'a': [0.763, np.nan]}) - html_df = read_html(html_data, na_values=[0.244])[0] - tm.assert_frame_equal(expected_df, html_df) + tm.assert_frame_equal(result, expected) def test_keep_default_na(self): html_data = """ + @@ -753,13 +949,56 @@ def test_keep_default_na(self):
    a
    """ expected_df = DataFrame({'a': ['N/A', 'NA']}) - html_df = read_html(html_data, keep_default_na=False)[0] + html_df = self.read_html(html_data, keep_default_na=False)[0] tm.assert_frame_equal(expected_df, html_df) expected_df = DataFrame({'a': [np.nan, np.nan]}) - html_df = read_html(html_data, keep_default_na=True)[0] + html_df = self.read_html(html_data, keep_default_na=True)[0] tm.assert_frame_equal(expected_df, html_df) + def test_preserve_empty_rows(self): + result = self.read_html(""" + + + + + + + + + + + + + +
    AB
    ab
    + """)[0] + + expected = DataFrame(data=[['a', 'b'], [np.nan, np.nan]], + columns=['A', 'B']) + + tm.assert_frame_equal(result, expected) + + def test_ignore_empty_rows_when_inferring_header(self): + result = self.read_html(""" + + + + + + + + + +
    AB
    ab
    12
    + """)[0] + + columns = MultiIndex(levels=[['A', 'B'], ['a', 'b']], + codes=[[0, 1], [0, 1]]) + expected = DataFrame(data=[[1, 2]], columns=columns) + + tm.assert_frame_equal(result, expected) + def test_multiple_header_rows(self): # Issue #13434 expected_df = DataFrame(data=[("Hillary", 68, "D"), @@ -769,164 +1008,154 @@ def test_multiple_header_rows(self): ["Name", "Unnamed: 1_level_1", "Unnamed: 2_level_1"]] html = expected_df.to_html(index=False) - html_df = read_html(html, )[0] + html_df = self.read_html(html, )[0] tm.assert_frame_equal(expected_df, html_df) - -def _lang_enc(filename): - return os.path.splitext(os.path.basename(filename))[0].split('_') - - -class TestReadHtmlEncoding(tm.TestCase): - files = glob.glob(os.path.join(DATA_PATH, 'html_encoding', '*.html')) - flavor = 'bs4' - - @classmethod - def setUpClass(cls): - super(TestReadHtmlEncoding, cls).setUpClass() - _skip_if_none_of((cls.flavor, 'html5lib')) - - def read_html(self, *args, **kwargs): - kwargs['flavor'] = self.flavor - return read_html(*args, **kwargs) - - def read_filename(self, f, encoding): - return self.read_html(f, encoding=encoding, index_col=0) - - def read_file_like(self, f, encoding): - with open(f, 'rb') as fobj: - return self.read_html(BytesIO(fobj.read()), encoding=encoding, - index_col=0) - - def read_string(self, f, encoding): - with open(f, 'rb') as fobj: - return self.read_html(fobj.read(), encoding=encoding, index_col=0) - - def test_encode(self): - assert self.files, 'no files read from the data folder' - for f in self.files: - _, encoding = _lang_enc(f) - try: - from_string = self.read_string(f, encoding).pop() - from_file_like = self.read_file_like(f, encoding).pop() - from_filename = self.read_filename(f, encoding).pop() - tm.assert_frame_equal(from_string, from_file_like) - tm.assert_frame_equal(from_string, from_filename) - except Exception: - # seems utf-16/32 fail on windows - if is_platform_windows(): - if '16' in encoding or '32' in encoding: - continue - raise - - -class TestReadHtmlEncodingLxml(TestReadHtmlEncoding): - flavor = 'lxml' - - @classmethod - def setUpClass(cls): - super(TestReadHtmlEncodingLxml, cls).setUpClass() - _skip_if_no(cls.flavor) - - -class TestReadHtmlLxml(tm.TestCase, ReadHtmlMixin): - flavor = 'lxml' - - @classmethod - def setUpClass(cls): - super(TestReadHtmlLxml, cls).setUpClass() - _skip_if_no('lxml') - - def test_data_fail(self): - from lxml.etree import XMLSyntaxError - spam_data = os.path.join(DATA_PATH, 'spam.html') - banklist_data = os.path.join(DATA_PATH, 'banklist.html') - - with tm.assertRaises(XMLSyntaxError): - self.read_html(spam_data) - - with tm.assertRaises(XMLSyntaxError): - self.read_html(banklist_data) - - def test_works_on_valid_markup(self): - filename = os.path.join(DATA_PATH, 'valid_markup.html') + def test_works_on_valid_markup(self, datapath): + filename = datapath('io', 'data', 'valid_markup.html') dfs = self.read_html(filename, index_col=0) - tm.assertIsInstance(dfs, list) - tm.assertIsInstance(dfs[0], DataFrame) + assert isinstance(dfs, list) + assert isinstance(dfs[0], DataFrame) - @tm.slow - def test_fallback_success(self): - _skip_if_none_of(('bs4', 'html5lib')) - banklist_data = os.path.join(DATA_PATH, 'banklist.html') + @pytest.mark.slow + def test_fallback_success(self, datapath): + banklist_data = datapath('io', 'data', 'banklist.html') self.read_html(banklist_data, '.*Water.*', flavor=['lxml', 'html5lib']) - def test_parse_dates_list(self): - df = DataFrame({'date': date_range('1/1/2001', periods=10)}) - expected = df.to_html() - res = self.read_html(expected, parse_dates=[1], index_col=0) - tm.assert_frame_equal(df, res[0]) - res = self.read_html(expected, parse_dates=['date'], index_col=0) - tm.assert_frame_equal(df, res[0]) - - def test_parse_dates_combine(self): - raw_dates = Series(date_range('1/1/2001', periods=10)) - df = DataFrame({'date': raw_dates.map(lambda x: str(x.date())), - 'time': raw_dates.map(lambda x: str(x.time()))}) - res = self.read_html(df.to_html(), parse_dates={'datetime': [1, 2]}, - index_col=1) - newdf = DataFrame({'datetime': raw_dates}) - tm.assert_frame_equal(newdf, res[0]) - - def test_computer_sales_page(self): - data = os.path.join(DATA_PATH, 'computer_sales_page.html') - self.read_html(data, header=[0, 1]) - - -def test_invalid_flavor(): - url = 'google.com' - with tm.assertRaises(ValueError): - read_html(url, 'google', flavor='not a* valid**++ flaver') - - -def get_elements_from_file(url, element='table'): - _skip_if_none_of(('bs4', 'html5lib')) - url = file_path_to_url(url) - from bs4 import BeautifulSoup - with urlopen(url) as f: - soup = BeautifulSoup(f, features='html5lib') - return soup.find_all(element) - - -@tm.slow -def test_bs4_finds_tables(): - filepath = os.path.join(DATA_PATH, "spam.html") - with warnings.catch_warnings(): - warnings.filterwarnings('ignore') - assert get_elements_from_file(filepath, 'table') - - -def get_lxml_elements(url, element): - _skip_if_no('lxml') - from lxml.html import parse - doc = parse(url) - return doc.xpath('.//{0}'.format(element)) - - -@tm.slow -def test_lxml_finds_tables(): - filepath = os.path.join(DATA_PATH, "spam.html") - assert get_lxml_elements(filepath, 'table') + def test_to_html_timestamp(self): + rng = date_range('2000-01-01', periods=10) + df = DataFrame(np.random.randn(10, 4), index=rng) + + result = df.to_html() + assert '2000-01-01' in result + + @pytest.mark.parametrize("displayed_only,exp0,exp1", [ + (True, DataFrame(["foo"]), None), + (False, DataFrame(["foo bar baz qux"]), DataFrame(["foo"]))]) + def test_displayed_only(self, displayed_only, exp0, exp1): + # GH 20027 + data = StringIO(""" + + + + + +
    + foo + bar + baz + qux +
    + + + + +
    foo
    + + """) + dfs = self.read_html(data, displayed_only=displayed_only) + tm.assert_frame_equal(dfs[0], exp0) -@tm.slow -def test_lxml_finds_tbody(): - filepath = os.path.join(DATA_PATH, "spam.html") - assert get_lxml_elements(filepath, 'tbody') + if exp1 is not None: + tm.assert_frame_equal(dfs[1], exp1) + else: + assert len(dfs) == 1 # Should not parse hidden table + def test_encode(self, html_encoding_file): + _, encoding = os.path.splitext( + os.path.basename(html_encoding_file) + )[0].split('_') -def test_same_ordering(): - _skip_if_none_of(['bs4', 'lxml', 'html5lib']) - filename = os.path.join(DATA_PATH, 'valid_markup.html') - dfs_lxml = read_html(filename, index_col=0, flavor=['lxml']) - dfs_bs4 = read_html(filename, index_col=0, flavor=['bs4']) - assert_framelist_equal(dfs_lxml, dfs_bs4) + try: + with open(html_encoding_file, 'rb') as fobj: + from_string = self.read_html(fobj.read(), encoding=encoding, + index_col=0).pop() + + with open(html_encoding_file, 'rb') as fobj: + from_file_like = self.read_html(BytesIO(fobj.read()), + encoding=encoding, + index_col=0).pop() + + from_filename = self.read_html(html_encoding_file, + encoding=encoding, + index_col=0).pop() + tm.assert_frame_equal(from_string, from_file_like) + tm.assert_frame_equal(from_string, from_filename) + except Exception: + # seems utf-16/32 fail on windows + if is_platform_windows(): + if '16' in encoding or '32' in encoding: + pytest.skip() + raise + + def test_parse_failure_unseekable(self): + # Issue #17975 + + if self.read_html.keywords.get('flavor') == 'lxml': + pytest.skip("Not applicable for lxml") + + class UnseekableStringIO(StringIO): + def seekable(self): + return False + + bad = UnseekableStringIO(''' +
    spameggs
    ''') + + assert self.read_html(bad) + + with pytest.raises(ValueError, + match='passed a non-rewindable file object'): + self.read_html(bad) + + def test_parse_failure_rewinds(self): + # Issue #17975 + + class MockFile(object): + def __init__(self, data): + self.data = data + self.at_end = False + + def read(self, size=None): + data = '' if self.at_end else self.data + self.at_end = True + return data + + def seek(self, offset): + self.at_end = False + + def seekable(self): + return True + + good = MockFile('
    spam
    eggs
    ') + bad = MockFile('
    spameggs
    ') + + assert self.read_html(good) + assert self.read_html(bad) + + @pytest.mark.slow + def test_importcheck_thread_safety(self, datapath): + # see gh-16928 + + class ErrorThread(threading.Thread): + def run(self): + try: + super(ErrorThread, self).run() + except Exception as e: + self.err = e + else: + self.err = None + + # force import check by reinitalising global vars in html.py + reload(pandas.io.html) + + filename = datapath('io', 'data', 'valid_markup.html') + helper_thread1 = ErrorThread(target=self.read_html, args=(filename,)) + helper_thread2 = ErrorThread(target=self.read_html, args=(filename,)) + + helper_thread1.start() + helper_thread2.start() + + while helper_thread1.is_alive() or helper_thread2.is_alive(): + pass + assert None is helper_thread1.err is helper_thread2.err diff --git a/pandas/tests/io/test_packers.py b/pandas/tests/io/test_packers.py index efa8587d64657..375557c43a3ae 100644 --- a/pandas/tests/io/test_packers.py +++ b/pandas/tests/io/test_packers.py @@ -1,29 +1,26 @@ -import pytest - -import os import datetime -import numpy as np -import sys from distutils.version import LooseVersion +import glob +import os +from warnings import catch_warnings -from pandas import compat -from pandas.compat import u, PY3 -from pandas import (Series, DataFrame, Panel, MultiIndex, bdate_range, - date_range, period_range, Index, Categorical) -from pandas.core.common import PerformanceWarning -from pandas.io.packers import to_msgpack, read_msgpack -import pandas.util.testing as tm -from pandas.util.testing import (ensure_clean, - assert_categorical_equal, - assert_frame_equal, - assert_index_equal, - assert_series_equal, - patch) -from pandas.tests.test_panel import assert_panel_equal +import numpy as np +import pytest -import pandas -from pandas import Timestamp, NaT from pandas._libs.tslib import iNaT +from pandas.compat import PY3, u +from pandas.errors import PerformanceWarning + +import pandas +from pandas import ( + Categorical, DataFrame, Index, Interval, MultiIndex, NaT, Period, Series, + Timestamp, bdate_range, compat, date_range, period_range) +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_categorical_equal, assert_frame_equal, assert_index_equal, + assert_series_equal, ensure_clean) + +from pandas.io.packers import read_msgpack, to_msgpack nan = np.nan @@ -64,8 +61,6 @@ def check_arbitrary(a, b): assert(len(a) == len(b)) for a_, b_ in zip(a, b): check_arbitrary(a_, b_) - elif isinstance(a, Panel): - assert_panel_equal(a, b) elif isinstance(a, DataFrame): assert_frame_equal(a, b) elif isinstance(a, Series): @@ -89,12 +84,13 @@ def check_arbitrary(a, b): assert(a == b) -class TestPackers(tm.TestCase): +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") +class TestPackers(object): - def setUp(self): + def setup_method(self, method): self.path = '__%s__.msg' % tm.rands(10) - def tearDown(self): + def teardown_method(self, method): pass def encode_decode(self, x, compress=None, **kwargs): @@ -103,6 +99,7 @@ def encode_decode(self, x, compress=None, **kwargs): return read_msgpack(p, **kwargs) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestAPI(TestPackers): def test_string_io(self): @@ -127,12 +124,21 @@ def test_string_io(self): with ensure_clean(self.path) as p: s = df.to_msgpack() - fh = open(p, 'wb') - fh.write(s) - fh.close() + with open(p, 'wb') as fh: + fh.write(s) result = read_msgpack(p) tm.assert_frame_equal(result, df) + def test_path_pathlib(self): + df = tm.makeDataFrame() + result = tm.round_trip_pathlib(df.to_msgpack, read_msgpack) + tm.assert_frame_equal(df, result) + + def test_path_localpath(self): + df = tm.makeDataFrame() + result = tm.round_trip_localpath(df.to_msgpack, read_msgpack) + tm.assert_frame_equal(df, result) + def test_iterator_with_string_io(self): dfs = [DataFrame(np.random.randn(10, 2)) for i in range(5)] @@ -147,9 +153,14 @@ class A(object): def __init__(self): self.read = 0 - tm.assertRaises(ValueError, read_msgpack, path_or_buf=None) - tm.assertRaises(ValueError, read_msgpack, path_or_buf={}) - tm.assertRaises(ValueError, read_msgpack, path_or_buf=A()) + msg = (r"Invalid file path or buffer object type: <(class|type)" + r" '{}'>") + with pytest.raises(ValueError, match=msg.format('NoneType')): + read_msgpack(path_or_buf=None) + with pytest.raises(ValueError, match=msg.format('dict')): + read_msgpack(path_or_buf={}) + with pytest.raises(ValueError, match=msg.format(r'.*\.A')): + read_msgpack(path_or_buf=A()) class TestNumpy(TestPackers): @@ -162,17 +173,26 @@ def test_numpy_scalar_float(self): def test_numpy_scalar_complex(self): x = np.complex64(np.random.rand() + 1j * np.random.rand()) x_rec = self.encode_decode(x) - self.assertTrue(np.allclose(x, x_rec)) + assert np.allclose(x, x_rec) def test_scalar_float(self): x = np.random.rand() x_rec = self.encode_decode(x) tm.assert_almost_equal(x, x_rec) + def test_scalar_bool(self): + x = np.bool_(1) + x_rec = self.encode_decode(x) + tm.assert_almost_equal(x, x_rec) + + x = np.bool_(0) + x_rec = self.encode_decode(x) + tm.assert_almost_equal(x, x_rec) + def test_scalar_complex(self): x = np.random.rand() + 1j * np.random.rand() x_rec = self.encode_decode(x) - self.assertTrue(np.allclose(x, x_rec)) + assert np.allclose(x, x_rec) def test_list_numpy_float(self): x = [np.float32(np.random.rand()) for i in range(5)] @@ -185,13 +205,13 @@ def test_list_numpy_float(self): def test_list_numpy_float_complex(self): if not hasattr(np, 'complex128'): - pytest.skip('numpy cant handle complex128') + pytest.skip('numpy can not handle complex128') x = [np.float32(np.random.rand()) for i in range(5)] + \ [np.complex128(np.random.rand() + 1j * np.random.rand()) for i in range(5)] x_rec = self.encode_decode(x) - self.assertTrue(np.allclose(x, x_rec)) + assert np.allclose(x, x_rec) def test_list_float(self): x = [np.random.rand() for i in range(5)] @@ -206,7 +226,7 @@ def test_list_float_complex(self): x = [np.random.rand() for i in range(5)] + \ [(np.random.rand() + 1j * np.random.rand()) for i in range(5)] x_rec = self.encode_decode(x) - self.assertTrue(np.allclose(x, x_rec)) + assert np.allclose(x, x_rec) def test_dict_float(self): x = {'foo': 1.0, 'bar': 2.0} @@ -216,9 +236,10 @@ def test_dict_float(self): def test_dict_complex(self): x = {'foo': 1.0 + 1.0j, 'bar': 2.0 + 2.0j} x_rec = self.encode_decode(x) - self.assertEqual(x, x_rec) + tm.assert_dict_equal(x, x_rec) + for key in x: - self.assertEqual(type(x[key]), type(x_rec[key])) + tm.assert_class_equal(x[key], x_rec[key], obj="complex value") def test_dict_numpy_float(self): x = {'foo': np.float32(1.0), 'bar': np.float32(2.0)} @@ -229,9 +250,10 @@ def test_dict_numpy_complex(self): x = {'foo': np.complex128(1.0 + 1.0j), 'bar': np.complex128(2.0 + 2.0j)} x_rec = self.encode_decode(x) - self.assertEqual(x, x_rec) + tm.assert_dict_equal(x, x_rec) + for key in x: - self.assertEqual(type(x[key]), type(x_rec[key])) + tm.assert_class_equal(x[key], x_rec[key], obj="numpy complex128") def test_numpy_array_float(self): @@ -246,11 +268,11 @@ def test_numpy_array_float(self): def test_numpy_array_complex(self): x = (np.random.rand(5) + 1j * np.random.rand(5)).astype(np.complex128) x_rec = self.encode_decode(x) - self.assertTrue(all(map(lambda x, y: x == y, x, x_rec)) and - x.dtype == x_rec.dtype) + assert (all(map(lambda x, y: x == y, x, x_rec)) and + x.dtype == x_rec.dtype) def test_list_mixed(self): - x = [1.0, np.float32(3.5), np.complex128(4.25), u('foo')] + x = [1.0, np.float32(3.5), np.complex128(4.25), u('foo'), np.bool_(1)] x_rec = self.encode_decode(x) # current msgpack cannot distinguish list/tuple tm.assert_almost_equal(tuple(x), x_rec) @@ -267,25 +289,20 @@ def test_timestamp(self): '20130101'), Timestamp('20130101', tz='US/Eastern'), Timestamp('201301010501')]: i_rec = self.encode_decode(i) - self.assertEqual(i, i_rec) + assert i == i_rec def test_nat(self): nat_rec = self.encode_decode(NaT) - self.assertIs(NaT, nat_rec) + assert NaT is nat_rec def test_datetimes(self): - # fails under 2.6/win32 (np.datetime64 seems broken) - - if LooseVersion(sys.version) < '2.7': - pytest.skip('2.6 with np.datetime64 is broken') - for i in [datetime.datetime(2013, 1, 1), datetime.datetime(2013, 1, 1, 5, 1), datetime.date(2013, 1, 1), np.datetime64(datetime.datetime(2013, 1, 5, 2, 15))]: i_rec = self.encode_decode(i) - self.assertEqual(i, i_rec) + assert i == i_rec def test_timedeltas(self): @@ -293,13 +310,26 @@ def test_timedeltas(self): datetime.timedelta(days=1, seconds=10), np.timedelta64(1000000)]: i_rec = self.encode_decode(i) - self.assertEqual(i, i_rec) + assert i == i_rec + + def test_periods(self): + # 13463 + for i in [Period('2010-09', 'M'), Period('2014-Q1', 'Q')]: + i_rec = self.encode_decode(i) + assert i == i_rec + + def test_intervals(self): + # 19967 + for i in [Interval(0, 1), Interval(0, 1, 'left'), + Interval(10, 25., 'right')]: + i_rec = self.encode_decode(i) + assert i == i_rec class TestIndex(TestPackers): - def setUp(self): - super(TestIndex, self).setUp() + def setup_method(self, method): + super(TestIndex, self).setup_method(method) self.d = { 'string': tm.makeStringIndex(100), @@ -312,7 +342,9 @@ def setUp(self): 'period': Index(period_range('2012-1-1', freq='M', periods=3)), 'date2': Index(date_range('2013-01-1', periods=10)), 'bdate': Index(bdate_range('2013-01-02', periods=10)), - 'cat': tm.makeCategoricalIndex(100) + 'cat': tm.makeCategoricalIndex(100), + 'interval': tm.makeIntervalIndex(100), + 'timedelta': tm.makeTimedeltaIndex(100, 'H') } self.mi = { @@ -326,30 +358,30 @@ def test_basic_index(self): for s, i in self.d.items(): i_rec = self.encode_decode(i) - self.assert_index_equal(i, i_rec) + tm.assert_index_equal(i, i_rec) # datetime with no freq (GH5506) i = Index([Timestamp('20130101'), Timestamp('20130103')]) i_rec = self.encode_decode(i) - self.assert_index_equal(i, i_rec) + tm.assert_index_equal(i, i_rec) # datetime with timezone i = Index([Timestamp('20130101 9:00:00'), Timestamp( '20130103 11:00:00')]).tz_localize('US/Eastern') i_rec = self.encode_decode(i) - self.assert_index_equal(i, i_rec) + tm.assert_index_equal(i, i_rec) def test_multi_index(self): for s, i in self.mi.items(): i_rec = self.encode_decode(i) - self.assert_index_equal(i, i_rec) + tm.assert_index_equal(i, i_rec) def test_unicode(self): i = tm.makeUnicodeIndex(100) i_rec = self.encode_decode(i) - self.assert_index_equal(i, i_rec) + tm.assert_index_equal(i, i_rec) def categorical_index(self): # GH15487 @@ -361,8 +393,8 @@ def categorical_index(self): class TestSeries(TestPackers): - def setUp(self): - super(TestSeries, self).setUp() + def setup_method(self, method): + super(TestSeries, self).setup_method(method) self.d = {} @@ -388,6 +420,7 @@ def setUp(self): 'G': [Timestamp('20130102', tz='US/Eastern')] * 5, 'H': Categorical([1, 2, 3, 4, 5]), 'I': Categorical([1, 2, 3, 4, 5], ordered=True), + 'J': (np.bool_(1), 2, 3, 4, 5), } self.d['float'] = Series(data['A']) @@ -397,6 +430,7 @@ def setUp(self): self.d['dt_tz'] = Series(data['G']) self.d['cat_ordered'] = Series(data['H']) self.d['cat_unordered'] = Series(data['I']) + self.d['numpy_bool_mixed'] = Series(data['J']) def test_basic(self): @@ -409,8 +443,8 @@ def test_basic(self): class TestCategorical(TestPackers): - def setUp(self): - super(TestCategorical, self).setUp() + def setup_method(self, method): + super(TestCategorical, self).setup_method(method) self.d = {} @@ -430,10 +464,11 @@ def test_basic(self): assert_categorical_equal(i, i_rec) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestNDFrame(TestPackers): - def setUp(self): - super(TestNDFrame, self).setUp() + def setup_method(self, method): + super(TestNDFrame, self).setup_method(method) data = { 'A': [0., 1., 2., 3., np.nan], @@ -452,49 +487,39 @@ def setUp(self): 'int': DataFrame(dict(A=data['B'], B=Series(data['B']) + 1)), 'mixed': DataFrame(data)} - self.panel = { - 'float': Panel(dict(ItemA=self.frame['float'], - ItemB=self.frame['float'] + 1))} - def test_basic_frame(self): for s, i in self.frame.items(): i_rec = self.encode_decode(i) assert_frame_equal(i, i_rec) - def test_basic_panel(self): - - for s, i in self.panel.items(): - i_rec = self.encode_decode(i) - assert_panel_equal(i, i_rec) - def test_multi(self): i_rec = self.encode_decode(self.frame) for k in self.frame.keys(): assert_frame_equal(self.frame[k], i_rec[k]) - l = tuple([self.frame['float'], self.frame['float'].A, - self.frame['float'].B, None]) - l_rec = self.encode_decode(l) - check_arbitrary(l, l_rec) + packed_items = tuple([self.frame['float'], self.frame['float'].A, + self.frame['float'].B, None]) + l_rec = self.encode_decode(packed_items) + check_arbitrary(packed_items, l_rec) # this is an oddity in that packed lists will be returned as tuples - l = [self.frame['float'], self.frame['float'] - .A, self.frame['float'].B, None] - l_rec = self.encode_decode(l) - self.assertIsInstance(l_rec, tuple) - check_arbitrary(l, l_rec) + packed_items = [self.frame['float'], self.frame['float'].A, + self.frame['float'].B, None] + l_rec = self.encode_decode(packed_items) + assert isinstance(l_rec, tuple) + check_arbitrary(packed_items, l_rec) def test_iterator(self): - l = [self.frame['float'], self.frame['float'] - .A, self.frame['float'].B, None] + packed_items = [self.frame['float'], self.frame['float'].A, + self.frame['float'].B, None] with ensure_clean(self.path) as path: - to_msgpack(path, *l) + to_msgpack(path, *packed_items) for i, packed in enumerate(read_msgpack(path, iterator=True)): - check_arbitrary(packed, l[i]) + check_arbitrary(packed, packed_items[i]) def tests_datetimeindex_freq_issue(self): @@ -533,7 +558,9 @@ def _check_roundtrip(self, obj, comparator, **kwargs): # currently these are not implemetned # i_rec = self.encode_decode(obj) # comparator(obj, i_rec, **kwargs) - self.assertRaises(NotImplementedError, self.encode_decode, obj) + msg = r"msgpack sparse (series|frame) is not implemented" + with pytest.raises(NotImplementedError, match=msg): + self.encode_decode(obj) def test_sparse_series(self): @@ -574,7 +601,7 @@ class TestCompression(TestPackers): """See https://github.com/pandas-dev/pandas/pull/9783 """ - def setUp(self): + def setup_method(self, method): try: from sqlalchemy import create_engine self._create_sql_engine = create_engine @@ -583,7 +610,7 @@ def setUp(self): else: self._SQLALCHEMY_INSTALLED = True - super(TestCompression, self).setUp() + super(TestCompression, self).setup_method(method) data = { 'A': np.arange(1000, dtype=np.float64), 'B': np.arange(1000, dtype=np.int32), @@ -592,8 +619,8 @@ def setUp(self): 'E': [datetime.timedelta(days=x) for x in range(1000)], } self.frame = { - 'float': DataFrame(dict((k, data[k]) for k in ['A', 'A'])), - 'int': DataFrame(dict((k, data[k]) for k in ['B', 'B'])), + 'float': DataFrame({k: data[k] for k in ['A', 'A']}), + 'int': DataFrame({k: data[k] for k in ['B', 'B']}), 'mixed': DataFrame(data), } @@ -610,7 +637,7 @@ def _test_compression(self, compress): assert_frame_equal(value, expected) # make sure that we can write to the new frames for block in value._data.blocks: - self.assertTrue(block.values.flags.writeable) + assert block.values.flags.writeable def test_compression_zlib(self): if not _ZLIB_INSTALLED: @@ -622,7 +649,8 @@ def test_compression_blosc(self): pytest.skip('no blosc') self._test_compression('blosc') - def _test_compression_warns_when_decompress_caches(self, compress): + def _test_compression_warns_when_decompress_caches( + self, monkeypatch, compress): not_garbage = [] control = [] # copied data @@ -647,9 +675,9 @@ def decompress(ob): np.dtype('timedelta64[ns]'): np.timedelta64(1, 'ns'), } - with patch(compress_module, 'decompress', decompress), \ + with monkeypatch.context() as m, \ tm.assert_produces_warning(PerformanceWarning) as ws: - + m.setattr(compress_module, 'decompress', decompress) i_rec = self.encode_decode(self.frame, compress=compress) for k in self.frame.keys(): @@ -659,32 +687,32 @@ def decompress(ob): # make sure that we can write to the new frames even though # we needed to copy the data for block in value._data.blocks: - self.assertTrue(block.values.flags.writeable) + assert block.values.flags.writeable # mutate the data in some way block.values[0] += rhs[block.dtype] for w in ws: # check the messages from our warnings - self.assertEqual( - str(w.message), - 'copying data after decompressing; this may mean that' - ' decompress is caching its result', - ) + assert str(w.message) == ('copying data after decompressing; ' + 'this may mean that decompress is ' + 'caching its result') for buf, control_buf in zip(not_garbage, control): # make sure none of our mutations above affected the # original buffers - self.assertEqual(buf, control_buf) + assert buf == control_buf - def test_compression_warns_when_decompress_caches_zlib(self): + def test_compression_warns_when_decompress_caches_zlib(self, monkeypatch): if not _ZLIB_INSTALLED: pytest.skip('no zlib') - self._test_compression_warns_when_decompress_caches('zlib') + self._test_compression_warns_when_decompress_caches( + monkeypatch, 'zlib') - def test_compression_warns_when_decompress_caches_blosc(self): + def test_compression_warns_when_decompress_caches_blosc(self, monkeypatch): if not _BLOSC_INSTALLED: pytest.skip('no blosc') - self._test_compression_warns_when_decompress_caches('blosc') + self._test_compression_warns_when_decompress_caches( + monkeypatch, 'blosc') def _test_small_strings_no_warn(self, compress): empty = np.array([], dtype='uint8') @@ -692,14 +720,14 @@ def _test_small_strings_no_warn(self, compress): empty_unpacked = self.encode_decode(empty, compress=compress) tm.assert_numpy_array_equal(empty_unpacked, empty) - self.assertTrue(empty_unpacked.flags.writeable) + assert empty_unpacked.flags.writeable char = np.array([ord(b'a')], dtype='uint8') with tm.assert_produces_warning(None): char_unpacked = self.encode_decode(char, compress=compress) tm.assert_numpy_array_equal(char_unpacked, char) - self.assertTrue(char_unpacked.flags.writeable) + assert char_unpacked.flags.writeable # if this test fails I am sorry because the interpreter is now in a # bad state where b'a' points to 98 == ord(b'b'). char_unpacked[0] = ord(b'b') @@ -707,7 +735,7 @@ def _test_small_strings_no_warn(self, compress): # we compare the ord of bytes b'a' with unicode u'a' because the should # always be the same (unless we were able to mutate the shared # character singleton in which case ord(b'a') == ord(b'b'). - self.assertEqual(ord(b'a'), ord(u'a')) + assert ord(b'a') == ord(u'a') tm.assert_numpy_array_equal( char_unpacked, np.array([ord(b'b')], dtype='uint8'), @@ -729,15 +757,15 @@ def test_readonly_axis_blosc(self): pytest.skip('no blosc') df1 = DataFrame({'A': list('abcd')}) df2 = DataFrame(df1, index=[1., 2., 3., 4.]) - self.assertTrue(1 in self.encode_decode(df1['A'], compress='blosc')) - self.assertTrue(1. in self.encode_decode(df2['A'], compress='blosc')) + assert 1 in self.encode_decode(df1['A'], compress='blosc') + assert 1. in self.encode_decode(df2['A'], compress='blosc') def test_readonly_axis_zlib(self): # GH11880 df1 = DataFrame({'A': list('abcd')}) df2 = DataFrame(df1, index=[1., 2., 3., 4.]) - self.assertTrue(1 in self.encode_decode(df1['A'], compress='zlib')) - self.assertTrue(1. in self.encode_decode(df2['A'], compress='zlib')) + assert 1 in self.encode_decode(df1['A'], compress='zlib') + assert 1. in self.encode_decode(df2['A'], compress='zlib') def test_readonly_axis_blosc_to_sql(self): # GH11880 @@ -770,8 +798,8 @@ def test_readonly_axis_zlib_to_sql(self): class TestEncoding(TestPackers): - def setUp(self): - super(TestEncoding, self).setUp() + def setup_method(self, method): + super(TestEncoding, self).setup_method(method) data = { 'A': [compat.u('\u2019')] * 1000, 'B': np.arange(1000, dtype=np.int32), @@ -781,8 +809,8 @@ def setUp(self): 'G': [400] * 1000 } self.frame = { - 'float': DataFrame(dict((k, data[k]) for k in ['A', 'A'])), - 'int': DataFrame(dict((k, data[k]) for k in ['B', 'B'])), + 'float': DataFrame({k: data[k] for k in ['A', 'A']}), + 'int': DataFrame({k: data[k] for k in ['B', 'B']}), 'mixed': DataFrame(data), } self.utf_encodings = ['utf8', 'utf16', 'utf32'] @@ -798,20 +826,21 @@ def test_default_encoding(self): for frame in compat.itervalues(self.frame): result = frame.to_msgpack() expected = frame.to_msgpack(encoding='utf8') - self.assertEqual(result, expected) + assert result == expected result = self.encode_decode(frame) assert_frame_equal(result, frame) -def legacy_packers_versions(): - # yield the packers versions - path = tm.get_data_path('legacy_msgpack') - for v in os.listdir(path): - p = os.path.join(path, v) - if os.path.isdir(p): - yield v +files = glob.glob(os.path.join(os.path.dirname(__file__), "data", + "legacy_msgpack", "*", "*.msgpack")) + + +@pytest.fixture(params=files) +def legacy_packer(request, datapath): + return datapath(request.param) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") class TestMsgpack(object): """ How to add msgpack tests: @@ -833,6 +862,10 @@ class TestMsgpack(object): def check_min_structure(self, data, version): for typ, v in self.minimum_structure.items(): + if typ == "panel": + # FIXME: kludge; get this key out of the legacy file + continue + assert typ in data, '"{0}" not found in unpacked data'.format(typ) for kind in v: msg = '"{0}" not found in data["{1}"]'.format(kind, typ) @@ -840,10 +873,15 @@ def check_min_structure(self, data, version): def compare(self, current_data, all_data, vf, version): # GH12277 encoding default used to be latin-1, now utf-8 - if LooseVersion(version) < '0.18.0': + if LooseVersion(version) < LooseVersion('0.18.0'): data = read_msgpack(vf, encoding='latin-1') else: data = read_msgpack(vf) + + if "panel" in data: + # FIXME: kludge; get the key out of the stored file + del data["panel"] + self.check_min_structure(data, version) for typ, dv in data.items(): assert typ in all_data, ('unpacked data contains ' @@ -871,7 +909,7 @@ def compare(self, current_data, all_data, vf, version): def compare_series_dt_tz(self, result, expected, typ, version): # 8260 # dtype is object < 0.17.0 - if LooseVersion(version) < '0.17.0': + if LooseVersion(version) < LooseVersion('0.17.0'): expected = expected.astype(object) tm.assert_series_equal(result, expected) else: @@ -880,29 +918,32 @@ def compare_series_dt_tz(self, result, expected, typ, version): def compare_frame_dt_mixed_tzs(self, result, expected, typ, version): # 8260 # dtype is object < 0.17.0 - if LooseVersion(version) < '0.17.0': + if LooseVersion(version) < LooseVersion('0.17.0'): expected = expected.astype(object) tm.assert_frame_equal(result, expected) else: tm.assert_frame_equal(result, expected) - @pytest.mark.parametrize('version', legacy_packers_versions()) def test_msgpacks_legacy(self, current_packers_data, all_packers_data, - version): - - pth = tm.get_data_path('legacy_msgpack/{0}'.format(version)) - n = 0 - for f in os.listdir(pth): - # GH12142 0.17 files packed in P2 can't be read in P3 - if (compat.PY3 and version.startswith('0.17.') and - f.split('.')[-4][-1] == '2'): - continue - vf = os.path.join(pth, f) - try: + legacy_packer, datapath): + + version = os.path.basename(os.path.dirname(legacy_packer)) + + # GH12142 0.17 files packed in P2 can't be read in P3 + if (compat.PY3 and version.startswith('0.17.') and + legacy_packer.split('.')[-4][-1] == '2'): + msg = "Files packed in Py2 can't be read in Py3 ({})" + pytest.skip(msg.format(version)) + try: + with catch_warnings(record=True): self.compare(current_packers_data, all_packers_data, - vf, version) - except ImportError: - # blosc not installed - continue - n += 1 - assert n > 0, 'Msgpack files are not tested' + legacy_packer, version) + except ImportError: + # blosc not installed + pass + + def test_msgpack_period_freq(self): + # https://github.com/pandas-dev/pandas/issues/24135 + s = Series(np.random.rand(5), index=date_range('20130101', periods=5)) + r = read_msgpack(s.to_msgpack()) + repr(r) diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py new file mode 100644 index 0000000000000..01a47a67ad1b6 --- /dev/null +++ b/pandas/tests/io/test_parquet.py @@ -0,0 +1,541 @@ +""" test parquet compat """ +import datetime +from distutils.version import LooseVersion +import os +from warnings import catch_warnings + +import numpy as np +import pytest + +from pandas.compat import PY3 +import pandas.util._test_decorators as td + +import pandas as pd +from pandas.util import testing as tm + +from pandas.io.parquet import ( + FastParquetImpl, PyArrowImpl, get_engine, read_parquet, to_parquet) + +try: + import pyarrow # noqa + _HAVE_PYARROW = True +except ImportError: + _HAVE_PYARROW = False + +try: + import fastparquet # noqa + _HAVE_FASTPARQUET = True +except ImportError: + _HAVE_FASTPARQUET = False + + +# setup engines & skips +@pytest.fixture(params=[ + pytest.param('fastparquet', + marks=pytest.mark.skipif(not _HAVE_FASTPARQUET, + reason='fastparquet is ' + 'not installed')), + pytest.param('pyarrow', + marks=pytest.mark.skipif(not _HAVE_PYARROW, + reason='pyarrow is ' + 'not installed'))]) +def engine(request): + return request.param + + +@pytest.fixture +def pa(): + if not _HAVE_PYARROW: + pytest.skip("pyarrow is not installed") + return 'pyarrow' + + +@pytest.fixture +def fp(): + if not _HAVE_FASTPARQUET: + pytest.skip("fastparquet is not installed") + return 'fastparquet' + + +@pytest.fixture +def df_compat(): + return pd.DataFrame({'A': [1, 2, 3], 'B': 'foo'}) + + +@pytest.fixture +def df_cross_compat(): + df = pd.DataFrame({'a': list('abc'), + 'b': list(range(1, 4)), + # 'c': np.arange(3, 6).astype('u1'), + 'd': np.arange(4.0, 7.0, dtype='float64'), + 'e': [True, False, True], + 'f': pd.date_range('20130101', periods=3), + # 'g': pd.date_range('20130101', periods=3, + # tz='US/Eastern'), + # 'h': pd.date_range('20130101', periods=3, freq='ns') + }) + return df + + +@pytest.fixture +def df_full(): + return pd.DataFrame( + {'string': list('abc'), + 'string_with_nan': ['a', np.nan, 'c'], + 'string_with_none': ['a', None, 'c'], + 'bytes': [b'foo', b'bar', b'baz'], + 'unicode': [u'foo', u'bar', u'baz'], + 'int': list(range(1, 4)), + 'uint': np.arange(3, 6).astype('u1'), + 'float': np.arange(4.0, 7.0, dtype='float64'), + 'float_with_nan': [2., np.nan, 3.], + 'bool': [True, False, True], + 'datetime': pd.date_range('20130101', periods=3), + 'datetime_with_nat': [pd.Timestamp('20130101'), + pd.NaT, + pd.Timestamp('20130103')]}) + + +def check_round_trip(df, engine=None, path=None, + write_kwargs=None, read_kwargs=None, + expected=None, check_names=True, + repeat=2): + """Verify parquet serializer and deserializer produce the same results. + + Performs a pandas to disk and disk to pandas round trip, + then compares the 2 resulting DataFrames to verify equality. + + Parameters + ---------- + df: Dataframe + engine: str, optional + 'pyarrow' or 'fastparquet' + path: str, optional + write_kwargs: dict of str:str, optional + read_kwargs: dict of str:str, optional + expected: DataFrame, optional + Expected deserialization result, otherwise will be equal to `df` + check_names: list of str, optional + Closed set of column names to be compared + repeat: int, optional + How many times to repeat the test + """ + + write_kwargs = write_kwargs or {'compression': None} + read_kwargs = read_kwargs or {} + + if expected is None: + expected = df + + if engine: + write_kwargs['engine'] = engine + read_kwargs['engine'] = engine + + def compare(repeat): + for _ in range(repeat): + df.to_parquet(path, **write_kwargs) + with catch_warnings(record=True): + actual = read_parquet(path, **read_kwargs) + tm.assert_frame_equal(expected, actual, + check_names=check_names) + + if path is None: + with tm.ensure_clean() as path: + compare(repeat) + else: + compare(repeat) + + +def test_invalid_engine(df_compat): + with pytest.raises(ValueError): + check_round_trip(df_compat, 'foo', 'bar') + + +def test_options_py(df_compat, pa): + # use the set option + + with pd.option_context('io.parquet.engine', 'pyarrow'): + check_round_trip(df_compat) + + +def test_options_fp(df_compat, fp): + # use the set option + + with pd.option_context('io.parquet.engine', 'fastparquet'): + check_round_trip(df_compat) + + +def test_options_auto(df_compat, fp, pa): + # use the set option + + with pd.option_context('io.parquet.engine', 'auto'): + check_round_trip(df_compat) + + +def test_options_get_engine(fp, pa): + assert isinstance(get_engine('pyarrow'), PyArrowImpl) + assert isinstance(get_engine('fastparquet'), FastParquetImpl) + + with pd.option_context('io.parquet.engine', 'pyarrow'): + assert isinstance(get_engine('auto'), PyArrowImpl) + assert isinstance(get_engine('pyarrow'), PyArrowImpl) + assert isinstance(get_engine('fastparquet'), FastParquetImpl) + + with pd.option_context('io.parquet.engine', 'fastparquet'): + assert isinstance(get_engine('auto'), FastParquetImpl) + assert isinstance(get_engine('pyarrow'), PyArrowImpl) + assert isinstance(get_engine('fastparquet'), FastParquetImpl) + + with pd.option_context('io.parquet.engine', 'auto'): + assert isinstance(get_engine('auto'), PyArrowImpl) + assert isinstance(get_engine('pyarrow'), PyArrowImpl) + assert isinstance(get_engine('fastparquet'), FastParquetImpl) + + +def test_cross_engine_pa_fp(df_cross_compat, pa, fp): + # cross-compat with differing reading/writing engines + + df = df_cross_compat + with tm.ensure_clean() as path: + df.to_parquet(path, engine=pa, compression=None) + + result = read_parquet(path, engine=fp) + tm.assert_frame_equal(result, df) + + result = read_parquet(path, engine=fp, columns=['a', 'd']) + tm.assert_frame_equal(result, df[['a', 'd']]) + + +def test_cross_engine_fp_pa(df_cross_compat, pa, fp): + # cross-compat with differing reading/writing engines + + df = df_cross_compat + with tm.ensure_clean() as path: + df.to_parquet(path, engine=fp, compression=None) + + with catch_warnings(record=True): + result = read_parquet(path, engine=pa) + tm.assert_frame_equal(result, df) + + result = read_parquet(path, engine=pa, columns=['a', 'd']) + tm.assert_frame_equal(result, df[['a', 'd']]) + + +class Base(object): + + def check_error_on_write(self, df, engine, exc): + # check that we are raising the exception on writing + with tm.ensure_clean() as path: + with pytest.raises(exc): + to_parquet(df, path, engine, compression=None) + + +class TestBasic(Base): + + def test_error(self, engine): + for obj in [pd.Series([1, 2, 3]), 1, 'foo', pd.Timestamp('20130101'), + np.array([1, 2, 3])]: + self.check_error_on_write(obj, engine, ValueError) + + def test_columns_dtypes(self, engine): + df = pd.DataFrame({'string': list('abc'), + 'int': list(range(1, 4))}) + + # unicode + df.columns = [u'foo', u'bar'] + check_round_trip(df, engine) + + def test_columns_dtypes_invalid(self, engine): + df = pd.DataFrame({'string': list('abc'), + 'int': list(range(1, 4))}) + + # numeric + df.columns = [0, 1] + self.check_error_on_write(df, engine, ValueError) + + if PY3: + # bytes on PY3, on PY2 these are str + df.columns = [b'foo', b'bar'] + self.check_error_on_write(df, engine, ValueError) + + # python object + df.columns = [datetime.datetime(2011, 1, 1, 0, 0), + datetime.datetime(2011, 1, 1, 1, 1)] + self.check_error_on_write(df, engine, ValueError) + + @pytest.mark.parametrize('compression', [None, 'gzip', 'snappy', 'brotli']) + def test_compression(self, engine, compression): + + if compression == 'snappy': + pytest.importorskip('snappy') + + elif compression == 'brotli': + pytest.importorskip('brotli') + + df = pd.DataFrame({'A': [1, 2, 3]}) + check_round_trip(df, engine, write_kwargs={'compression': compression}) + + def test_read_columns(self, engine): + # GH18154 + df = pd.DataFrame({'string': list('abc'), + 'int': list(range(1, 4))}) + + expected = pd.DataFrame({'string': list('abc')}) + check_round_trip(df, engine, expected=expected, + read_kwargs={'columns': ['string']}) + + def test_write_index(self, engine): + check_names = engine != 'fastparquet' + + df = pd.DataFrame({'A': [1, 2, 3]}) + check_round_trip(df, engine) + + indexes = [ + [2, 3, 4], + pd.date_range('20130101', periods=3), + list('abc'), + [1, 3, 4], + ] + # non-default index + for index in indexes: + df.index = index + check_round_trip(df, engine, check_names=check_names) + + # index with meta-data + df.index = [0, 1, 2] + df.index.name = 'foo' + check_round_trip(df, engine) + + def test_write_multiindex(self, pa): + # Not suppoprted in fastparquet as of 0.1.3 or older pyarrow version + engine = pa + + df = pd.DataFrame({'A': [1, 2, 3]}) + index = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)]) + df.index = index + check_round_trip(df, engine) + + def test_write_column_multiindex(self, engine): + # column multi-index + mi_columns = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)]) + df = pd.DataFrame(np.random.randn(4, 3), columns=mi_columns) + self.check_error_on_write(df, engine, ValueError) + + def test_multiindex_with_columns(self, pa): + engine = pa + dates = pd.date_range('01-Jan-2018', '01-Dec-2018', freq='MS') + df = pd.DataFrame(np.random.randn(2 * len(dates), 3), + columns=list('ABC')) + index1 = pd.MultiIndex.from_product( + [['Level1', 'Level2'], dates], + names=['level', 'date']) + index2 = index1.copy(names=None) + for index in [index1, index2]: + df.index = index + + check_round_trip(df, engine) + check_round_trip(df, engine, read_kwargs={'columns': ['A', 'B']}, + expected=df[['A', 'B']]) + + def test_write_ignoring_index(self, engine): + # ENH 20768 + # Ensure index=False omits the index from the written Parquet file. + df = pd.DataFrame({'a': [1, 2, 3], 'b': ['q', 'r', 's']}) + + write_kwargs = { + 'compression': None, + 'index': False, + } + + # Because we're dropping the index, we expect the loaded dataframe to + # have the default integer index. + expected = df.reset_index(drop=True) + + check_round_trip(df, engine, write_kwargs=write_kwargs, + expected=expected) + + # Ignore custom index + df = pd.DataFrame({'a': [1, 2, 3], 'b': ['q', 'r', 's']}, + index=['zyx', 'wvu', 'tsr']) + + check_round_trip(df, engine, write_kwargs=write_kwargs, + expected=expected) + + # Ignore multi-indexes as well. + arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], + ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']] + df = pd.DataFrame({'one': [i for i in range(8)], + 'two': [-i for i in range(8)]}, index=arrays) + + expected = df.reset_index(drop=True) + check_round_trip(df, engine, write_kwargs=write_kwargs, + expected=expected) + + +class TestParquetPyArrow(Base): + + def test_basic(self, pa, df_full): + + df = df_full + + # additional supported types for pyarrow + df['datetime_tz'] = pd.date_range('20130101', periods=3, + tz='Europe/Brussels') + df['bool_with_none'] = [True, None, True] + + check_round_trip(df, pa) + + # TODO: This doesn't fail on all systems; track down which + @pytest.mark.xfail(reason="pyarrow fails on this (ARROW-1883)", + strict=False) + def test_basic_subset_columns(self, pa, df_full): + # GH18628 + + df = df_full + # additional supported types for pyarrow + df['datetime_tz'] = pd.date_range('20130101', periods=3, + tz='Europe/Brussels') + + check_round_trip(df, pa, expected=df[['string', 'int']], + read_kwargs={'columns': ['string', 'int']}) + + def test_duplicate_columns(self, pa): + # not currently able to handle duplicate columns + df = pd.DataFrame(np.arange(12).reshape(4, 3), + columns=list('aaa')).copy() + self.check_error_on_write(df, pa, ValueError) + + def test_unsupported(self, pa): + # period + df = pd.DataFrame({'a': pd.period_range('2013', freq='M', periods=3)}) + # pyarrow 0.11 raises ArrowTypeError + # older pyarrows raise ArrowInvalid + self.check_error_on_write(df, pa, Exception) + + # timedelta + df = pd.DataFrame({'a': pd.timedelta_range('1 day', + periods=3)}) + self.check_error_on_write(df, pa, NotImplementedError) + + # mixed python objects + df = pd.DataFrame({'a': ['a', 1, 2.0]}) + # pyarrow 0.11 raises ArrowTypeError + # older pyarrows raise ArrowInvalid + self.check_error_on_write(df, pa, Exception) + + def test_categorical(self, pa): + + # supported in >= 0.7.0 + df = pd.DataFrame({'a': pd.Categorical(list('abc'))}) + + # de-serialized as object + expected = df.assign(a=df.a.astype(object)) + check_round_trip(df, pa, expected=expected) + + def test_s3_roundtrip(self, df_compat, s3_resource, pa): + # GH #19134 + check_round_trip(df_compat, pa, + path='s3://pandas-test/pyarrow.parquet') + + def test_partition_cols_supported(self, pa, df_full): + # GH #23283 + partition_cols = ['bool', 'int'] + df = df_full + with tm.ensure_clean_dir() as path: + df.to_parquet(path, partition_cols=partition_cols, + compression=None) + import pyarrow.parquet as pq + dataset = pq.ParquetDataset(path, validate_schema=False) + assert len(dataset.partitions.partition_names) == 2 + assert dataset.partitions.partition_names == set(partition_cols) + + +class TestParquetFastParquet(Base): + + @td.skip_if_no('fastparquet', min_version="0.2.1") + def test_basic(self, fp, df_full): + df = df_full + + # additional supported types for fastparquet + if LooseVersion(fastparquet.__version__) >= LooseVersion('0.1.4'): + df['datetime_tz'] = pd.date_range('20130101', periods=3, + tz='US/Eastern') + df['timedelta'] = pd.timedelta_range('1 day', periods=3) + check_round_trip(df, fp) + + @pytest.mark.skip(reason="not supported") + def test_duplicate_columns(self, fp): + + # not currently able to handle duplicate columns + df = pd.DataFrame(np.arange(12).reshape(4, 3), + columns=list('aaa')).copy() + self.check_error_on_write(df, fp, ValueError) + + def test_bool_with_none(self, fp): + df = pd.DataFrame({'a': [True, None, False]}) + expected = pd.DataFrame({'a': [1.0, np.nan, 0.0]}, dtype='float16') + check_round_trip(df, fp, expected=expected) + + def test_unsupported(self, fp): + + # period + df = pd.DataFrame({'a': pd.period_range('2013', freq='M', periods=3)}) + self.check_error_on_write(df, fp, ValueError) + + # mixed + df = pd.DataFrame({'a': ['a', 1, 2.0]}) + self.check_error_on_write(df, fp, ValueError) + + def test_categorical(self, fp): + if LooseVersion(fastparquet.__version__) < LooseVersion("0.1.3"): + pytest.skip("CategoricalDtype not supported for older fp") + df = pd.DataFrame({'a': pd.Categorical(list('abc'))}) + check_round_trip(df, fp) + + def test_filter_row_groups(self, fp): + d = {'a': list(range(0, 3))} + df = pd.DataFrame(d) + with tm.ensure_clean() as path: + df.to_parquet(path, fp, compression=None, + row_group_offsets=1) + result = read_parquet(path, fp, filters=[('a', '==', 0)]) + assert len(result) == 1 + + def test_s3_roundtrip(self, df_compat, s3_resource, fp): + # GH #19134 + check_round_trip(df_compat, fp, + path='s3://pandas-test/fastparquet.parquet') + + def test_partition_cols_supported(self, fp, df_full): + # GH #23283 + partition_cols = ['bool', 'int'] + df = df_full + with tm.ensure_clean_dir() as path: + df.to_parquet(path, engine="fastparquet", + partition_cols=partition_cols, compression=None) + assert os.path.exists(path) + import fastparquet + actual_partition_cols = fastparquet.ParquetFile(path, False).cats + assert len(actual_partition_cols) == 2 + + def test_partition_on_supported(self, fp, df_full): + # GH #23283 + partition_cols = ['bool', 'int'] + df = df_full + with tm.ensure_clean_dir() as path: + df.to_parquet(path, engine="fastparquet", compression=None, + partition_on=partition_cols) + assert os.path.exists(path) + import fastparquet + actual_partition_cols = fastparquet.ParquetFile(path, False).cats + assert len(actual_partition_cols) == 2 + + def test_error_on_using_partition_cols_and_partition_on(self, fp, df_full): + # GH #23283 + partition_cols = ['bool', 'int'] + df = df_full + with pytest.raises(ValueError): + with tm.ensure_clean_dir() as path: + df.to_parquet(path, engine="fastparquet", compression=None, + partition_on=partition_cols, + partition_cols=partition_cols) diff --git a/pandas/tests/io/test_pickle.py b/pandas/tests/io/test_pickle.py index f46f62e781006..b4befadaddc42 100644 --- a/pandas/tests/io/test_pickle.py +++ b/pandas/tests/io/test_pickle.py @@ -12,17 +12,22 @@ 3. Move the created pickle to "data/legacy_pickle/" directory. """ +from distutils.version import LooseVersion +import glob +import os +import shutil +from warnings import catch_warnings, simplefilter import pytest -import os -from distutils.version import LooseVersion + +from pandas.compat import PY3, is_platform_little_endian +import pandas.util._test_decorators as td + import pandas as pd from pandas import Index -from pandas.compat import is_platform_little_endian -import pandas import pandas.util.testing as tm + from pandas.tseries.offsets import Day, MonthEnd -import shutil @pytest.fixture(scope='module') @@ -34,7 +39,7 @@ def current_pickle_data(): # --------------------- -# comparision functions +# comparison functions # --------------------- def compare_element(result, expected, typ, version=None): if isinstance(expected, Index): @@ -48,8 +53,8 @@ def compare_element(result, expected, typ, version=None): if expected is pd.NaT: assert result is pd.NaT else: - tm.assert_equal(result, expected) - tm.assert_equal(result.freq, expected.freq) + assert result == expected + assert result.freq == expected.freq else: comparator = getattr(tm, "assert_%s_equal" % typ, tm.assert_almost_equal) @@ -60,7 +65,7 @@ def compare(data, vf, version): # py3 compat when reading py2 pickle try: - data = pandas.read_pickle(vf) + data = pd.read_pickle(vf) except (ValueError) as e: if 'unsupported pickle protocol:' in str(e): # trying to read a py3 pickle in py2 @@ -70,6 +75,10 @@ def compare(data, vf, version): m = globals() for typ, dv in data.items(): + if typ == "panel": + # FIXME: kludge; get this key out of the legacy file + continue + for dt, result in dv.items(): try: expected = data[typ][dt] @@ -91,7 +100,7 @@ def compare(data, vf, version): def compare_sp_series_ts(res, exp, typ, version): # SparseTimeSeries integrated into SparseSeries in 0.12.0 # and deprecated in 0.17.0 - if version and LooseVersion(version) <= "0.12.0": + if version and LooseVersion(version) <= LooseVersion("0.12.0"): tm.assert_sp_series_equal(res, exp, check_series_type=False) else: tm.assert_sp_series_equal(res, exp) @@ -100,27 +109,27 @@ def compare_sp_series_ts(res, exp, typ, version): def compare_series_ts(result, expected, typ, version): # GH 7748 tm.assert_series_equal(result, expected) - tm.assert_equal(result.index.freq, expected.index.freq) - tm.assert_equal(result.index.freq.normalize, False) + assert result.index.freq == expected.index.freq + assert not result.index.freq.normalize tm.assert_series_equal(result > 0, expected > 0) # GH 9291 freq = result.index.freq - tm.assert_equal(freq + Day(1), Day(2)) + assert freq + Day(1) == Day(2) - res = freq + pandas.Timedelta(hours=1) - tm.assert_equal(isinstance(res, pandas.Timedelta), True) - tm.assert_equal(res, pandas.Timedelta(days=1, hours=1)) + res = freq + pd.Timedelta(hours=1) + assert isinstance(res, pd.Timedelta) + assert res == pd.Timedelta(days=1, hours=1) - res = freq + pandas.Timedelta(nanoseconds=1) - tm.assert_equal(isinstance(res, pandas.Timedelta), True) - tm.assert_equal(res, pandas.Timedelta(days=1, nanoseconds=1)) + res = freq + pd.Timedelta(nanoseconds=1) + assert isinstance(res, pd.Timedelta) + assert res == pd.Timedelta(days=1, nanoseconds=1) def compare_series_dt_tz(result, expected, typ, version): # 8260 # dtype is object < 0.17.0 - if LooseVersion(version) < '0.17.0': + if LooseVersion(version) < LooseVersion('0.17.0'): expected = expected.astype(object) tm.assert_series_equal(result, expected) else: @@ -130,10 +139,10 @@ def compare_series_dt_tz(result, expected, typ, version): def compare_series_cat(result, expected, typ, version): # Categorical dtype is added in 0.15.0 # ordered is changed in 0.16.0 - if LooseVersion(version) < '0.15.0': + if LooseVersion(version) < LooseVersion('0.15.0'): tm.assert_series_equal(result, expected, check_dtype=False, check_categorical=False) - elif LooseVersion(version) < '0.16.0': + elif LooseVersion(version) < LooseVersion('0.16.0'): tm.assert_series_equal(result, expected, check_categorical=False) else: tm.assert_series_equal(result, expected) @@ -142,7 +151,7 @@ def compare_series_cat(result, expected, typ, version): def compare_frame_dt_mixed_tzs(result, expected, typ, version): # 8260 # dtype is object < 0.17.0 - if LooseVersion(version) < '0.17.0': + if LooseVersion(version) < LooseVersion('0.17.0'): expected = expected.astype(object) tm.assert_frame_equal(result, expected) else: @@ -152,10 +161,10 @@ def compare_frame_dt_mixed_tzs(result, expected, typ, version): def compare_frame_cat_onecol(result, expected, typ, version): # Categorical dtype is added in 0.15.0 # ordered is changed in 0.16.0 - if LooseVersion(version) < '0.15.0': + if LooseVersion(version) < LooseVersion('0.15.0'): tm.assert_frame_equal(result, expected, check_dtype=False, check_categorical=False) - elif LooseVersion(version) < '0.16.0': + elif LooseVersion(version) < LooseVersion('0.16.0'): tm.assert_frame_equal(result, expected, check_categorical=False) else: tm.assert_frame_equal(result, expected) @@ -167,48 +176,40 @@ def compare_frame_cat_and_float(result, expected, typ, version): def compare_index_period(result, expected, typ, version): tm.assert_index_equal(result, expected) - tm.assertIsInstance(result.freq, MonthEnd) - tm.assert_equal(result.freq, MonthEnd()) - tm.assert_equal(result.freqstr, 'M') + assert isinstance(result.freq, MonthEnd) + assert result.freq == MonthEnd() + assert result.freqstr == 'M' tm.assert_index_equal(result.shift(2), expected.shift(2)) def compare_sp_frame_float(result, expected, typ, version): - if LooseVersion(version) <= '0.18.1': + if LooseVersion(version) <= LooseVersion('0.18.1'): tm.assert_sp_frame_equal(result, expected, exact_indices=False, check_dtype=False) else: tm.assert_sp_frame_equal(result, expected) +files = glob.glob(os.path.join(os.path.dirname(__file__), "data", + "legacy_pickle", "*", "*.pickle")) + + +@pytest.fixture(params=files) +def legacy_pickle(request, datapath): + return datapath(request.param) + + # --------------------- # tests # --------------------- -def legacy_pickle_versions(): - # yield the pickle versions - path = tm.get_data_path('legacy_pickle') - for v in os.listdir(path): - p = os.path.join(path, v) - if os.path.isdir(p): - yield v - - -@pytest.mark.parametrize('version', legacy_pickle_versions()) -def test_pickles(current_pickle_data, version): +def test_pickles(current_pickle_data, legacy_pickle): if not is_platform_little_endian(): pytest.skip("known failure on non-little endian") - pth = tm.get_data_path('legacy_pickle/{0}'.format(version)) - n = 0 - for f in os.listdir(pth): - vf = os.path.join(pth, f) - data = compare(current_pickle_data, vf, version) - - if data is None: - continue - n += 1 - assert n > 0, ('Pickle files are not ' - 'tested: {version}'.format(version=version)) + version = os.path.basename(os.path.dirname(legacy_pickle)) + with catch_warnings(record=True): + simplefilter("ignore") + compare(current_pickle_data, legacy_pickle, version) def test_round_trip_current(current_pickle_data): @@ -224,7 +225,7 @@ def c_unpickler(path): with open(path, 'rb') as fh: fh.seek(0) return c_pickle.load(fh) - except: + except ImportError: c_pickler = None c_unpickler = None @@ -264,12 +265,11 @@ def python_unpickler(path): compare_element(result, expected, typ) -def test_pickle_v0_14_1(): +def test_pickle_v0_14_1(datapath): cat = pd.Categorical(values=['a', 'b', 'c'], ordered=False, categories=['a', 'b', 'c', 'd']) - pickle_path = os.path.join(tm.get_data_path(), - 'categorical_0_14_1.pickle') + pickle_path = datapath('io', 'data', 'categorical_0_14_1.pickle') # This code was executed once on v0.14.1 to generate the pickle: # # cat = Categorical(labels=np.arange(3), levels=['a', 'b', 'c', 'd'], @@ -279,14 +279,13 @@ def test_pickle_v0_14_1(): tm.assert_categorical_equal(cat, pd.read_pickle(pickle_path)) -def test_pickle_v0_15_2(): +def test_pickle_v0_15_2(datapath): # ordered -> _ordered # GH 9347 cat = pd.Categorical(values=['a', 'b', 'c'], ordered=False, categories=['a', 'b', 'c', 'd']) - pickle_path = os.path.join(tm.get_data_path(), - 'categorical_0_15_2.pickle') + pickle_path = datapath('io', 'data', 'categorical_0_15_2.pickle') # This code was executed once on v0.15.2 to generate the pickle: # # cat = Categorical(labels=np.arange(3), levels=['a', 'b', 'c', 'd'], @@ -296,6 +295,18 @@ def test_pickle_v0_15_2(): tm.assert_categorical_equal(cat, pd.read_pickle(pickle_path)) +def test_pickle_path_pathlib(): + df = tm.makeDataFrame() + result = tm.round_trip_pathlib(df.to_pickle, pd.read_pickle) + tm.assert_frame_equal(df, result) + + +def test_pickle_path_localpath(): + df = tm.makeDataFrame() + result = tm.round_trip_localpath(df.to_pickle, pd.read_pickle) + tm.assert_frame_equal(df, result) + + # --------------------- # test pickle compression # --------------------- @@ -328,56 +339,21 @@ def compress_file(self, src_path, dest_path, compression): f = bz2.BZ2File(dest_path, "w") elif compression == 'zip': import zipfile - zip_file = zipfile.ZipFile(dest_path, "w", - compression=zipfile.ZIP_DEFLATED) - zip_file.write(src_path, os.path.basename(src_path)) + with zipfile.ZipFile(dest_path, "w", + compression=zipfile.ZIP_DEFLATED) as f: + f.write(src_path, os.path.basename(src_path)) elif compression == 'xz': - lzma = pandas.compat.import_lzma() + lzma = pd.compat.import_lzma() f = lzma.LZMAFile(dest_path, "w") else: msg = 'Unrecognized compression type: {}'.format(compression) raise ValueError(msg) if compression != "zip": - f.write(open(src_path, "rb").read()) - f.close() + with open(src_path, "rb") as fh, f: + f.write(fh.read()) - def decompress_file(self, src_path, dest_path, compression): - if compression is None: - shutil.copyfile(src_path, dest_path) - return - - if compression == 'gzip': - import gzip - f = gzip.open(src_path, "r") - elif compression == 'bz2': - import bz2 - f = bz2.BZ2File(src_path, "r") - elif compression == 'zip': - import zipfile - zip_file = zipfile.ZipFile(src_path) - zip_names = zip_file.namelist() - if len(zip_names) == 1: - f = zip_file.open(zip_names.pop()) - else: - raise ValueError('ZIP file {} error. Only one file per ZIP.' - .format(src_path)) - elif compression == 'xz': - lzma = pandas.compat.import_lzma() - f = lzma.LZMAFile(src_path, "r") - else: - msg = 'Unrecognized compression type: {}'.format(compression) - raise ValueError(msg) - - open(dest_path, "wb").write(f.read()) - f.close() - - @pytest.mark.parametrize('compression', [None, 'gzip', 'bz2', 'xz']) def test_write_explicit(self, compression, get_random_path): - # issue 11666 - if compression == 'xz': - tm._skip_if_no_lzma() - base = get_random_path path1 = base + ".compressed" path2 = base + ".raw" @@ -389,7 +365,9 @@ def test_write_explicit(self, compression, get_random_path): df.to_pickle(p1, compression=compression) # decompress - self.decompress_file(p1, p2, compression=compression) + with tm.decompress_file(p1, compression=compression) as f: + with open(p2, "wb") as fh: + fh.write(f.read()) # read decompressed file df2 = pd.read_pickle(p2, compression=None) @@ -398,17 +376,16 @@ def test_write_explicit(self, compression, get_random_path): @pytest.mark.parametrize('compression', ['', 'None', 'bad', '7z']) def test_write_explicit_bad(self, compression, get_random_path): - with tm.assertRaisesRegexp(ValueError, - "Unrecognized compression type"): + with pytest.raises(ValueError, match="Unrecognized compression type"): with tm.ensure_clean(get_random_path) as path: df = tm.makeDataFrame() df.to_pickle(path, compression=compression) - @pytest.mark.parametrize('ext', ['', '.gz', '.bz2', '.xz', '.no_compress']) + @pytest.mark.parametrize('ext', [ + '', '.gz', '.bz2', '.no_compress', + pytest.param('.xz', marks=td.skip_if_no_lzma) + ]) def test_write_infer(self, ext, get_random_path): - if ext == '.xz': - tm._skip_if_no_lzma() - base = get_random_path path1 = base + ext path2 = base + ".raw" @@ -425,19 +402,16 @@ def test_write_infer(self, ext, get_random_path): df.to_pickle(p1) # decompress - self.decompress_file(p1, p2, compression=compression) + with tm.decompress_file(p1, compression=compression) as f: + with open(p2, "wb") as fh: + fh.write(f.read()) # read decompressed file df2 = pd.read_pickle(p2, compression=None) tm.assert_frame_equal(df, df2) - @pytest.mark.parametrize('compression', [None, 'gzip', 'bz2', 'xz', "zip"]) def test_read_explicit(self, compression, get_random_path): - # issue 11666 - if compression == 'xz': - tm._skip_if_no_lzma() - base = get_random_path path1 = base + ".raw" path2 = base + ".compressed" @@ -456,12 +430,11 @@ def test_read_explicit(self, compression, get_random_path): tm.assert_frame_equal(df, df2) - @pytest.mark.parametrize('ext', ['', '.gz', '.bz2', '.xz', '.zip', - '.no_compress']) + @pytest.mark.parametrize('ext', [ + '', '.gz', '.bz2', '.zip', '.no_compress', + pytest.param('.xz', marks=td.skip_if_no_lzma) + ]) def test_read_infer(self, ext, get_random_path): - if ext == '.xz': - tm._skip_if_no_lzma() - base = get_random_path path1 = base + ".raw" path2 = base + ext @@ -484,3 +457,29 @@ def test_read_infer(self, ext, get_random_path): df2 = pd.read_pickle(p2) tm.assert_frame_equal(df, df2) + + +# --------------------- +# test pickle compression +# --------------------- + +class TestProtocol(object): + + @pytest.mark.parametrize('protocol', [-1, 0, 1, 2]) + def test_read(self, protocol, get_random_path): + with tm.ensure_clean(get_random_path) as path: + df = tm.makeDataFrame() + df.to_pickle(path, protocol=protocol) + df2 = pd.read_pickle(path) + tm.assert_frame_equal(df, df2) + + @pytest.mark.parametrize('protocol', [3, 4]) + @pytest.mark.skipif(PY3, reason="Testing invalid parameters for Python 2") + def test_read_bad_versions(self, protocol, get_random_path): + # For Python 2, HIGHEST_PROTOCOL should be 2. + msg = ("pickle protocol {protocol} asked for; the highest available " + "protocol is 2").format(protocol=protocol) + with pytest.raises(ValueError, match=msg): + with tm.ensure_clean(get_random_path) as path: + df = tm.makeDataFrame() + df.to_pickle(path, protocol=protocol) diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py index 82a98f5d08488..69ff32d1b728b 100644 --- a/pandas/tests/io/test_pytables.py +++ b/pandas/tests/io/test_pytables.py @@ -1,50 +1,55 @@ -import pytest -import sys -import os -from warnings import catch_warnings -import tempfile from contextlib import contextmanager - import datetime from datetime import timedelta +from distutils.version import LooseVersion +import os +import tempfile +from warnings import catch_warnings, simplefilter + import numpy as np +import pytest + +from pandas.compat import ( + PY35, PY36, BytesIO, is_platform_little_endian, is_platform_windows, + lrange, range, text_type, u) +import pandas.util._test_decorators as td + +from pandas.core.dtypes.common import is_categorical_dtype -import pandas import pandas as pd -from pandas import (Series, DataFrame, Panel, MultiIndex, Int64Index, - RangeIndex, Categorical, bdate_range, - date_range, timedelta_range, Index, DatetimeIndex, - isnull) +from pandas import ( + Categorical, DataFrame, DatetimeIndex, Index, Int64Index, MultiIndex, + RangeIndex, Series, Timestamp, bdate_range, compat, concat, date_range, + isna, timedelta_range) +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_frame_equal, assert_series_equal, set_timezone) -from pandas.compat import is_platform_windows, PY3, PY35 -from pandas.formats.printing import pprint_thing +from pandas.io import pytables as pytables # noqa:E402 +from pandas.io.formats.printing import pprint_thing +from pandas.io.pytables import ( + ClosedFileError, HDFStore, PossibleDataLossError, Term, read_hdf) +from pandas.io.pytables import TableIterator # noqa:E402 tables = pytest.importorskip('tables') -from pandas.io.pytables import TableIterator -from pandas.io.pytables import (HDFStore, get_store, Term, read_hdf, - IncompatibilityWarning, PerformanceWarning, - AttributeConflictWarning, - PossibleDataLossError, ClosedFileError) -from pandas.io import pytables as pytables -import pandas.util.testing as tm -from pandas.util.testing import (assert_panel4d_equal, - assert_panel_equal, - assert_frame_equal, - assert_series_equal, - set_timezone) -from pandas import concat, Timestamp -from pandas import compat -from pandas.compat import range, lrange, u -from distutils.version import LooseVersion -_default_compressor = ('blosc' if LooseVersion(tables.__version__) >= '2.2' - else 'zlib') +# TODO: +# remove when gh-24839 is fixed; this affects numpy 1.16 +# and pytables 3.4.4 +xfail_non_writeable = pytest.mark.xfail( + LooseVersion(np.__version__) >= LooseVersion('1.16'), + reason=('gh-25511, gh-24839. pytables needs a ' + 'release beyong 3.4.4 to support numpy 1.16x')) + +_default_compressor = ('blosc' if LooseVersion(tables.__version__) >= + LooseVersion('2.2') else 'zlib') -# testing on windows/py3 seems to fault -# for using compression -skip_compression = PY3 and is_platform_windows() + +ignore_natural_naming_warning = pytest.mark.filterwarnings( + "ignore:object name:tables.exceptions.NaturalNameWarning" +) # contextmanager to ensure the file cleanup @@ -53,7 +58,7 @@ def safe_remove(path): if path is not None: try: os.remove(path) - except: + except OSError: pass @@ -61,7 +66,7 @@ def safe_close(store): try: if store is not None: store.close() - except: + except IOError: pass @@ -119,55 +124,38 @@ def _maybe_remove(store, key): no content from previous tests using the same table name.""" try: store.remove(key) - except: + except (ValueError, KeyError): pass -class Base(tm.TestCase): +class Base(object): @classmethod - def setUpClass(cls): - super(Base, cls).setUpClass() + def setup_class(cls): # Pytables 3.0.0 deprecates lots of things tm.reset_testing_mode() @classmethod - def tearDownClass(cls): - super(Base, cls).tearDownClass() + def teardown_class(cls): # Pytables 3.0.0 deprecates lots of things tm.set_testing_mode() - def setUp(self): + def setup_method(self, method): self.path = 'tmp.__%s__.h5' % tm.rands(10) - def tearDown(self): + def teardown_method(self, method): pass @pytest.mark.single -class TestHDFStore(Base, tm.TestCase): - - def test_factory_fun(self): - path = create_tempfile(self.path) - try: - with get_store(path) as tbl: - raise ValueError('blah') - except ValueError: - pass - finally: - safe_remove(path) - - try: - with get_store(path) as tbl: - tbl['a'] = tm.makeDataFrame() +class TestHDFStore(Base): - with get_store(path) as tbl: - self.assertEqual(len(tbl), 1) - self.assertEqual(type(tbl['a']), DataFrame) - finally: - safe_remove(self.path) + def test_format_kwarg_in_constructor(self): + # GH 13291 + with ensure_clean_path(self.path) as path: + pytest.raises(ValueError, HDFStore, path, format='table') def test_context(self): path = create_tempfile(self.path) @@ -184,8 +172,8 @@ def test_context(self): tbl['a'] = tm.makeDataFrame() with HDFStore(path) as tbl: - self.assertEqual(len(tbl), 1) - self.assertEqual(type(tbl['a']), DataFrame) + assert len(tbl) == 1 + assert type(tbl['a']) == DataFrame finally: safe_remove(path) @@ -205,9 +193,6 @@ def roundtrip(key, obj, **kwargs): o = tm.makeDataFrame() assert_frame_equal(o, roundtrip('frame', o)) - o = tm.makePanel() - assert_panel_equal(o, roundtrip('panel', o)) - # table df = DataFrame(dict(A=lrange(5), B=lrange(5))) df.to_hdf(path, 'table', append=True) @@ -220,8 +205,6 @@ def roundtrip(key, obj, **kwargs): def test_long_strings(self): # GH6166 - # unconversion of long strings was being chopped in earlier - # versions of numpy < 1.7.2 df = DataFrame({'a': tm.rands_array(100, size=10)}, index=tm.rands_array(100, size=10)) @@ -305,20 +288,20 @@ def test_api(self): # invalid df = tm.makeDataFrame() - self.assertRaises(ValueError, df.to_hdf, path, - 'df', append=True, format='f') - self.assertRaises(ValueError, df.to_hdf, path, - 'df', append=True, format='fixed') + pytest.raises(ValueError, df.to_hdf, path, + 'df', append=True, format='f') + pytest.raises(ValueError, df.to_hdf, path, + 'df', append=True, format='fixed') - self.assertRaises(TypeError, df.to_hdf, path, - 'df', append=True, format='foo') - self.assertRaises(TypeError, df.to_hdf, path, - 'df', append=False, format='bar') + pytest.raises(TypeError, df.to_hdf, path, + 'df', append=True, format='foo') + pytest.raises(TypeError, df.to_hdf, path, + 'df', append=False, format='bar') # File path doesn't exist path = "" - self.assertRaises(compat.FileNotFoundError, - read_hdf, path, 'df') + pytest.raises(compat.FileNotFoundError, + read_hdf, path, 'df') def test_api_default_format(self): @@ -326,41 +309,41 @@ def test_api_default_format(self): with ensure_clean_store(self.path) as store: df = tm.makeDataFrame() - pandas.set_option('io.hdf.default_format', 'fixed') + pd.set_option('io.hdf.default_format', 'fixed') _maybe_remove(store, 'df') store.put('df', df) - self.assertFalse(store.get_storer('df').is_table) - self.assertRaises(ValueError, store.append, 'df2', df) + assert not store.get_storer('df').is_table + pytest.raises(ValueError, store.append, 'df2', df) - pandas.set_option('io.hdf.default_format', 'table') + pd.set_option('io.hdf.default_format', 'table') _maybe_remove(store, 'df') store.put('df', df) - self.assertTrue(store.get_storer('df').is_table) + assert store.get_storer('df').is_table _maybe_remove(store, 'df2') store.append('df2', df) - self.assertTrue(store.get_storer('df').is_table) + assert store.get_storer('df').is_table - pandas.set_option('io.hdf.default_format', None) + pd.set_option('io.hdf.default_format', None) with ensure_clean_path(self.path) as path: df = tm.makeDataFrame() - pandas.set_option('io.hdf.default_format', 'fixed') + pd.set_option('io.hdf.default_format', 'fixed') df.to_hdf(path, 'df') - with get_store(path) as store: - self.assertFalse(store.get_storer('df').is_table) - self.assertRaises(ValueError, df.to_hdf, path, 'df2', append=True) + with HDFStore(path) as store: + assert not store.get_storer('df').is_table + pytest.raises(ValueError, df.to_hdf, path, 'df2', append=True) - pandas.set_option('io.hdf.default_format', 'table') + pd.set_option('io.hdf.default_format', 'table') df.to_hdf(path, 'df3') with HDFStore(path) as store: - self.assertTrue(store.get_storer('df3').is_table) + assert store.get_storer('df3').is_table df.to_hdf(path, 'df4', append=True) with HDFStore(path) as store: - self.assertTrue(store.get_storer('df4').is_table) + assert store.get_storer('df4').is_table - pandas.set_option('io.hdf.default_format', None) + pd.set_option('io.hdf.default_format', None) def test_keys(self): @@ -368,29 +351,43 @@ def test_keys(self): store['a'] = tm.makeTimeSeries() store['b'] = tm.makeStringSeries() store['c'] = tm.makeDataFrame() - store['d'] = tm.makePanel() - store['foo/bar'] = tm.makePanel() - self.assertEqual(len(store), 5) - expected = set(['/a', '/b', '/c', '/d', '/foo/bar']) - self.assertTrue(set(store.keys()) == expected) - self.assertTrue(set(store) == expected) + + assert len(store) == 3 + expected = {'/a', '/b', '/c'} + assert set(store.keys()) == expected + assert set(store) == expected + + def test_keys_ignore_hdf_softlink(self): + + # GH 20523 + # Puts a softlink into HDF file and rereads + + with ensure_clean_store(self.path) as store: + + df = DataFrame(dict(A=lrange(5), B=lrange(5))) + store.put("df", df) + + assert store.keys() == ["/df"] + + store._handle.create_soft_link(store._handle.root, "symlink", "df") + + # Should ignore the softlink + assert store.keys() == ["/df"] def test_iter_empty(self): with ensure_clean_store(self.path) as store: # GH 12221 - self.assertTrue(list(store) == []) + assert list(store) == [] def test_repr(self): with ensure_clean_store(self.path) as store: repr(store) + store.info() store['a'] = tm.makeTimeSeries() store['b'] = tm.makeStringSeries() store['c'] = tm.makeDataFrame() - store['d'] = tm.makePanel() - store['foo/bar'] = tm.makePanel() - store.append('e', tm.makePanel()) df = tm.makeDataFrame() df['obj1'] = 'foo' @@ -407,15 +404,16 @@ def test_repr(self): df.loc[3:6, ['obj1']] = np.nan df = df._consolidate()._convert(datetime=True) - # PerformanceWarning with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) store['df'] = df # make a random group in hdf space store._handle.create_group(store._handle.root, 'bah') - repr(store) - str(store) + assert store.filename in repr(store) + assert store.filename in str(store) + store.info() # storers with ensure_clean_store(self.path) as store: @@ -427,25 +425,25 @@ def test_repr(self): repr(s) str(s) + @ignore_natural_naming_warning def test_contains(self): with ensure_clean_store(self.path) as store: store['a'] = tm.makeTimeSeries() store['b'] = tm.makeDataFrame() store['foo/bar'] = tm.makeDataFrame() - self.assertIn('a', store) - self.assertIn('b', store) - self.assertNotIn('c', store) - self.assertIn('foo/bar', store) - self.assertIn('/foo/bar', store) - self.assertNotIn('/foo/b', store) - self.assertNotIn('bar', store) - - # GH 2694 - # tables.NaturalNameWarning + assert 'a' in store + assert 'b' in store + assert 'c' not in store + assert 'foo/bar' in store + assert '/foo/bar' in store + assert '/foo/b' not in store + assert 'bar' not in store + + # gh-2694: tables.NaturalNameWarning with catch_warnings(record=True): store['node())'] = tm.makeDataFrame() - self.assertIn('node())', store) + assert 'node())' in store def test_versioning(self): @@ -456,9 +454,9 @@ def test_versioning(self): _maybe_remove(store, 'df1') store.append('df1', df[:10]) store.append('df1', df[10:]) - self.assertEqual(store.root.a._v_attrs.pandas_version, '0.15.2') - self.assertEqual(store.root.b._v_attrs.pandas_version, '0.15.2') - self.assertEqual(store.root.df1._v_attrs.pandas_version, '0.15.2') + assert store.root.a._v_attrs.pandas_version == '0.15.2' + assert store.root.b._v_attrs.pandas_version == '0.15.2' + assert store.root.df1._v_attrs.pandas_version == '0.15.2' # write a file and wipe its versioning _maybe_remove(store, 'df2') @@ -467,7 +465,7 @@ def test_versioning(self): # this is an error because its table_type is appendable, but no # version info store.get_node('df2')._v_attrs.pandas_version = None - self.assertRaises(Exception, store.select, 'df2') + pytest.raises(Exception, store.select, 'df2') def test_mode(self): @@ -479,11 +477,11 @@ def check(mode): # constructor if mode in ['r', 'r+']: - self.assertRaises(IOError, HDFStore, path, mode=mode) + pytest.raises(IOError, HDFStore, path, mode=mode) else: store = HDFStore(path, mode=mode) - self.assertEqual(store._handle.mode, mode) + assert store._handle.mode == mode store.close() with ensure_clean_path(self.path) as path: @@ -493,25 +491,25 @@ def check(mode): def f(): with HDFStore(path, mode=mode) as store: # noqa pass - self.assertRaises(IOError, f) + pytest.raises(IOError, f) else: with HDFStore(path, mode=mode) as store: - self.assertEqual(store._handle.mode, mode) + assert store._handle.mode == mode with ensure_clean_path(self.path) as path: # conv write if mode in ['r', 'r+']: - self.assertRaises(IOError, df.to_hdf, - path, 'df', mode=mode) + pytest.raises(IOError, df.to_hdf, + path, 'df', mode=mode) df.to_hdf(path, 'df', mode='w') else: df.to_hdf(path, 'df', mode=mode) # conv read if mode in ['w']: - self.assertRaises(ValueError, read_hdf, - path, 'df', mode=mode) + pytest.raises(ValueError, read_hdf, + path, 'df', mode=mode) else: result = read_hdf(path, 'df', mode=mode) assert_frame_equal(result, df) @@ -538,43 +536,43 @@ def test_reopen_handle(self): store['a'] = tm.makeTimeSeries() # invalid mode change - self.assertRaises(PossibleDataLossError, store.open, 'w') + pytest.raises(PossibleDataLossError, store.open, 'w') store.close() - self.assertFalse(store.is_open) + assert not store.is_open # truncation ok here store.open('w') - self.assertTrue(store.is_open) - self.assertEqual(len(store), 0) + assert store.is_open + assert len(store) == 0 store.close() - self.assertFalse(store.is_open) + assert not store.is_open store = HDFStore(path, mode='a') store['a'] = tm.makeTimeSeries() # reopen as read store.open('r') - self.assertTrue(store.is_open) - self.assertEqual(len(store), 1) - self.assertEqual(store._mode, 'r') + assert store.is_open + assert len(store) == 1 + assert store._mode == 'r' store.close() - self.assertFalse(store.is_open) + assert not store.is_open # reopen as append store.open('a') - self.assertTrue(store.is_open) - self.assertEqual(len(store), 1) - self.assertEqual(store._mode, 'a') + assert store.is_open + assert len(store) == 1 + assert store._mode == 'a' store.close() - self.assertFalse(store.is_open) + assert not store.is_open # reopen as append (again) store.open('a') - self.assertTrue(store.is_open) - self.assertEqual(len(store), 1) - self.assertEqual(store._mode, 'a') + assert store.is_open + assert len(store) == 1 + assert store._mode == 'a' store.close() - self.assertFalse(store.is_open) + assert not store.is_open def test_open_args(self): @@ -594,7 +592,7 @@ def test_open_args(self): store.close() # the file should not have actually been written - self.assertFalse(os.path.exists(path)) + assert not os.path.exists(path) def test_flush(self): @@ -615,7 +613,58 @@ def test_get(self): right = store['/a'] tm.assert_series_equal(left, right) - self.assertRaises(KeyError, store.get, 'b') + pytest.raises(KeyError, store.get, 'b') + + @pytest.mark.parametrize('where, expected', [ + ('/', { + '': ({'first_group', 'second_group'}, set()), + '/first_group': (set(), {'df1', 'df2'}), + '/second_group': ({'third_group'}, {'df3', 's1'}), + '/second_group/third_group': (set(), {'df4'}), + }), + ('/second_group', { + '/second_group': ({'third_group'}, {'df3', 's1'}), + '/second_group/third_group': (set(), {'df4'}), + }) + ]) + def test_walk(self, where, expected): + # GH10143 + objs = { + 'df1': pd.DataFrame([1, 2, 3]), + 'df2': pd.DataFrame([4, 5, 6]), + 'df3': pd.DataFrame([6, 7, 8]), + 'df4': pd.DataFrame([9, 10, 11]), + 's1': pd.Series([10, 9, 8]), + # Next 3 items aren't pandas objects and should be ignored + 'a1': np.array([[1, 2, 3], [4, 5, 6]]), + 'tb1': np.array([(1, 2, 3), (4, 5, 6)], dtype='i,i,i'), + 'tb2': np.array([(7, 8, 9), (10, 11, 12)], dtype='i,i,i') + } + + with ensure_clean_store('walk_groups.hdf', mode='w') as store: + store.put('/first_group/df1', objs['df1']) + store.put('/first_group/df2', objs['df2']) + store.put('/second_group/df3', objs['df3']) + store.put('/second_group/s1', objs['s1']) + store.put('/second_group/third_group/df4', objs['df4']) + # Create non-pandas objects + store._handle.create_array('/first_group', 'a1', objs['a1']) + store._handle.create_table('/first_group', 'tb1', obj=objs['tb1']) + store._handle.create_table('/second_group', 'tb2', obj=objs['tb2']) + + assert len(list(store.walk(where=where))) == len(expected) + for path, groups, leaves in store.walk(where=where): + assert path in expected + expected_groups, expected_frames = expected[path] + assert expected_groups == set(groups) + assert expected_frames == set(leaves) + for leaf in leaves: + frame_path = '/'.join([path, leaf]) + obj = store.get(frame_path) + if 'df' in leaf: + tm.assert_frame_equal(obj, objs[leaf]) + else: + tm.assert_series_equal(obj, objs[leaf]) def test_getattr(self): @@ -636,10 +685,10 @@ def test_getattr(self): tm.assert_frame_equal(result, df) # errors - self.assertRaises(AttributeError, getattr, store, 'd') + pytest.raises(AttributeError, getattr, store, 'd') for x in ['mode', 'path', 'handle', 'complib']: - self.assertRaises(AttributeError, getattr, store, x) + pytest.raises(AttributeError, getattr, store, x) # not stores for x in ['mode', 'path', 'handle', 'complib']: @@ -659,17 +708,17 @@ def test_put(self): store.put('c', df[:10], format='table') # not OK, not a table - self.assertRaises( + pytest.raises( ValueError, store.put, 'b', df[10:], append=True) # node does not currently exist, test _is_table_type returns False # in this case # _maybe_remove(store, 'f') - # self.assertRaises(ValueError, store.put, 'f', df[10:], + # pytest.raises(ValueError, store.put, 'f', df[10:], # append=True) # can't put to a table (use append instead) - self.assertRaises(ValueError, store.put, 'c', df[10:], append=True) + pytest.raises(ValueError, store.put, 'c', df[10:], append=True) # overwrite table store.put('c', df[:10], format='table', append=False) @@ -711,31 +760,118 @@ def test_put_compression(self): tm.assert_frame_equal(store['c'], df) # can't compress if format='fixed' - self.assertRaises(ValueError, store.put, 'b', df, - format='fixed', complib='zlib') + pytest.raises(ValueError, store.put, 'b', df, + format='fixed', complib='zlib') + @td.skip_if_windows_python_3 def test_put_compression_blosc(self): - tm.skip_if_no_package('tables', min_version='2.2', - app='blosc support') - if skip_compression: - pytest.skip("skipping on windows/PY3") - df = tm.makeTimeDataFrame() with ensure_clean_store(self.path) as store: # can't compress if format='fixed' - self.assertRaises(ValueError, store.put, 'b', df, - format='fixed', complib='blosc') + pytest.raises(ValueError, store.put, 'b', df, + format='fixed', complib='blosc') store.put('c', df, format='table', complib='blosc') tm.assert_frame_equal(store['c'], df) + def test_complibs_default_settings(self): + # GH15943 + df = tm.makeDataFrame() + + # Set complevel and check if complib is automatically set to + # default value + with ensure_clean_path(self.path) as tmpfile: + df.to_hdf(tmpfile, 'df', complevel=9) + result = pd.read_hdf(tmpfile, 'df') + tm.assert_frame_equal(result, df) + + with tables.open_file(tmpfile, mode='r') as h5file: + for node in h5file.walk_nodes(where='/df', classname='Leaf'): + assert node.filters.complevel == 9 + assert node.filters.complib == 'zlib' + + # Set complib and check to see if compression is disabled + with ensure_clean_path(self.path) as tmpfile: + df.to_hdf(tmpfile, 'df', complib='zlib') + result = pd.read_hdf(tmpfile, 'df') + tm.assert_frame_equal(result, df) + + with tables.open_file(tmpfile, mode='r') as h5file: + for node in h5file.walk_nodes(where='/df', classname='Leaf'): + assert node.filters.complevel == 0 + assert node.filters.complib is None + + # Check if not setting complib or complevel results in no compression + with ensure_clean_path(self.path) as tmpfile: + df.to_hdf(tmpfile, 'df') + result = pd.read_hdf(tmpfile, 'df') + tm.assert_frame_equal(result, df) + + with tables.open_file(tmpfile, mode='r') as h5file: + for node in h5file.walk_nodes(where='/df', classname='Leaf'): + assert node.filters.complevel == 0 + assert node.filters.complib is None + + # Check if file-defaults can be overridden on a per table basis + with ensure_clean_path(self.path) as tmpfile: + store = pd.HDFStore(tmpfile) + store.append('dfc', df, complevel=9, complib='blosc') + store.append('df', df) + store.close() + + with tables.open_file(tmpfile, mode='r') as h5file: + for node in h5file.walk_nodes(where='/df', classname='Leaf'): + assert node.filters.complevel == 0 + assert node.filters.complib is None + for node in h5file.walk_nodes(where='/dfc', classname='Leaf'): + assert node.filters.complevel == 9 + assert node.filters.complib == 'blosc' + + def test_complibs(self): + # GH14478 + df = tm.makeDataFrame() + + # Building list of all complibs and complevels tuples + all_complibs = tables.filters.all_complibs + # Remove lzo if its not available on this platform + if not tables.which_lib_version('lzo'): + all_complibs.remove('lzo') + # Remove bzip2 if its not available on this platform + if not tables.which_lib_version("bzip2"): + all_complibs.remove("bzip2") + + all_levels = range(0, 10) + all_tests = [(lib, lvl) for lib in all_complibs for lvl in all_levels] + + for (lib, lvl) in all_tests: + with ensure_clean_path(self.path) as tmpfile: + gname = 'foo' + + # Write and read file to see if data is consistent + df.to_hdf(tmpfile, gname, complib=lib, complevel=lvl) + result = pd.read_hdf(tmpfile, gname) + tm.assert_frame_equal(result, df) + + # Open file and check metadata + # for correct amount of compression + h5table = tables.open_file(tmpfile, mode='r') + for node in h5table.walk_nodes(where='/' + gname, + classname='Leaf'): + assert node.filters.complevel == lvl + if lvl == 0: + assert node.filters.complib is None + else: + assert node.filters.complib == lib + h5table.close() + def test_put_integer(self): # non-date, non-string index df = DataFrame(np.random.randn(50, 100)) self._check_roundtrip(df, tm.assert_frame_equal) + @xfail_non_writeable def test_put_mixed_type(self): df = tm.makeTimeDataFrame() df['obj1'] = 'foo' @@ -755,107 +891,76 @@ def test_put_mixed_type(self): with ensure_clean_store(self.path) as store: _maybe_remove(store, 'df') + # PerformanceWarning with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) store.put('df', df) expected = store.get('df') tm.assert_frame_equal(expected, df) + @pytest.mark.filterwarnings( + "ignore:object name:tables.exceptions.NaturalNameWarning" + ) def test_append(self): with ensure_clean_store(self.path) as store: - df = tm.makeTimeDataFrame() - _maybe_remove(store, 'df1') - store.append('df1', df[:10]) - store.append('df1', df[10:]) - tm.assert_frame_equal(store['df1'], df) - - _maybe_remove(store, 'df2') - store.put('df2', df[:10], format='table') - store.append('df2', df[10:]) - tm.assert_frame_equal(store['df2'], df) - - _maybe_remove(store, 'df3') - store.append('/df3', df[:10]) - store.append('/df3', df[10:]) - tm.assert_frame_equal(store['df3'], df) # this is allowed by almost always don't want to do it # tables.NaturalNameWarning): with catch_warnings(record=True): + + df = tm.makeTimeDataFrame() + _maybe_remove(store, 'df1') + store.append('df1', df[:10]) + store.append('df1', df[10:]) + tm.assert_frame_equal(store['df1'], df) + + _maybe_remove(store, 'df2') + store.put('df2', df[:10], format='table') + store.append('df2', df[10:]) + tm.assert_frame_equal(store['df2'], df) + + _maybe_remove(store, 'df3') + store.append('/df3', df[:10]) + store.append('/df3', df[10:]) + tm.assert_frame_equal(store['df3'], df) + + # this is allowed by almost always don't want to do it + # tables.NaturalNameWarning _maybe_remove(store, '/df3 foo') store.append('/df3 foo', df[:10]) store.append('/df3 foo', df[10:]) tm.assert_frame_equal(store['df3 foo'], df) - # panel - wp = tm.makePanel() - _maybe_remove(store, 'wp1') - store.append('wp1', wp.iloc[:, :10, :]) - store.append('wp1', wp.iloc[:, 10:, :]) - assert_panel_equal(store['wp1'], wp) - - # ndim - with catch_warnings(record=True): - p4d = tm.makePanel4D() - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :]) - store.append('p4d', p4d.iloc[:, :, 10:, :]) - assert_panel4d_equal(store['p4d'], p4d) - - # test using axis labels - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :], axes=[ - 'items', 'major_axis', 'minor_axis']) - store.append('p4d', p4d.iloc[:, :, 10:, :], axes=[ - 'items', 'major_axis', 'minor_axis']) - assert_panel4d_equal(store['p4d'], p4d) - - # test using differnt number of items on each axis - p4d2 = p4d.copy() - p4d2['l4'] = p4d['l1'] - p4d2['l5'] = p4d['l1'] - _maybe_remove(store, 'p4d2') - store.append( - 'p4d2', p4d2, axes=['items', 'major_axis', 'minor_axis']) - assert_panel4d_equal(store['p4d2'], p4d2) - - # test using differt order of items on the non-index axes - _maybe_remove(store, 'wp1') - wp_append1 = wp.iloc[:, :10, :] - store.append('wp1', wp_append1) - wp_append2 = wp.iloc[:, 10:, :].reindex(items=wp.items[::-1]) - store.append('wp1', wp_append2) - assert_panel_equal(store['wp1'], wp) - - # dtype issues - mizxed type in a single object column - df = DataFrame(data=[[1, 2], [0, 1], [1, 2], [0, 0]]) - df['mixed_column'] = 'testing' - df.loc[2, 'mixed_column'] = np.nan - _maybe_remove(store, 'df') - store.append('df', df) - tm.assert_frame_equal(store['df'], df) - - # uints - test storage of uints - uint_data = DataFrame({ - 'u08': Series(np.random.randint(0, high=255, size=5), - dtype=np.uint8), - 'u16': Series(np.random.randint(0, high=65535, size=5), - dtype=np.uint16), - 'u32': Series(np.random.randint(0, high=2**30, size=5), - dtype=np.uint32), - 'u64': Series([2**58, 2**59, 2**60, 2**61, 2**62], - dtype=np.uint64)}, index=np.arange(5)) - _maybe_remove(store, 'uints') - store.append('uints', uint_data) - tm.assert_frame_equal(store['uints'], uint_data) - - # uints - test storage of uints in indexable columns - _maybe_remove(store, 'uints') - # 64-bit indices not yet supported - store.append('uints', uint_data, data_columns=[ - 'u08', 'u16', 'u32']) - tm.assert_frame_equal(store['uints'], uint_data) + # dtype issues - mizxed type in a single object column + df = DataFrame(data=[[1, 2], [0, 1], [1, 2], [0, 0]]) + df['mixed_column'] = 'testing' + df.loc[2, 'mixed_column'] = np.nan + _maybe_remove(store, 'df') + store.append('df', df) + tm.assert_frame_equal(store['df'], df) + + # uints - test storage of uints + uint_data = DataFrame({ + 'u08': Series(np.random.randint(0, high=255, size=5), + dtype=np.uint8), + 'u16': Series(np.random.randint(0, high=65535, size=5), + dtype=np.uint16), + 'u32': Series(np.random.randint(0, high=2**30, size=5), + dtype=np.uint32), + 'u64': Series([2**58, 2**59, 2**60, 2**61, 2**62], + dtype=np.uint64)}, index=np.arange(5)) + _maybe_remove(store, 'uints') + store.append('uints', uint_data) + tm.assert_frame_equal(store['uints'], uint_data) + + # uints - test storage of uints in indexable columns + _maybe_remove(store, 'uints') + # 64-bit indices not yet supported + store.append('uints', uint_data, data_columns=[ + 'u08', 'u16', 'u32']) + tm.assert_frame_equal(store['uints'], uint_data) def test_append_series(self): @@ -869,18 +974,18 @@ def test_append_series(self): store.append('ss', ss) result = store['ss'] tm.assert_series_equal(result, ss) - self.assertIsNone(result.name) + assert result.name is None store.append('ts', ts) result = store['ts'] tm.assert_series_equal(result, ts) - self.assertIsNone(result.name) + assert result.name is None ns.name = 'foo' store.append('ns', ns) result = store['ns'] tm.assert_series_equal(result, ns) - self.assertEqual(result.name, ns.name) + assert result.name == ns.name # select on the values expected = ns[ns > 60] @@ -936,16 +1041,17 @@ def check(format, index): else: # only support for fixed types (and they have a perf warning) - self.assertRaises(TypeError, check, 'table', index) - with tm.assert_produces_warning( - expected_warning=PerformanceWarning): + pytest.raises(TypeError, check, 'table', index) + + # PerformanceWarning + with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) check('fixed', index) + @pytest.mark.skipif(not is_platform_little_endian(), + reason="reason platform is not little endian") def test_encoding(self): - if sys.byteorder != 'little': - pytest.skip('system byteorder is not little') - with ensure_clean_store(self.path) as store: df = DataFrame(dict(A='foo', B='bar'), index=range(5)) df.loc[2, 'A'] = np.nan @@ -961,9 +1067,7 @@ def test_encoding(self): def test_latin_encoding(self): if compat.PY2: - self.assertRaisesRegexp( - TypeError, r'\[unicode\] is not implemented as a table column') - return + pytest.skip("[unicode] is not implemented as a table column") values = [[b'E\xc9, 17', b'', b'a', b'b', b'c'], [b'E\xc9, 17', b'a', b'b', b'c'], @@ -986,7 +1090,7 @@ def _try_decode(x, encoding='latin-1'): examples = [] for dtype in ['category', object]: for val in values: - examples.append(pandas.Series(val, dtype=dtype)) + examples.append(pd.Series(val, dtype=dtype)) def roundtrip(s, key='data', encoding='latin-1', nan_rep=''): with ensure_clean_path(self.path) as store: @@ -994,7 +1098,12 @@ def roundtrip(s, key='data', encoding='latin-1', nan_rep=''): nan_rep=nan_rep) retr = read_hdf(store, key) s_nan = s.replace(nan_rep, np.nan) - assert_series_equal(s_nan, retr, check_categorical=False) + if is_categorical_dtype(s_nan): + assert is_categorical_dtype(retr) + assert_series_equal(s_nan, retr, check_dtype=False, + check_categorical=False) + else: + assert_series_equal(s_nan, retr) for s in examples: roundtrip(s) @@ -1066,13 +1175,13 @@ def test_append_all_nans(self): tm.assert_frame_equal(store['df2'], df) # tests the option io.hdf.dropna_table - pandas.set_option('io.hdf.dropna_table', False) + pd.set_option('io.hdf.dropna_table', False) _maybe_remove(store, 'df3') store.append('df3', df[:10]) store.append('df3', df[10:]) tm.assert_frame_equal(store['df3'], df) - pandas.set_option('io.hdf.dropna_table', True) + pd.set_option('io.hdf.dropna_table', True) _maybe_remove(store, 'df4') store.append('df4', df[:10]) store.append('df4', df[10:]) @@ -1127,20 +1236,6 @@ def test_append_all_nans(self): reloaded = read_hdf(path, 'df_with_missing') tm.assert_frame_equal(df_with_missing, reloaded) - matrix = [[[np.nan, np.nan, np.nan], [1, np.nan, np.nan]], - [[np.nan, np.nan, np.nan], [np.nan, 5, 6]], - [[np.nan, np.nan, np.nan], [np.nan, 3, np.nan]]] - - panel_with_missing = Panel(matrix, items=['Item1', 'Item2', 'Item3'], - major_axis=[1, 2], - minor_axis=['A', 'B', 'C']) - - with ensure_clean_path(self.path) as path: - panel_with_missing.to_hdf( - path, 'panel_with_missing', format='table') - reloaded_panel = read_hdf(path, 'panel_with_missing') - tm.assert_panel_equal(panel_with_missing, reloaded_panel) - def test_append_frame_column_oriented(self): with ensure_clean_store(self.path) as store: @@ -1158,13 +1253,14 @@ def test_append_frame_column_oriented(self): # selection on the non-indexable result = store.select( - 'df1', ('columns=A', Term('index=df.index[0:4]'))) + 'df1', ('columns=A', 'index=df.index[0:4]')) expected = df.reindex(columns=['A'], index=df.index[0:4]) tm.assert_frame_equal(expected, result) # this isn't supported - self.assertRaises(TypeError, store.select, 'df1', ( - 'columns=A', Term('index>df.index[4]'))) + with pytest.raises(TypeError): + store.select('df1', + 'columns=A and index>df.index[4]') def test_append_with_different_block_ordering(self): @@ -1200,187 +1296,92 @@ def test_append_with_different_block_ordering(self): df['int16'] = Series([1] * len(df), dtype='int16') store.append('df', df) - # store additonal fields in different blocks + # store additional fields in different blocks df['int16_2'] = Series([1] * len(df), dtype='int16') - self.assertRaises(ValueError, store.append, 'df', df) + pytest.raises(ValueError, store.append, 'df', df) - # store multile additonal fields in different blocks + # store multile additional fields in different blocks df['float_3'] = Series([1.] * len(df), dtype='float64') - self.assertRaises(ValueError, store.append, 'df', df) - - def test_ndim_indexables(self): - # test using ndim tables in new ways - - with catch_warnings(record=True): - with ensure_clean_store(self.path) as store: - - p4d = tm.makePanel4D() - - def check_indexers(key, indexers): - for i, idx in enumerate(indexers): - descr = getattr(store.root, key).table.description - self.assertTrue(getattr(descr, idx)._v_pos == i) - - # append then change (will take existing schema) - indexers = ['items', 'major_axis', 'minor_axis'] - - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :], axes=indexers) - store.append('p4d', p4d.iloc[:, :, 10:, :]) - assert_panel4d_equal(store.select('p4d'), p4d) - check_indexers('p4d', indexers) - - # same as above, but try to append with differnt axes - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :], axes=indexers) - store.append('p4d', p4d.iloc[:, :, 10:, :], axes=[ - 'labels', 'items', 'major_axis']) - assert_panel4d_equal(store.select('p4d'), p4d) - check_indexers('p4d', indexers) - - # pass incorrect number of axes - _maybe_remove(store, 'p4d') - self.assertRaises(ValueError, store.append, 'p4d', p4d.iloc[ - :, :, :10, :], axes=['major_axis', 'minor_axis']) - - # different than default indexables #1 - indexers = ['labels', 'major_axis', 'minor_axis'] - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :], axes=indexers) - store.append('p4d', p4d.iloc[:, :, 10:, :]) - assert_panel4d_equal(store['p4d'], p4d) - check_indexers('p4d', indexers) - - # different than default indexables #2 - indexers = ['major_axis', 'labels', 'minor_axis'] - _maybe_remove(store, 'p4d') - store.append('p4d', p4d.iloc[:, :, :10, :], axes=indexers) - store.append('p4d', p4d.iloc[:, :, 10:, :]) - assert_panel4d_equal(store['p4d'], p4d) - check_indexers('p4d', indexers) - - # partial selection - result = store.select('p4d', ['labels=l1']) - expected = p4d.reindex(labels=['l1']) - assert_panel4d_equal(result, expected) - - # partial selection2 - result = store.select('p4d', [Term( - 'labels=l1'), Term('items=ItemA'), Term('minor_axis=B')]) - expected = p4d.reindex( - labels=['l1'], items=['ItemA'], minor_axis=['B']) - assert_panel4d_equal(result, expected) - - # non-existant partial selection - result = store.select('p4d', [Term( - 'labels=l1'), Term('items=Item1'), Term('minor_axis=B')]) - expected = p4d.reindex(labels=['l1'], items=[], - minor_axis=['B']) - assert_panel4d_equal(result, expected) + pytest.raises(ValueError, store.append, 'df', df) def test_append_with_strings(self): with ensure_clean_store(self.path) as store: - wp = tm.makePanel() - wp2 = wp.rename_axis( - dict([(x, "%s_extra" % x) for x in wp.minor_axis]), axis=2) - - def check_col(key, name, size): - self.assertEqual(getattr(store.get_storer( - key).table.description, name).itemsize, size) - - store.append('s1', wp, min_itemsize=20) - store.append('s1', wp2) - expected = concat([wp, wp2], axis=2) - expected = expected.reindex(minor_axis=sorted(expected.minor_axis)) - assert_panel_equal(store['s1'], expected) - check_col('s1', 'minor_axis', 20) - - # test dict format - store.append('s2', wp, min_itemsize={'minor_axis': 20}) - store.append('s2', wp2) - expected = concat([wp, wp2], axis=2) - expected = expected.reindex(minor_axis=sorted(expected.minor_axis)) - assert_panel_equal(store['s2'], expected) - check_col('s2', 'minor_axis', 20) - - # apply the wrong field (similar to #1) - store.append('s3', wp, min_itemsize={'major_axis': 20}) - self.assertRaises(ValueError, store.append, 's3', wp2) - - # test truncation of bigger strings - store.append('s4', wp) - self.assertRaises(ValueError, store.append, 's4', wp2) - - # avoid truncation on elements - df = DataFrame([[123, 'asdqwerty'], [345, 'dggnhebbsdfbdfb']]) - store.append('df_big', df) - tm.assert_frame_equal(store.select('df_big'), df) - check_col('df_big', 'values_block_1', 15) - - # appending smaller string ok - df2 = DataFrame([[124, 'asdqy'], [346, 'dggnhefbdfb']]) - store.append('df_big', df2) - expected = concat([df, df2]) - tm.assert_frame_equal(store.select('df_big'), expected) - check_col('df_big', 'values_block_1', 15) - - # avoid truncation on elements - df = DataFrame([[123, 'asdqwerty'], [345, 'dggnhebbsdfbdfb']]) - store.append('df_big2', df, min_itemsize={'values': 50}) - tm.assert_frame_equal(store.select('df_big2'), df) - check_col('df_big2', 'values_block_1', 50) - - # bigger string on next append - store.append('df_new', df) - df_new = DataFrame( - [[124, 'abcdefqhij'], [346, 'abcdefghijklmnopqrtsuvwxyz']]) - self.assertRaises(ValueError, store.append, 'df_new', df_new) - - # min_itemsize on Series index (GH 11412) - df = tm.makeMixedDataFrame().set_index('C') - store.append('ss', df['B'], min_itemsize={'index': 4}) - tm.assert_series_equal(store.select('ss'), df['B']) - - # same as above, with data_columns=True - store.append('ss2', df['B'], data_columns=True, - min_itemsize={'index': 4}) - tm.assert_series_equal(store.select('ss2'), df['B']) - - # min_itemsize in index without appending (GH 10381) - store.put('ss3', df, format='table', - min_itemsize={'index': 6}) - # just make sure there is a longer string: - df2 = df.copy().reset_index().assign(C='longer').set_index('C') - store.append('ss3', df2) - tm.assert_frame_equal(store.select('ss3'), - pd.concat([df, df2])) - - # same as above, with a Series - store.put('ss4', df['B'], format='table', - min_itemsize={'index': 6}) - store.append('ss4', df2['B']) - tm.assert_series_equal(store.select('ss4'), - pd.concat([df['B'], df2['B']])) + with catch_warnings(record=True): - # with nans - _maybe_remove(store, 'df') - df = tm.makeTimeDataFrame() - df['string'] = 'foo' - df.loc[1:4, 'string'] = np.nan - df['string2'] = 'bar' - df.loc[4:8, 'string2'] = np.nan - df['string3'] = 'bah' - df.loc[1:, 'string3'] = np.nan - store.append('df', df) - result = store.select('df') - tm.assert_frame_equal(result, df) + def check_col(key, name, size): + assert getattr(store.get_storer(key) + .table.description, name).itemsize == size + + # avoid truncation on elements + df = DataFrame([[123, 'asdqwerty'], [345, 'dggnhebbsdfbdfb']]) + store.append('df_big', df) + tm.assert_frame_equal(store.select('df_big'), df) + check_col('df_big', 'values_block_1', 15) + + # appending smaller string ok + df2 = DataFrame([[124, 'asdqy'], [346, 'dggnhefbdfb']]) + store.append('df_big', df2) + expected = concat([df, df2]) + tm.assert_frame_equal(store.select('df_big'), expected) + check_col('df_big', 'values_block_1', 15) + + # avoid truncation on elements + df = DataFrame([[123, 'asdqwerty'], [345, 'dggnhebbsdfbdfb']]) + store.append('df_big2', df, min_itemsize={'values': 50}) + tm.assert_frame_equal(store.select('df_big2'), df) + check_col('df_big2', 'values_block_1', 50) + + # bigger string on next append + store.append('df_new', df) + df_new = DataFrame( + [[124, 'abcdefqhij'], [346, 'abcdefghijklmnopqrtsuvwxyz']]) + pytest.raises(ValueError, store.append, 'df_new', df_new) + + # min_itemsize on Series index (GH 11412) + df = tm.makeMixedDataFrame().set_index('C') + store.append('ss', df['B'], min_itemsize={'index': 4}) + tm.assert_series_equal(store.select('ss'), df['B']) + + # same as above, with data_columns=True + store.append('ss2', df['B'], data_columns=True, + min_itemsize={'index': 4}) + tm.assert_series_equal(store.select('ss2'), df['B']) + + # min_itemsize in index without appending (GH 10381) + store.put('ss3', df, format='table', + min_itemsize={'index': 6}) + # just make sure there is a longer string: + df2 = df.copy().reset_index().assign(C='longer').set_index('C') + store.append('ss3', df2) + tm.assert_frame_equal(store.select('ss3'), + pd.concat([df, df2])) + + # same as above, with a Series + store.put('ss4', df['B'], format='table', + min_itemsize={'index': 6}) + store.append('ss4', df2['B']) + tm.assert_series_equal(store.select('ss4'), + pd.concat([df['B'], df2['B']])) + + # with nans + _maybe_remove(store, 'df') + df = tm.makeTimeDataFrame() + df['string'] = 'foo' + df.loc[1:4, 'string'] = np.nan + df['string2'] = 'bar' + df.loc[4:8, 'string2'] = np.nan + df['string3'] = 'bah' + df.loc[1:, 'string3'] = np.nan + store.append('df', df) + result = store.select('df') + tm.assert_frame_equal(result, df) with ensure_clean_store(self.path) as store: def check_col(key, name, size): - self.assertEqual(getattr(store.get_storer( - key).table.description, name).itemsize, size) + assert getattr(store.get_storer(key) + .table.description, name).itemsize, size df = DataFrame(dict(A='foo', B='bar'), index=range(10)) @@ -1388,13 +1389,13 @@ def check_col(key, name, size): _maybe_remove(store, 'df') store.append('df', df, min_itemsize={'A': 200}) check_col('df', 'A', 200) - self.assertEqual(store.get_storer('df').data_columns, ['A']) + assert store.get_storer('df').data_columns == ['A'] # a min_itemsize that creates a data_column2 _maybe_remove(store, 'df') store.append('df', df, data_columns=['B'], min_itemsize={'A': 200}) check_col('df', 'A', 200) - self.assertEqual(store.get_storer('df').data_columns, ['B', 'A']) + assert store.get_storer('df').data_columns == ['B', 'A'] # a min_itemsize that creates a data_column2 _maybe_remove(store, 'df') @@ -1402,7 +1403,7 @@ def check_col(key, name, size): 'B'], min_itemsize={'values': 200}) check_col('df', 'B', 200) check_col('df', 'values_block_0', 200) - self.assertEqual(store.get_storer('df').data_columns, ['B']) + assert store.get_storer('df').data_columns == ['B'] # infer the .typ on subsequent appends _maybe_remove(store, 'df') @@ -1414,8 +1415,18 @@ def check_col(key, name, size): df = DataFrame(['foo', 'foo', 'foo', 'barh', 'barh', 'barh'], columns=['A']) _maybe_remove(store, 'df') - self.assertRaises(ValueError, store.append, 'df', - df, min_itemsize={'foo': 20, 'foobar': 20}) + pytest.raises(ValueError, store.append, 'df', + df, min_itemsize={'foo': 20, 'foobar': 20}) + + def test_append_with_empty_string(self): + + with ensure_clean_store(self.path) as store: + + # with all empty strings (GH 12242) + df = DataFrame({'x': ['a', 'b', 'c', 'd', 'e', 'f', '']}) + store.append('df', df[:-1], min_itemsize={'x': 1}) + store.append('df', df[-1:], min_itemsize={'x': 1}) + tm.assert_frame_equal(store.select('df'), df) def test_to_hdf_with_min_itemsize(self): @@ -1437,6 +1448,21 @@ def test_to_hdf_with_min_itemsize(self): tm.assert_series_equal(pd.read_hdf(path, 'ss4'), pd.concat([df['B'], df2['B']])) + @pytest.mark.parametrize( + "format", + [pytest.param('fixed', marks=xfail_non_writeable), + 'table']) + def test_to_hdf_errors(self, format): + + data = ['\ud800foo'] + ser = pd.Series(data, index=pd.Index(data)) + with ensure_clean_path(self.path) as path: + # GH 20835 + ser.to_hdf(path, 'table', format=format, errors='surrogatepass') + + result = pd.read_hdf(path, 'table', errors='surrogatepass') + tm.assert_series_equal(result, ser) + def test_append_with_data_columns(self): with ensure_clean_store(self.path) as store: @@ -1447,18 +1473,18 @@ def test_append_with_data_columns(self): store.append('df', df[2:]) tm.assert_frame_equal(store['df'], df) - # check that we have indicies created + # check that we have indices created assert(store._handle.root.df.table.cols.index.is_indexed is True) assert(store._handle.root.df.table.cols.B.is_indexed is True) # data column searching - result = store.select('df', [Term('B>0')]) + result = store.select('df', 'B>0') expected = df[df.B > 0] tm.assert_frame_equal(result, expected) # data column searching (with an indexable and a data_columns) result = store.select( - 'df', [Term('B>0'), Term('index>df.index[3]')]) + 'df', 'B>0 and index>df.index[3]') df_new = df.reindex(index=df.index[4:]) expected = df_new[df_new.B > 0] tm.assert_frame_equal(result, expected) @@ -1470,14 +1496,14 @@ def test_append_with_data_columns(self): df_new.loc[5:6, 'string'] = 'bar' _maybe_remove(store, 'df') store.append('df', df_new, data_columns=['string']) - result = store.select('df', [Term('string=foo')]) + result = store.select('df', "string='foo'") expected = df_new[df_new.string == 'foo'] tm.assert_frame_equal(result, expected) # using min_itemsize and a data column def check_col(key, name, size): - self.assertEqual(getattr(store.get_storer( - key).table.description, name).itemsize, size) + assert getattr(store.get_storer(key) + .table.description, name).itemsize == size with ensure_clean_store(self.path) as store: _maybe_remove(store, 'df') @@ -1523,15 +1549,15 @@ def check_col(key, name, size): _maybe_remove(store, 'df') store.append( 'df', df_new, data_columns=['A', 'B', 'string', 'string2']) - result = store.select('df', [Term('string=foo'), Term( - 'string2=foo'), Term('A>0'), Term('B<0')]) + result = store.select('df', + "string='foo' and string2='foo'" + " and A>0 and B<0") expected = df_new[(df_new.string == 'foo') & ( df_new.string2 == 'foo') & (df_new.A > 0) & (df_new.B < 0)] tm.assert_frame_equal(result, expected, check_index_type=False) # yield an empty frame - result = store.select('df', [Term('string=foo'), Term( - 'string2=cool')]) + result = store.select('df', "string='foo' and string2='cool'") expected = df_new[(df_new.string == 'foo') & ( df_new.string2 == 'cool')] tm.assert_frame_equal(result, expected, check_index_type=False) @@ -1551,7 +1577,7 @@ def check_col(key, name, size): store.append('df_dc', df_dc, data_columns=['B', 'C', 'string', 'string2', 'datetime']) - result = store.select('df_dc', [Term('B>0')]) + result = store.select('df_dc', 'B>0') expected = df_dc[df_dc.B > 0] tm.assert_frame_equal(result, expected, check_index_type=False) @@ -1578,7 +1604,7 @@ def check_col(key, name, size): store.append('df_dc', df_dc, data_columns=[ 'B', 'C', 'string', 'string2']) - result = store.select('df_dc', [Term('B>0')]) + result = store.select('df_dc', 'B>0') expected = df_dc[df_dc.B > 0] tm.assert_frame_equal(result, expected) @@ -1588,106 +1614,41 @@ def check_col(key, name, size): (df_dc.string == 'foo')] tm.assert_frame_equal(result, expected) - with ensure_clean_store(self.path) as store: - # panel - # GH5717 not handling data_columns - np.random.seed(1234) - p = tm.makePanel() - - store.append('p1', p) - tm.assert_panel_equal(store.select('p1'), p) - - store.append('p2', p, data_columns=True) - tm.assert_panel_equal(store.select('p2'), p) - - result = store.select('p2', where='ItemA>0') - expected = p.to_frame() - expected = expected[expected['ItemA'] > 0] - tm.assert_frame_equal(result.to_frame(), expected) - - result = store.select('p2', where='ItemA>0 & minor_axis=["A","B"]') - expected = p.to_frame() - expected = expected[expected['ItemA'] > 0] - expected = expected[expected.reset_index( - level=['major']).index.isin(['A', 'B'])] - tm.assert_frame_equal(result.to_frame(), expected) - def test_create_table_index(self): with ensure_clean_store(self.path) as store: - def col(t, column): - return getattr(store.get_storer(t).table.cols, column) - - # index=False - wp = tm.makePanel() - store.append('p5', wp, index=False) - store.create_table_index('p5', columns=['major_axis']) - assert(col('p5', 'major_axis').is_indexed is True) - assert(col('p5', 'minor_axis').is_indexed is False) - - # index=True - store.append('p5i', wp, index=True) - assert(col('p5i', 'major_axis').is_indexed is True) - assert(col('p5i', 'minor_axis').is_indexed is True) - - # default optlevels - store.get_storer('p5').create_index() - assert(col('p5', 'major_axis').index.optlevel == 6) - assert(col('p5', 'minor_axis').index.kind == 'medium') - - # let's change the indexing scheme - store.create_table_index('p5') - assert(col('p5', 'major_axis').index.optlevel == 6) - assert(col('p5', 'minor_axis').index.kind == 'medium') - store.create_table_index('p5', optlevel=9) - assert(col('p5', 'major_axis').index.optlevel == 9) - assert(col('p5', 'minor_axis').index.kind == 'medium') - store.create_table_index('p5', kind='full') - assert(col('p5', 'major_axis').index.optlevel == 9) - assert(col('p5', 'minor_axis').index.kind == 'full') - store.create_table_index('p5', optlevel=1, kind='light') - assert(col('p5', 'major_axis').index.optlevel == 1) - assert(col('p5', 'minor_axis').index.kind == 'light') - - # data columns - df = tm.makeTimeDataFrame() - df['string'] = 'foo' - df['string2'] = 'bar' - store.append('f', df, data_columns=['string', 'string2']) - assert(col('f', 'index').is_indexed is True) - assert(col('f', 'string').is_indexed is True) - assert(col('f', 'string2').is_indexed is True) - - # specify index=columns - store.append( - 'f2', df, index=['string'], data_columns=['string', 'string2']) - assert(col('f2', 'index').is_indexed is False) - assert(col('f2', 'string').is_indexed is True) - assert(col('f2', 'string2').is_indexed is False) - - # try to index a non-table - _maybe_remove(store, 'f2') - store.put('f2', df) - self.assertRaises(TypeError, store.create_table_index, 'f2') + with catch_warnings(record=True): + def col(t, column): + return getattr(store.get_storer(t).table.cols, column) - def test_append_diff_item_order(self): + # data columns + df = tm.makeTimeDataFrame() + df['string'] = 'foo' + df['string2'] = 'bar' + store.append('f', df, data_columns=['string', 'string2']) + assert(col('f', 'index').is_indexed is True) + assert(col('f', 'string').is_indexed is True) + assert(col('f', 'string2').is_indexed is True) - wp = tm.makePanel() - wp1 = wp.iloc[:, :10, :] - wp2 = wp.iloc[wp.items.get_indexer(['ItemC', 'ItemB', 'ItemA']), - 10:, :] + # specify index=columns + store.append( + 'f2', df, index=['string'], + data_columns=['string', 'string2']) + assert(col('f2', 'index').is_indexed is False) + assert(col('f2', 'string').is_indexed is True) + assert(col('f2', 'string2').is_indexed is False) - with ensure_clean_store(self.path) as store: - store.put('panel', wp1, format='table') - self.assertRaises(ValueError, store.put, 'panel', wp2, - append=True) + # try to index a non-table + _maybe_remove(store, 'f2') + store.put('f2', df) + pytest.raises(TypeError, store.create_table_index, 'f2') def test_append_hierarchical(self): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['foo', 'bar']) df = DataFrame(np.random.randn(10, 3), index=index, columns=['A', 'B', 'C']) @@ -1732,10 +1693,10 @@ def test_column_multiindex(self): check_index_type=True, check_column_type=True) - self.assertRaises(ValueError, store.put, 'df2', df, - format='table', data_columns=['A']) - self.assertRaises(ValueError, store.put, 'df3', df, - format='table', data_columns=True) + pytest.raises(ValueError, store.put, 'df2', df, + format='table', data_columns=['A']) + pytest.raises(ValueError, store.put, 'df3', df, + format='table', data_columns=True) # appending multi-column on existing table (see GH 6167) with ensure_clean_store(self.path) as store: @@ -1798,13 +1759,13 @@ def make_index(names=None): _maybe_remove(store, 'df') df = DataFrame(np.zeros((12, 2)), columns=[ 'a', 'b'], index=make_index(['date', 'a', 't'])) - self.assertRaises(ValueError, store.append, 'df', df) + pytest.raises(ValueError, store.append, 'df', df) # dup within level _maybe_remove(store, 'df') df = DataFrame(np.zeros((12, 2)), columns=['a', 'b'], index=make_index(['date', 'date', 'date'])) - self.assertRaises(ValueError, store.append, 'df', df) + pytest.raises(ValueError, store.append, 'df', df) # fully names _maybe_remove(store, 'df') @@ -1820,8 +1781,8 @@ def test_select_columns_in_where(self): # in the `where` argument index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['foo_name', 'bar_name']) # With a DataFrame @@ -1863,34 +1824,22 @@ def test_pass_spec_to_storer(self): with ensure_clean_store(self.path) as store: store.put('df', df) - self.assertRaises(TypeError, store.select, 'df', columns=['A']) - self.assertRaises(TypeError, store.select, - 'df', where=[('columns=A')]) + pytest.raises(TypeError, store.select, 'df', columns=['A']) + pytest.raises(TypeError, store.select, + 'df', where=[('columns=A')]) + @xfail_non_writeable def test_append_misc(self): with ensure_clean_store(self.path) as store: + df = tm.makeDataFrame() + store.append('df', df, chunksize=1) + result = store.select('df') + tm.assert_frame_equal(result, df) - with catch_warnings(record=True): - - # unsuported data types for non-tables - p4d = tm.makePanel4D() - self.assertRaises(TypeError, store.put, 'p4d', p4d) - - # unsuported data types - self.assertRaises(TypeError, store.put, 'abc', None) - self.assertRaises(TypeError, store.put, 'abc', '123') - self.assertRaises(TypeError, store.put, 'abc', 123) - self.assertRaises(TypeError, store.put, 'abc', np.arange(5)) - - df = tm.makeDataFrame() - store.append('df', df, chunksize=1) - result = store.select('df') - tm.assert_frame_equal(result, df) - - store.append('df1', df, expectedrows=10) - result = store.select('df1') - tm.assert_frame_equal(result, df) + store.append('df1', df, expectedrows=10) + result = store.select('df1') + tm.assert_frame_equal(result, df) # more chunksize in append tests def check(obj, comparator): @@ -1909,20 +1858,13 @@ def check(obj, comparator): df['time2'] = Timestamp('20130102') check(df, tm.assert_frame_equal) - p = tm.makePanel() - check(p, assert_panel_equal) - - with catch_warnings(record=True): - p4d = tm.makePanel4D() - check(p4d, assert_panel4d_equal) - # empty frame, GH4273 with ensure_clean_store(self.path) as store: # 0 len df_empty = DataFrame(columns=list('ABC')) store.append('df', df_empty) - self.assertRaises(KeyError, store.select, 'df') + pytest.raises(KeyError, store.select, 'df') # repeated append of 0/non-zero frames df = DataFrame(np.random.rand(10, 3), columns=list('ABC')) @@ -1936,22 +1878,6 @@ def check(obj, comparator): store.put('df2', df) assert_frame_equal(store.select('df2'), df) - # 0 len - p_empty = Panel(items=list('ABC')) - store.append('p', p_empty) - self.assertRaises(KeyError, store.select, 'p') - - # repeated append of 0/non-zero frames - p = Panel(np.random.randn(3, 4, 5), items=list('ABC')) - store.append('p', p) - assert_panel_equal(store.select('p'), p) - store.append('p', p_empty) - assert_panel_equal(store.select('p'), p) - - # store - store.put('p2', p_empty) - assert_panel_equal(store.select('p2'), p_empty) - def test_append_raise(self): with ensure_clean_store(self.path) as store: @@ -1961,13 +1887,13 @@ def test_append_raise(self): # list in column df = tm.makeDataFrame() df['invalid'] = [['a']] * len(df) - self.assertEqual(df.dtypes['invalid'], np.object_) - self.assertRaises(TypeError, store.append, 'df', df) + assert df.dtypes['invalid'] == np.object_ + pytest.raises(TypeError, store.append, 'df', df) # multiple invalid columns df['invalid2'] = [['a']] * len(df) df['invalid3'] = [['a']] * len(df) - self.assertRaises(TypeError, store.append, 'df', df) + pytest.raises(TypeError, store.append, 'df', df) # datetime with embedded nans as object df = tm.makeDataFrame() @@ -1975,22 +1901,22 @@ def test_append_raise(self): s = s.astype(object) s[0:5] = np.nan df['invalid'] = s - self.assertEqual(df.dtypes['invalid'], np.object_) - self.assertRaises(TypeError, store.append, 'df', df) + assert df.dtypes['invalid'] == np.object_ + pytest.raises(TypeError, store.append, 'df', df) - # directy ndarray - self.assertRaises(TypeError, store.append, 'df', np.arange(10)) + # directly ndarray + pytest.raises(TypeError, store.append, 'df', np.arange(10)) # series directly - self.assertRaises(TypeError, store.append, - 'df', Series(np.arange(10))) + pytest.raises(TypeError, store.append, + 'df', Series(np.arange(10))) # appending an incompatible table df = tm.makeDataFrame() store.append('df', df) df['foo'] = 'foo' - self.assertRaises(ValueError, store.append, 'df', df) + pytest.raises(ValueError, store.append, 'df', df) def test_table_index_incompatible_dtypes(self): df1 = DataFrame({'a': [1, 2, 3]}) @@ -1999,8 +1925,8 @@ def test_table_index_incompatible_dtypes(self): with ensure_clean_store(self.path) as store: store.put('frame', df1, format='table') - self.assertRaises(TypeError, store.put, 'frame', df2, - format='table', append=True) + pytest.raises(TypeError, store.put, 'frame', df2, + format='table', append=True) def test_table_values_dtypes_roundtrip(self): @@ -2014,7 +1940,7 @@ def test_table_values_dtypes_roundtrip(self): assert_series_equal(df2.dtypes, store['df_i8'].dtypes) # incompatible dtype - self.assertRaises(ValueError, store.append, 'df_i8', df1) + pytest.raises(ValueError, store.append, 'df_i8', df1) # check creation/storage/retrieval of float32 (a bit hacky to # actually create them thought) @@ -2025,9 +1951,9 @@ def test_table_values_dtypes_roundtrip(self): assert df1.dtypes[0] == 'float32' # check with mixed dtypes - df1 = DataFrame(dict([(c, Series(np.random.randn(5), dtype=c)) - for c in ['float32', 'float64', 'int32', - 'int64', 'int16', 'int8']])) + df1 = DataFrame({c: Series(np.random.randint(5), dtype=c) + for c in ['float32', 'float64', 'int32', + 'int64', 'int16', 'int8']}) df1['string'] = 'foo' df1['float322'] = 1. df1['float322'] = df1['float322'].astype('float32') @@ -2041,7 +1967,7 @@ def test_table_values_dtypes_roundtrip(self): 'bool': 1, 'int16': 1, 'int8': 1, 'int64': 1, 'object': 1, 'datetime64[ns]': 2}) result = result.sort_index() - result = expected.sort_index() + expected = expected.sort_index() tm.assert_series_equal(result, expected) def test_table_mixed_dtypes(self): @@ -2066,51 +1992,21 @@ def test_table_mixed_dtypes(self): store.append('df1_mixed', df) tm.assert_frame_equal(store.select('df1_mixed'), df) - # panel - wp = tm.makePanel() - wp['obj1'] = 'foo' - wp['obj2'] = 'bar' - wp['bool1'] = wp['ItemA'] > 0 - wp['bool2'] = wp['ItemB'] > 0 - wp['int1'] = 1 - wp['int2'] = 2 - wp = wp._consolidate() - - with ensure_clean_store(self.path) as store: - store.append('p1_mixed', wp) - assert_panel_equal(store.select('p1_mixed'), wp) - - with catch_warnings(record=True): - - # ndim - wp = tm.makePanel4D() - wp['obj1'] = 'foo' - wp['obj2'] = 'bar' - wp['bool1'] = wp['l1'] > 0 - wp['bool2'] = wp['l2'] > 0 - wp['int1'] = 1 - wp['int2'] = 2 - wp = wp._consolidate() - - with ensure_clean_store(self.path) as store: - store.append('p4d_mixed', wp) - assert_panel4d_equal(store.select('p4d_mixed'), wp) - def test_unimplemented_dtypes_table_columns(self): with ensure_clean_store(self.path) as store: - l = [('date', datetime.date(2001, 1, 2))] + dtypes = [('date', datetime.date(2001, 1, 2))] # py3 ok for unicode if not compat.PY3: - l.append(('unicode', u('\\u03c3'))) + dtypes.append(('unicode', u('\\u03c3'))) # currently not supported dtypes #### - for n, f in l: + for n, f in dtypes: df = tm.makeDataFrame() df[n] = f - self.assertRaises( + pytest.raises( TypeError, store.append, 'df1_%s' % n, df) # frame @@ -2122,8 +2018,13 @@ def test_unimplemented_dtypes_table_columns(self): with ensure_clean_store(self.path) as store: # this fails because we have a date in the object block...... - self.assertRaises(TypeError, store.append, 'df_unimplemented', df) + pytest.raises(TypeError, store.append, 'df_unimplemented', df) + @xfail_non_writeable + @pytest.mark.skipif( + LooseVersion(np.__version__) == LooseVersion('1.15.0'), + reason=("Skipping pytables test when numpy version is " + "exactly equal to 1.15.0: gh-22098")) def test_calendar_roundtrip_issue(self): # 8591 @@ -2131,7 +2032,7 @@ def test_calendar_roundtrip_issue(self): weekmask_egypt = 'Sun Mon Tue Wed Thu' holidays = ['2012-05-01', datetime.datetime(2013, 5, 1), np.datetime64('2014-05-01')] - bday_egypt = pandas.offsets.CustomBusinessDay( + bday_egypt = pd.offsets.CustomBusinessDay( holidays=holidays, weekmask=weekmask_egypt) dt = datetime.datetime(2013, 4, 30) dts = date_range(dt, periods=5, freq=bday_egypt) @@ -2149,6 +2050,17 @@ def test_calendar_roundtrip_issue(self): result = store.select('table') assert_series_equal(result, s) + def test_roundtrip_tz_aware_index(self): + # GH 17618 + time = pd.Timestamp('2000-01-01 01:00:00', tz='US/Eastern') + df = pd.DataFrame(data=[0], index=[time]) + + with ensure_clean_store(self.path) as store: + store.put('frame', df, format='fixed') + recons = store['frame'] + tm.assert_frame_equal(recons, df) + assert recons.index[0].value == 946706400000000000 + def test_append_with_timedelta(self): # GH 3577 # append timedelta @@ -2166,9 +2078,12 @@ def test_append_with_timedelta(self): result = store.select('df') assert_frame_equal(result, df) - result = store.select('df', "C<100000") + result = store.select('df', where="C<100000") assert_frame_equal(result, df) + result = store.select('df', where="Cfoo') - self.assertRaises(KeyError, store.remove, 'a', [crit1]) - - # try to remove non-table (with crit) - # non-table ok (where = None) - wp = tm.makePanel(30) - store.put('wp', wp, format='table') - store.remove('wp', ["minor_axis=['A', 'D']"]) - rs = store.select('wp') - expected = wp.reindex(minor_axis=['B', 'C']) - assert_panel_equal(rs, expected) - - # empty where - _maybe_remove(store, 'wp') - store.put('wp', wp, format='table') - - # deleted number (entire table) - n = store.remove('wp', []) - self.assertTrue(n == 120) - - # non - empty where - _maybe_remove(store, 'wp') - store.put('wp', wp, format='table') - self.assertRaises(ValueError, store.remove, - 'wp', ['foo']) - - # selectin non-table with a where - # store.put('wp2', wp, format='f') - # self.assertRaises(ValueError, store.remove, - # 'wp2', [('column', ['A', 'D'])]) - - def test_remove_startstop(self): - # GH #4835 and #6177 - - with ensure_clean_store(self.path) as store: - - wp = tm.makePanel(30) - - # start - _maybe_remove(store, 'wp1') - store.put('wp1', wp, format='t') - n = store.remove('wp1', start=32) - self.assertTrue(n == 120 - 32) - result = store.select('wp1') - expected = wp.reindex(major_axis=wp.major_axis[:32 // 4]) - assert_panel_equal(result, expected) - - _maybe_remove(store, 'wp2') - store.put('wp2', wp, format='t') - n = store.remove('wp2', start=-32) - self.assertTrue(n == 32) - result = store.select('wp2') - expected = wp.reindex(major_axis=wp.major_axis[:-32 // 4]) - assert_panel_equal(result, expected) - - # stop - _maybe_remove(store, 'wp3') - store.put('wp3', wp, format='t') - n = store.remove('wp3', stop=32) - self.assertTrue(n == 32) - result = store.select('wp3') - expected = wp.reindex(major_axis=wp.major_axis[32 // 4:]) - assert_panel_equal(result, expected) - - _maybe_remove(store, 'wp4') - store.put('wp4', wp, format='t') - n = store.remove('wp4', stop=-32) - self.assertTrue(n == 120 - 32) - result = store.select('wp4') - expected = wp.reindex(major_axis=wp.major_axis[-32 // 4:]) - assert_panel_equal(result, expected) - - # start n stop - _maybe_remove(store, 'wp5') - store.put('wp5', wp, format='t') - n = store.remove('wp5', start=16, stop=-16) - self.assertTrue(n == 120 - 32) - result = store.select('wp5') - expected = wp.reindex(major_axis=wp.major_axis[ - :16 // 4].union(wp.major_axis[-16 // 4:])) - assert_panel_equal(result, expected) - - _maybe_remove(store, 'wp6') - store.put('wp6', wp, format='t') - n = store.remove('wp6', start=16, stop=16) - self.assertTrue(n == 0) - result = store.select('wp6') - expected = wp.reindex(major_axis=wp.major_axis) - assert_panel_equal(result, expected) - - # with where - _maybe_remove(store, 'wp7') - - # TODO: unused? - date = wp.major_axis.take(np.arange(0, 30, 3)) # noqa - - crit = Term('major_axis=date') - store.put('wp7', wp, format='t') - n = store.remove('wp7', where=[crit], stop=80) - self.assertTrue(n == 28) - result = store.select('wp7') - expected = wp.reindex(major_axis=wp.major_axis.difference( - wp.major_axis[np.arange(0, 20, 3)])) - assert_panel_equal(result, expected) - - def test_remove_crit(self): - - with ensure_clean_store(self.path) as store: - - wp = tm.makePanel(30) - - # group row removal - _maybe_remove(store, 'wp3') - date4 = wp.major_axis.take([0, 1, 2, 4, 5, 6, 8, 9, 10]) - crit4 = Term('major_axis=date4') - store.put('wp3', wp, format='t') - n = store.remove('wp3', where=[crit4]) - self.assertTrue(n == 36) - - result = store.select('wp3') - expected = wp.reindex(major_axis=wp.major_axis.difference(date4)) - assert_panel_equal(result, expected) - - # upper half - _maybe_remove(store, 'wp') - store.put('wp', wp, format='table') - date = wp.major_axis[len(wp.major_axis) // 2] - - crit1 = Term('major_axis>date') - crit2 = Term("minor_axis=['A', 'D']") - n = store.remove('wp', where=[crit1]) - self.assertTrue(n == 56) - - n = store.remove('wp', where=[crit2]) - self.assertTrue(n == 32) - - result = store['wp'] - expected = wp.truncate(after=date).reindex(minor=['B', 'C']) - assert_panel_equal(result, expected) - - # individual row elements - _maybe_remove(store, 'wp2') - store.put('wp2', wp, format='table') - - date1 = wp.major_axis[1:3] - crit1 = Term('major_axis=date1') - store.remove('wp2', where=[crit1]) - result = store.select('wp2') - expected = wp.reindex(major_axis=wp.major_axis.difference(date1)) - assert_panel_equal(result, expected) - - date2 = wp.major_axis[5] - crit2 = Term('major_axis=date2') - store.remove('wp2', where=[crit2]) - result = store['wp2'] - expected = wp.reindex(major_axis=wp.major_axis.difference(date1) - .difference(Index([date2]))) - assert_panel_equal(result, expected) - - date3 = [wp.major_axis[7], wp.major_axis[9]] - crit3 = Term('major_axis=date3') - store.remove('wp2', where=[crit3]) - result = store['wp2'] - expected = wp.reindex(major_axis=wp.major_axis - .difference(date1) - .difference(Index([date2])) - .difference(Index(date3))) - assert_panel_equal(result, expected) - - # corners - _maybe_remove(store, 'wp4') - store.put('wp4', wp, format='table') - n = store.remove( - 'wp4', where=[Term('major_axis>wp.major_axis[-1]')]) - result = store.select('wp4') - assert_panel_equal(result, wp) + assert len(store) == 0 def test_invalid_terms(self): @@ -2416,29 +2149,16 @@ def test_invalid_terms(self): df = tm.makeTimeDataFrame() df['string'] = 'foo' df.loc[0:4, 'string'] = 'bar' - wp = tm.makePanel() - p4d = tm.makePanel4D() store.put('df', df, format='table') - store.put('wp', wp, format='table') - store.put('p4d', p4d, format='table') # some invalid terms - self.assertRaises(ValueError, store.select, - 'wp', "minor=['A', 'B']") - self.assertRaises(ValueError, store.select, - 'wp', ["index=['20121114']"]) - self.assertRaises(ValueError, store.select, 'wp', [ - "index=['20121114', '20121114']"]) - self.assertRaises(TypeError, Term) + pytest.raises(TypeError, Term) # more invalid - self.assertRaises( + pytest.raises( ValueError, store.select, 'df', 'df.index[3]') - self.assertRaises(SyntaxError, store.select, 'df', 'index>') - self.assertRaises( - ValueError, store.select, 'wp', - "major_axis<'20000108' & minor_axis['A', 'B']") + pytest.raises(SyntaxError, store.select, 'df', 'index>') # from the docs with ensure_clean_path(self.path) as path: @@ -2457,133 +2177,8 @@ def test_invalid_terms(self): 'ABCD'), index=date_range('20130101', periods=10)) dfq.to_hdf(path, 'dfq', format='table') - self.assertRaises(ValueError, read_hdf, path, - 'dfq', where="A>0 or C>0") - - def test_terms(self): - - with ensure_clean_store(self.path) as store: - - wp = tm.makePanel() - wpneg = Panel.fromDict({-1: tm.makeDataFrame(), - 0: tm.makeDataFrame(), - 1: tm.makeDataFrame()}) - - with catch_warnings(record=True): - - p4d = tm.makePanel4D() - store.put('p4d', p4d, format='table') - - store.put('wp', wp, format='table') - store.put('wpneg', wpneg, format='table') - - # panel - result = store.select( - 'wp', "major_axis<'20000108' and minor_axis=['A', 'B']") - expected = wp.truncate(after='20000108').reindex(minor=['A', 'B']) - assert_panel_equal(result, expected) - - # p4d - with catch_warnings(record=True): - - result = store.select('p4d', - ("major_axis<'20000108' and " - "minor_axis=['A', 'B'] and " - "items=['ItemA', 'ItemB']")) - expected = p4d.truncate(after='20000108').reindex( - minor=['A', 'B'], items=['ItemA', 'ItemB']) - assert_panel4d_equal(result, expected) - - with catch_warnings(record=True): - - # valid terms - terms = [('major_axis=20121114'), - ('major_axis>20121114'), - (("major_axis=['20121114', '20121114']"),), - ('major_axis=datetime.datetime(2012, 11, 14)'), - 'major_axis> 20121114', - 'major_axis >20121114', - 'major_axis > 20121114', - (("minor_axis=['A', 'B']"),), - (("minor_axis=['A', 'B']"),), - ((("minor_axis==['A', 'B']"),),), - (("items=['ItemA', 'ItemB']"),), - ('items=ItemA'), - ] - - for t in terms: - store.select('wp', t) - store.select('p4d', t) - - # valid for p4d only - terms = [(("labels=['l1', 'l2']"),), - Term("labels=['l1', 'l2']"), - ] - - for t in terms: - store.select('p4d', t) - - with tm.assertRaisesRegexp(TypeError, - 'Only named functions are supported'): - store.select('wp', Term( - 'major_axis == (lambda x: x)("20130101")')) - - # check USub node parsing - res = store.select('wpneg', Term('items == -1')) - expected = Panel({-1: wpneg[-1]}) - tm.assert_panel_equal(res, expected) - - with tm.assertRaisesRegexp(NotImplementedError, - 'Unary addition not supported'): - store.select('wpneg', Term('items == +1')) - - def test_term_compat(self): - with ensure_clean_store(self.path) as store: - - wp = Panel(np.random.randn(2, 5, 4), items=['Item1', 'Item2'], - major_axis=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B', 'C', 'D']) - store.append('wp', wp) - - result = store.select( - 'wp', "major_axis>20000102 and minor_axis=['A', 'B']") - expected = wp.loc[:, wp.major_axis > - Timestamp('20000102'), ['A', 'B']] - assert_panel_equal(result, expected) - - store.remove('wp', 'major_axis>20000103') - result = store.select('wp') - expected = wp.loc[:, wp.major_axis <= Timestamp('20000103'), :] - assert_panel_equal(result, expected) - - with ensure_clean_store(self.path) as store: - - wp = Panel(np.random.randn(2, 5, 4), items=['Item1', 'Item2'], - major_axis=date_range('1/1/2000', periods=5), - minor_axis=['A', 'B', 'C', 'D']) - store.append('wp', wp) - - # stringified datetimes - result = store.select( - 'wp', "major_axis>datetime.datetime(2000, 1, 2)") - expected = wp.loc[:, wp.major_axis > Timestamp('20000102')] - assert_panel_equal(result, expected) - - result = store.select( - 'wp', "major_axis>datetime.datetime(2000, 1, 2, 0, 0)") - expected = wp.loc[:, wp.major_axis > Timestamp('20000102')] - assert_panel_equal(result, expected) - - result = store.select( - 'wp', ("major_axis=[datetime.datetime(2000, 1, 2, 0, 0), " - "datetime.datetime(2000, 1, 3, 0, 0)]")) - expected = wp.loc[:, [Timestamp('20000102'), - Timestamp('20000103')]] - assert_panel_equal(result, expected) - - result = store.select('wp', "minor_axis=['A', 'B']") - expected = wp.loc[:, :, ['A', 'B']] - assert_panel_equal(result, expected) + pytest.raises(ValueError, read_hdf, path, + 'dfq', where="A>0 or C>0") def test_same_name_scoping(self): @@ -2665,6 +2260,7 @@ def test_float_index(self): s = Series(np.random.randn(10), index=index) self._check_roundtrip(s, tm.assert_series_equal) + @xfail_non_writeable def test_tuple_index(self): # GH #492 @@ -2674,16 +2270,20 @@ def test_tuple_index(self): DF = DataFrame(data, index=idx, columns=col) with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) self._check_roundtrip(DF, tm.assert_frame_equal) + @xfail_non_writeable + @pytest.mark.filterwarnings("ignore::pandas.errors.PerformanceWarning") def test_index_types(self): - values = np.random.randn(2) + with catch_warnings(record=True): + values = np.random.randn(2) - func = lambda l, r: tm.assert_series_equal(l, r, - check_dtype=True, - check_index_type=True, - check_series_type=True) + func = lambda l, r: tm.assert_series_equal(l, r, + check_dtype=True, + check_index_type=True, + check_series_type=True) with catch_warnings(record=True): ser = Series(values, [0, 'y']) @@ -2702,24 +2302,34 @@ def test_index_types(self): self._check_roundtrip(ser, func) with catch_warnings(record=True): + + ser = Series(values, [0, 'y']) + self._check_roundtrip(ser, func) + + ser = Series(values, [datetime.datetime.today(), 0]) + self._check_roundtrip(ser, func) + + ser = Series(values, ['y', 0]) + self._check_roundtrip(ser, func) + + ser = Series(values, [datetime.date.today(), 'a']) + self._check_roundtrip(ser, func) + ser = Series(values, [1.23, 'b']) self._check_roundtrip(ser, func) - ser = Series(values, [1, 1.53]) - self._check_roundtrip(ser, func) + ser = Series(values, [1, 1.53]) + self._check_roundtrip(ser, func) - ser = Series(values, [1, 5]) - self._check_roundtrip(ser, func) + ser = Series(values, [1, 5]) + self._check_roundtrip(ser, func) - ser = Series(values, [datetime.datetime( - 2012, 1, 1), datetime.datetime(2012, 1, 2)]) - self._check_roundtrip(ser, func) + ser = Series(values, [datetime.datetime( + 2012, 1, 1), datetime.datetime(2012, 1, 2)]) + self._check_roundtrip(ser, func) def test_timeseries_preepoch(self): - if sys.version_info[0] == 2 and sys.version_info[1] < 7: - pytest.skip("won't work on Python < 2.7") - dr = bdate_range('1/1/1940', '1/1/1960') ts = Series(np.random.randn(len(dr)), index=dr) try: @@ -2727,7 +2337,11 @@ def test_timeseries_preepoch(self): except OverflowError: pytest.skip('known failer on some windows platforms') - def test_frame(self): + @xfail_non_writeable + @pytest.mark.parametrize("compression", [ + False, pytest.param(True, marks=td.skip_if_windows_python_3) + ]) + def test_frame(self, compression): df = tm.makeDataFrame() @@ -2735,32 +2349,26 @@ def test_frame(self): df.values[0, 0] = np.nan df.values[5, 3] = np.nan - self._check_roundtrip_table(df, tm.assert_frame_equal) - self._check_roundtrip(df, tm.assert_frame_equal) - - if not skip_compression: - self._check_roundtrip_table(df, tm.assert_frame_equal, - compression=True) - self._check_roundtrip(df, tm.assert_frame_equal, - compression=True) + self._check_roundtrip_table(df, tm.assert_frame_equal, + compression=compression) + self._check_roundtrip(df, tm.assert_frame_equal, + compression=compression) tdf = tm.makeTimeDataFrame() - self._check_roundtrip(tdf, tm.assert_frame_equal) - - if not skip_compression: - self._check_roundtrip(tdf, tm.assert_frame_equal, - compression=True) + self._check_roundtrip(tdf, tm.assert_frame_equal, + compression=compression) with ensure_clean_store(self.path) as store: # not consolidated df['foo'] = np.random.randn(len(df)) store['df'] = df recons = store['df'] - self.assertTrue(recons._data.is_consolidated()) + assert recons._data.is_consolidated() # empty self._check_roundtrip(df[:0], tm.assert_frame_equal) + @xfail_non_writeable def test_empty_series_frame(self): s0 = Series() s1 = Series(name='myseries') @@ -2774,8 +2382,10 @@ def test_empty_series_frame(self): self._check_roundtrip(df1, tm.assert_frame_equal) self._check_roundtrip(df2, tm.assert_frame_equal) - def test_empty_series(self): - for dtype in [np.int64, np.float64, np.object, 'm8[ns]', 'M8[ns]']: + @xfail_non_writeable + @pytest.mark.parametrize( + 'dtype', [np.int64, np.float64, np.object, 'm8[ns]', 'M8[ns]']) + def test_empty_series(self, dtype): s = Series(dtype=dtype) self._check_roundtrip(s, tm.assert_series_equal) @@ -2789,8 +2399,8 @@ def test_can_serialize_dates(self): def test_store_hierarchical(self): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['foo', 'bar']) frame = DataFrame(np.random.randn(10, 3), index=index, columns=['A', 'B', 'C']) @@ -2826,6 +2436,27 @@ def test_store_index_name_with_tz(self): recons = store['frame'] tm.assert_frame_equal(recons, df) + @pytest.mark.parametrize('table_format', ['table', 'fixed']) + def test_store_index_name_numpy_str(self, table_format): + # GH #13492 + idx = pd.Index(pd.to_datetime([datetime.date(2000, 1, 1), + datetime.date(2000, 1, 2)]), + name=u('cols\u05d2')) + idx1 = pd.Index(pd.to_datetime([datetime.date(2010, 1, 1), + datetime.date(2010, 1, 2)]), + name=u('rows\u05d0')) + df = pd.DataFrame(np.arange(4).reshape(2, 2), columns=idx, index=idx1) + + # This used to fail, returning numpy strings instead of python strings. + with ensure_clean_path(self.path) as path: + df.to_hdf(path, 'df', format=table_format) + df2 = read_hdf(path, 'df') + + assert_frame_equal(df, df2, check_names=True) + + assert type(df2.index.name) == text_type + assert type(df2.columns.name) == text_type + def test_store_series_name(self): df = tm.makeDataFrame() series = df['A'] @@ -2835,7 +2466,11 @@ def test_store_series_name(self): recons = store['series'] tm.assert_series_equal(recons, series) - def test_store_mixed(self): + @xfail_non_writeable + @pytest.mark.parametrize("compression", [ + False, pytest.param(True, marks=td.skip_if_windows_python_3) + ]) + def test_store_mixed(self, compression): def _make_one(): df = tm.makeDataFrame() @@ -2860,30 +2495,16 @@ def _make_one(): tm.assert_frame_equal(store['obj'], df2) # check that can store Series of all of these types - self._check_roundtrip(df1['obj1'], tm.assert_series_equal) - self._check_roundtrip(df1['bool1'], tm.assert_series_equal) - self._check_roundtrip(df1['int1'], tm.assert_series_equal) - - if not skip_compression: - self._check_roundtrip(df1['obj1'], tm.assert_series_equal, - compression=True) - self._check_roundtrip(df1['bool1'], tm.assert_series_equal, - compression=True) - self._check_roundtrip(df1['int1'], tm.assert_series_equal, - compression=True) - self._check_roundtrip(df1, tm.assert_frame_equal, - compression=True) - - def test_wide(self): - - wp = tm.makePanel() - self._check_roundtrip(wp, assert_panel_equal) - - def test_wide_table(self): - - wp = tm.makePanel() - self._check_roundtrip_table(wp, assert_panel_equal) - + self._check_roundtrip(df1['obj1'], tm.assert_series_equal, + compression=compression) + self._check_roundtrip(df1['bool1'], tm.assert_series_equal, + compression=compression) + self._check_roundtrip(df1['int1'], tm.assert_series_equal, + compression=compression) + + @pytest.mark.filterwarnings( + "ignore:\\nduplicate:pandas.io.pytables.DuplicateWarning" + ) def test_select_with_dups(self): # single dtypes @@ -2905,7 +2526,7 @@ def test_select_with_dups(self): expected = df.loc[:, ['A']] assert_frame_equal(result, expected) - # dups accross dtypes + # dups across dtypes df = concat([DataFrame(np.random.randn(10, 4), columns=['A', 'A', 'B', 'B']), DataFrame(np.random.randint(0, 10, size=20) @@ -2943,30 +2564,6 @@ def test_select_with_dups(self): result = store.select('df', columns=['B', 'A']) assert_frame_equal(result, expected, by_blocks=True) - def test_wide_table_dups(self): - wp = tm.makePanel() - with ensure_clean_store(self.path) as store: - store.put('panel', wp, format='table') - store.put('panel', wp, format='table', append=True) - - with catch_warnings(record=True): - recons = store['panel'] - - assert_panel_equal(recons, wp) - - def test_long(self): - def _check(left, right): - assert_panel_equal(left.to_panel(), right.to_panel()) - - wp = tm.makePanel() - self._check_roundtrip(wp.to_frame(), _check) - - # empty - # self._check_roundtrip(wp.to_frame()[:0], _check) - - def test_longpanel(self): - pass - def test_overwrite_node(self): with ensure_clean_store(self.path) as store: @@ -3009,70 +2606,44 @@ def test_sparse_with_compression(self): check_frame_type=True) def test_select(self): - wp = tm.makePanel() with ensure_clean_store(self.path) as store: - # put/select ok - _maybe_remove(store, 'wp') - store.put('wp', wp, format='table') - store.select('wp') - - # non-table ok (where = None) - _maybe_remove(store, 'wp') - store.put('wp2', wp) - store.select('wp2') - - # selection on the non-indexable with a large number of columns - wp = Panel(np.random.randn(100, 100, 100), - items=['Item%03d' % i for i in range(100)], - major_axis=date_range('1/1/2000', periods=100), - minor_axis=['E%03d' % i for i in range(100)]) - - _maybe_remove(store, 'wp') - store.append('wp', wp) - items = ['Item%03d' % i for i in range(80)] - result = store.select('wp', Term('items=items')) - expected = wp.reindex(items=items) - assert_panel_equal(expected, result) - - # selectin non-table with a where - # self.assertRaises(ValueError, store.select, - # 'wp2', ('column', ['A', 'D'])) - - # select with columns= - df = tm.makeTimeDataFrame() - _maybe_remove(store, 'df') - store.append('df', df) - result = store.select('df', columns=['A', 'B']) - expected = df.reindex(columns=['A', 'B']) - tm.assert_frame_equal(expected, result) + with catch_warnings(record=True): - # equivalentsly - result = store.select('df', [("columns=['A', 'B']")]) - expected = df.reindex(columns=['A', 'B']) - tm.assert_frame_equal(expected, result) + # select with columns= + df = tm.makeTimeDataFrame() + _maybe_remove(store, 'df') + store.append('df', df) + result = store.select('df', columns=['A', 'B']) + expected = df.reindex(columns=['A', 'B']) + tm.assert_frame_equal(expected, result) - # with a data column - _maybe_remove(store, 'df') - store.append('df', df, data_columns=['A']) - result = store.select('df', ['A > 0'], columns=['A', 'B']) - expected = df[df.A > 0].reindex(columns=['A', 'B']) - tm.assert_frame_equal(expected, result) + # equivalentsly + result = store.select('df', [("columns=['A', 'B']")]) + expected = df.reindex(columns=['A', 'B']) + tm.assert_frame_equal(expected, result) - # all a data columns - _maybe_remove(store, 'df') - store.append('df', df, data_columns=True) - result = store.select('df', ['A > 0'], columns=['A', 'B']) - expected = df[df.A > 0].reindex(columns=['A', 'B']) - tm.assert_frame_equal(expected, result) + # with a data column + _maybe_remove(store, 'df') + store.append('df', df, data_columns=['A']) + result = store.select('df', ['A > 0'], columns=['A', 'B']) + expected = df[df.A > 0].reindex(columns=['A', 'B']) + tm.assert_frame_equal(expected, result) - # with a data column, but different columns - _maybe_remove(store, 'df') - store.append('df', df, data_columns=['A']) - result = store.select('df', ['A > 0'], columns=['C', 'D']) - expected = df[df.A > 0].reindex(columns=['C', 'D']) - tm.assert_frame_equal(expected, result) + # all a data columns + _maybe_remove(store, 'df') + store.append('df', df, data_columns=True) + result = store.select('df', ['A > 0'], columns=['A', 'B']) + expected = df[df.A > 0].reindex(columns=['A', 'B']) + tm.assert_frame_equal(expected, result) + + # with a data column, but different columns + _maybe_remove(store, 'df') + store.append('df', df, data_columns=['A']) + result = store.select('df', ['A > 0'], columns=['C', 'D']) + expected = df[df.A > 0].reindex(columns=['C', 'D']) + tm.assert_frame_equal(expected, result) def test_select_dtypes(self): @@ -3084,7 +2655,7 @@ def test_select_dtypes(self): _maybe_remove(store, 'df') store.append('df', df, data_columns=['ts', 'A']) - result = store.select('df', [Term("ts>=Timestamp('2012-02-01')")]) + result = store.select('df', "ts>=Timestamp('2012-02-01')") expected = df[df.ts >= Timestamp('2012-02-01')] tm.assert_frame_equal(expected, result) @@ -3099,15 +2670,15 @@ def test_select_dtypes(self): expected = (df[df.boolv == True] # noqa .reindex(columns=['A', 'boolv'])) for v in [True, 'true', 1]: - result = store.select('df', Term( - 'boolv == %s' % str(v)), columns=['A', 'boolv']) + result = store.select('df', 'boolv == %s' % str(v), + columns=['A', 'boolv']) tm.assert_frame_equal(expected, result) expected = (df[df.boolv == False] # noqa .reindex(columns=['A', 'boolv'])) for v in [False, 'false', 0]: - result = store.select('df', Term( - 'boolv == %s' % str(v)), columns=['A', 'boolv']) + result = store.select( + 'df', 'boolv == %s' % str(v), columns=['A', 'boolv']) tm.assert_frame_equal(expected, result) # integer index @@ -3115,7 +2686,7 @@ def test_select_dtypes(self): _maybe_remove(store, 'df_int') store.append('df_int', df) result = store.select( - 'df_int', [Term("index<10"), Term("columns=['A']")]) + 'df_int', "index<10 and columns=['A']") expected = df.reindex(index=list(df.index)[0:10], columns=['A']) tm.assert_frame_equal(expected, result) @@ -3125,7 +2696,7 @@ def test_select_dtypes(self): _maybe_remove(store, 'df_float') store.append('df_float', df) result = store.select( - 'df_float', [Term("index<10.0"), Term("columns=['A']")]) + 'df_float', "index<10.0 and columns=['A']") expected = df.reindex(index=list(df.index)[0:10], columns=['A']) tm.assert_frame_equal(expected, result) @@ -3196,14 +2767,14 @@ def test_select_with_many_inputs(self): store.append('df', df, data_columns=['ts', 'A', 'B', 'users']) # regular select - result = store.select('df', [Term("ts>=Timestamp('2012-02-01')")]) + result = store.select('df', "ts>=Timestamp('2012-02-01')") expected = df[df.ts >= Timestamp('2012-02-01')] tm.assert_frame_equal(expected, result) # small selector result = store.select( - 'df', [Term("ts>=Timestamp('2012-02-01') & " - "users=['a','b','c']")]) + 'df', + "ts>=Timestamp('2012-02-01') & users=['a','b','c']") expected = df[(df.ts >= Timestamp('2012-02-01')) & df.users.isin(['a', 'b', 'c'])] tm.assert_frame_equal(expected, result) @@ -3211,24 +2782,24 @@ def test_select_with_many_inputs(self): # big selector along the columns selector = ['a', 'b', 'c'] + ['a%03d' % i for i in range(60)] result = store.select( - 'df', [Term("ts>=Timestamp('2012-02-01')"), - Term('users=selector')]) + 'df', + "ts>=Timestamp('2012-02-01') and users=selector") expected = df[(df.ts >= Timestamp('2012-02-01')) & df.users.isin(selector)] tm.assert_frame_equal(expected, result) selector = range(100, 200) - result = store.select('df', [Term('B=selector')]) + result = store.select('df', 'B=selector') expected = df[df.B.isin(selector)] tm.assert_frame_equal(expected, result) - self.assertEqual(len(result), 100) + assert len(result) == 100 # big selector along the index selector = Index(df.ts[0:100].values) - result = store.select('df', [Term('ts=selector')]) + result = store.select('df', 'ts=selector') expected = df[df.ts.isin(selector.values)] tm.assert_frame_equal(expected, result) - self.assertEqual(len(result), 100) + assert len(result) == 100 def test_select_iterator(self): @@ -3246,7 +2817,7 @@ def test_select_iterator(self): tm.assert_frame_equal(expected, result) results = [s for s in store.select('df', chunksize=100)] - self.assertEqual(len(results), 5) + assert len(results) == 5 result = concat(results) tm.assert_frame_equal(expected, result) @@ -3258,10 +2829,10 @@ def test_select_iterator(self): df = tm.makeTimeDataFrame(500) df.to_hdf(path, 'df_non_table') - self.assertRaises(TypeError, read_hdf, path, - 'df_non_table', chunksize=100) - self.assertRaises(TypeError, read_hdf, path, - 'df_non_table', iterator=True) + pytest.raises(TypeError, read_hdf, path, + 'df_non_table', chunksize=100) + pytest.raises(TypeError, read_hdf, path, + 'df_non_table', iterator=True) with ensure_clean_path(self.path) as path: @@ -3271,7 +2842,7 @@ def test_select_iterator(self): results = [s for s in read_hdf(path, 'df', chunksize=100)] result = concat(results) - self.assertEqual(len(results), 5) + assert len(results) == 5 tm.assert_frame_equal(result, df) tm.assert_frame_equal(result, read_hdf(path, 'df')) @@ -3296,17 +2867,6 @@ def test_select_iterator(self): result = concat(results) tm.assert_frame_equal(expected, result) - # where selection - # expected = store.select_as_multiple( - # ['df1', 'df2'], where= Term('A>0'), selector='df1') - # results = [] - # for s in store.select_as_multiple( - # ['df1', 'df2'], where= Term('A>0'), selector='df1', - # chunksize=25): - # results.append(s) - # result = concat(results) - # tm.assert_frame_equal(expected, result) - def test_select_iterator_complete_8014(self): # GH 8014 @@ -3435,7 +2995,7 @@ def test_select_iterator_non_complete_8014(self): where = "index > '%s'" % end_dt results = [s for s in store.select( 'df', where=where, chunksize=chunksize)] - self.assertEqual(0, len(results)) + assert 0 == len(results) def test_select_iterator_many_empty_frames(self): @@ -3467,7 +3027,7 @@ def test_select_iterator_many_empty_frames(self): results = [s for s in store.select( 'df', where=where, chunksize=chunksize)] - tm.assert_equal(1, len(results)) + assert len(results) == 1 result = concat(results) rexpected = expected[expected.index <= end_dt] tm.assert_frame_equal(rexpected, result) @@ -3478,7 +3038,7 @@ def test_select_iterator_many_empty_frames(self): 'df', where=where, chunksize=chunksize)] # should be 1, is 10 - tm.assert_equal(1, len(results)) + assert len(results) == 1 result = concat(results) rexpected = expected[(expected.index >= beg_dt) & (expected.index <= end_dt)] @@ -3496,8 +3056,11 @@ def test_select_iterator_many_empty_frames(self): 'df', where=where, chunksize=chunksize)] # should be [] - tm.assert_equal(0, len(results)) + assert len(results) == 0 + @pytest.mark.filterwarnings( + "ignore:\\nthe :pandas.io.pytables.AttributeConflictWarning" + ) def test_retain_index_attributes(self): # GH 3499, losing frequency info on index recreation @@ -3514,19 +3077,18 @@ def test_retain_index_attributes(self): for attr in ['freq', 'tz', 'name']: for idx in ['index', 'columns']: - self.assertEqual(getattr(getattr(df, idx), attr, None), - getattr(getattr(result, idx), attr, None)) + assert (getattr(getattr(df, idx), attr, None) == + getattr(getattr(result, idx), attr, None)) # try to append a table with a different frequency - with tm.assert_produces_warning( - expected_warning=AttributeConflictWarning): + with catch_warnings(record=True): df2 = DataFrame(dict( A=Series(lrange(3), index=date_range('2002-1-1', periods=3, freq='D')))) store.append('data', df2) - self.assertIsNone(store.get_storer('data').info['index']['freq']) + assert store.get_storer('data').info['index']['freq'] is None # this is ok _maybe_remove(store, 'df2') @@ -3541,12 +3103,13 @@ def test_retain_index_attributes(self): freq='D')))) store.append('df2', df3) + @pytest.mark.filterwarnings( + "ignore:\\nthe :pandas.io.pytables.AttributeConflictWarning" + ) def test_retain_index_attributes2(self): with ensure_clean_path(self.path) as path: - expected_warning = Warning if PY35 else AttributeConflictWarning - with tm.assert_produces_warning(expected_warning=expected_warning, - check_stacklevel=False): + with catch_warnings(record=True): df = DataFrame(dict( A=Series(lrange(3), @@ -3564,37 +3127,16 @@ def test_retain_index_attributes2(self): df = DataFrame(dict(A=Series(lrange(3), index=idx))) df.to_hdf(path, 'data', mode='w', append=True) - self.assertEqual(read_hdf(path, 'data').index.name, 'foo') + assert read_hdf(path, 'data').index.name == 'foo' - with tm.assert_produces_warning(expected_warning=expected_warning, - check_stacklevel=False): + with catch_warnings(record=True): idx2 = date_range('2001-1-1', periods=3, freq='H') idx2.name = 'bar' df2 = DataFrame(dict(A=Series(lrange(3), index=idx2))) df2.to_hdf(path, 'data', append=True) - self.assertIsNone(read_hdf(path, 'data').index.name) - - def test_panel_select(self): - - wp = tm.makePanel() - - with ensure_clean_store(self.path) as store: - store.put('wp', wp, format='table') - date = wp.major_axis[len(wp.major_axis) // 2] - - crit1 = ('major_axis>=date') - crit2 = ("minor_axis=['A', 'D']") - - result = store.select('wp', [crit1, crit2]) - expected = wp.truncate(before=date).reindex(minor=['A', 'D']) - assert_panel_equal(result, expected) - - result = store.select( - 'wp', ['major_axis>="20000124"', ("minor_axis=['A', 'B']")]) - expected = wp.truncate(before='20000124').reindex(minor=['A', 'B']) - assert_panel_equal(result, expected) + assert read_hdf(path, 'data').index.name is None def test_frame_select(self): @@ -3605,7 +3147,7 @@ def test_frame_select(self): date = df.index[len(df) // 2] crit1 = Term('index>=date') - self.assertEqual(crit1.env.scope['date'], date) + assert crit1.env.scope['date'] == date crit2 = ("columns=['A', 'D']") crit3 = ('columns=A') @@ -3621,12 +3163,12 @@ def test_frame_select(self): # invalid terms df = tm.makeTimeDataFrame() store.append('df_time', df) - self.assertRaises( - ValueError, store.select, 'df_time', [Term("index>0")]) + pytest.raises( + ValueError, store.select, 'df_time', "index>0") # can't select if not written as table # store['frame'] = df - # self.assertRaises(ValueError, store.select, + # pytest.raises(ValueError, store.select, # 'frame', [crit1, crit2]) def test_frame_select_complex(self): @@ -3665,8 +3207,8 @@ def test_frame_select_complex(self): tm.assert_frame_equal(result, expected) # invert not implemented in numexpr :( - self.assertRaises(NotImplementedError, - store.select, 'df', '~(string="bar")') + pytest.raises(NotImplementedError, + store.select, 'df', '~(string="bar")') # invert ok for filters result = store.select('df', "~(columns=['A','B'])") @@ -3701,7 +3243,7 @@ def test_frame_select_complex2(self): hist.to_hdf(hh, 'df', mode='w', format='table') - expected = read_hdf(hh, 'df', where="l1=[2, 3, 4]") + expected = read_hdf(hh, 'df', where='l1=[2, 3, 4]') # sccope with list like l = selection.index.tolist() # noqa @@ -3754,12 +3296,12 @@ def test_invalid_filtering(self): store.put('df', df, format='table') # not implemented - self.assertRaises(NotImplementedError, store.select, - 'df', "columns=['A'] | columns=['B']") + pytest.raises(NotImplementedError, store.select, + 'df', "columns=['A'] | columns=['B']") # in theory we could deal with this - self.assertRaises(NotImplementedError, store.select, - 'df', "columns=['A','B'] & columns=['C']") + pytest.raises(NotImplementedError, store.select, + 'df', "columns=['A','B'] & columns=['C']") def test_string_select(self): # GH 2973 @@ -3791,7 +3333,7 @@ def test_string_select(self): store.append('df2', df2, data_columns=['x']) result = store.select('df2', 'x!=none') - expected = df2[isnull(df2.x)] + expected = df2[isna(df2.x)] assert_frame_equal(result, expected) # int ==/!= @@ -3814,22 +3356,29 @@ def test_read_column(self): with ensure_clean_store(self.path) as store: _maybe_remove(store, 'df') - store.append('df', df) + # GH 17912 + # HDFStore.select_column should raise a KeyError + # exception if the key is not a valid store + with pytest.raises(KeyError, + match='No object named df in the file'): + store.select_column('df', 'index') + + store.append('df', df) # error - self.assertRaises(KeyError, store.select_column, 'df', 'foo') + pytest.raises(KeyError, store.select_column, 'df', 'foo') def f(): store.select_column('df', 'index', where=['index>5']) - self.assertRaises(Exception, f) + pytest.raises(Exception, f) # valid result = store.select_column('df', 'index') tm.assert_almost_equal(result.values, Series(df.index).values) - self.assertIsInstance(result, Series) + assert isinstance(result, Series) # not a data indexable column - self.assertRaises( + pytest.raises( ValueError, store.select_column, 'df', 'values_block_0') # a data column @@ -3901,7 +3450,7 @@ def test_coordinates(self): result = store.select('df', where=c) expected = df.loc[3:4, :] tm.assert_frame_equal(result, expected) - self.assertIsInstance(c, Index) + assert isinstance(c, Index) # multiple tables _maybe_remove(store, 'df1') @@ -3939,14 +3488,14 @@ def test_coordinates(self): tm.assert_frame_equal(result, expected) # invalid - self.assertRaises(ValueError, store.select, 'df', - where=np.arange(len(df), dtype='float64')) - self.assertRaises(ValueError, store.select, 'df', - where=np.arange(len(df) + 1)) - self.assertRaises(ValueError, store.select, 'df', - where=np.arange(len(df)), start=5) - self.assertRaises(ValueError, store.select, 'df', - where=np.arange(len(df)), start=5, stop=10) + pytest.raises(ValueError, store.select, 'df', + where=np.arange(len(df), dtype='float64')) + pytest.raises(ValueError, store.select, 'df', + where=np.arange(len(df) + 1)) + pytest.raises(ValueError, store.select, 'df', + where=np.arange(len(df)), start=5) + pytest.raises(ValueError, store.select, 'df', + where=np.arange(len(df)), start=5, stop=10) # selection with filter selection = date_range('20000101', periods=500) @@ -3982,12 +3531,12 @@ def test_append_to_multiple(self): with ensure_clean_store(self.path) as store: # exceptions - self.assertRaises(ValueError, store.append_to_multiple, - {'df1': ['A', 'B'], 'df2': None}, df, - selector='df3') - self.assertRaises(ValueError, store.append_to_multiple, - {'df1': None, 'df2': None}, df, selector='df3') - self.assertRaises( + pytest.raises(ValueError, store.append_to_multiple, + {'df1': ['A', 'B'], 'df2': None}, df, + selector='df3') + pytest.raises(ValueError, store.append_to_multiple, + {'df1': None, 'df2': None}, df, selector='df3') + pytest.raises( ValueError, store.append_to_multiple, 'df1', df, 'df1') # regular operation @@ -4005,6 +3554,7 @@ def test_append_to_multiple_dropna(self): df = concat([df1, df2], axis=1) with ensure_clean_store(self.path) as store: + # dropna=True should guarantee rows are synchronized store.append_to_multiple( {'df1': ['A', 'B'], 'df2': None}, df, selector='df1', @@ -4015,14 +3565,27 @@ def test_append_to_multiple_dropna(self): tm.assert_index_equal(store.select('df1').index, store.select('df2').index) + @pytest.mark.xfail(run=False, + reason="append_to_multiple_dropna_false " + "is not raising as failed") + def test_append_to_multiple_dropna_false(self): + df1 = tm.makeTimeDataFrame() + df2 = tm.makeTimeDataFrame().rename(columns=lambda x: "%s_2" % x) + df1.iloc[1, df1.columns.get_indexer(['A', 'B'])] = np.nan + df = concat([df1, df2], axis=1) + + with ensure_clean_store(self.path) as store: + # dropna=False shouldn't synchronize row indexes store.append_to_multiple( - {'df1': ['A', 'B'], 'df2': None}, df, selector='df1', + {'df1a': ['A', 'B'], 'df2a': None}, df, selector='df1a', dropna=False) - self.assertRaises( - ValueError, store.select_as_multiple, ['df1', 'df2']) - assert not store.select('df1').index.equals( - store.select('df2').index) + + with pytest.raises(ValueError): + store.select_as_multiple(['df1a', 'df2a']) + + assert not store.select('df1a').index.equals( + store.select('df2a').index) def test_select_as_multiple(self): @@ -4033,25 +3596,25 @@ def test_select_as_multiple(self): with ensure_clean_store(self.path) as store: # no tables stored - self.assertRaises(Exception, store.select_as_multiple, - None, where=['A>0', 'B>0'], selector='df1') + pytest.raises(Exception, store.select_as_multiple, + None, where=['A>0', 'B>0'], selector='df1') store.append('df1', df1, data_columns=['A', 'B']) store.append('df2', df2) # exceptions - self.assertRaises(Exception, store.select_as_multiple, - None, where=['A>0', 'B>0'], selector='df1') - self.assertRaises(Exception, store.select_as_multiple, - [None], where=['A>0', 'B>0'], selector='df1') - self.assertRaises(KeyError, store.select_as_multiple, - ['df1', 'df3'], where=['A>0', 'B>0'], - selector='df1') - self.assertRaises(KeyError, store.select_as_multiple, - ['df3'], where=['A>0', 'B>0'], selector='df1') - self.assertRaises(KeyError, store.select_as_multiple, - ['df1', 'df2'], where=['A>0', 'B>0'], - selector='df4') + pytest.raises(Exception, store.select_as_multiple, + None, where=['A>0', 'B>0'], selector='df1') + pytest.raises(Exception, store.select_as_multiple, + [None], where=['A>0', 'B>0'], selector='df1') + pytest.raises(KeyError, store.select_as_multiple, + ['df1', 'df3'], where=['A>0', 'B>0'], + selector='df1') + pytest.raises(KeyError, store.select_as_multiple, + ['df3'], where=['A>0', 'B>0'], selector='df1') + pytest.raises(KeyError, store.select_as_multiple, + ['df1', 'df2'], where=['A>0', 'B>0'], + selector='df4') # default select result = store.select('df1', ['A>0', 'B>0']) @@ -4078,17 +3641,16 @@ def test_select_as_multiple(self): # test excpection for diff rows store.append('df3', tm.makeTimeDataFrame(nper=50)) - self.assertRaises(ValueError, store.select_as_multiple, - ['df1', 'df3'], where=['A>0', 'B>0'], - selector='df1') - + pytest.raises(ValueError, store.select_as_multiple, + ['df1', 'df3'], where=['A>0', 'B>0'], + selector='df1') + + @pytest.mark.skipif( + LooseVersion(tables.__version__) < LooseVersion('3.1.0'), + reason=("tables version does not support fix for nan selection " + "bug: GH 4858")) def test_nan_selection_bug_4858(self): - # GH 4858; nan selection bug, only works for pytables >= 3.1 - if LooseVersion(tables.__version__) < '3.1.0': - pytest.skip('tables version does not support fix for nan ' - 'selection bug: GH 4858') - with ensure_clean_store(self.path) as store: df = DataFrame(dict(cols=range(6), values=range(6)), @@ -4120,10 +3682,25 @@ def test_start_stop_table(self): # out of range result = store.select( 'df', "columns=['A']", start=30, stop=40) - self.assertTrue(len(result) == 0) + assert len(result) == 0 expected = df.loc[30:40, ['A']] tm.assert_frame_equal(result, expected) + def test_start_stop_multiple(self): + + # GH 16209 + with ensure_clean_store(self.path) as store: + + df = DataFrame({"foo": [1, 2], "bar": [1, 2]}) + + store.append_to_multiple({'selector': ['foo'], 'data': None}, df, + selector='selector') + result = store.select_as_multiple(['selector', 'data'], + selector='selector', start=0, + stop=1) + expected = df.loc[[0], ['foo', 'bar']] + tm.assert_frame_equal(result, expected) + def test_start_stop_fixed(self): with ensure_clean_store(self.path) as store: @@ -4167,7 +3744,7 @@ def test_start_stop_fixed(self): df.iloc[8:10, -2] = np.nan dfs = df.to_sparse() store.put('dfs', dfs) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): store.select('dfs', start=0, stop=5) def test_select_filter_corner(self): @@ -4187,6 +3764,62 @@ def test_select_filter_corner(self): result = store.select('frame', [crit]) tm.assert_frame_equal(result, df.loc[:, df.columns[:75:2]]) + def test_path_pathlib(self): + df = tm.makeDataFrame() + + result = tm.round_trip_pathlib( + lambda p: df.to_hdf(p, 'df'), + lambda p: pd.read_hdf(p, 'df')) + tm.assert_frame_equal(df, result) + + @pytest.mark.parametrize('start, stop', [(0, 2), (1, 2), (None, None)]) + def test_contiguous_mixed_data_table(self, start, stop): + # GH 17021 + # ValueError when reading a contiguous mixed-data table ft. VLArray + df = DataFrame({'a': Series([20111010, 20111011, 20111012]), + 'b': Series(['ab', 'cd', 'ab'])}) + + with ensure_clean_store(self.path) as store: + store.append('test_dataset', df) + + result = store.select('test_dataset', start=start, stop=stop) + assert_frame_equal(df[start:stop], result) + + def test_path_pathlib_hdfstore(self): + df = tm.makeDataFrame() + + def writer(path): + with pd.HDFStore(path) as store: + df.to_hdf(store, 'df') + + def reader(path): + with pd.HDFStore(path) as store: + return pd.read_hdf(store, 'df') + + result = tm.round_trip_pathlib(writer, reader) + tm.assert_frame_equal(df, result) + + def test_pickle_path_localpath(self): + df = tm.makeDataFrame() + result = tm.round_trip_pathlib( + lambda p: df.to_hdf(p, 'df'), + lambda p: pd.read_hdf(p, 'df')) + tm.assert_frame_equal(df, result) + + def test_path_localpath_hdfstore(self): + df = tm.makeDataFrame() + + def writer(path): + with pd.HDFStore(path) as store: + df.to_hdf(store, 'df') + + def reader(path): + with pd.HDFStore(path) as store: + return pd.read_hdf(store, 'df') + + result = tm.round_trip_localpath(writer, reader) + tm.assert_frame_equal(df, result) + def _check_roundtrip(self, obj, comparator, compression=False, **kwargs): options = {} @@ -4220,11 +3853,11 @@ def _check_roundtrip_table(self, obj, comparator, compression=False): with ensure_clean_store(self.path, 'w', **options) as store: store.put('obj', obj, format='table') retrieved = store['obj'] - # sorted_obj = _test_sort(obj) + comparator(retrieved, obj) def test_multiple_open_close(self): - # GH 4409, open & close multiple times + # gh-4409: open & close multiple times with ensure_clean_path(self.path) as path: @@ -4233,11 +3866,12 @@ def test_multiple_open_close(self): # single store = HDFStore(path) - self.assertNotIn('CLOSED', str(store)) - self.assertTrue(store.is_open) + assert 'CLOSED' not in store.info() + assert store.is_open + store.close() - self.assertIn('CLOSED', str(store)) - self.assertFalse(store.is_open) + assert 'CLOSED' in store.info() + assert not store.is_open with ensure_clean_path(self.path) as path: @@ -4248,7 +3882,7 @@ def test_multiple_open_close(self): def f(): HDFStore(path) - self.assertRaises(ValueError, f) + pytest.raises(ValueError, f) store1.close() else: @@ -4257,22 +3891,22 @@ def f(): store1 = HDFStore(path) store2 = HDFStore(path) - self.assertNotIn('CLOSED', str(store1)) - self.assertNotIn('CLOSED', str(store2)) - self.assertTrue(store1.is_open) - self.assertTrue(store2.is_open) + assert 'CLOSED' not in store1.info() + assert 'CLOSED' not in store2.info() + assert store1.is_open + assert store2.is_open store1.close() - self.assertIn('CLOSED', str(store1)) - self.assertFalse(store1.is_open) - self.assertNotIn('CLOSED', str(store2)) - self.assertTrue(store2.is_open) + assert 'CLOSED' in store1.info() + assert not store1.is_open + assert 'CLOSED' not in store2.info() + assert store2.is_open store2.close() - self.assertIn('CLOSED', str(store1)) - self.assertIn('CLOSED', str(store2)) - self.assertFalse(store1.is_open) - self.assertFalse(store2.is_open) + assert 'CLOSED' in store1.info() + assert 'CLOSED' in store2.info() + assert not store1.is_open + assert not store2.is_open # nested close store = HDFStore(path, mode='w') @@ -4281,12 +3915,12 @@ def f(): store2 = HDFStore(path) store2.append('df2', df) store2.close() - self.assertIn('CLOSED', str(store2)) - self.assertFalse(store2.is_open) + assert 'CLOSED' in store2.info() + assert not store2.is_open store.close() - self.assertIn('CLOSED', str(store)) - self.assertFalse(store.is_open) + assert 'CLOSED' in store.info() + assert not store.is_open # double closing store = HDFStore(path, mode='w') @@ -4294,12 +3928,12 @@ def f(): store2 = HDFStore(path) store.close() - self.assertIn('CLOSED', str(store)) - self.assertFalse(store.is_open) + assert 'CLOSED' in store.info() + assert not store.is_open store2.close() - self.assertIn('CLOSED', str(store2)) - self.assertFalse(store2.is_open) + assert 'CLOSED' in store2.info() + assert not store2.is_open # ops on a closed store with ensure_clean_path(self.path) as path: @@ -4310,100 +3944,75 @@ def f(): store = HDFStore(path) store.close() - self.assertRaises(ClosedFileError, store.keys) - self.assertRaises(ClosedFileError, lambda: 'df' in store) - self.assertRaises(ClosedFileError, lambda: len(store)) - self.assertRaises(ClosedFileError, lambda: store['df']) - self.assertRaises(ClosedFileError, lambda: store.df) - self.assertRaises(ClosedFileError, store.select, 'df') - self.assertRaises(ClosedFileError, store.get, 'df') - self.assertRaises(ClosedFileError, store.append, 'df2', df) - self.assertRaises(ClosedFileError, store.put, 'df3', df) - self.assertRaises(ClosedFileError, store.get_storer, 'df2') - self.assertRaises(ClosedFileError, store.remove, 'df2') - - def f(): + pytest.raises(ClosedFileError, store.keys) + pytest.raises(ClosedFileError, lambda: 'df' in store) + pytest.raises(ClosedFileError, lambda: len(store)) + pytest.raises(ClosedFileError, lambda: store['df']) + pytest.raises(AttributeError, lambda: store.df) + pytest.raises(ClosedFileError, store.select, 'df') + pytest.raises(ClosedFileError, store.get, 'df') + pytest.raises(ClosedFileError, store.append, 'df2', df) + pytest.raises(ClosedFileError, store.put, 'df3', df) + pytest.raises(ClosedFileError, store.get_storer, 'df2') + pytest.raises(ClosedFileError, store.remove, 'df2') + + with pytest.raises(ClosedFileError, match='file is not open'): store.select('df') - tm.assertRaisesRegexp(ClosedFileError, 'file is not open', f) - - def test_pytables_native_read(self): + def test_pytables_native_read(self, datapath): with ensure_clean_store( - tm.get_data_path('legacy_hdf/pytables_native.h5'), + datapath('io', 'data', 'legacy_hdf/pytables_native.h5'), mode='r') as store: d2 = store['detector/readout'] - self.assertIsInstance(d2, DataFrame) - - def test_pytables_native2_read(self): - # fails on win/3.5 oddly - if PY35 and is_platform_windows(): - pytest.skip("native2 read fails oddly on windows / 3.5") + assert isinstance(d2, DataFrame) + @pytest.mark.skipif(PY35 and is_platform_windows(), + reason="native2 read fails oddly on windows / 3.5") + def test_pytables_native2_read(self, datapath): with ensure_clean_store( - tm.get_data_path('legacy_hdf/pytables_native2.h5'), + datapath('io', 'data', 'legacy_hdf', 'pytables_native2.h5'), mode='r') as store: str(store) d1 = store['detector'] - self.assertIsInstance(d1, DataFrame) + assert isinstance(d1, DataFrame) - def test_legacy_table_read(self): - # legacy table types + @xfail_non_writeable + def test_legacy_table_fixed_format_read_py2(self, datapath): + # GH 24510 + # legacy table with fixed format written in Python 2 with ensure_clean_store( - tm.get_data_path('legacy_hdf/legacy_table.h5'), + datapath('io', 'data', 'legacy_hdf', + 'legacy_table_fixed_py2.h5'), mode='r') as store: - store.select('df1') - store.select('df2') - store.select('wp1') - - # force the frame - store.select('df2', typ='legacy_frame') - - # old version warning - with tm.assert_produces_warning( - expected_warning=IncompatibilityWarning): - self.assertRaises( - Exception, store.select, 'wp1', 'minor_axis=B') - - df2 = store.select('df2') - result = store.select('df2', 'index>df2.index[2]') - expected = df2[df2.index > df2.index[2]] - assert_frame_equal(expected, result) - - def test_legacy_0_10_read(self): - # legacy from 0.10 - with catch_warnings(record=True): - path = tm.get_data_path('legacy_hdf/legacy_0.10.h5') - with ensure_clean_store(path, mode='r') as store: - str(store) - for k in store.keys(): - store.select(k) - - def test_legacy_0_11_read(self): - # legacy from 0.11 - path = os.path.join('legacy_hdf', 'legacy_table_0.11.h5') - with ensure_clean_store(tm.get_data_path(path), mode='r') as store: - str(store) - assert 'df' in store - assert 'df1' in store - assert 'mi' in store - df = store.select('df') - df1 = store.select('df1') - mi = store.select('mi') - assert isinstance(df, DataFrame) - assert isinstance(df1, DataFrame) - assert isinstance(mi, DataFrame) + result = store.select('df') + expected = pd.DataFrame([[1, 2, 3, 'D']], + columns=['A', 'B', 'C', 'D'], + index=pd.Index(['ABC'], + name='INDEX_NAME')) + assert_frame_equal(expected, result) + + def test_legacy_table_read_py2(self, datapath): + # issue: 24925 + # legacy table written in Python 2 + with ensure_clean_store( + datapath('io', 'data', 'legacy_hdf', + 'legacy_table_py2.h5'), + mode='r') as store: + result = store.select('table') + + expected = pd.DataFrame({ + "a": ["a", "b"], + "b": [2, 3] + }) + assert_frame_equal(expected, result) def test_copy(self): with catch_warnings(record=True): - def do_copy(f=None, new_f=None, keys=None, + def do_copy(f, new_f=None, keys=None, propindexes=True, **kwargs): try: - if f is None: - f = tm.get_data_path(os.path.join('legacy_hdf', - 'legacy_0.10.h5')) - store = HDFStore(f, 'r') if new_f is None: @@ -4416,36 +4025,31 @@ def do_copy(f=None, new_f=None, keys=None, # check keys if keys is None: keys = store.keys() - self.assertEqual(set(keys), set(tstore.keys())) + assert set(keys) == set(tstore.keys()) - # check indicies & nrows + # check indices & nrows for k in tstore.keys(): if tstore.get_storer(k).is_table: new_t = tstore.get_storer(k) orig_t = store.get_storer(k) - self.assertEqual(orig_t.nrows, new_t.nrows) + assert orig_t.nrows == new_t.nrows # check propindixes if propindexes: for a in orig_t.axes: if a.is_indexed: - self.assertTrue( - new_t[a.name].is_indexed) + assert new_t[a.name].is_indexed finally: safe_close(store) safe_close(tstore) try: os.close(fd) - except: + except (OSError, ValueError): pass safe_remove(new_f) - do_copy() - do_copy(keys=['/a', '/b', '/df1_mixed']) - do_copy(propindexes=False) - # new table df = tm.makeDataFrame() @@ -4459,37 +4063,13 @@ def do_copy(f=None, new_f=None, keys=None, finally: safe_remove(path) - def test_legacy_table_write(self): - pytest.skip("cannot write legacy tables") - - store = HDFStore(tm.get_data_path( - 'legacy_hdf/legacy_table_%s.h5' % pandas.__version__), 'a') - - df = tm.makeDataFrame() - wp = tm.makePanel() - - index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], - ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], - names=['foo', 'bar']) - df = DataFrame(np.random.randn(10, 3), index=index, - columns=['A', 'B', 'C']) - store.append('mi', df) - - df = DataFrame(dict(A='foo', B='bar'), index=lrange(10)) - store.append('df', df, data_columns=['B'], min_itemsize={'A': 200}) - store.append('wp', wp) - - store.close() - def test_store_datetime_fractional_secs(self): with ensure_clean_store(self.path) as store: dt = datetime.datetime(2012, 1, 2, 3, 4, 5, 123456) series = Series([0], [dt]) store['a'] = series - self.assertEqual(store['a'].index[0], dt) + assert store['a'].index[0] == dt def test_tseries_indices_series(self): @@ -4499,18 +4079,18 @@ def test_tseries_indices_series(self): store['a'] = ser result = store['a'] - assert_series_equal(result, ser) - self.assertEqual(type(result.index), type(ser.index)) - self.assertEqual(result.index.freq, ser.index.freq) + tm.assert_series_equal(result, ser) + assert result.index.freq == ser.index.freq + tm.assert_class_equal(result.index, ser.index, obj="series index") idx = tm.makePeriodIndex(10) ser = Series(np.random.randn(len(idx)), idx) store['a'] = ser result = store['a'] - assert_series_equal(result, ser) - self.assertEqual(type(result.index), type(ser.index)) - self.assertEqual(result.index.freq, ser.index.freq) + tm.assert_series_equal(result, ser) + assert result.index.freq == ser.index.freq + tm.assert_class_equal(result.index, ser.index, obj="series index") def test_tseries_indices_frame(self): @@ -4521,8 +4101,9 @@ def test_tseries_indices_frame(self): result = store['a'] assert_frame_equal(result, df) - self.assertEqual(type(result.index), type(df.index)) - self.assertEqual(result.index.freq, df.index.freq) + assert result.index.freq == df.index.freq + tm.assert_class_equal(result.index, df.index, + obj="dataframe index") idx = tm.makePeriodIndex(10) df = DataFrame(np.random.randn(len(idx), 3), idx) @@ -4530,8 +4111,9 @@ def test_tseries_indices_frame(self): result = store['a'] assert_frame_equal(result, df) - self.assertEqual(type(result.index), type(df.index)) - self.assertEqual(result.index.freq, df.index.freq) + assert result.index.freq == df.index.freq + tm.assert_class_equal(result.index, df.index, + obj="dataframe index") def test_unicode_index(self): @@ -4539,6 +4121,7 @@ def test_unicode_index(self): # PerformanceWarning with catch_warnings(record=True): + simplefilter("ignore", pd.errors.PerformanceWarning) s = Series(np.random.randn(len(unicode_values)), unicode_values) self._check_roundtrip(s, tm.assert_series_equal) @@ -4557,6 +4140,7 @@ def test_unicode_longer_encoded(self): result = store.get('df') tm.assert_frame_equal(result, df) + @xfail_non_writeable def test_store_datetime_mixed(self): df = DataFrame( @@ -4571,7 +4155,7 @@ def test_store_datetime_mixed(self): # index=[np.arange(5).repeat(2), # np.tile(np.arange(2), 5)]) - # self.assertRaises(Exception, store.put, 'foo', df, format='table') + # pytest.raises(Exception, store.put, 'foo', df, format='table') def test_append_with_diff_col_name_types_raises_value_error(self): df = DataFrame(np.random.randn(10, 1)) @@ -4585,7 +4169,7 @@ def test_append_with_diff_col_name_types_raises_value_error(self): store.append(name, df) for d in (df2, df3, df4, df5): - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): store.append(name, d) def test_query_with_nested_special_character(self): @@ -4602,7 +4186,7 @@ def test_categorical(self): with ensure_clean_store(self.path) as store: - # basic + # Basic _maybe_remove(store, 's') s = Series(Categorical(['a', 'b', 'b', 'a', 'a', 'c'], categories=[ 'a', 'b', 'c', 'd'], ordered=False)) @@ -4623,37 +4207,43 @@ def test_categorical(self): result = store.select('df') tm.assert_frame_equal(result, df) - # dtypes + # Dtypes + _maybe_remove(store, 'si') s = Series([1, 1, 2, 2, 3, 4, 5]).astype('category') store.append('si', s) result = store.select('si') tm.assert_series_equal(result, s) + _maybe_remove(store, 'si2') s = Series([1, 1, np.nan, 2, 3, 4, 5]).astype('category') store.append('si2', s) result = store.select('si2') tm.assert_series_equal(result, s) - # multiple + # Multiple + _maybe_remove(store, 'df2') df2 = df.copy() df2['s2'] = Series(list('abcdefg')).astype('category') store.append('df2', df2) result = store.select('df2') tm.assert_frame_equal(result, df2) - # make sure the metadata is ok - self.assertTrue('/df2 ' in str(store)) - self.assertTrue('/df2/meta/values_block_0/meta' in str(store)) - self.assertTrue('/df2/meta/values_block_1/meta' in str(store)) + # Make sure the metadata is OK + info = store.info() + assert '/df2 ' in info + # assert '/df2/meta/values_block_0/meta' in info + assert '/df2/meta/values_block_1/meta' in info # unordered + _maybe_remove(store, 's2') s = Series(Categorical(['a', 'b', 'b', 'a', 'a', 'c'], categories=[ 'a', 'b', 'c', 'd'], ordered=False)) store.append('s2', s, format='table') result = store.select('s2') tm.assert_series_equal(result, s) - # query + # Query + _maybe_remove(store, 'df3') store.append('df3', df, data_columns=['s']) expected = df[df.s.isin(['b', 'c'])] result = store.select('df3', where=['s in ["b","c"]']) @@ -4671,7 +4261,7 @@ def test_categorical(self): result = store.select('df3', where=['s in ["f"]']) tm.assert_frame_equal(result, expected) - # appending with same categories is ok + # Appending with same categories is ok store.append('df3', df) df = concat([df, df]) @@ -4679,20 +4269,21 @@ def test_categorical(self): result = store.select('df3', where=['s in ["b","c"]']) tm.assert_frame_equal(result, expected) - # appending must have the same categories + # Appending must have the same categories df3 = df.copy() df3['s'].cat.remove_unused_categories(inplace=True) - self.assertRaises(ValueError, lambda: store.append('df3', df3)) + with pytest.raises(ValueError): + store.append('df3', df3) - # remove - # make sure meta data is removed (its a recursive removal so should - # be) + # Remove, and make sure meta data is removed (its a recursive + # removal so should be). result = store.select('df3/meta/s/meta') - self.assertIsNotNone(result) + assert result is not None store.remove('df3') - self.assertRaises( - KeyError, lambda: store.select('df3/meta/s/meta')) + + with pytest.raises(KeyError): + store.select('df3/meta/s/meta') def test_categorical_conversion(self): @@ -4724,19 +4315,38 @@ def test_categorical_conversion(self): result = read_hdf(path, 'df', where='obsids=B') tm.assert_frame_equal(result, expected) + def test_categorical_nan_only_columns(self): + # GH18413 + # Check that read_hdf with categorical columns with NaN-only values can + # be read back. + df = pd.DataFrame({ + 'a': ['a', 'b', 'c', np.nan], + 'b': [np.nan, np.nan, np.nan, np.nan], + 'c': [1, 2, 3, 4], + 'd': pd.Series([None] * 4, dtype=object) + }) + df['a'] = df.a.astype('category') + df['b'] = df.b.astype('category') + df['d'] = df.b.astype('category') + expected = df + with ensure_clean_path(self.path) as path: + df.to_hdf(path, 'df', format='table', data_columns=True) + result = read_hdf(path, 'df') + tm.assert_frame_equal(result, expected) + def test_duplicate_column_name(self): df = DataFrame(columns=["a", "a"], data=[[0, 0]]) with ensure_clean_path(self.path) as path: - self.assertRaises(ValueError, df.to_hdf, - path, 'df', format='fixed') + pytest.raises(ValueError, df.to_hdf, + path, 'df', format='fixed') df.to_hdf(path, 'df', format='table') other = read_hdf(path, 'df') tm.assert_frame_equal(df, other) - self.assertTrue(df.equals(other)) - self.assertTrue(other.equals(df)) + assert df.equals(other) + assert other.equals(df) def test_round_trip_equals(self): # GH 9330 @@ -4746,8 +4356,8 @@ def test_round_trip_equals(self): df.to_hdf(path, 'df', format='table') other = read_hdf(path, 'df') tm.assert_frame_equal(df, other) - self.assertTrue(df.equals(other)) - self.assertTrue(other.equals(df)) + assert df.equals(other) + assert other.equals(df) def test_preserve_timedeltaindex_type(self): # GH9635 @@ -4762,7 +4372,7 @@ def test_preserve_timedeltaindex_type(self): store['df'] = df assert_frame_equal(store['df'], df) - def test_colums_multiindex_modified(self): + def test_columns_multiindex_modified(self): # BUG: 7212 # read_hdf store.select modified the passed columns parameters # when multi-indexed. @@ -4783,8 +4393,9 @@ def test_colums_multiindex_modified(self): cols2load = list('BCD') cols2load_original = list(cols2load) df_loaded = read_hdf(path, 'df', columns=cols2load) # noqa - self.assertTrue(cols2load_original == cols2load) + assert cols2load_original == cols2load + @ignore_natural_naming_warning def test_to_hdf_with_object_column_names(self): # GH9057 # Writing HDF5 table format should only work for string-like @@ -4798,17 +4409,17 @@ def test_to_hdf_with_object_column_names(self): if compat.PY3: types_should_run.append(tm.makeUnicodeIndex) else: - types_should_fail.append(tm.makeUnicodeIndex) + # TODO: Add back to types_should_fail + # https://github.com/pandas-dev/pandas/issues/20907 + pass for index in types_should_fail: df = DataFrame(np.random.randn(10, 2), columns=index(2)) with ensure_clean_path(self.path) as path: - with self.assertRaises( - ValueError, msg=("cannot have non-object label " - "DataIndexableCol")): - with catch_warnings(record=True): - df.to_hdf(path, 'df', - format='table', + with catch_warnings(record=True): + msg = "cannot have non-object label DataIndexableCol" + with pytest.raises(ValueError, match=msg): + df.to_hdf(path, 'df', format='table', data_columns=True) for index in types_should_run: @@ -4835,7 +4446,7 @@ def test_read_hdf_open_store(self): store = HDFStore(path, mode='r') indirect = read_hdf(store, 'df') tm.assert_frame_equal(direct, indirect) - self.assertTrue(store.is_open) + assert store.is_open store.close() def test_read_hdf_iterator(self): @@ -4849,7 +4460,7 @@ def test_read_hdf_iterator(self): df.to_hdf(path, 'df', mode='w', format='t') direct = read_hdf(path, 'df') iterator = read_hdf(path, 'df', iterator=True) - self.assertTrue(isinstance(iterator, TableIterator)) + assert isinstance(iterator, TableIterator) indirect = next(iterator.__iter__()) tm.assert_frame_equal(direct, indirect) iterator.store.close() @@ -4860,21 +4471,22 @@ def test_read_hdf_errors(self): columns=list('ABCDE')) with ensure_clean_path(self.path) as path: - self.assertRaises(IOError, read_hdf, path, 'key') + pytest.raises(IOError, read_hdf, path, 'key') df.to_hdf(path, 'df') store = HDFStore(path, mode='r') store.close() - self.assertRaises(IOError, read_hdf, store, 'df') - with open(path, mode='r') as store: - self.assertRaises(NotImplementedError, read_hdf, store, 'df') + pytest.raises(IOError, read_hdf, store, 'df') + + def test_read_hdf_generic_buffer_errors(self): + pytest.raises(NotImplementedError, read_hdf, BytesIO(b''), 'df') def test_invalid_complib(self): df = DataFrame(np.random.rand(4, 5), index=list('abcd'), columns=list('ABCDE')) with ensure_clean_path(self.path) as path: - self.assertRaises(ValueError, df.to_hdf, path, - 'df', complib='blosc:zlib') + with pytest.raises(ValueError): + df.to_hdf(path, 'df', complib='foolib') # GH10443 def test_read_nokey(self): @@ -4889,7 +4501,7 @@ def test_read_nokey(self): reread = read_hdf(path) assert_frame_equal(df, reread) df.to_hdf(path, 'df2', mode='a') - self.assertRaises(ValueError, read_hdf, path) + pytest.raises(ValueError, read_hdf, path) def test_read_nokey_table(self): # GH13231 @@ -4901,19 +4513,18 @@ def test_read_nokey_table(self): reread = read_hdf(path) assert_frame_equal(df, reread) df.to_hdf(path, 'df2', mode='a', format='table') - self.assertRaises(ValueError, read_hdf, path) + pytest.raises(ValueError, read_hdf, path) def test_read_nokey_empty(self): with ensure_clean_path(self.path) as path: store = HDFStore(path) store.close() - self.assertRaises(ValueError, read_hdf, path) + pytest.raises(ValueError, read_hdf, path) + @td.skip_if_no('pathlib') def test_read_from_pathlib_path(self): # GH11773 - tm._skip_if_no_pathlib() - from pathlib import Path expected = DataFrame(np.random.rand(4, 5), @@ -4927,11 +4538,10 @@ def test_read_from_pathlib_path(self): tm.assert_frame_equal(expected, actual) + @td.skip_if_no('py.path') def test_read_from_py_localpath(self): # GH11773 - tm._skip_if_no_localpath() - from py.path import local as LocalPath expected = DataFrame(np.random.rand(4, 5), @@ -4956,7 +4566,7 @@ def test_query_long_float_literal(self): cutoff = 1000000000.0006 result = store.select('test', "A < %.4f" % cutoff) - self.assertTrue(result.empty) + assert result.empty cutoff = 1000000000.0010 result = store.select('test', "A > %.4f" % cutoff) @@ -4979,7 +4589,7 @@ def test_query_compare_column_type(self): with ensure_clean_store(self.path) as store: store.append('test', df, format='table', data_columns=True) - ts = pd.Timestamp('2014-01-01') # noqa + ts = pd.Timestamp('2014-01-01') # noqa result = store.select('test', where='real_date > ts') expected = df.loc[[1], :] tm.assert_frame_equal(expected, result) @@ -4989,15 +4599,15 @@ def test_query_compare_column_type(self): for v in [2.1, True, pd.Timestamp('2014-01-01'), pd.Timedelta(1, 's')]: query = 'date {op} v'.format(op=op) - with tm.assertRaises(TypeError): - result = store.select('test', where=query) + with pytest.raises(TypeError): + store.select('test', where=query) # strings to other columns must be convertible to type v = 'a' for col in ['int', 'float', 'real_date']: query = '{col} {op} v'.format(op=op, col=col) - with tm.assertRaises(ValueError): - result = store.select('test', where=query) + with pytest.raises(ValueError): + store.select('test', where=query) for v, col in zip(['1', '1.1', '2014-01-01'], ['int', 'float', 'real_date']): @@ -5012,6 +4622,44 @@ def test_query_compare_column_type(self): expected = df.loc[[], :] tm.assert_frame_equal(expected, result) + @pytest.mark.parametrize('format', ['fixed', 'table']) + def test_read_hdf_series_mode_r(self, format): + # GH 16583 + # Tests that reading a Series saved to an HDF file + # still works if a mode='r' argument is supplied + series = tm.makeFloatSeries() + with ensure_clean_path(self.path) as path: + series.to_hdf(path, key='data', format=format) + result = pd.read_hdf(path, key='data', mode='r') + tm.assert_series_equal(result, series) + + @pytest.mark.skipif(not PY36, reason="Need python 3.6") + def test_fspath(self): + with tm.ensure_clean('foo.h5') as path: + with pd.HDFStore(path) as store: + assert os.fspath(store) == str(path) + + def test_read_py2_hdf_file_in_py3(self, datapath): + # GH 16781 + + # tests reading a PeriodIndex DataFrame written in Python2 in Python3 + + # the file was generated in Python 2.7 like so: + # + # df = pd.DataFrame([1.,2,3], index=pd.PeriodIndex( + # ['2015-01-01', '2015-01-02', '2015-01-05'], freq='B')) + # df.to_hdf('periodindex_0.20.1_x86_64_darwin_2.7.13.h5', 'p') + + expected = pd.DataFrame([1., 2, 3], index=pd.PeriodIndex( + ['2015-01-01', '2015-01-02', '2015-01-05'], freq='B')) + + with ensure_clean_store( + datapath('io', 'data', 'legacy_hdf', + 'periodindex_0.20.1_x86_64_darwin_2.7.13.h5'), + mode='r') as store: + result = store['p'] + assert_frame_equal(result, expected) + class TestHDFComplexValues(Base): # GH10447 @@ -5053,6 +4701,7 @@ def test_complex_table(self): reread = read_hdf(path, 'df') assert_frame_equal(df, reread) + @xfail_non_writeable def test_complex_mixed_fixed(self): complex64 = np.array([1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j], dtype=np.complex64) @@ -5092,32 +4741,29 @@ def test_complex_mixed_table(self): assert_frame_equal(df, reread) def test_complex_across_dimensions_fixed(self): - complex128 = np.array([1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j]) - s = Series(complex128, index=list('abcd')) - df = DataFrame({'A': s, 'B': s}) - p = Panel({'One': df, 'Two': df}) + with catch_warnings(record=True): + complex128 = np.array( + [1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j]) + s = Series(complex128, index=list('abcd')) + df = DataFrame({'A': s, 'B': s}) - objs = [s, df, p] - comps = [tm.assert_series_equal, tm.assert_frame_equal, - tm.assert_panel_equal] - for obj, comp in zip(objs, comps): - with ensure_clean_path(self.path) as path: - obj.to_hdf(path, 'obj', format='fixed') - reread = read_hdf(path, 'obj') - comp(obj, reread) + objs = [s, df] + comps = [tm.assert_series_equal, tm.assert_frame_equal] + for obj, comp in zip(objs, comps): + with ensure_clean_path(self.path) as path: + obj.to_hdf(path, 'obj', format='fixed') + reread = read_hdf(path, 'obj') + comp(obj, reread) def test_complex_across_dimensions(self): complex128 = np.array([1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j]) s = Series(complex128, index=list('abcd')) df = DataFrame({'A': s, 'B': s}) - p = Panel({'One': df, 'Two': df}) with catch_warnings(record=True): - p4d = pd.Panel4D({'i': p, 'ii': p}) - objs = [df, p, p4d] - comps = [tm.assert_frame_equal, tm.assert_panel_equal, - tm.assert_panel4d_equal] + objs = [df] + comps = [tm.assert_frame_equal] for obj, comp in zip(objs, comps): with ensure_clean_path(self.path) as path: obj.to_hdf(path, 'obj', format='table') @@ -5132,15 +4778,15 @@ def test_complex_indexing_error(self): 'C': complex128}, index=list('abcd')) with ensure_clean_store(self.path) as store: - self.assertRaises(TypeError, store.append, - 'df', df, data_columns=['C']) + pytest.raises(TypeError, store.append, + 'df', df, data_columns=['C']) def test_complex_series_error(self): complex128 = np.array([1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j, 1.0 + 1.0j]) s = Series(complex128, index=list('abcd')) with ensure_clean_path(self.path) as path: - self.assertRaises(TypeError, s.to_hdf, path, 'obj', format='t') + pytest.raises(TypeError, s.to_hdf, path, 'obj', format='t') with ensure_clean_path(self.path) as path: s.to_hdf(path, 'obj', format='t', index=False) @@ -5158,7 +4804,7 @@ def test_complex_append(self): assert_frame_equal(pd.concat([df, df], 0), result) -class TestTimezones(Base, tm.TestCase): +class TestTimezones(Base): def _compare_with_tz(self, a, b): tm.assert_frame_equal(a, b) @@ -5170,16 +4816,15 @@ def _compare_with_tz(self, a, b): b_e = b.loc[i, c] if not (a_e == b_e and a_e.tz == b_e.tz): raise AssertionError( - "invalid tz comparsion [%s] [%s]" % (a_e, b_e)) + "invalid tz comparison [%s] [%s]" % (a_e, b_e)) def test_append_with_timezones_dateutil(self): from datetime import timedelta - tm._skip_if_no_dateutil() # use maybe_get_tz instead of dateutil.tz.gettz to handle the windows # filename issues. - from pandas._libs.tslib import maybe_get_tz + from pandas._libs.tslibs.timezones import maybe_get_tz gettz = lambda x: maybe_get_tz('dateutil/' + x) # as columns @@ -5215,7 +4860,7 @@ def test_append_with_timezones_dateutil(self): tz=gettz('US/Eastern')), B=Timestamp('20130102', tz=gettz('EET'))), index=range(5)) - self.assertRaises(ValueError, store.append, 'df_tz', df) + pytest.raises(ValueError, store.append, 'df_tz', df) # this is ok _maybe_remove(store, 'df_tz') @@ -5229,7 +4874,7 @@ def test_append_with_timezones_dateutil(self): tz=gettz('US/Eastern')), B=Timestamp('20130102', tz=gettz('CET'))), index=range(5)) - self.assertRaises(ValueError, store.append, 'df_tz', df) + pytest.raises(ValueError, store.append, 'df_tz', df) # as index with ensure_clean_store(self.path) as store: @@ -5282,7 +4927,7 @@ def test_append_with_timezones_pytz(self): df = DataFrame(dict(A=Timestamp('20130102', tz='US/Eastern'), B=Timestamp('20130102', tz='EET')), index=range(5)) - self.assertRaises(ValueError, store.append, 'df_tz', df) + pytest.raises(ValueError, store.append, 'df_tz', df) # this is ok _maybe_remove(store, 'df_tz') @@ -5295,7 +4940,7 @@ def test_append_with_timezones_pytz(self): df = DataFrame(dict(A=Timestamp('20130102', tz='US/Eastern'), B=Timestamp('20130102', tz='CET')), index=range(5)) - self.assertRaises(ValueError, store.append, 'df_tz', df) + pytest.raises(ValueError, store.append, 'df_tz', df) # as index with ensure_clean_store(self.path) as store: @@ -5326,7 +4971,7 @@ def test_tseries_select_index_column(self): with ensure_clean_store(self.path) as store: store.append('frame', frame) result = store.select_column('frame', 'index') - self.assertEqual(rng.tz, DatetimeIndex(result.values).tz) + assert rng.tz == DatetimeIndex(result.values).tz # check utc rng = date_range('1/1/2000', '1/30/2000', tz='UTC') @@ -5335,7 +4980,7 @@ def test_tseries_select_index_column(self): with ensure_clean_store(self.path) as store: store.append('frame', frame) result = store.select_column('frame', 'index') - self.assertEqual(rng.tz, result.dt.tz) + assert rng.tz == result.dt.tz # double check non-utc rng = date_range('1/1/2000', '1/30/2000', tz='US/Eastern') @@ -5344,7 +4989,7 @@ def test_tseries_select_index_column(self): with ensure_clean_store(self.path) as store: store.append('frame', frame) result = store.select_column('frame', 'index') - self.assertEqual(rng.tz, result.dt.tz) + assert rng.tz == result.dt.tz def test_timezones_fixed(self): with ensure_clean_store(self.path) as store: @@ -5374,9 +5019,10 @@ def test_fixed_offset_tz(self): with ensure_clean_store(self.path) as store: store['frame'] = frame recons = store['frame'] - self.assert_index_equal(recons.index, rng) - self.assertEqual(rng.tz, recons.index.tz) + tm.assert_index_equal(recons.index, rng) + assert rng.tz == recons.index.tz + @td.skip_if_windows def test_store_timezone(self): # GH2852 # issue storing datetime.date with a timezone as it resets when read @@ -5404,14 +5050,14 @@ def test_store_timezone(self): assert_frame_equal(result, df) - def test_legacy_datetimetz_object(self): + def test_legacy_datetimetz_object(self, datapath): # legacy from < 0.17.0 # 8260 expected = DataFrame(dict(A=Timestamp('20130102', tz='US/Eastern'), B=Timestamp('20130603', tz='CET')), index=range(5)) with ensure_clean_store( - tm.get_data_path('legacy_hdf/datetimetz_object.h5'), + datapath('io', 'data', 'legacy_hdf', 'datetimetz_object.h5'), mode='r') as store: result = store['df'] assert_frame_equal(result, expected) @@ -5430,12 +5076,3 @@ def test_dst_transitions(self): store.append('df', df) result = store.select('df') assert_frame_equal(result, df) - - -def _test_sort(obj): - if isinstance(obj, DataFrame): - return obj.reindex(sorted(obj.index)) - elif isinstance(obj, Panel): - return obj.reindex(major=sorted(obj.major_axis)) - else: - raise ValueError('type not supported here') diff --git a/pandas/tests/io/test_s3.py b/pandas/tests/io/test_s3.py index 2983fa647445c..32eae8ed328f4 100644 --- a/pandas/tests/io/test_s3.py +++ b/pandas/tests/io/test_s3.py @@ -1,10 +1,29 @@ -from pandas.util import testing as tm +import pytest -from pandas.io.common import _is_s3_url +from pandas.compat import BytesIO +from pandas import read_csv -class TestS3URL(tm.TestCase): +from pandas.io.common import is_s3_url + + +class TestS3URL(object): def test_is_s3_url(self): - self.assertTrue(_is_s3_url("s3://pandas/somethingelse.com")) - self.assertFalse(_is_s3_url("s4://pandas/somethingelse.com")) + assert is_s3_url("s3://pandas/somethingelse.com") + assert not is_s3_url("s4://pandas/somethingelse.com") + + +def test_streaming_s3_objects(): + # GH17135 + # botocore gained iteration support in 1.10.47, can now be used in read_* + pytest.importorskip('botocore', minversion='1.10.47') + from botocore.response import StreamingBody + + data = [ + b'foo,bar,baz\n1,2,3\n4,5,6\n', + b'just,the,header\n', + ] + for el in data: + body = StreamingBody(BytesIO(el), content_length=len(el)) + read_csv(body) diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 890f52e8c65e9..806bd7f2b7c93 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -18,31 +18,29 @@ """ from __future__ import print_function -import pytest -import unittest -import sqlite3 -import csv -import os -import sys +import csv +from datetime import date, datetime, time +import sqlite3 import warnings -import numpy as np -import pandas as pd -from datetime import datetime, date, time +import numpy as np +import pytest -from pandas.types.common import (is_object_dtype, is_datetime64_dtype, - is_datetime64tz_dtype) -from pandas import DataFrame, Series, Index, MultiIndex, isnull, concat -from pandas import date_range, to_datetime, to_timedelta, Timestamp import pandas.compat as compat -from pandas.compat import StringIO, range, lrange, string_types, PY36 -from pandas.tseries.tools import format as date_format +from pandas.compat import PY36, lrange, range, string_types -import pandas.io.sql as sql -from pandas.io.sql import read_sql_table, read_sql_query +from pandas.core.dtypes.common import ( + is_datetime64_dtype, is_datetime64tz_dtype) + +import pandas as pd +from pandas import ( + DataFrame, Index, MultiIndex, Series, Timestamp, concat, date_range, isna, + to_datetime, to_timedelta) import pandas.util.testing as tm +import pandas.io.sql as sql +from pandas.io.sql import read_sql_query, read_sql_table try: import sqlalchemy @@ -88,6 +86,7 @@ "TextCol" TEXT, "DateCol" TEXT, "IntDateCol" INTEGER, + "IntDateOnlyCol" INTEGER, "FloatCol" REAL, "IntCol" INTEGER, "BoolCol" INTEGER, @@ -98,6 +97,7 @@ `TextCol` TEXT, `DateCol` DATETIME, `IntDateCol` INTEGER, + `IntDateOnlyCol` INTEGER, `FloatCol` DOUBLE, `IntCol` INTEGER, `BoolCol` BOOLEAN, @@ -109,6 +109,7 @@ "DateCol" TIMESTAMP, "DateColWithTz" TIMESTAMP WITH TIME ZONE, "IntDateCol" INTEGER, + "IntDateOnlyCol" INTEGER, "FloatCol" DOUBLE PRECISION, "IntCol" INTEGER, "BoolCol" BOOLEAN, @@ -120,31 +121,33 @@ 'sqlite': { 'query': """ INSERT INTO types_test_data - VALUES(?, ?, ?, ?, ?, ?, ?, ?) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) """, 'fields': ( - 'TextCol', 'DateCol', 'IntDateCol', 'FloatCol', - 'IntCol', 'BoolCol', 'IntColWithNull', 'BoolColWithNull' + 'TextCol', 'DateCol', 'IntDateCol', 'IntDateOnlyCol', + 'FloatCol', 'IntCol', 'BoolCol', 'IntColWithNull', + 'BoolColWithNull' ) }, 'mysql': { 'query': """ INSERT INTO types_test_data - VALUES("%s", %s, %s, %s, %s, %s, %s, %s) + VALUES("%s", %s, %s, %s, %s, %s, %s, %s, %s) """, 'fields': ( - 'TextCol', 'DateCol', 'IntDateCol', 'FloatCol', - 'IntCol', 'BoolCol', 'IntColWithNull', 'BoolColWithNull' + 'TextCol', 'DateCol', 'IntDateCol', 'IntDateOnlyCol', + 'FloatCol', 'IntCol', 'BoolCol', 'IntColWithNull', + 'BoolColWithNull' ) }, 'postgresql': { 'query': """ INSERT INTO types_test_data - VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s) + VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, 'fields': ( 'TextCol', 'DateCol', 'DateColWithTz', - 'IntDateCol', 'FloatCol', + 'IntDateCol', 'IntDateOnlyCol', 'FloatCol', 'IntCol', 'BoolCol', 'IntColWithNull', 'BoolColWithNull' ) }, @@ -178,10 +181,12 @@ class MixInBase(object): - def tearDown(self): - for tbl in self._get_all_tables(): - self.drop_table(tbl) - self._close_conn() + def teardown_method(self, method): + # if setup fails, there may not be a connection to close. + if hasattr(self, 'conn'): + for tbl in self._get_all_tables(): + self.drop_table(tbl) + self._close_conn() class MySQLMixIn(MixInBase): @@ -248,9 +253,13 @@ def _get_exec(self): else: return self.conn.cursor() - def _load_iris_data(self): + @pytest.fixture(params=[('io', 'data', 'iris.csv')]) + def load_iris_data(self, datapath, request): import io - iris_csv_file = os.path.join(tm.get_data_path(), 'iris.csv') + iris_csv_file = datapath(*request.param) + + if not hasattr(self, 'conn'): + self.setup_connect() self.drop_table('iris') self._get_exec().execute(SQL_STRINGS['create_iris'][self.flavor]) @@ -271,8 +280,7 @@ def _check_iris_loaded_frame(self, iris_frame): pytype = iris_frame.dtypes[0].type row = iris_frame.iloc[0] - self.assertTrue( - issubclass(pytype, np.floating), 'Loaded frame has incorrect type') + assert issubclass(pytype, np.floating) tm.equalContents(row.values, [5.1, 3.5, 1.4, 0.2, 'Iris-setosa']) def _load_test1_data(self): @@ -314,13 +322,13 @@ def _load_raw_sql(self): self.drop_table('types_test_data') self._get_exec().execute(SQL_STRINGS['create_test_types'][self.flavor]) ins = SQL_STRINGS['insert_test_types'][self.flavor] - data = [ { 'TextCol': 'first', 'DateCol': '2000-01-03 00:00:00', 'DateColWithTz': '2000-01-01 00:00:00-08:00', 'IntDateCol': 535852800, + 'IntDateOnlyCol': 20101010, 'FloatCol': 10.10, 'IntCol': 1, 'BoolCol': False, @@ -332,6 +340,7 @@ def _load_raw_sql(self): 'DateCol': '2000-01-04 00:00:00', 'DateColWithTz': '2000-06-01 00:00:00-07:00', 'IntDateCol': 1356998400, + 'IntDateOnlyCol': 20101212, 'FloatCol': 10.10, 'IntCol': 1, 'BoolCol': False, @@ -367,12 +376,15 @@ def _read_sql_iris_named_parameter(self): iris_frame = self.pandasSQL.read_query(query, params=params) self._check_iris_loaded_frame(iris_frame) - def _to_sql(self): + def _to_sql(self, method=None): self.drop_table('test_frame1') - self.pandasSQL.to_sql(self.test_frame1, 'test_frame1') - self.assertTrue(self.pandasSQL.has_table( - 'test_frame1'), 'Table not written to DB') + self.pandasSQL.to_sql(self.test_frame1, 'test_frame1', method=method) + assert self.pandasSQL.has_table('test_frame1') + + num_entries = len(self.test_frame1) + num_rows = self._count_rows('test_frame1') + assert num_rows == num_entries # Nuke table self.drop_table('test_frame1') @@ -386,11 +398,10 @@ def _to_sql_fail(self): self.pandasSQL.to_sql( self.test_frame1, 'test_frame1', if_exists='fail') - self.assertTrue(self.pandasSQL.has_table( - 'test_frame1'), 'Table not written to DB') + assert self.pandasSQL.has_table('test_frame1') - self.assertRaises(ValueError, self.pandasSQL.to_sql, - self.test_frame1, 'test_frame1', if_exists='fail') + pytest.raises(ValueError, self.pandasSQL.to_sql, + self.test_frame1, 'test_frame1', if_exists='fail') self.drop_table('test_frame1') @@ -402,15 +413,12 @@ def _to_sql_replace(self): # Add to table again self.pandasSQL.to_sql( self.test_frame1, 'test_frame1', if_exists='replace') - self.assertTrue(self.pandasSQL.has_table( - 'test_frame1'), 'Table not written to DB') + assert self.pandasSQL.has_table('test_frame1') num_entries = len(self.test_frame1) num_rows = self._count_rows('test_frame1') - self.assertEqual( - num_rows, num_entries, "not the same number of rows as entries") - + assert num_rows == num_entries self.drop_table('test_frame1') def _to_sql_append(self): @@ -423,15 +431,31 @@ def _to_sql_append(self): # Add to table again self.pandasSQL.to_sql( self.test_frame1, 'test_frame1', if_exists='append') - self.assertTrue(self.pandasSQL.has_table( - 'test_frame1'), 'Table not written to DB') + assert self.pandasSQL.has_table('test_frame1') num_entries = 2 * len(self.test_frame1) num_rows = self._count_rows('test_frame1') - self.assertEqual( - num_rows, num_entries, "not the same number of rows as entries") + assert num_rows == num_entries + self.drop_table('test_frame1') + + def _to_sql_method_callable(self): + check = [] # used to double check function below is really being used + + def sample(pd_table, conn, keys, data_iter): + check.append(1) + data = [dict(zip(keys, row)) for row in data_iter] + conn.execute(pd_table.table.insert(), data) + self.drop_table('test_frame1') + + self.pandasSQL.to_sql(self.test_frame1, 'test_frame1', method=sample) + assert self.pandasSQL.has_table('test_frame1') + assert check == [1] + num_entries = len(self.test_frame1) + num_rows = self._count_rows('test_frame1') + assert num_rows == num_entries + # Nuke table self.drop_table('test_frame1') def _roundtrip(self): @@ -458,7 +482,7 @@ def _to_sql_save_index(self): columns=['A', 'B', 'C'], index=['A']) self.pandasSQL.to_sql(df, 'test_to_sql_saves_index') ix_cols = self._get_index_columns('test_to_sql_saves_index') - self.assertEqual(ix_cols, [['A', ], ]) + assert ix_cols == [['A', ], ] def _transaction_test(self): self.pandasSQL.execute("CREATE TABLE test_trans (A INT, B TEXT)") @@ -470,17 +494,17 @@ def _transaction_test(self): with self.pandasSQL.run_transaction() as trans: trans.execute(ins_sql) raise Exception('error') - except: + except Exception: # ignore raised exception pass res = self.pandasSQL.read_query('SELECT * FROM test_trans') - self.assertEqual(len(res), 0) + assert len(res) == 0 # Make sure when transaction is committed, rows do get inserted with self.pandasSQL.run_transaction() as trans: trans.execute(ins_sql) res2 = self.pandasSQL.read_query('SELECT * FROM test_trans') - self.assertEqual(len(res2), 1) + assert len(res2) == 1 # ----------------------------------------------------------------------------- @@ -506,9 +530,14 @@ class _TestSQLApi(PandasSQLTest): flavor = 'sqlite' mode = None - def setUp(self): + def setup_connect(self): self.conn = self.connect() - self._load_iris_data() + + @pytest.fixture(autouse=True) + def setup_method(self, load_iris_data): + self.load_test_data_and_sql() + + def load_test_data_and_sql(self): self._load_iris_view() self._load_test1_data() self._load_test2_data() @@ -527,19 +556,15 @@ def test_read_sql_view(self): def test_to_sql(self): sql.to_sql(self.test_frame1, 'test_frame1', self.conn) - self.assertTrue( - sql.has_table('test_frame1', self.conn), - 'Table not written to DB') + assert sql.has_table('test_frame1', self.conn) def test_to_sql_fail(self): sql.to_sql(self.test_frame1, 'test_frame2', self.conn, if_exists='fail') - self.assertTrue( - sql.has_table('test_frame2', self.conn), - 'Table not written to DB') + assert sql.has_table('test_frame2', self.conn) - self.assertRaises(ValueError, sql.to_sql, self.test_frame1, - 'test_frame2', self.conn, if_exists='fail') + pytest.raises(ValueError, sql.to_sql, self.test_frame1, + 'test_frame2', self.conn, if_exists='fail') def test_to_sql_replace(self): sql.to_sql(self.test_frame1, 'test_frame3', @@ -547,15 +572,12 @@ def test_to_sql_replace(self): # Add to table again sql.to_sql(self.test_frame1, 'test_frame3', self.conn, if_exists='replace') - self.assertTrue( - sql.has_table('test_frame3', self.conn), - 'Table not written to DB') + assert sql.has_table('test_frame3', self.conn) num_entries = len(self.test_frame1) num_rows = self._count_rows('test_frame3') - self.assertEqual( - num_rows, num_entries, "not the same number of rows as entries") + assert num_rows == num_entries def test_to_sql_append(self): sql.to_sql(self.test_frame1, 'test_frame4', @@ -564,15 +586,12 @@ def test_to_sql_append(self): # Add to table again sql.to_sql(self.test_frame1, 'test_frame4', self.conn, if_exists='append') - self.assertTrue( - sql.has_table('test_frame4', self.conn), - 'Table not written to DB') + assert sql.has_table('test_frame4', self.conn) num_entries = 2 * len(self.test_frame1) num_rows = self._count_rows('test_frame4') - self.assertEqual( - num_rows, num_entries, "not the same number of rows as entries") + assert num_rows == num_entries def test_to_sql_type_mapping(self): sql.to_sql(self.test_frame3, 'test_frame5', self.conn, index=False) @@ -586,11 +605,6 @@ def test_to_sql_series(self): s2 = sql.read_sql_query("SELECT * FROM test_series", self.conn) tm.assert_frame_equal(s.to_frame(), s2) - def test_to_sql_panel(self): - panel = tm.makePanel() - self.assertRaises(NotImplementedError, sql.to_sql, panel, - 'test_panel', self.conn) - def test_roundtrip(self): sql.to_sql(self.test_frame1, 'test_frame_roundtrip', con=self.conn) @@ -620,36 +634,50 @@ def test_execute_sql(self): tm.equalContents(row, [5.1, 3.5, 1.4, 0.2, 'Iris-setosa']) def test_date_parsing(self): - # Test date parsing in read_sq + # Test date parsing in read_sql # No Parsing df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn) - self.assertFalse( - issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert not issubclass(df.DateCol.dtype.type, np.datetime64) df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, parse_dates=['DateCol']) - self.assertTrue( - issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) + assert df.DateCol.tolist() == [ + pd.Timestamp(2000, 1, 3, 0, 0, 0), + pd.Timestamp(2000, 1, 4, 0, 0, 0) + ] df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, parse_dates={'DateCol': '%Y-%m-%d %H:%M:%S'}) - self.assertTrue( - issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) + assert df.DateCol.tolist() == [ + pd.Timestamp(2000, 1, 3, 0, 0, 0), + pd.Timestamp(2000, 1, 4, 0, 0, 0) + ] df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, parse_dates=['IntDateCol']) - - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) + assert df.IntDateCol.tolist() == [ + pd.Timestamp(1986, 12, 25, 0, 0, 0), + pd.Timestamp(2013, 1, 1, 0, 0, 0) + ] df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, parse_dates={'IntDateCol': 's'}) + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) + assert df.IntDateCol.tolist() == [ + pd.Timestamp(1986, 12, 25, 0, 0, 0), + pd.Timestamp(2013, 1, 1, 0, 0, 0) + ] - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + df = sql.read_sql_query("SELECT * FROM types_test_data", self.conn, + parse_dates={'IntDateOnlyCol': '%Y%m%d'}) + assert issubclass(df.IntDateOnlyCol.dtype.type, np.datetime64) + assert df.IntDateOnlyCol.tolist() == [ + pd.Timestamp('2010-10-10'), + pd.Timestamp('2010-12-12') + ] def test_date_and_index(self): # Test case where same column appears in parse_date and index_col @@ -658,11 +686,8 @@ def test_date_and_index(self): index_col='DateCol', parse_dates=['DateCol', 'IntDateCol']) - self.assertTrue(issubclass(df.index.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") - - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.index.dtype.type, np.datetime64) + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) def test_timedelta(self): @@ -677,52 +702,31 @@ def test_timedelta(self): def test_complex(self): df = DataFrame({'a': [1 + 1j, 2j]}) # Complex data type should raise error - self.assertRaises(ValueError, df.to_sql, 'test_complex', self.conn) - - def test_to_sql_index_label(self): - temp_frame = DataFrame({'col1': range(4)}) + pytest.raises(ValueError, df.to_sql, 'test_complex', self.conn) + @pytest.mark.parametrize("index_name,index_label,expected", [ # no index name, defaults to 'index' - sql.to_sql(temp_frame, 'test_index_label', self.conn) - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], 'index') - + (None, None, "index"), # specifying index_label - sql.to_sql(temp_frame, 'test_index_label', self.conn, - if_exists='replace', index_label='other_label') - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], 'other_label', - "Specified index_label not written to database") - + (None, "other_label", "other_label"), # using the index name - temp_frame.index.name = 'index_name' - sql.to_sql(temp_frame, 'test_index_label', self.conn, - if_exists='replace') - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], 'index_name', - "Index name not written to database") - + ("index_name", None, "index_name"), # has index name, but specifying index_label - sql.to_sql(temp_frame, 'test_index_label', self.conn, - if_exists='replace', index_label='other_label') - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], 'other_label', - "Specified index_label not written to database") - + ("index_name", "other_label", "other_label"), # index name is integer - temp_frame.index.name = 0 - sql.to_sql(temp_frame, 'test_index_label', self.conn, - if_exists='replace') - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], '0', - "Integer index label not written to database") - - temp_frame.index.name = None + (0, None, "0"), + # index name is None but index label is integer + (None, 0, "0"), + ]) + def test_to_sql_index_label(self, index_name, + index_label, expected): + temp_frame = DataFrame({'col1': range(4)}) + temp_frame.index.name = index_name + query = 'SELECT * FROM test_index_label' sql.to_sql(temp_frame, 'test_index_label', self.conn, - if_exists='replace', index_label=0) - frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], '0', - "Integer index label not written to database") + index_label=index_label) + frame = sql.read_sql_query(query, self.conn) + assert frame.columns[0] == expected def test_to_sql_index_label_multiindex(self): temp_frame = DataFrame({'col1': range(4)}, @@ -732,35 +736,32 @@ def test_to_sql_index_label_multiindex(self): # no index name, defaults to 'level_0' and 'level_1' sql.to_sql(temp_frame, 'test_index_label', self.conn) frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[0], 'level_0') - self.assertEqual(frame.columns[1], 'level_1') + assert frame.columns[0] == 'level_0' + assert frame.columns[1] == 'level_1' # specifying index_label sql.to_sql(temp_frame, 'test_index_label', self.conn, if_exists='replace', index_label=['A', 'B']) frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[:2].tolist(), ['A', 'B'], - "Specified index_labels not written to database") + assert frame.columns[:2].tolist() == ['A', 'B'] # using the index name temp_frame.index.names = ['A', 'B'] sql.to_sql(temp_frame, 'test_index_label', self.conn, if_exists='replace') frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[:2].tolist(), ['A', 'B'], - "Index names not written to database") + assert frame.columns[:2].tolist() == ['A', 'B'] # has index name, but specifying index_label sql.to_sql(temp_frame, 'test_index_label', self.conn, if_exists='replace', index_label=['C', 'D']) frame = sql.read_sql_query('SELECT * FROM test_index_label', self.conn) - self.assertEqual(frame.columns[:2].tolist(), ['C', 'D'], - "Specified index_labels not written to database") + assert frame.columns[:2].tolist() == ['C', 'D'] # wrong length of index_label - self.assertRaises(ValueError, sql.to_sql, temp_frame, - 'test_index_label', self.conn, if_exists='replace', - index_label='C') + pytest.raises(ValueError, sql.to_sql, temp_frame, + 'test_index_label', self.conn, if_exists='replace', + index_label='C') def test_multiindex_roundtrip(self): df = DataFrame.from_records([(1, 2.1, 'line1'), (2, 1.5, 'line2')], @@ -778,27 +779,27 @@ def test_integer_col_names(self): def test_get_schema(self): create_sql = sql.get_schema(self.test_frame1, 'test', con=self.conn) - self.assertTrue('CREATE' in create_sql) + assert 'CREATE' in create_sql def test_get_schema_dtypes(self): float_frame = DataFrame({'a': [1.1, 1.2], 'b': [2.1, 2.2]}) dtype = sqlalchemy.Integer if self.mode == 'sqlalchemy' else 'INTEGER' create_sql = sql.get_schema(float_frame, 'test', con=self.conn, dtype={'b': dtype}) - self.assertTrue('CREATE' in create_sql) - self.assertTrue('INTEGER' in create_sql) + assert 'CREATE' in create_sql + assert 'INTEGER' in create_sql def test_get_schema_keys(self): frame = DataFrame({'Col1': [1.1, 1.2], 'Col2': [2.1, 2.2]}) create_sql = sql.get_schema(frame, 'test', con=self.conn, keys='Col1') constraint_sentence = 'CONSTRAINT test_pk PRIMARY KEY ("Col1")' - self.assertTrue(constraint_sentence in create_sql) + assert constraint_sentence in create_sql # multiple columns as key (GH10385) create_sql = sql.get_schema(self.test_frame1, 'test', con=self.conn, keys=['A', 'B']) constraint_sentence = 'CONSTRAINT test_pk PRIMARY KEY ("A", "B")' - self.assertTrue(constraint_sentence in create_sql) + assert constraint_sentence in create_sql def test_chunksize_read(self): df = DataFrame(np.random.randn(22, 5), columns=list('abcde')) @@ -815,7 +816,7 @@ def test_chunksize_read(self): for chunk in sql.read_sql_query("select * from test_chunksize", self.conn, chunksize=5): res2 = concat([res2, chunk], ignore_index=True) - self.assertEqual(len(chunk), sizes[i]) + assert len(chunk) == sizes[i] i += 1 tm.assert_frame_equal(res1, res2) @@ -829,7 +830,7 @@ def test_chunksize_read(self): for chunk in sql.read_sql_table("test_chunksize", self.conn, chunksize=5): res3 = concat([res3, chunk], ignore_index=True) - self.assertEqual(len(chunk), sizes[i]) + assert len(chunk) == sizes[i] i += 1 tm.assert_frame_equal(res1, res3) @@ -853,9 +854,19 @@ def test_unicode_column_name(self): df = DataFrame([[1, 2], [3, 4]], columns=[u'\xe9', u'b']) df.to_sql('test_unicode', self.conn, index=False) + def test_escaped_table_name(self): + # GH 13206 + df = DataFrame({'A': [0, 1, 2], 'B': [0.2, np.nan, 5.6]}) + df.to_sql('d1187b08-4943-4c8d-a7f6', self.conn, index=False) + + res = sql.read_sql_query('SELECT * FROM `d1187b08-4943-4c8d-a7f6`', + self.conn) + + tm.assert_frame_equal(res, df) + @pytest.mark.single -class TestSQLApi(SQLAlchemyMixIn, _TestSQLApi, unittest.TestCase): +class TestSQLApi(SQLAlchemyMixIn, _TestSQLApi): """ Test the public API as it would be used directly @@ -878,29 +889,24 @@ def test_read_table_columns(self): cols = ['A', 'B'] result = sql.read_sql_table('test_frame', self.conn, columns=cols) - self.assertEqual(result.columns.tolist(), cols, - "Columns not correctly selected") + assert result.columns.tolist() == cols def test_read_table_index_col(self): # test columns argument in read_table sql.to_sql(self.test_frame1, 'test_frame', self.conn) result = sql.read_sql_table('test_frame', self.conn, index_col="index") - self.assertEqual(result.index.names, ["index"], - "index_col not correctly set") + assert result.index.names == ["index"] result = sql.read_sql_table( 'test_frame', self.conn, index_col=["A", "B"]) - self.assertEqual(result.index.names, ["A", "B"], - "index_col not correctly set") + assert result.index.names == ["A", "B"] result = sql.read_sql_table('test_frame', self.conn, index_col=["A", "B"], columns=["C", "D"]) - self.assertEqual(result.index.names, ["A", "B"], - "index_col not correctly set") - self.assertEqual(result.columns.tolist(), ["C", "D"], - "columns not set correctly whith index_col") + assert result.index.names == ["A", "B"] + assert result.columns.tolist() == ["C", "D"] def test_read_sql_delegate(self): iris_frame1 = sql.read_sql_query( @@ -927,10 +933,11 @@ def test_not_reflect_all_tables(self): sql.read_sql_table('other_table', self.conn) sql.read_sql_query('SELECT * FROM other_table', self.conn) # Verify some things - self.assertEqual(len(w), 0, "Warning triggered for other table") + assert len(w) == 0 def test_warning_case_insensitive_table_name(self): - # see GH7815. + # see gh-7815 + # # We can't test that this warning is triggered, a the database # configuration would have to be altered. But here we test that # the warning is certainly NOT triggered in a normal case. @@ -940,8 +947,7 @@ def test_warning_case_insensitive_table_name(self): # This should not trigger a Warning self.test_frame1.to_sql('CaseSensitive', self.conn) # Verify some things - self.assertEqual( - len(w), 0, "Warning triggered for writing a table") + assert len(w) == 0 def _get_index_columns(self, tbl_name): from sqlalchemy.engine import reflection @@ -957,8 +963,8 @@ def test_sqlalchemy_type_mapping(self): utc=True)}) db = sql.SQLDatabase(self.conn) table = sql.SQLTable("test_type", db, frame=df) - self.assertTrue(isinstance( - table.table.c['time'].type, sqltypes.DateTime)) + # GH 9086: TIMESTAMP is the suggested type for datetimes with timezones + assert isinstance(table.table.c['time'].type, sqltypes.TIMESTAMP) def test_database_uri_string(self): @@ -981,8 +987,15 @@ def test_database_uri_string(self): # using driver that will not be installed on Travis to trigger error # in sqlalchemy.create_engine -> test passing of this error to user + try: + # the rest of this test depends on pg8000's being absent + import pg8000 # noqa + pytest.skip("pg8000 is installed") + except ImportError: + pass + db_uri = "postgresql+pg8000://user:pass@host/dbname" - with tm.assertRaisesRegexp(ImportError, "pg8000"): + with pytest.raises(ImportError, match="pg8000"): sql.read_sql("select * from table", db_uri) def _make_iris_table_metadata(self): @@ -1004,7 +1017,7 @@ def test_query_by_text_obj(self): iris_df = sql.read_sql(name_text, self.conn, params={ 'name': 'Iris-versicolor'}) all_names = set(iris_df['Name']) - self.assertEqual(all_names, set(['Iris-versicolor'])) + assert all_names == {'Iris-versicolor'} def test_query_by_select_obj(self): # WIP : GH10846 @@ -1015,7 +1028,7 @@ def test_query_by_select_obj(self): iris_df = sql.read_sql(name_select, self.conn, params={'name': 'Iris-setosa'}) all_names = set(iris_df['Name']) - self.assertEqual(all_names, set(['Iris-setosa'])) + assert all_names == {'Iris-setosa'} class _EngineToConnMixin(object): @@ -1023,8 +1036,9 @@ class _EngineToConnMixin(object): A mixin that causes setup_connect to create a conn rather than an engine. """ - def setUp(self): - super(_EngineToConnMixin, self).setUp() + @pytest.fixture(autouse=True) + def setup_method(self, load_iris_data): + super(_EngineToConnMixin, self).load_test_data_and_sql() engine = self.conn conn = engine.connect() self.__tx = conn.begin() @@ -1032,21 +1046,23 @@ def setUp(self): self.__engine = engine self.conn = conn - def tearDown(self): + yield + self.__tx.rollback() self.conn.close() self.conn = self.__engine self.pandasSQL = sql.SQLDatabase(self.__engine) - super(_EngineToConnMixin, self).tearDown() + # XXX: + # super(_EngineToConnMixin, self).teardown_method(method) @pytest.mark.single -class TestSQLApiConn(_EngineToConnMixin, TestSQLApi, unittest.TestCase): +class TestSQLApiConn(_EngineToConnMixin, TestSQLApi): pass @pytest.mark.single -class TestSQLiteFallbackApi(SQLiteMixIn, _TestSQLApi, unittest.TestCase): +class TestSQLiteFallbackApi(SQLiteMixIn, _TestSQLApi): """ Test the public sqlite connection fallback API @@ -1078,8 +1094,8 @@ def test_sql_open_close(self): def test_con_string_import_error(self): if not SQLALCHEMY_INSTALLED: conn = 'mysql://root@localhost/pandas_nosetest' - self.assertRaises(ImportError, sql.read_sql, "SELECT * FROM iris", - conn) + pytest.raises(ImportError, sql.read_sql, "SELECT * FROM iris", + conn) else: pytest.skip('SQLAlchemy is installed') @@ -1088,7 +1104,7 @@ def test_read_sql_delegate(self): iris_frame2 = sql.read_sql("SELECT * FROM iris", self.conn) tm.assert_frame_equal(iris_frame1, iris_frame2) - self.assertRaises(sql.DatabaseError, sql.read_sql, 'iris', self.conn) + pytest.raises(sql.DatabaseError, sql.read_sql, 'iris', self.conn) def test_safe_names_warning(self): # GH 6798 @@ -1100,7 +1116,7 @@ def test_safe_names_warning(self): def test_get_schema2(self): # without providing a connection object (available for backwards comp) create_sql = sql.get_schema(self.test_frame1, 'test') - self.assertTrue('CREATE' in create_sql) + assert 'CREATE' in create_sql def _get_sqlite_column_type(self, schema, column): @@ -1117,8 +1133,7 @@ def test_sqlite_type_mapping(self): db = sql.SQLiteDatabase(self.conn) table = sql.SQLiteTable("test_type", db, frame=df) schema = table.sql_schema() - self.assertEqual(self._get_sqlite_column_type(schema, 'time'), - "TIMESTAMP") + assert self._get_sqlite_column_type(schema, 'time') == "TIMESTAMP" # ----------------------------------------------------------------------------- @@ -1135,26 +1150,21 @@ class _TestSQLAlchemy(SQLAlchemyMixIn, PandasSQLTest): """ flavor = None - @classmethod - def setUpClass(cls): + @pytest.fixture(autouse=True, scope='class') + def setup_class(cls): cls.setup_import() cls.setup_driver() + conn = cls.connect() + conn.connect() - # test connection - try: - conn = cls.connect() - conn.connect() - except sqlalchemy.exc.OperationalError: - msg = "{0} - can't connect to {1} server".format(cls, cls.flavor) - pytest.skip(msg) - - def setUp(self): - self.setup_connect() - - self._load_iris_data() + def load_test_data_and_sql(self): self._load_raw_sql() self._load_test1_data() + @pytest.fixture(autouse=True) + def setup_method(self, load_iris_data): + self.load_test_data_and_sql() + @classmethod def setup_import(cls): # Skip this test if SQLAlchemy not available @@ -1179,7 +1189,7 @@ def setup_connect(self): pytest.skip( "Can't connect to {0} server".format(self.flavor)) - def test_aread_sql(self): + def test_read_sql(self): self._read_sql_iris() def test_read_sql_parameter(self): @@ -1203,6 +1213,12 @@ def test_to_sql_replace(self): def test_to_sql_append(self): self._to_sql_append() + def test_to_sql_method_multi(self): + self._to_sql(method='multi') + + def test_to_sql_method_callable(self): + self._to_sql_method_callable() + def test_create_table(self): temp_conn = self.connect() temp_frame = DataFrame( @@ -1211,8 +1227,7 @@ def test_create_table(self): pandasSQL = sql.SQLDatabase(temp_conn) pandasSQL.to_sql(temp_frame, 'temp_frame') - self.assertTrue( - temp_conn.has_table('temp_frame'), 'Table not written to DB') + assert temp_conn.has_table('temp_frame') def test_drop_table(self): temp_conn = self.connect() @@ -1223,13 +1238,11 @@ def test_drop_table(self): pandasSQL = sql.SQLDatabase(temp_conn) pandasSQL.to_sql(temp_frame, 'temp_frame') - self.assertTrue( - temp_conn.has_table('temp_frame'), 'Table not written to DB') + assert temp_conn.has_table('temp_frame') pandasSQL.drop_table('temp_frame') - self.assertFalse( - temp_conn.has_table('temp_frame'), 'Table not deleted from DB') + assert not temp_conn.has_table('temp_frame') def test_roundtrip(self): self._roundtrip() @@ -1248,25 +1261,20 @@ def test_read_table_columns(self): iris_frame.columns.values, ['SepalLength', 'SepalLength']) def test_read_table_absent(self): - self.assertRaises( + pytest.raises( ValueError, sql.read_sql_table, "this_doesnt_exist", con=self.conn) def test_default_type_conversion(self): df = sql.read_sql_table("types_test_data", self.conn) - self.assertTrue(issubclass(df.FloatCol.dtype.type, np.floating), - "FloatCol loaded with incorrect type") - self.assertTrue(issubclass(df.IntCol.dtype.type, np.integer), - "IntCol loaded with incorrect type") - self.assertTrue(issubclass(df.BoolCol.dtype.type, np.bool_), - "BoolCol loaded with incorrect type") + assert issubclass(df.FloatCol.dtype.type, np.floating) + assert issubclass(df.IntCol.dtype.type, np.integer) + assert issubclass(df.BoolCol.dtype.type, np.bool_) # Int column with NA values stays as float - self.assertTrue(issubclass(df.IntColWithNull.dtype.type, np.floating), - "IntColWithNull loaded with incorrect type") + assert issubclass(df.IntColWithNull.dtype.type, np.floating) # Bool column with NA values becomes object - self.assertTrue(issubclass(df.BoolColWithNull.dtype.type, np.object), - "BoolColWithNull loaded with incorrect type") + assert issubclass(df.BoolColWithNull.dtype.type, np.object) def test_bigint(self): # int64 should be converted to BigInteger, GH7433 @@ -1281,8 +1289,7 @@ def test_default_date_load(self): # IMPORTANT - sqlite has no native date type, so shouldn't parse, but # MySQL SHOULD be converted. - self.assertTrue(issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) def test_datetime_with_timezone(self): # edge case that converts postgresql datetime with time zone types @@ -1296,24 +1303,24 @@ def check(col): # "2000-01-01 00:00:00-08:00" should convert to # "2000-01-01 08:00:00" - self.assertEqual(col[0], Timestamp('2000-01-01 08:00:00')) + assert col[0] == Timestamp('2000-01-01 08:00:00') # "2000-06-01 00:00:00-07:00" should convert to # "2000-06-01 07:00:00" - self.assertEqual(col[1], Timestamp('2000-06-01 07:00:00')) + assert col[1] == Timestamp('2000-06-01 07:00:00') elif is_datetime64tz_dtype(col.dtype): - self.assertTrue(str(col.dt.tz) == 'UTC') + assert str(col.dt.tz) == 'UTC' # "2000-01-01 00:00:00-08:00" should convert to # "2000-01-01 08:00:00" - self.assertEqual(col[0], Timestamp( - '2000-01-01 08:00:00', tz='UTC')) - # "2000-06-01 00:00:00-07:00" should convert to # "2000-06-01 07:00:00" - self.assertEqual(col[1], Timestamp( - '2000-06-01 07:00:00', tz='UTC')) + # GH 6415 + expected_data = [Timestamp('2000-01-01 08:00:00', tz='UTC'), + Timestamp('2000-06-01 07:00:00', tz='UTC')] + expected = Series(expected_data, name=col.name) + tm.assert_series_equal(col, expected) else: raise AssertionError("DateCol loaded with incorrect type " @@ -1328,69 +1335,102 @@ def check(col): # even with the same versions of psycopg2 & sqlalchemy, possibly a # Postgrsql server version difference col = df.DateColWithTz - self.assertTrue(is_object_dtype(col.dtype) or - is_datetime64_dtype(col.dtype) or - is_datetime64tz_dtype(col.dtype), - "DateCol loaded with incorrect type -> {0}" - .format(col.dtype)) + assert is_datetime64tz_dtype(col.dtype) df = pd.read_sql_query("select * from types_test_data", self.conn, parse_dates=['DateColWithTz']) if not hasattr(df, 'DateColWithTz'): pytest.skip("no column with datetime with time zone") + col = df.DateColWithTz + assert is_datetime64tz_dtype(col.dtype) + assert str(col.dt.tz) == 'UTC' check(df.DateColWithTz) df = pd.concat(list(pd.read_sql_query("select * from types_test_data", self.conn, chunksize=1)), ignore_index=True) col = df.DateColWithTz - self.assertTrue(is_datetime64tz_dtype(col.dtype), - "DateCol loaded with incorrect type -> {0}" - .format(col.dtype)) - self.assertTrue(str(col.dt.tz) == 'UTC') + assert is_datetime64tz_dtype(col.dtype) + assert str(col.dt.tz) == 'UTC' expected = sql.read_sql_table("types_test_data", self.conn) - tm.assert_series_equal(df.DateColWithTz, - expected.DateColWithTz - .astype('datetime64[ns, UTC]')) + col = expected.DateColWithTz + assert is_datetime64tz_dtype(col.dtype) + tm.assert_series_equal(df.DateColWithTz, expected.DateColWithTz) # xref #7139 # this might or might not be converted depending on the postgres driver df = sql.read_sql_table("types_test_data", self.conn) check(df.DateColWithTz) + def test_datetime_with_timezone_roundtrip(self): + # GH 9086 + # Write datetimetz data to a db and read it back + # For dbs that support timestamps with timezones, should get back UTC + # otherwise naive data should be returned + expected = DataFrame({'A': date_range( + '2013-01-01 09:00:00', periods=3, tz='US/Pacific' + )}) + expected.to_sql('test_datetime_tz', self.conn, index=False) + + if self.flavor == 'postgresql': + # SQLAlchemy "timezones" (i.e. offsets) are coerced to UTC + expected['A'] = expected['A'].dt.tz_convert('UTC') + else: + # Otherwise, timestamps are returned as local, naive + expected['A'] = expected['A'].dt.tz_localize(None) + + result = sql.read_sql_table('test_datetime_tz', self.conn) + tm.assert_frame_equal(result, expected) + + result = sql.read_sql_query( + 'SELECT * FROM test_datetime_tz', self.conn + ) + if self.flavor == 'sqlite': + # read_sql_query does not return datetime type like read_sql_table + assert isinstance(result.loc[0, 'A'], string_types) + result['A'] = to_datetime(result['A']) + tm.assert_frame_equal(result, expected) + + def test_naive_datetimeindex_roundtrip(self): + # GH 23510 + # Ensure that a naive DatetimeIndex isn't converted to UTC + dates = date_range('2018-01-01', periods=5, freq='6H') + expected = DataFrame({'nums': range(5)}, index=dates) + expected.to_sql('foo_table', self.conn, index_label='info_date') + result = sql.read_sql_table('foo_table', self.conn, + index_col='info_date') + # result index with gain a name from a set_index operation; expected + tm.assert_frame_equal(result, expected, check_names=False) + def test_date_parsing(self): # No Parsing df = sql.read_sql_table("types_test_data", self.conn) + expected_type = object if self.flavor == 'sqlite' else np.datetime64 + assert issubclass(df.DateCol.dtype.type, expected_type) df = sql.read_sql_table("types_test_data", self.conn, parse_dates=['DateCol']) - self.assertTrue(issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) df = sql.read_sql_table("types_test_data", self.conn, parse_dates={'DateCol': '%Y-%m-%d %H:%M:%S'}) - self.assertTrue(issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) df = sql.read_sql_table("types_test_data", self.conn, parse_dates={ 'DateCol': {'format': '%Y-%m-%d %H:%M:%S'}}) - self.assertTrue(issubclass(df.DateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.DateCol.dtype.type, np.datetime64) df = sql.read_sql_table( "types_test_data", self.conn, parse_dates=['IntDateCol']) - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) df = sql.read_sql_table( "types_test_data", self.conn, parse_dates={'IntDateCol': 's'}) - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) df = sql.read_sql_table("types_test_data", self.conn, parse_dates={'IntDateCol': {'unit': 's'}}) - self.assertTrue(issubclass(df.IntDateCol.dtype.type, np.datetime64), - "IntDateCol loaded with incorrect type") + assert issubclass(df.IntDateCol.dtype.type, np.datetime64) def test_datetime(self): df = DataFrame({'A': date_range('2013-01-01 09:00:00', periods=3), @@ -1406,7 +1446,7 @@ def test_datetime(self): result = sql.read_sql_query('SELECT * FROM test_datetime', self.conn) result = result.drop('index', axis=1) if self.flavor == 'sqlite': - self.assertTrue(isinstance(result.loc[0, 'A'], string_types)) + assert isinstance(result.loc[0, 'A'], string_types) result['A'] = to_datetime(result['A']) tm.assert_frame_equal(result, df) else: @@ -1425,7 +1465,7 @@ def test_datetime_NaT(self): # with read_sql -> no type information -> sqlite has no native result = sql.read_sql_query('SELECT * FROM test_datetime', self.conn) if self.flavor == 'sqlite': - self.assertTrue(isinstance(result.loc[0, 'A'], string_types)) + assert isinstance(result.loc[0, 'A'], string_types) result['A'] = to_datetime(result['A'], errors='coerce') tm.assert_frame_equal(result, df) else: @@ -1436,8 +1476,10 @@ def test_datetime_date(self): df = DataFrame([date(2014, 1, 1), date(2014, 1, 2)], columns=["a"]) df.to_sql('test_date', self.conn, index=False) res = read_sql_table('test_date', self.conn) + result = res['a'] + expected = to_datetime(df['a']) # comes back as datetime64 - tm.assert_series_equal(res['a'], to_datetime(df['a'])) + tm.assert_series_equal(result, expected) def test_datetime_time(self): # test support for datetime.time @@ -1558,16 +1600,16 @@ def test_dtype(self): meta = sqlalchemy.schema.MetaData(bind=self.conn) meta.reflect() sqltype = meta.tables['dtype_test2'].columns['B'].type - self.assertTrue(isinstance(sqltype, sqlalchemy.TEXT)) - self.assertRaises(ValueError, df.to_sql, - 'error', self.conn, dtype={'B': str}) + assert isinstance(sqltype, sqlalchemy.TEXT) + pytest.raises(ValueError, df.to_sql, + 'error', self.conn, dtype={'B': str}) # GH9083 df.to_sql('dtype_test3', self.conn, dtype={'B': sqlalchemy.String(10)}) meta.reflect() sqltype = meta.tables['dtype_test3'].columns['B'].type - self.assertTrue(isinstance(sqltype, sqlalchemy.String)) - self.assertEqual(sqltype.length, 10) + assert isinstance(sqltype, sqlalchemy.String) + assert sqltype.length == 10 # single dtype df.to_sql('single_dtype_test', self.conn, dtype=sqlalchemy.TEXT) @@ -1575,10 +1617,10 @@ def test_dtype(self): meta.reflect() sqltypea = meta.tables['single_dtype_test'].columns['A'].type sqltypeb = meta.tables['single_dtype_test'].columns['B'].type - self.assertTrue(isinstance(sqltypea, sqlalchemy.TEXT)) - self.assertTrue(isinstance(sqltypeb, sqlalchemy.TEXT)) + assert isinstance(sqltypea, sqlalchemy.TEXT) + assert isinstance(sqltypeb, sqlalchemy.TEXT) - def test_notnull_dtype(self): + def test_notna_dtype(self): cols = {'Bool': Series([True, None]), 'Date': Series([datetime(2012, 5, 1), None]), 'Int': Series([1, None], dtype='object'), @@ -1586,7 +1628,7 @@ def test_notnull_dtype(self): } df = DataFrame(cols) - tbl = 'notnull_dtype_test' + tbl = 'notna_dtype_test' df.to_sql(tbl, self.conn) returned_df = sql.read_sql_table(tbl, self.conn) # noqa meta = sqlalchemy.schema.MetaData(bind=self.conn) @@ -1598,10 +1640,10 @@ def test_notnull_dtype(self): col_dict = meta.tables[tbl].columns - self.assertTrue(isinstance(col_dict['Bool'].type, my_type)) - self.assertTrue(isinstance(col_dict['Date'].type, sqltypes.DateTime)) - self.assertTrue(isinstance(col_dict['Int'].type, sqltypes.Integer)) - self.assertTrue(isinstance(col_dict['Float'].type, sqltypes.Float)) + assert isinstance(col_dict['Bool'].type, my_type) + assert isinstance(col_dict['Date'].type, sqltypes.DateTime) + assert isinstance(col_dict['Int'].type, sqltypes.Integer) + assert isinstance(col_dict['Float'].type, sqltypes.Float) def test_double_precision(self): V = 1.23456789101112131415 @@ -1618,19 +1660,18 @@ def test_double_precision(self): res = sql.read_sql_table('test_dtypes', self.conn) # check precision of float64 - self.assertEqual(np.round(df['f64'].iloc[0], 14), - np.round(res['f64'].iloc[0], 14)) + assert (np.round(df['f64'].iloc[0], 14) == + np.round(res['f64'].iloc[0], 14)) # check sql types meta = sqlalchemy.schema.MetaData(bind=self.conn) meta.reflect() col_dict = meta.tables['test_dtypes'].columns - self.assertEqual(str(col_dict['f32'].type), - str(col_dict['f64_as_f32'].type)) - self.assertTrue(isinstance(col_dict['f32'].type, sqltypes.Float)) - self.assertTrue(isinstance(col_dict['f64'].type, sqltypes.Float)) - self.assertTrue(isinstance(col_dict['i32'].type, sqltypes.Integer)) - self.assertTrue(isinstance(col_dict['i64'].type, sqltypes.BigInteger)) + assert str(col_dict['f32'].type) == str(col_dict['f64_as_f32'].type) + assert isinstance(col_dict['f32'].type, sqltypes.Float) + assert isinstance(col_dict['f64'].type, sqltypes.Float) + assert isinstance(col_dict['i32'].type, sqltypes.Integer) + assert isinstance(col_dict['i64'].type, sqltypes.BigInteger) def test_connectable_issue_example(self): # This tests the example raised in issue @@ -1706,27 +1747,23 @@ def setup_driver(cls): def test_default_type_conversion(self): df = sql.read_sql_table("types_test_data", self.conn) - self.assertTrue(issubclass(df.FloatCol.dtype.type, np.floating), - "FloatCol loaded with incorrect type") - self.assertTrue(issubclass(df.IntCol.dtype.type, np.integer), - "IntCol loaded with incorrect type") + assert issubclass(df.FloatCol.dtype.type, np.floating) + assert issubclass(df.IntCol.dtype.type, np.integer) + # sqlite has no boolean type, so integer type is returned - self.assertTrue(issubclass(df.BoolCol.dtype.type, np.integer), - "BoolCol loaded with incorrect type") + assert issubclass(df.BoolCol.dtype.type, np.integer) # Int column with NA values stays as float - self.assertTrue(issubclass(df.IntColWithNull.dtype.type, np.floating), - "IntColWithNull loaded with incorrect type") + assert issubclass(df.IntColWithNull.dtype.type, np.floating) + # Non-native Bool column with NA values stays as float - self.assertTrue(issubclass(df.BoolColWithNull.dtype.type, np.floating), - "BoolColWithNull loaded with incorrect type") + assert issubclass(df.BoolColWithNull.dtype.type, np.floating) def test_default_date_load(self): df = sql.read_sql_table("types_test_data", self.conn) # IMPORTANT - sqlite has no native date type, so shouldn't parse, but - self.assertFalse(issubclass(df.DateCol.dtype.type, np.datetime64), - "DateCol loaded with incorrect type") + assert not issubclass(df.DateCol.dtype.type, np.datetime64) def test_bigint_warning(self): # test no warning for BIGINT (to support int64) is raised (GH7433) @@ -1736,7 +1773,7 @@ def test_bigint_warning(self): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") sql.read_sql_table('test_bigintwarning', self.conn) - self.assertEqual(len(w), 0, "Warning triggered for other table") + assert len(w) == 0 class _TestMySQLAlchemy(object): @@ -1749,35 +1786,33 @@ class _TestMySQLAlchemy(object): @classmethod def connect(cls): url = 'mysql+{driver}://root@localhost/pandas_nosetest' - return sqlalchemy.create_engine(url.format(driver=cls.driver)) + return sqlalchemy.create_engine(url.format(driver=cls.driver), + connect_args=cls.connect_args) @classmethod def setup_driver(cls): - try: - import pymysql # noqa - cls.driver = 'pymysql' - except ImportError: - pytest.skip('pymysql not installed') + pymysql = pytest.importorskip('pymysql') + cls.driver = 'pymysql' + cls.connect_args = { + 'client_flag': pymysql.constants.CLIENT.MULTI_STATEMENTS} def test_default_type_conversion(self): df = sql.read_sql_table("types_test_data", self.conn) - self.assertTrue(issubclass(df.FloatCol.dtype.type, np.floating), - "FloatCol loaded with incorrect type") - self.assertTrue(issubclass(df.IntCol.dtype.type, np.integer), - "IntCol loaded with incorrect type") + assert issubclass(df.FloatCol.dtype.type, np.floating) + assert issubclass(df.IntCol.dtype.type, np.integer) + # MySQL has no real BOOL type (it's an alias for TINYINT) - self.assertTrue(issubclass(df.BoolCol.dtype.type, np.integer), - "BoolCol loaded with incorrect type") + assert issubclass(df.BoolCol.dtype.type, np.integer) # Int column with NA values stays as float - self.assertTrue(issubclass(df.IntColWithNull.dtype.type, np.floating), - "IntColWithNull loaded with incorrect type") + assert issubclass(df.IntColWithNull.dtype.type, np.floating) + # Bool column with NA = int column with NA values => becomes float - self.assertTrue(issubclass(df.BoolColWithNull.dtype.type, np.floating), - "BoolColWithNull loaded with incorrect type") + assert issubclass(df.BoolColWithNull.dtype.type, np.floating) def test_read_procedure(self): + import pymysql # see GH7324. Although it is more an api test, it is added to the # mysql tests as sqlite does not have stored procedures df = DataFrame({'a': [1, 2, 3], 'b': [0.1, 0.2, 0.3]}) @@ -1796,7 +1831,7 @@ def test_read_procedure(self): try: r1 = connection.execute(proc) # noqa trans.commit() - except: + except pymysql.Error: trans.rollback() raise @@ -1822,11 +1857,8 @@ def connect(cls): @classmethod def setup_driver(cls): - try: - import psycopg2 # noqa - cls.driver = 'psycopg2' - except ImportError: - pytest.skip('psycopg2 not installed') + pytest.importorskip('psycopg2') + cls.driver = 'psycopg2' def test_schema_support(self): # only test this for postgresql (schema's not supported in @@ -1855,8 +1887,8 @@ def test_schema_support(self): res4 = sql.read_sql_table('test_schema_other', self.conn, schema='other') tm.assert_frame_equal(df, res4) - self.assertRaises(ValueError, sql.read_sql_table, 'test_schema_other', - self.conn, schema='public') + pytest.raises(ValueError, sql.read_sql_table, 'test_schema_other', + self.conn, schema='public') # different if_exists options @@ -1892,39 +1924,68 @@ def test_schema_support(self): res2 = pdsql.read_table('test_schema_other2') tm.assert_frame_equal(res1, res2) + def test_copy_from_callable_insertion_method(self): + # GH 8953 + # Example in io.rst found under _io.sql.method + # not available in sqlite, mysql + def psql_insert_copy(table, conn, keys, data_iter): + # gets a DBAPI connection that can provide a cursor + dbapi_conn = conn.connection + with dbapi_conn.cursor() as cur: + s_buf = compat.StringIO() + writer = csv.writer(s_buf) + writer.writerows(data_iter) + s_buf.seek(0) + + columns = ', '.join('"{}"'.format(k) for k in keys) + if table.schema: + table_name = '{}.{}'.format(table.schema, table.name) + else: + table_name = table.name + + sql_query = 'COPY {} ({}) FROM STDIN WITH CSV'.format( + table_name, columns) + cur.copy_expert(sql=sql_query, file=s_buf) + + expected = DataFrame({'col1': [1, 2], 'col2': [0.1, 0.2], + 'col3': ['a', 'n']}) + expected.to_sql('test_copy_insert', self.conn, index=False, + method=psql_insert_copy) + result = sql.read_sql_table('test_copy_insert', self.conn) + tm.assert_frame_equal(result, expected) + @pytest.mark.single -class TestMySQLAlchemy(_TestMySQLAlchemy, _TestSQLAlchemy, unittest.TestCase): +@pytest.mark.db +class TestMySQLAlchemy(_TestMySQLAlchemy, _TestSQLAlchemy): pass @pytest.mark.single -class TestMySQLAlchemyConn(_TestMySQLAlchemy, _TestSQLAlchemyConn, - unittest.TestCase): +@pytest.mark.db +class TestMySQLAlchemyConn(_TestMySQLAlchemy, _TestSQLAlchemyConn): pass @pytest.mark.single -class TestPostgreSQLAlchemy(_TestPostgreSQLAlchemy, _TestSQLAlchemy, - unittest.TestCase): +@pytest.mark.db +class TestPostgreSQLAlchemy(_TestPostgreSQLAlchemy, _TestSQLAlchemy): pass @pytest.mark.single -class TestPostgreSQLAlchemyConn(_TestPostgreSQLAlchemy, _TestSQLAlchemyConn, - unittest.TestCase): +@pytest.mark.db +class TestPostgreSQLAlchemyConn(_TestPostgreSQLAlchemy, _TestSQLAlchemyConn): pass @pytest.mark.single -class TestSQLiteAlchemy(_TestSQLiteAlchemy, _TestSQLAlchemy, - unittest.TestCase): +class TestSQLiteAlchemy(_TestSQLiteAlchemy, _TestSQLAlchemy): pass @pytest.mark.single -class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn, - unittest.TestCase): +class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn): pass @@ -1932,7 +1993,7 @@ class TestSQLiteAlchemyConn(_TestSQLiteAlchemy, _TestSQLAlchemyConn, # -- Test Sqlite / MySQL fallback @pytest.mark.single -class TestSQLiteFallback(SQLiteMixIn, PandasSQLTest, unittest.TestCase): +class TestSQLiteFallback(SQLiteMixIn, PandasSQLTest): """ Test the fallback mode against an in-memory sqlite database. @@ -1943,14 +2004,17 @@ class TestSQLiteFallback(SQLiteMixIn, PandasSQLTest, unittest.TestCase): def connect(cls): return sqlite3.connect(':memory:') - def setUp(self): + def setup_connect(self): self.conn = self.connect() - self.pandasSQL = sql.SQLiteDatabase(self.conn) - - self._load_iris_data() + def load_test_data_and_sql(self): + self.pandasSQL = sql.SQLiteDatabase(self.conn) self._load_test1_data() + @pytest.fixture(autouse=True) + def setup_method(self, load_iris_data): + self.load_test_data_and_sql() + def test_read_sql(self): self._read_sql_iris() @@ -1981,13 +2045,11 @@ def test_create_and_drop_table(self): self.pandasSQL.to_sql(temp_frame, 'drop_test_frame') - self.assertTrue(self.pandasSQL.has_table('drop_test_frame'), - 'Table not written to DB') + assert self.pandasSQL.has_table('drop_test_frame') self.pandasSQL.drop_table('drop_test_frame') - self.assertFalse(self.pandasSQL.has_table('drop_test_frame'), - 'Table not deleted from DB') + assert not self.pandasSQL.has_table('drop_test_frame') def test_roundtrip(self): self._roundtrip() @@ -2053,22 +2115,22 @@ def test_dtype(self): df.to_sql('dtype_test2', self.conn, dtype={'B': 'STRING'}) # sqlite stores Boolean values as INTEGER - self.assertEqual(self._get_sqlite_column_type( - 'dtype_test', 'B'), 'INTEGER') + assert self._get_sqlite_column_type( + 'dtype_test', 'B') == 'INTEGER' - self.assertEqual(self._get_sqlite_column_type( - 'dtype_test2', 'B'), 'STRING') - self.assertRaises(ValueError, df.to_sql, - 'error', self.conn, dtype={'B': bool}) + assert self._get_sqlite_column_type( + 'dtype_test2', 'B') == 'STRING' + pytest.raises(ValueError, df.to_sql, + 'error', self.conn, dtype={'B': bool}) # single dtype df.to_sql('single_dtype_test', self.conn, dtype='STRING') - self.assertEqual( - self._get_sqlite_column_type('single_dtype_test', 'A'), 'STRING') - self.assertEqual( - self._get_sqlite_column_type('single_dtype_test', 'B'), 'STRING') + assert self._get_sqlite_column_type( + 'single_dtype_test', 'A') == 'STRING' + assert self._get_sqlite_column_type( + 'single_dtype_test', 'B') == 'STRING' - def test_notnull_dtype(self): + def test_notna_dtype(self): if self.flavor == 'mysql': pytest.skip('Not applicable to MySQL legacy') @@ -2079,21 +2141,20 @@ def test_notnull_dtype(self): } df = DataFrame(cols) - tbl = 'notnull_dtype_test' + tbl = 'notna_dtype_test' df.to_sql(tbl, self.conn) - self.assertEqual(self._get_sqlite_column_type(tbl, 'Bool'), 'INTEGER') - self.assertEqual(self._get_sqlite_column_type( - tbl, 'Date'), 'TIMESTAMP') - self.assertEqual(self._get_sqlite_column_type(tbl, 'Int'), 'INTEGER') - self.assertEqual(self._get_sqlite_column_type(tbl, 'Float'), 'REAL') + assert self._get_sqlite_column_type(tbl, 'Bool') == 'INTEGER' + assert self._get_sqlite_column_type(tbl, 'Date') == 'TIMESTAMP' + assert self._get_sqlite_column_type(tbl, 'Int') == 'INTEGER' + assert self._get_sqlite_column_type(tbl, 'Float') == 'REAL' def test_illegal_names(self): # For sqlite, these should work fine df = DataFrame([[1, 2], [3, 4]], columns=['a', 'b']) # Raise error on blank - self.assertRaises(ValueError, df.to_sql, "", self.conn) + pytest.raises(ValueError, df.to_sql, "", self.conn) for ndx, weird_name in enumerate( ['test_weird_name]', 'test_weird_name[', @@ -2113,6 +2174,11 @@ def test_illegal_names(self): # -- Old tests from 0.13.1 (before refactor using sqlalchemy) +def date_format(dt): + """Returns date in YYYYMMDD format.""" + return dt.strftime('%Y%m%d') + + _formatters = { datetime: lambda dt: "'%s'" % date_format(dt), str: lambda x: "'%s'" % x, @@ -2133,7 +2199,7 @@ def format_query(sql, *args): """ processed_args = [] for arg in args: - if isinstance(arg, float) and isnull(arg): + if isinstance(arg, float) and isna(arg): arg = None formatter = _formatters[type(arg)] @@ -2151,17 +2217,18 @@ def tquery(query, con=None, cur=None): return list(res) -def _skip_if_no_pymysql(): - try: - import pymysql # noqa - except ImportError: - pytest.skip('pymysql not installed, skipping') - - @pytest.mark.single -class TestXSQLite(SQLiteMixIn, tm.TestCase): +class TestXSQLite(SQLiteMixIn): - def setUp(self): + @pytest.fixture(autouse=True) + def setup_method(self, request, datapath): + self.method = request.function + self.conn = sqlite3.connect(':memory:') + + # In some test cases we may close db connection + # Re-open conn here so we can perform cleanup in teardown + yield + self.method = request.function self.conn = sqlite3.connect(':memory:') def test_basic(self): @@ -2187,7 +2254,7 @@ def test_write_row_by_row(self): result = sql.read_sql("select * from test", con=self.conn) result.index = frame.index - tm.assert_frame_equal(result, frame) + tm.assert_frame_equal(result, frame, check_less_precise=True) def test_execute(self): frame = tm.makeTimeDataFrame() @@ -2211,12 +2278,12 @@ def test_schema(self): for l in lines: tokens = l.split(' ') if len(tokens) == 2 and tokens[0] == 'A': - self.assertTrue(tokens[1] == 'DATETIME') + assert tokens[1] == 'DATETIME' frame = tm.makeTimeDataFrame() create_sql = sql.get_schema(frame, 'test', keys=['A', 'B']) lines = create_sql.splitlines() - self.assertTrue('PRIMARY KEY ("A", "B")' in create_sql) + assert 'PRIMARY KEY ("A", "B")' in create_sql cur = self.conn.cursor() cur.execute(create_sql) @@ -2236,13 +2303,8 @@ def test_execute_fail(self): sql.execute('INSERT INTO test VALUES("foo", "bar", 1.234)', self.conn) sql.execute('INSERT INTO test VALUES("foo", "baz", 2.567)', self.conn) - try: - sys.stdout = StringIO() - self.assertRaises(Exception, sql.execute, - 'INSERT INTO test VALUES("foo", "bar", 7)', - self.conn) - finally: - sys.stdout = sys.__stdout__ + with pytest.raises(Exception): + sql.execute('INSERT INTO test VALUES("foo", "bar", 7)', self.conn) def test_execute_closed_connection(self): create_sql = """ @@ -2259,15 +2321,9 @@ def test_execute_closed_connection(self): sql.execute('INSERT INTO test VALUES("foo", "bar", 1.234)', self.conn) self.conn.close() - try: - sys.stdout = StringIO() - self.assertRaises(Exception, tquery, "select * from test", - con=self.conn) - finally: - sys.stdout = sys.__stdout__ - # Initialize connection again (needed for tearDown) - self.setUp() + with pytest.raises(Exception): + tquery("select * from test", con=self.conn) def test_na_roundtrip(self): pass @@ -2305,10 +2361,10 @@ def test_onecolumn_of_integer(self): sql.to_sql(mono_df, con=self.conn, name='mono_df', index=False) # computing the sum via sql con_x = self.conn - the_sum = sum([my_c0[0] - for my_c0 in con_x.execute("select * from mono_df")]) + the_sum = sum(my_c0[0] + for my_c0 in con_x.execute("select * from mono_df")) # it should not fail, and gives 3 ( Issue #3628 ) - self.assertEqual(the_sum, 3) + assert the_sum == 3 result = sql.read_sql("select * from mono_df", con_x) tm.assert_frame_equal(result, mono_df) @@ -2328,140 +2384,96 @@ def clean_up(test_table_to_drop): self.drop_table(test_table_to_drop) # test if invalid value for if_exists raises appropriate error - self.assertRaises(ValueError, - sql.to_sql, - frame=df_if_exists_1, - con=self.conn, - name=table_name, - if_exists='notvalidvalue') + pytest.raises(ValueError, + sql.to_sql, + frame=df_if_exists_1, + con=self.conn, + name=table_name, + if_exists='notvalidvalue') clean_up(table_name) # test if_exists='fail' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='fail') - self.assertRaises(ValueError, - sql.to_sql, - frame=df_if_exists_1, - con=self.conn, - name=table_name, - if_exists='fail') + pytest.raises(ValueError, + sql.to_sql, + frame=df_if_exists_1, + con=self.conn, + name=table_name, + if_exists='fail') # test if_exists='replace' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='replace', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B')]) + assert tquery(sql_select, con=self.conn) == [(1, 'A'), (2, 'B')] sql.to_sql(frame=df_if_exists_2, con=self.conn, name=table_name, if_exists='replace', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(3, 'C'), (4, 'D'), (5, 'E')]) + assert (tquery(sql_select, con=self.conn) == + [(3, 'C'), (4, 'D'), (5, 'E')]) clean_up(table_name) # test if_exists='append' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='fail', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B')]) + assert tquery(sql_select, con=self.conn) == [(1, 'A'), (2, 'B')] sql.to_sql(frame=df_if_exists_2, con=self.conn, name=table_name, if_exists='append', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E')]) + assert (tquery(sql_select, con=self.conn) == + [(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E')]) clean_up(table_name) @pytest.mark.single -class TestSQLFlavorDeprecation(tm.TestCase): - """ - gh-13611: test that the 'flavor' parameter - is appropriately deprecated by checking the - functions that directly raise the warning - """ - - con = 1234 # don't need real connection for this - funcs = ['SQLiteDatabase', 'pandasSQL_builder'] - - def test_unsupported_flavor(self): - msg = 'is not supported' - - for func in self.funcs: - tm.assertRaisesRegexp(ValueError, msg, getattr(sql, func), - self.con, flavor='mysql') - - def test_deprecated_flavor(self): - for func in self.funcs: - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - getattr(sql, func)(self.con, flavor='sqlite') - - -@pytest.mark.single +@pytest.mark.db @pytest.mark.skip(reason="gh-13611: there is no support for MySQL " "if SQLAlchemy is not installed") -class TestXMySQL(MySQLMixIn, tm.TestCase): - - @classmethod - def setUpClass(cls): - _skip_if_no_pymysql() +class TestXMySQL(MySQLMixIn): - # test connection - import pymysql - try: - # Try Travis defaults. - # No real user should allow root access with a blank password. - pymysql.connect(host='localhost', user='root', passwd='', - db='pandas_nosetest') - except: - pass - else: - return + @pytest.fixture(autouse=True, scope='class') + def setup_class(cls): + pymysql = pytest.importorskip('pymysql') + pymysql.connect(host='localhost', user='root', passwd='', + db='pandas_nosetest') try: pymysql.connect(read_default_group='pandas') except pymysql.ProgrammingError: - pytest.skip( + raise RuntimeError( "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " - "typically located at ~/.my.cnf or /etc/.my.cnf. ") + "typically located at ~/.my.cnf or /etc/.my.cnf.") except pymysql.Error: - pytest.skip( + raise RuntimeError( "Cannot connect to database. " "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " - "typically located at ~/.my.cnf or /etc/.my.cnf. ") + "typically located at ~/.my.cnf or /etc/.my.cnf.") - def setUp(self): - _skip_if_no_pymysql() - import pymysql - try: - # Try Travis defaults. - # No real user should allow root access with a blank password. - self.conn = pymysql.connect(host='localhost', user='root', - passwd='', db='pandas_nosetest') - except: - pass - else: - return + @pytest.fixture(autouse=True) + def setup_method(self, request, datapath): + pymysql = pytest.importorskip('pymysql') + pymysql.connect(host='localhost', user='root', passwd='', + db='pandas_nosetest') try: - self.conn = pymysql.connect(read_default_group='pandas') + pymysql.connect(read_default_group='pandas') except pymysql.ProgrammingError: - pytest.skip( + raise RuntimeError( "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " - "typically located at ~/.my.cnf or /etc/.my.cnf. ") + "typically located at ~/.my.cnf or /etc/.my.cnf.") except pymysql.Error: - pytest.skip( + raise RuntimeError( "Cannot connect to database. " "Create a group of connection parameters under the heading " "[pandas] in your system's mysql default file, " - "typically located at ~/.my.cnf or /etc/.my.cnf. ") + "typically located at ~/.my.cnf or /etc/.my.cnf.") + + self.method = request.function def test_basic(self): - _skip_if_no_pymysql() frame = tm.makeTimeDataFrame() self._check_roundtrip(frame) def test_write_row_by_row(self): - - _skip_if_no_pymysql() frame = tm.makeTimeDataFrame() frame.iloc[0, 0] = np.nan drop_sql = "DROP TABLE IF EXISTS test" @@ -2478,10 +2490,9 @@ def test_write_row_by_row(self): result = sql.read_sql("select * from test", con=self.conn) result.index = frame.index - tm.assert_frame_equal(result, frame) + tm.assert_frame_equal(result, frame, check_less_precise=True) def test_chunksize_read_type(self): - _skip_if_no_pymysql() frame = tm.makeTimeDataFrame() frame.index.name = "index" drop_sql = "DROP TABLE IF EXISTS test" @@ -2496,7 +2507,6 @@ def test_chunksize_read_type(self): tm.assert_frame_equal(frame[:chunksize], chunk_df) def test_execute(self): - _skip_if_no_pymysql() frame = tm.makeTimeDataFrame() drop_sql = "DROP TABLE IF EXISTS test" create_sql = sql.get_schema(frame, 'test') @@ -2516,26 +2526,24 @@ def test_execute(self): tm.assert_frame_equal(result, frame[:1]) def test_schema(self): - _skip_if_no_pymysql() frame = tm.makeTimeDataFrame() create_sql = sql.get_schema(frame, 'test') lines = create_sql.splitlines() for l in lines: tokens = l.split(' ') if len(tokens) == 2 and tokens[0] == 'A': - self.assertTrue(tokens[1] == 'DATETIME') + assert tokens[1] == 'DATETIME' frame = tm.makeTimeDataFrame() drop_sql = "DROP TABLE IF EXISTS test" create_sql = sql.get_schema(frame, 'test', keys=['A', 'B']) lines = create_sql.splitlines() - self.assertTrue('PRIMARY KEY (`A`, `B`)' in create_sql) + assert 'PRIMARY KEY (`A`, `B`)' in create_sql cur = self.conn.cursor() cur.execute(drop_sql) cur.execute(create_sql) def test_execute_fail(self): - _skip_if_no_pymysql() drop_sql = "DROP TABLE IF EXISTS test" create_sql = """ CREATE TABLE test @@ -2553,16 +2561,10 @@ def test_execute_fail(self): sql.execute('INSERT INTO test VALUES("foo", "bar", 1.234)', self.conn) sql.execute('INSERT INTO test VALUES("foo", "baz", 2.567)', self.conn) - try: - sys.stdout = StringIO() - self.assertRaises(Exception, sql.execute, - 'INSERT INTO test VALUES("foo", "bar", 7)', - self.conn) - finally: - sys.stdout = sys.__stdout__ + with pytest.raises(Exception): + sql.execute('INSERT INTO test VALUES("foo", "bar", 7)', self.conn) - def test_execute_closed_connection(self): - _skip_if_no_pymysql() + def test_execute_closed_connection(self, request, datapath): drop_sql = "DROP TABLE IF EXISTS test" create_sql = """ CREATE TABLE test @@ -2579,22 +2581,17 @@ def test_execute_closed_connection(self): sql.execute('INSERT INTO test VALUES("foo", "bar", 1.234)', self.conn) self.conn.close() - try: - sys.stdout = StringIO() - self.assertRaises(Exception, tquery, "select * from test", - con=self.conn) - finally: - sys.stdout = sys.__stdout__ + + with pytest.raises(Exception): + tquery("select * from test", con=self.conn) # Initialize connection again (needed for tearDown) - self.setUp() + self.setup_method(request, datapath) def test_na_roundtrip(self): - _skip_if_no_pymysql() pass def _check_roundtrip(self, frame): - _skip_if_no_pymysql() drop_sql = "DROP TABLE IF EXISTS test_table" cur = self.conn.cursor() with warnings.catch_warnings(): @@ -2631,13 +2628,11 @@ def _check_roundtrip(self, frame): tm.assert_frame_equal(expected, result) def test_keyword_as_column_names(self): - _skip_if_no_pymysql() df = DataFrame({'From': np.ones(5)}) sql.to_sql(df, con=self.conn, name='testkeywords', if_exists='replace', index=False) def test_if_exists(self): - _skip_if_no_pymysql() df_if_exists_1 = DataFrame({'col1': [1, 2], 'col2': ['A', 'B']}) df_if_exists_2 = DataFrame( {'col1': [3, 4, 5], 'col2': ['C', 'D', 'E']}) @@ -2652,42 +2647,40 @@ def clean_up(test_table_to_drop): self.drop_table(test_table_to_drop) # test if invalid value for if_exists raises appropriate error - self.assertRaises(ValueError, - sql.to_sql, - frame=df_if_exists_1, - con=self.conn, - name=table_name, - if_exists='notvalidvalue') + pytest.raises(ValueError, + sql.to_sql, + frame=df_if_exists_1, + con=self.conn, + name=table_name, + if_exists='notvalidvalue') clean_up(table_name) # test if_exists='fail' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='fail', index=False) - self.assertRaises(ValueError, - sql.to_sql, - frame=df_if_exists_1, - con=self.conn, - name=table_name, - if_exists='fail') + pytest.raises(ValueError, + sql.to_sql, + frame=df_if_exists_1, + con=self.conn, + name=table_name, + if_exists='fail') # test if_exists='replace' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='replace', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B')]) + assert tquery(sql_select, con=self.conn) == [(1, 'A'), (2, 'B')] sql.to_sql(frame=df_if_exists_2, con=self.conn, name=table_name, if_exists='replace', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(3, 'C'), (4, 'D'), (5, 'E')]) + assert (tquery(sql_select, con=self.conn) == + [(3, 'C'), (4, 'D'), (5, 'E')]) clean_up(table_name) # test if_exists='append' sql.to_sql(frame=df_if_exists_1, con=self.conn, name=table_name, if_exists='fail', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B')]) + assert tquery(sql_select, con=self.conn) == [(1, 'A'), (2, 'B')] sql.to_sql(frame=df_if_exists_2, con=self.conn, name=table_name, if_exists='append', index=False) - self.assertEqual(tquery(sql_select, con=self.conn), - [(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E')]) + assert (tquery(sql_select, con=self.conn) == + [(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E')]) clean_up(table_name) diff --git a/pandas/tests/io/test_stata.py b/pandas/tests/io/test_stata.py index db594889c91ee..586297d2e3872 100644 --- a/pandas/tests/io/test_stata.py +++ b/pandas/tests/io/test_stata.py @@ -1,32 +1,51 @@ # -*- coding: utf-8 -*- # pylint: disable=E1101 +from collections import OrderedDict import datetime as dt +from datetime import datetime +import gzip +import io import os import struct -import sys import warnings -from datetime import datetime -from distutils.version import LooseVersion -import pytest import numpy as np +import pytest + +import pandas.compat as compat +from pandas.compat import PY3, ResourceWarning, iterkeys + +from pandas.core.dtypes.common import is_categorical_dtype + import pandas as pd -import pandas.util.testing as tm -from pandas import compat -from pandas.compat import iterkeys from pandas.core.frame import DataFrame, Series +import pandas.util.testing as tm + from pandas.io.parsers import read_csv -from pandas.io.stata import (read_stata, StataReader, InvalidColumnName, - PossiblePrecisionLoss, StataMissingValue) -from pandas._libs.tslib import NaT -from pandas.types.common import is_categorical_dtype +from pandas.io.stata import ( + InvalidColumnName, PossiblePrecisionLoss, StataMissingValue, StataReader, + read_stata) + + +@pytest.fixture +def dirpath(datapath): + return datapath("io", "data") + + +@pytest.fixture +def parsed_114(dirpath): + dta14_114 = os.path.join(dirpath, 'stata5_114.dta') + parsed_114 = read_stata(dta14_114, convert_dates=True) + parsed_114.index.name = 'index' + return parsed_114 -class TestStata(tm.TestCase): +class TestStata(object): - def setUp(self): - self.dirpath = tm.get_data_path() + @pytest.fixture(autouse=True) + def setup_method(self, datapath): + self.dirpath = datapath("io", "data") self.dta1_114 = os.path.join(self.dirpath, 'stata1_114.dta') self.dta1_117 = os.path.join(self.dirpath, 'stata1_117.dta') @@ -82,6 +101,9 @@ def setUp(self): self.dta23 = os.path.join(self.dirpath, 'stata15.dta') self.dta24_111 = os.path.join(self.dirpath, 'stata7_111.dta') + self.dta25_118 = os.path.join(self.dirpath, 'stata16_118.dta') + + self.stata_dates = os.path.join(self.dirpath, 'stata13_dates.dta') def read_dta(self, file): # Legacy default reader configuration @@ -90,28 +112,31 @@ def read_dta(self, file): def read_csv(self, file): return read_csv(file, parse_dates=True) - def test_read_empty_dta(self): + @pytest.mark.parametrize('version', [114, 117]) + def test_read_empty_dta(self, version): empty_ds = DataFrame(columns=['unit']) # GH 7369, make sure can read a 0-obs dta file with tm.ensure_clean() as path: - empty_ds.to_stata(path, write_index=False) + empty_ds.to_stata(path, write_index=False, version=version) empty_ds2 = read_stata(path) tm.assert_frame_equal(empty_ds, empty_ds2) def test_data_method(self): # Minimal testing of legacy data method with StataReader(self.dta1_114) as rdr: - with warnings.catch_warnings(record=True) as w: # noqa + with tm.assert_produces_warning(UserWarning): parsed_114_data = rdr.data() with StataReader(self.dta1_114) as rdr: parsed_114_read = rdr.read() tm.assert_frame_equal(parsed_114_data, parsed_114_read) - def test_read_dta1(self): + @pytest.mark.parametrize( + 'file', ['dta1_114', 'dta1_117']) + def test_read_dta1(self, file): - parsed_114 = self.read_dta(self.dta1_114) - parsed_117 = self.read_dta(self.dta1_117) + file = getattr(self, file) + parsed = self.read_dta(file) # Pandas uses np.nan as missing value. # Thus, all columns will be of type float, regardless of their name. @@ -123,12 +148,9 @@ def test_read_dta1(self): # the casting doesn't fail so need to match stata here expected['float_miss'] = expected['float_miss'].astype(np.float32) - tm.assert_frame_equal(parsed_114, expected) - tm.assert_frame_equal(parsed_117, expected) + tm.assert_frame_equal(parsed, expected) def test_read_dta2(self): - if LooseVersion(sys.version) < '2.7': - pytest.skip('datetime interp under 2.6 is faulty') expected = DataFrame.from_records( [ @@ -181,7 +203,7 @@ def test_read_dta2(self): w = [x for x in w if x.category is UserWarning] # should get warning for each call to read_dta - self.assertEqual(len(w), 3) + assert len(w) == 3 # buggy test because of the NaT comparison on certain platforms # Format 113 test fails since it does not support tc and tC formats @@ -193,11 +215,12 @@ def test_read_dta2(self): tm.assert_frame_equal(parsed_117, expected, check_datetimelike_compat=True) - def test_read_dta3(self): - parsed_113 = self.read_dta(self.dta3_113) - parsed_114 = self.read_dta(self.dta3_114) - parsed_115 = self.read_dta(self.dta3_115) - parsed_117 = self.read_dta(self.dta3_117) + @pytest.mark.parametrize( + 'file', ['dta3_113', 'dta3_114', 'dta3_115', 'dta3_117']) + def test_read_dta3(self, file): + + file = getattr(self, file) + parsed = self.read_dta(file) # match stata here expected = self.read_csv(self.csv3) @@ -205,16 +228,14 @@ def test_read_dta3(self): expected['year'] = expected['year'].astype(np.int16) expected['quarter'] = expected['quarter'].astype(np.int8) - tm.assert_frame_equal(parsed_113, expected) - tm.assert_frame_equal(parsed_114, expected) - tm.assert_frame_equal(parsed_115, expected) - tm.assert_frame_equal(parsed_117, expected) + tm.assert_frame_equal(parsed, expected) + + @pytest.mark.parametrize( + 'file', ['dta4_113', 'dta4_114', 'dta4_115', 'dta4_117']) + def test_read_dta4(self, file): - def test_read_dta4(self): - parsed_113 = self.read_dta(self.dta4_113) - parsed_114 = self.read_dta(self.dta4_114) - parsed_115 = self.read_dta(self.dta4_115) - parsed_117 = self.read_dta(self.dta4_117) + file = getattr(self, file) + parsed = self.read_dta(file) expected = DataFrame.from_records( [ @@ -237,10 +258,7 @@ def test_read_dta4(self): for col in expected], axis=1) # stata doesn't save .category metadata - tm.assert_frame_equal(parsed_113, expected, check_categorical=False) - tm.assert_frame_equal(parsed_114, expected, check_categorical=False) - tm.assert_frame_equal(parsed_115, expected, check_categorical=False) - tm.assert_frame_equal(parsed_117, expected, check_categorical=False) + tm.assert_frame_equal(parsed, expected, check_categorical=False) # File containing strls def test_read_dta12(self): @@ -283,7 +301,7 @@ def test_read_dta18(self): u'Floats': u'float data'} tm.assert_dict_equal(vl, vl_expected) - self.assertEqual(rdr.data_label, u'This is a Ünicode data label') + assert rdr.data_label == u'This is a Ünicode data label' def test_read_write_dta5(self): original = DataFrame([(np.nan, np.nan, np.nan, np.nan, np.nan)], @@ -310,7 +328,8 @@ def test_write_dta6(self): tm.assert_frame_equal(written_and_read_again.set_index('index'), original, check_index_type=False) - def test_read_write_dta10(self): + @pytest.mark.parametrize('version', [114, 117]) + def test_read_write_dta10(self, version): original = DataFrame(data=[["string", "object", 1, 1.1, np.datetime64('2003-12-25')]], columns=['string', 'object', 'integer', @@ -321,9 +340,9 @@ def test_read_write_dta10(self): original['integer'] = original['integer'].astype(np.int32) with tm.ensure_clean() as path: - original.to_stata(path, {'datetime': 'tc'}) + original.to_stata(path, {'datetime': 'tc'}, version=version) written_and_read_again = self.read_dta(path) - # original.index is np.int32, readed index is np.int64 + # original.index is np.int32, read index is np.int64 tm.assert_frame_equal(written_and_read_again.set_index('index'), original, check_index_type=False) @@ -342,25 +361,24 @@ def test_write_preserves_original(self): df.to_stata(path, write_index=False) tm.assert_frame_equal(df, df_copy) - def test_encoding(self): + @pytest.mark.parametrize('version', [114, 117]) + def test_encoding(self, version): # GH 4626, proper encoding handling raw = read_stata(self.dta_encoding) - encoded = read_stata(self.dta_encoding, encoding="latin-1") + with tm.assert_produces_warning(FutureWarning): + encoded = read_stata(self.dta_encoding, encoding='latin-1') result = encoded.kreis1849[0] - if compat.PY3: - expected = raw.kreis1849[0] - self.assertEqual(result, expected) - self.assertIsInstance(result, compat.string_types) - else: - expected = raw.kreis1849.str.decode("latin-1")[0] - self.assertEqual(result, expected) - self.assertIsInstance(result, unicode) # noqa + expected = raw.kreis1849[0] + assert result == expected + assert isinstance(result, compat.string_types) with tm.ensure_clean() as path: - encoded.to_stata(path, encoding='latin-1', write_index=False) - reread_encoded = read_stata(path, encoding='latin-1') + with tm.assert_produces_warning(FutureWarning): + encoded.to_stata(path, write_index=False, version=version, + encoding='latin-1') + reread_encoded = read_stata(path) tm.assert_frame_equal(encoded, reread_encoded) def test_read_write_dta11(self): @@ -374,16 +392,15 @@ def test_read_write_dta11(self): formatted = formatted.astype(np.int32) with tm.ensure_clean() as path: - with warnings.catch_warnings(record=True) as w: + with tm.assert_produces_warning(pd.io.stata.InvalidColumnName): original.to_stata(path, None) - # should get a warning for that format. - self.assertEqual(len(w), 1) written_and_read_again = self.read_dta(path) tm.assert_frame_equal( written_and_read_again.set_index('index'), formatted) - def test_read_write_dta12(self): + @pytest.mark.parametrize('version', [114, 117]) + def test_read_write_dta12(self, version): original = DataFrame([(1, 2, 3, 4, 5, 6)], columns=['astringwithmorethan32characters_1', 'astringwithmorethan32characters_2', @@ -403,9 +420,10 @@ def test_read_write_dta12(self): with tm.ensure_clean() as path: with warnings.catch_warnings(record=True) as w: - original.to_stata(path, None) + warnings.simplefilter('always', InvalidColumnName) + original.to_stata(path, None, version=version) # should get a warning for that format. - self.assertEqual(len(w), 1) + assert len(w) == 1 written_and_read_again = self.read_dta(path) tm.assert_frame_equal( @@ -427,7 +445,14 @@ def test_read_write_dta13(self): tm.assert_frame_equal(written_and_read_again.set_index('index'), formatted) - def test_read_write_reread_dta14(self): + @pytest.mark.parametrize('version', [114, 117]) + @pytest.mark.parametrize( + 'file', ['dta14_113', 'dta14_114', 'dta14_115', 'dta14_117']) + def test_read_write_reread_dta14(self, file, parsed_114, version): + file = getattr(self, file) + parsed = self.read_dta(file) + parsed.index.name = 'index' + expected = self.read_csv(self.csv14) cols = ['byte_', 'int_', 'long_', 'float_', 'double_'] for col in cols: @@ -436,26 +461,18 @@ def test_read_write_reread_dta14(self): expected['date_td'] = pd.to_datetime( expected['date_td'], errors='coerce') - parsed_113 = self.read_dta(self.dta14_113) - parsed_113.index.name = 'index' - parsed_114 = self.read_dta(self.dta14_114) - parsed_114.index.name = 'index' - parsed_115 = self.read_dta(self.dta14_115) - parsed_115.index.name = 'index' - parsed_117 = self.read_dta(self.dta14_117) - parsed_117.index.name = 'index' - - tm.assert_frame_equal(parsed_114, parsed_113) - tm.assert_frame_equal(parsed_114, parsed_115) - tm.assert_frame_equal(parsed_114, parsed_117) + tm.assert_frame_equal(parsed_114, parsed) with tm.ensure_clean() as path: - parsed_114.to_stata(path, {'date_td': 'td'}) + parsed_114.to_stata(path, {'date_td': 'td'}, version=version) written_and_read_again = self.read_dta(path) tm.assert_frame_equal( written_and_read_again.set_index('index'), parsed_114) - def test_read_write_reread_dta15(self): + @pytest.mark.parametrize( + 'file', ['dta15_113', 'dta15_114', 'dta15_115', 'dta15_117']) + def test_read_write_reread_dta15(self, file): + expected = self.read_csv(self.csv15) expected['byte_'] = expected['byte_'].astype(np.int8) expected['int_'] = expected['int_'].astype(np.int16) @@ -465,28 +482,35 @@ def test_read_write_reread_dta15(self): expected['date_td'] = expected['date_td'].apply( datetime.strptime, args=('%Y-%m-%d',)) - parsed_113 = self.read_dta(self.dta15_113) - parsed_114 = self.read_dta(self.dta15_114) - parsed_115 = self.read_dta(self.dta15_115) - parsed_117 = self.read_dta(self.dta15_117) + file = getattr(self, file) + parsed = self.read_dta(file) - tm.assert_frame_equal(expected, parsed_114) - tm.assert_frame_equal(parsed_113, parsed_114) - tm.assert_frame_equal(parsed_114, parsed_115) - tm.assert_frame_equal(parsed_114, parsed_117) + tm.assert_frame_equal(expected, parsed) - def test_timestamp_and_label(self): - original = DataFrame([(1,)], columns=['var']) + @pytest.mark.parametrize('version', [114, 117]) + def test_timestamp_and_label(self, version): + original = DataFrame([(1,)], columns=['variable']) time_stamp = datetime(2000, 2, 29, 14, 21) data_label = 'This is a data file.' with tm.ensure_clean() as path: original.to_stata(path, time_stamp=time_stamp, - data_label=data_label) + data_label=data_label, + version=version) with StataReader(path) as reader: assert reader.time_stamp == '29 Feb 2000 14:21' assert reader.data_label == data_label + @pytest.mark.parametrize('version', [114, 117]) + def test_invalid_timestamp(self, version): + original = DataFrame([(1,)], columns=['variable']) + time_stamp = '01 Jan 2000, 00:00:00' + with tm.ensure_clean() as path: + msg = "time_stamp should be datetime type" + with pytest.raises(ValueError, match=msg): + original.to_stata(path, time_stamp=time_stamp, + version=version) + def test_numeric_column_names(self): original = DataFrame(np.reshape(np.arange(25.0), (5, 5))) original.index.name = 'index' @@ -502,7 +526,8 @@ def test_numeric_column_names(self): written_and_read_again.columns = map(convert_col_name, columns) tm.assert_frame_equal(original, written_and_read_again) - def test_nan_to_missing_value(self): + @pytest.mark.parametrize('version', [114, 117]) + def test_nan_to_missing_value(self, version): s1 = Series(np.arange(4.0), dtype=np.float32) s2 = Series(np.arange(4.0), dtype=np.float64) s1[::2] = np.nan @@ -510,7 +535,7 @@ def test_nan_to_missing_value(self): original = DataFrame({'s1': s1, 's2': s2}) original.index.name = 'index' with tm.ensure_clean() as path: - original.to_stata(path) + original.to_stata(path, version=version) written_and_read_again = self.read_dta(path) written_and_read_again = written_and_read_again.set_index('index') tm.assert_frame_equal(written_and_read_again, original) @@ -523,8 +548,8 @@ def test_no_index(self): with tm.ensure_clean() as path: original.to_stata(path, write_index=False) written_and_read_again = self.read_dta(path) - tm.assertRaises( - KeyError, lambda: written_and_read_again['index_not_written']) + with pytest.raises(KeyError, match=original.index.name): + written_and_read_again['index_not_written'] def test_string_no_dates(self): s1 = Series(['a', 'A longer string']) @@ -583,9 +608,19 @@ def test_105(self): df0['psch_dis'] = df0["psch_dis"].astype(np.float32) tm.assert_frame_equal(df.head(3), df0) + def test_value_labels_old_format(self): + # GH 19417 + # + # Test that value_labels() returns an empty dict if the file format + # predates supporting value labels. + dpath = os.path.join(self.dirpath, 'S4_EDUC1.dta') + reader = StataReader(dpath) + assert reader.value_labels() == {} + reader.close() + def test_date_export_formats(self): columns = ['tc', 'td', 'tw', 'tm', 'tq', 'th', 'ty'] - conversions = dict(((c, c) for c in columns)) + conversions = {c: c for c in columns} data = [datetime(2006, 11, 20, 23, 13, 20)] * len(columns) original = DataFrame([data], columns=columns) original.index.name = 'index' @@ -615,7 +650,9 @@ def test_write_missing_strings(self): tm.assert_frame_equal(written_and_read_again.set_index('index'), expected) - def test_bool_uint(self): + @pytest.mark.parametrize('version', [114, 117]) + @pytest.mark.parametrize('byteorder', ['>', '<']) + def test_bool_uint(self, byteorder, version): s0 = Series([0, 1, True], dtype=np.bool) s1 = Series([0, 1, 100], dtype=np.uint8) s2 = Series([0, 1, 255], dtype=np.uint8) @@ -634,7 +671,7 @@ def test_bool_uint(self): expected[c] = expected[c].astype(t) with tm.ensure_clean() as path: - original.to_stata(path) + original.to_stata(path, byteorder=byteorder, version=version) written_and_read_again = self.read_dta(path) written_and_read_again = written_and_read_again.set_index('index') tm.assert_frame_equal(written_and_read_again, expected) @@ -647,10 +684,10 @@ def test_variable_labels(self): keys = ('var1', 'var2', 'var3') labels = ('label1', 'label2', 'label3') for k, v in compat.iteritems(sr_115): - self.assertTrue(k in sr_117) - self.assertTrue(v == sr_117[k]) - self.assertTrue(k in keys) - self.assertTrue(v in labels) + assert k in sr_117 + assert v == sr_117[k] + assert k in keys + assert v in labels def test_minimal_size_col(self): str_lens = (1, 100, 244) @@ -667,8 +704,8 @@ def test_minimal_size_col(self): variables = sr.varlist formats = sr.fmtlist for variable, fmt, typ in zip(variables, formats, typlist): - self.assertTrue(int(variable[1:]) == int(fmt[1:-1])) - self.assertTrue(int(variable[1:]) == typ) + assert int(variable[1:]) == int(fmt[1:-1]) + assert int(variable[1:]) == typ def test_excessively_long_string(self): str_lens = (1, 244, 500) @@ -677,7 +714,11 @@ def test_excessively_long_string(self): s['s' + str(str_len)] = Series(['a' * str_len, 'b' * str_len, 'c' * str_len]) original = DataFrame(s) - with tm.assertRaises(ValueError): + msg = (r"Fixed width strings in Stata \.dta files are limited to 244" + r" \(or fewer\)\ncharacters\. Column 's500' does not satisfy" + r" this restriction\. Use the\n'version=117' parameter to write" + r" the newer \(Stata 13 and later\) format\.") + with pytest.raises(ValueError, match=msg): with tm.ensure_clean() as path: original.to_stata(path) @@ -694,23 +735,25 @@ def test_missing_value_generator(self): offset = valid_range[t][1] for i in range(0, 27): val = StataMissingValue(offset + 1 + i) - self.assertTrue(val.string == expected_values[i]) + assert val.string == expected_values[i] # Test extremes for floats val = StataMissingValue(struct.unpack(' 0) + assert len(ax.get_children()) > 0 if layout is not None: - result = self._get_axes_layout(plotting._flatten(axes)) - self.assertEqual(result, layout) + result = self._get_axes_layout(_flatten(axes)) + assert result == layout - self.assert_numpy_array_equal( + tm.assert_numpy_array_equal( visible_axes[0].figure.get_size_inches(), np.array(figsize, dtype=np.float64)) @@ -379,7 +367,7 @@ def _flatten_visible(self, axes): axes : matplotlib Axes object, or its list-like """ - axes = plotting._flatten(axes) + axes = _flatten(axes) axes = [ax for ax in axes if ax.get_visible()] return axes @@ -407,8 +395,8 @@ def _check_has_errorbars(self, axes, xerr=0, yerr=0): xerr_count += 1 if has_yerr: yerr_count += 1 - self.assertEqual(xerr, xerr_count) - self.assertEqual(yerr, yerr_count) + assert xerr == xerr_count + assert yerr == yerr_count def _check_box_return_type(self, returned, return_type, expected_keys=None, check_ax_title=True): @@ -435,36 +423,36 @@ def _check_box_return_type(self, returned, return_type, expected_keys=None, if return_type is None: return_type = 'dict' - self.assertTrue(isinstance(returned, types[return_type])) + assert isinstance(returned, types[return_type]) if return_type == 'both': - self.assertIsInstance(returned.ax, Axes) - self.assertIsInstance(returned.lines, dict) + assert isinstance(returned.ax, Axes) + assert isinstance(returned.lines, dict) else: # should be fixed when the returning default is changed if return_type is None: for r in self._flatten_visible(returned): - self.assertIsInstance(r, Axes) + assert isinstance(r, Axes) return - self.assertTrue(isinstance(returned, Series)) + assert isinstance(returned, Series) - self.assertEqual(sorted(returned.keys()), sorted(expected_keys)) + assert sorted(returned.keys()) == sorted(expected_keys) for key, value in iteritems(returned): - self.assertTrue(isinstance(value, types[return_type])) + assert isinstance(value, types[return_type]) # check returned dict has correct mapping if return_type == 'axes': if check_ax_title: - self.assertEqual(value.get_title(), key) + assert value.get_title() == key elif return_type == 'both': if check_ax_title: - self.assertEqual(value.ax.get_title(), key) - self.assertIsInstance(value.ax, Axes) - self.assertIsInstance(value.lines, dict) + assert value.ax.get_title() == key + assert isinstance(value.ax, Axes) + assert isinstance(value.lines, dict) elif return_type == 'dict': line = value['medians'][0] - axes = line.axes if self.mpl_ge_1_5_0 else line.get_axes() + axes = line.axes if check_ax_title: - self.assertEqual(axes.get_title(), key) + assert axes.get_title() == key else: raise AssertionError @@ -489,40 +477,32 @@ def is_grid_on(): spndx += 1 mpl.rc('axes', grid=False) obj.plot(kind=kind, **kws) - self.assertFalse(is_grid_on()) + assert not is_grid_on() self.plt.subplot(1, 4 * len(kinds), spndx) spndx += 1 mpl.rc('axes', grid=True) obj.plot(kind=kind, grid=False, **kws) - self.assertFalse(is_grid_on()) + assert not is_grid_on() if kind != 'pie': self.plt.subplot(1, 4 * len(kinds), spndx) spndx += 1 mpl.rc('axes', grid=True) obj.plot(kind=kind, **kws) - self.assertTrue(is_grid_on()) + assert is_grid_on() self.plt.subplot(1, 4 * len(kinds), spndx) spndx += 1 mpl.rc('axes', grid=False) obj.plot(kind=kind, grid=True, **kws) - self.assertTrue(is_grid_on()) + assert is_grid_on() - def _maybe_unpack_cycler(self, rcParams, field='color'): + def _unpack_cycler(self, rcParams, field='color'): """ - Compat layer for MPL 1.5 change to color cycle - - Before: plt.rcParams['axes.color_cycle'] -> ['b', 'g', 'r'...] - After : plt.rcParams['axes.prop_cycle'] -> cycler(...) + Auxiliary function for correctly unpacking cycler after MPL >= 1.5 """ - if self.mpl_ge_1_5_0: - cyl = rcParams['axes.prop_cycle'] - colors = [v[field] for v in cyl] - else: - colors = rcParams['axes.color_cycle'] - return colors + return [v[field] for v in rcParams['axes.prop_cycle']] def _check_plot_works(f, filterwarnings='always', **kwargs): diff --git a/pandas/tests/plotting/test_boxplot_method.py b/pandas/tests/plotting/test_boxplot_method.py index 31c150bc1e64f..e6b9795aebe7c 100644 --- a/pandas/tests/plotting/test_boxplot_method.py +++ b/pandas/tests/plotting/test_boxplot_method.py @@ -1,42 +1,30 @@ # coding: utf-8 -import pytest import itertools import string -from distutils.version import LooseVersion - -from pandas import Series, DataFrame, MultiIndex -from pandas.compat import range, lzip -import pandas.util.testing as tm -from pandas.util.testing import slow import numpy as np from numpy import random -from numpy.random import randn +import pytest -import pandas.tools.plotting as plotting +from pandas.compat import lzip, range +import pandas.util._test_decorators as td -from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works) +from pandas import DataFrame, MultiIndex, Series +from pandas.tests.plotting.common import TestPlotBase, _check_plot_works +import pandas.util.testing as tm +import pandas.plotting as plotting """ Test cases for .boxplot method """ -def _skip_if_mpl_14_or_dev_boxplot(): - # GH 8382 - # Boxplot failures on 1.4 and 1.4.1 - # Don't need try / except since that's done at class level - import matplotlib - if str(matplotlib.__version__) >= LooseVersion('1.4'): - pytest.skip("Matplotlib Regression in 1.4 and current dev.") - - -@tm.mplskip +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): - @slow - def test_boxplot_legacy(self): - df = DataFrame(randn(6, 4), + @pytest.mark.slow + def test_boxplot_legacy1(self): + df = DataFrame(np.random.randn(6, 4), index=list(string.ascii_letters[:6]), columns=['one', 'two', 'three', 'four']) df['indic'] = ['foo', 'bar'] * 3 @@ -54,11 +42,14 @@ def test_boxplot_legacy(self): _check_plot_works(df.boxplot, by='indic') with tm.assert_produces_warning(UserWarning): _check_plot_works(df.boxplot, by=['indic', 'indic2']) - _check_plot_works(plotting.boxplot, data=df['one'], return_type='dict') + _check_plot_works(plotting._core.boxplot, data=df['one'], + return_type='dict') _check_plot_works(df.boxplot, notch=1, return_type='dict') with tm.assert_produces_warning(UserWarning): _check_plot_works(df.boxplot, by='indic', notch=1) + @pytest.mark.slow + def test_boxplot_legacy2(self): df = DataFrame(np.random.rand(10, 2), columns=['Col1', 'Col2']) df['X'] = Series(['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B']) df['Y'] = Series(['A'] * 10) @@ -69,43 +60,43 @@ def test_boxplot_legacy(self): # passed ax should be used: fig, ax = self.plt.subplots() axes = df.boxplot('Col1', by='X', ax=ax) - ax_axes = ax.axes if self.mpl_ge_1_5_0 else ax.get_axes() - self.assertIs(ax_axes, axes) + ax_axes = ax.axes + assert ax_axes is axes fig, ax = self.plt.subplots() axes = df.groupby('Y').boxplot(ax=ax, return_type='axes') - ax_axes = ax.axes if self.mpl_ge_1_5_0 else ax.get_axes() - self.assertIs(ax_axes, axes['A']) + ax_axes = ax.axes + assert ax_axes is axes['A'] # Multiple columns with an ax argument should use same figure fig, ax = self.plt.subplots() with tm.assert_produces_warning(UserWarning): axes = df.boxplot(column=['Col1', 'Col2'], by='X', ax=ax, return_type='axes') - self.assertIs(axes['Col1'].get_figure(), fig) + assert axes['Col1'].get_figure() is fig # When by is None, check that all relevant lines are present in the # dict fig, ax = self.plt.subplots() d = df.boxplot(ax=ax, return_type='dict') lines = list(itertools.chain.from_iterable(d.values())) - self.assertEqual(len(ax.get_lines()), len(lines)) + assert len(ax.get_lines()) == len(lines) - @slow + @pytest.mark.slow def test_boxplot_return_type_none(self): # GH 12216; return_type=None & by=None -> axes result = self.hist_df.boxplot() - self.assertTrue(isinstance(result, self.plt.Axes)) + assert isinstance(result, self.plt.Axes) - @slow + @pytest.mark.slow def test_boxplot_return_type_legacy(self): # API change in https://github.com/pandas-dev/pandas/pull/7096 import matplotlib as mpl # noqa - df = DataFrame(randn(6, 4), + df = DataFrame(np.random.randn(6, 4), index=list(string.ascii_letters[:6]), columns=['one', 'two', 'three', 'four']) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.boxplot(return_type='NOTATYPE') result = df.boxplot() @@ -123,13 +114,13 @@ def test_boxplot_return_type_legacy(self): result = df.boxplot(return_type='both') self._check_box_return_type(result, 'both') - @slow + @pytest.mark.slow def test_boxplot_axis_limits(self): def _check_ax_limits(col, ax): y_min, y_max = ax.get_ylim() - self.assertTrue(y_min <= col.min()) - self.assertTrue(y_max >= col.max()) + assert y_min <= col.min() + assert y_max >= col.max() df = self.hist_df.copy() df['age'] = np.random.randint(1, 20, df.shape[0]) @@ -137,7 +128,7 @@ def _check_ax_limits(col, ax): height_ax, weight_ax = df.boxplot(['height', 'weight'], by='category') _check_ax_limits(df['height'], height_ax) _check_ax_limits(df['weight'], weight_ax) - self.assertEqual(weight_ax._sharey, height_ax) + assert weight_ax._sharey == height_ax # Two rows, one partial p = df.boxplot(['height', 'weight', 'age'], by='category') @@ -147,28 +138,35 @@ def _check_ax_limits(col, ax): _check_ax_limits(df['height'], height_ax) _check_ax_limits(df['weight'], weight_ax) _check_ax_limits(df['age'], age_ax) - self.assertEqual(weight_ax._sharey, height_ax) - self.assertEqual(age_ax._sharey, height_ax) - self.assertIsNone(dummy_ax._sharey) + assert weight_ax._sharey == height_ax + assert age_ax._sharey == height_ax + assert dummy_ax._sharey is None - @slow + @pytest.mark.slow def test_boxplot_empty_column(self): - _skip_if_mpl_14_or_dev_boxplot() df = DataFrame(np.random.randn(20, 4)) df.loc[:, 0] = np.nan _check_plot_works(df.boxplot, return_type='axes') + @pytest.mark.slow + def test_figsize(self): + df = DataFrame(np.random.rand(10, 5), + columns=['A', 'B', 'C', 'D', 'E']) + result = df.boxplot(return_type='axes', figsize=(12, 8)) + assert result.figure.bbox_inches.width == 12 + assert result.figure.bbox_inches.height == 8 + def test_fontsize(self): df = DataFrame({"a": [1, 2, 3, 4, 5, 6]}) self._check_ticks_props(df.boxplot("a", fontsize=16), xlabelsize=16, ylabelsize=16) -@tm.mplskip +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): - @slow - def test_boxplot_legacy(self): + @pytest.mark.slow + def test_boxplot_legacy1(self): grouped = self.hist_df.groupby(by='gender') with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(grouped.boxplot, return_type='axes') @@ -176,10 +174,12 @@ def test_boxplot_legacy(self): axes = _check_plot_works(grouped.boxplot, subplots=False, return_type='axes') self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) + + @pytest.mark.slow + def test_boxplot_legacy2(self): tuples = lzip(string.ascii_letters[:10], range(10)) df = DataFrame(np.random.rand(10, 3), index=MultiIndex.from_tuples(tuples)) - grouped = df.groupby(level=1) with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(grouped.boxplot, return_type='axes') @@ -189,6 +189,11 @@ def test_boxplot_legacy(self): return_type='axes') self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) + @pytest.mark.slow + def test_boxplot_legacy3(self): + tuples = lzip(string.ascii_letters[:10], range(10)) + df = DataFrame(np.random.rand(10, 3), + index=MultiIndex.from_tuples(tuples)) grouped = df.unstack(level=1).groupby(level=0, axis=1) with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(grouped.boxplot, return_type='axes') @@ -197,7 +202,7 @@ def test_boxplot_legacy(self): return_type='axes') self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) - @slow + @pytest.mark.slow def test_grouped_plot_fignums(self): n = 10 weight = Series(np.random.normal(166, 20, size=n)) @@ -208,26 +213,26 @@ def test_grouped_plot_fignums(self): gb = df.groupby('gender') res = gb.plot() - self.assertEqual(len(self.plt.get_fignums()), 2) - self.assertEqual(len(res), 2) + assert len(self.plt.get_fignums()) == 2 + assert len(res) == 2 tm.close() res = gb.boxplot(return_type='axes') - self.assertEqual(len(self.plt.get_fignums()), 1) - self.assertEqual(len(res), 2) + assert len(self.plt.get_fignums()) == 1 + assert len(res) == 2 tm.close() # now works with GH 5610 as gender is excluded res = df.groupby('gender').hist() tm.close() - @slow + @pytest.mark.slow def test_grouped_box_return_type(self): df = self.hist_df # old style: return_type=None result = df.boxplot(by='gender') - self.assertIsInstance(result, np.ndarray) + assert isinstance(result, np.ndarray) self._check_box_return_type( result, None, expected_keys=['height', 'weight', 'category']) @@ -258,17 +263,24 @@ def test_grouped_box_return_type(self): returned = df2.boxplot(by='category', return_type=t) self._check_box_return_type(returned, t, expected_keys=columns2) - @slow + @pytest.mark.slow def test_grouped_box_layout(self): df = self.hist_df - self.assertRaises(ValueError, df.boxplot, column=['weight', 'height'], - by=df.gender, layout=(1, 1)) - self.assertRaises(ValueError, df.boxplot, - column=['height', 'weight', 'category'], - layout=(2, 1), return_type='dict') - self.assertRaises(ValueError, df.boxplot, column=['weight', 'height'], - by=df.gender, layout=(-1, -1)) + msg = "Layout of 1x1 must be larger than required size 2" + with pytest.raises(ValueError, match=msg): + df.boxplot(column=['weight', 'height'], by=df.gender, + layout=(1, 1)) + + msg = "The 'layout' keyword is not supported when 'by' is None" + with pytest.raises(ValueError, match=msg): + df.boxplot(column=['height', 'weight', 'category'], + layout=(2, 1), return_type='dict') + + msg = "At least one dimension of layout must be positive" + with pytest.raises(ValueError, match=msg): + df.boxplot(column=['weight', 'height'], by=df.gender, + layout=(-1, -1)) # _check_plot_works adds an ax so catch warning. see GH #13188 with tm.assert_produces_warning(UserWarning): @@ -332,7 +344,7 @@ def test_grouped_box_layout(self): return_type='dict') self._check_axes_shape(self.plt.gcf().axes, axes_num=3, layout=(1, 3)) - @slow + @pytest.mark.slow def test_grouped_box_multiple_axes(self): # GH 6970, GH 7069 df = self.hist_df @@ -355,8 +367,8 @@ def test_grouped_box_multiple_axes(self): by='gender', return_type='axes', ax=axes[0]) returned = np.array(list(returned.values)) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assert_numpy_array_equal(returned, axes[0]) - self.assertIs(returned[0].figure, fig) + tm.assert_numpy_array_equal(returned, axes[0]) + assert returned[0].figure is fig # draw on second row with tm.assert_produces_warning(UserWarning): @@ -365,10 +377,10 @@ def test_grouped_box_multiple_axes(self): return_type='axes', ax=axes[1]) returned = np.array(list(returned.values)) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assert_numpy_array_equal(returned, axes[1]) - self.assertIs(returned[0].figure, fig) + tm.assert_numpy_array_equal(returned, axes[1]) + assert returned[0].figure is fig - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): fig, axes = self.plt.subplots(2, 3) # pass different number of axes from required with tm.assert_produces_warning(UserWarning): diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py new file mode 100644 index 0000000000000..7dfc21562cc5d --- /dev/null +++ b/pandas/tests/plotting/test_converter.py @@ -0,0 +1,346 @@ +from datetime import date, datetime +import subprocess +import sys + +import numpy as np +import pytest + +from pandas.compat import u +from pandas.compat.numpy import np_datetime64_compat + +from pandas import Index, Period, Series, Timestamp, date_range +import pandas.core.config as cf +import pandas.util.testing as tm + +from pandas.tseries.offsets import Day, Micro, Milli, Second + +converter = pytest.importorskip('pandas.plotting._converter') +from pandas.plotting import (deregister_matplotlib_converters, # isort:skip + register_matplotlib_converters) + + +def test_timtetonum_accepts_unicode(): + assert (converter.time2num("00:01") == converter.time2num(u("00:01"))) + + +class TestRegistration(object): + + def test_register_by_default(self): + # Run in subprocess to ensure a clean state + code = ("'import matplotlib.units; " + "import pandas as pd; " + "units = dict(matplotlib.units.registry); " + "assert pd.Timestamp in units)'") + call = [sys.executable, '-c', code] + assert subprocess.check_call(call) == 0 + + def test_warns(self): + plt = pytest.importorskip("matplotlib.pyplot") + s = Series(range(12), index=date_range('2017', periods=12)) + _, ax = plt.subplots() + + # Set to the "warning" state, in case this isn't the first test run + converter._WARN = True + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False) as w: + ax.plot(s.index, s.values) + plt.close() + + assert len(w) == 1 + assert "Using an implicitly registered datetime converter" in str(w[0]) + + def test_registering_no_warning(self): + plt = pytest.importorskip("matplotlib.pyplot") + s = Series(range(12), index=date_range('2017', periods=12)) + _, ax = plt.subplots() + + # Set to the "warn" state, in case this isn't the first test run + converter._WARN = True + register_matplotlib_converters() + with tm.assert_produces_warning(None) as w: + ax.plot(s.index, s.values) + + assert len(w) == 0 + + def test_pandas_plots_register(self): + pytest.importorskip("matplotlib.pyplot") + s = Series(range(12), index=date_range('2017', periods=12)) + # Set to the "warn" state, in case this isn't the first test run + converter._WARN = True + with tm.assert_produces_warning(None) as w: + s.plot() + + assert len(w) == 0 + + def test_matplotlib_formatters(self): + units = pytest.importorskip("matplotlib.units") + assert Timestamp in units.registry + + ctx = cf.option_context("plotting.matplotlib.register_converters", + False) + with ctx: + assert Timestamp not in units.registry + + assert Timestamp in units.registry + + def test_option_no_warning(self): + pytest.importorskip("matplotlib.pyplot") + ctx = cf.option_context("plotting.matplotlib.register_converters", + False) + plt = pytest.importorskip("matplotlib.pyplot") + s = Series(range(12), index=date_range('2017', periods=12)) + _, ax = plt.subplots() + + converter._WARN = True + # Test without registering first, no warning + with ctx: + with tm.assert_produces_warning(None) as w: + ax.plot(s.index, s.values) + + assert len(w) == 0 + + # Now test with registering + converter._WARN = True + register_matplotlib_converters() + with ctx: + with tm.assert_produces_warning(None) as w: + ax.plot(s.index, s.values) + + assert len(w) == 0 + + def test_registry_resets(self): + units = pytest.importorskip("matplotlib.units") + dates = pytest.importorskip("matplotlib.dates") + + # make a copy, to reset to + original = dict(units.registry) + + try: + # get to a known state + units.registry.clear() + date_converter = dates.DateConverter() + units.registry[datetime] = date_converter + units.registry[date] = date_converter + + register_matplotlib_converters() + assert units.registry[date] is not date_converter + deregister_matplotlib_converters() + assert units.registry[date] is date_converter + + finally: + # restore original stater + units.registry.clear() + for k, v in original.items(): + units.registry[k] = v + + def test_old_import_warns(self): + with tm.assert_produces_warning(FutureWarning) as w: + from pandas.tseries import converter + converter.register() + + assert len(w) + assert ('pandas.plotting.register_matplotlib_converters' in + str(w[0].message)) + + +class TestDateTimeConverter(object): + + def setup_method(self, method): + self.dtc = converter.DatetimeConverter() + self.tc = converter.TimeFormatter(None) + + def test_convert_accepts_unicode(self): + r1 = self.dtc.convert("12:22", None, None) + r2 = self.dtc.convert(u("12:22"), None, None) + assert (r1 == r2), "DatetimeConverter.convert should accept unicode" + + def test_conversion(self): + rs = self.dtc.convert(['2012-1-1'], None, None)[0] + xp = datetime(2012, 1, 1).toordinal() + assert rs == xp + + rs = self.dtc.convert('2012-1-1', None, None) + assert rs == xp + + rs = self.dtc.convert(date(2012, 1, 1), None, None) + assert rs == xp + + rs = self.dtc.convert(datetime(2012, 1, 1).toordinal(), None, None) + assert rs == xp + + rs = self.dtc.convert('2012-1-1', None, None) + assert rs == xp + + rs = self.dtc.convert(Timestamp('2012-1-1'), None, None) + assert rs == xp + + # also testing datetime64 dtype (GH8614) + rs = self.dtc.convert(np_datetime64_compat('2012-01-01'), None, None) + assert rs == xp + + rs = self.dtc.convert(np_datetime64_compat( + '2012-01-01 00:00:00+0000'), None, None) + assert rs == xp + + rs = self.dtc.convert(np.array([ + np_datetime64_compat('2012-01-01 00:00:00+0000'), + np_datetime64_compat('2012-01-02 00:00:00+0000')]), None, None) + assert rs[0] == xp + + # we have a tz-aware date (constructed to that when we turn to utc it + # is the same as our sample) + ts = (Timestamp('2012-01-01') + .tz_localize('UTC') + .tz_convert('US/Eastern') + ) + rs = self.dtc.convert(ts, None, None) + assert rs == xp + + rs = self.dtc.convert(ts.to_pydatetime(), None, None) + assert rs == xp + + rs = self.dtc.convert(Index([ts - Day(1), ts]), None, None) + assert rs[1] == xp + + rs = self.dtc.convert(Index([ts - Day(1), ts]).to_pydatetime(), + None, None) + assert rs[1] == xp + + def test_conversion_float(self): + decimals = 9 + + rs = self.dtc.convert( + Timestamp('2012-1-1 01:02:03', tz='UTC'), None, None) + xp = converter.dates.date2num(Timestamp('2012-1-1 01:02:03', tz='UTC')) + tm.assert_almost_equal(rs, xp, decimals) + + rs = self.dtc.convert( + Timestamp('2012-1-1 09:02:03', tz='Asia/Hong_Kong'), None, None) + tm.assert_almost_equal(rs, xp, decimals) + + rs = self.dtc.convert(datetime(2012, 1, 1, 1, 2, 3), None, None) + tm.assert_almost_equal(rs, xp, decimals) + + def test_conversion_outofbounds_datetime(self): + # 2579 + values = [date(1677, 1, 1), date(1677, 1, 2)] + rs = self.dtc.convert(values, None, None) + xp = converter.dates.date2num(values) + tm.assert_numpy_array_equal(rs, xp) + rs = self.dtc.convert(values[0], None, None) + xp = converter.dates.date2num(values[0]) + assert rs == xp + + values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)] + rs = self.dtc.convert(values, None, None) + xp = converter.dates.date2num(values) + tm.assert_numpy_array_equal(rs, xp) + rs = self.dtc.convert(values[0], None, None) + xp = converter.dates.date2num(values[0]) + assert rs == xp + + @pytest.mark.parametrize('time,format_expected', [ + (0, '00:00'), # time2num(datetime.time.min) + (86399.999999, '23:59:59.999999'), # time2num(datetime.time.max) + (90000, '01:00'), + (3723, '01:02:03'), + (39723.2, '11:02:03.200') + ]) + def test_time_formatter(self, time, format_expected): + # issue 18478 + result = self.tc(time) + assert result == format_expected + + def test_dateindex_conversion(self): + decimals = 9 + + for freq in ('B', 'L', 'S'): + dateindex = tm.makeDateIndex(k=10, freq=freq) + rs = self.dtc.convert(dateindex, None, None) + xp = converter.dates.date2num(dateindex._mpl_repr()) + tm.assert_almost_equal(rs, xp, decimals) + + def test_resolution(self): + def _assert_less(ts1, ts2): + val1 = self.dtc.convert(ts1, None, None) + val2 = self.dtc.convert(ts2, None, None) + if not val1 < val2: + raise AssertionError('{0} is not less than {1}.'.format(val1, + val2)) + + # Matplotlib's time representation using floats cannot distinguish + # intervals smaller than ~10 microsecond in the common range of years. + ts = Timestamp('2012-1-1') + _assert_less(ts, ts + Second()) + _assert_less(ts, ts + Milli()) + _assert_less(ts, ts + Micro(50)) + + def test_convert_nested(self): + inner = [Timestamp('2017-01-01'), Timestamp('2017-01-02')] + data = [inner, inner] + result = self.dtc.convert(data, None, None) + expected = [self.dtc.convert(x, None, None) for x in data] + assert (np.array(result) == expected).all() + + +class TestPeriodConverter(object): + + def setup_method(self, method): + self.pc = converter.PeriodConverter() + + class Axis(object): + pass + + self.axis = Axis() + self.axis.freq = 'D' + + def test_convert_accepts_unicode(self): + r1 = self.pc.convert("2012-1-1", None, self.axis) + r2 = self.pc.convert(u("2012-1-1"), None, self.axis) + assert r1 == r2 + + def test_conversion(self): + rs = self.pc.convert(['2012-1-1'], None, self.axis)[0] + xp = Period('2012-1-1').ordinal + assert rs == xp + + rs = self.pc.convert('2012-1-1', None, self.axis) + assert rs == xp + + rs = self.pc.convert([date(2012, 1, 1)], None, self.axis)[0] + assert rs == xp + + rs = self.pc.convert(date(2012, 1, 1), None, self.axis) + assert rs == xp + + rs = self.pc.convert([Timestamp('2012-1-1')], None, self.axis)[0] + assert rs == xp + + rs = self.pc.convert(Timestamp('2012-1-1'), None, self.axis) + assert rs == xp + + rs = self.pc.convert( + np_datetime64_compat('2012-01-01'), None, self.axis) + assert rs == xp + + rs = self.pc.convert( + np_datetime64_compat('2012-01-01 00:00:00+0000'), None, self.axis) + assert rs == xp + + rs = self.pc.convert(np.array([ + np_datetime64_compat('2012-01-01 00:00:00+0000'), + np_datetime64_compat('2012-01-02 00:00:00+0000')]), + None, self.axis) + assert rs[0] == xp + + def test_integer_passthrough(self): + # GH9012 + rs = self.pc.convert([0, 1], None, self.axis) + xp = [0, 1] + assert rs == xp + + def test_convert_nested(self): + data = ['2012-1-1', '2012-1-2'] + r1 = self.pc.convert([data, data], None, self.axis) + r2 = [self.pc.convert(data, None, self.axis) for _ in range(2)] + assert r1 == r2 diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 673c34903b259..6702ad6cfb761 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1,31 +1,32 @@ """ Test cases for time series specific (freq conversion, etc) """ +from datetime import date, datetime, time, timedelta +import pickle +import sys -from datetime import datetime, timedelta, date, time - +import numpy as np import pytest -from pandas.compat import lrange, zip -import numpy as np -from pandas import Index, Series, DataFrame -from pandas.compat import is_platform_mac -from pandas.tseries.index import date_range, bdate_range -from pandas.tseries.tdi import timedelta_range -from pandas.tseries.offsets import DateOffset -from pandas.tseries.period import period_range, Period, PeriodIndex -from pandas.tseries.resample import DatetimeIndex +from pandas.compat import PY3, lrange, zip +import pandas.util._test_decorators as td -from pandas.util.testing import assert_series_equal, ensure_clean, slow +from pandas import DataFrame, Index, NaT, Series, isna +from pandas.core.indexes.datetimes import bdate_range, date_range +from pandas.core.indexes.period import Period, PeriodIndex, period_range +from pandas.core.indexes.timedeltas import timedelta_range +from pandas.core.resample import DatetimeIndex +from pandas.tests.plotting.common import ( + TestPlotBase, _skip_if_no_scipy_gaussian_kde) import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal, ensure_clean -from pandas.tests.plotting.common import (TestPlotBase, - _skip_if_no_scipy_gaussian_kde) +from pandas.tseries.offsets import DateOffset -@tm.mplskip +@td.skip_if_no_mpl class TestTSPlot(TestPlotBase): - def setUp(self): - TestPlotBase.setUp(self) + def setup_method(self, method): + TestPlotBase.setup_method(self, method) freq = ['S', 'T', 'H', 'D', 'W', 'M', 'Q', 'A'] idx = [period_range('12/31/1999', freq=x, periods=100) for x in freq] @@ -41,10 +42,10 @@ def setUp(self): columns=['A', 'B', 'C']) for x in idx] - def tearDown(self): + def teardown_method(self, method): tm.close() - @slow + @pytest.mark.slow def test_ts_plot_with_tz(self): # GH2877 index = date_range('1/1/2011', periods=2, freq='H', @@ -54,16 +55,15 @@ def test_ts_plot_with_tz(self): def test_fontsize_set_correctly(self): # For issue #8765 - import matplotlib.pyplot as plt # noqa df = DataFrame(np.random.randn(10, 9), index=range(10)) - ax = df.plot(fontsize=2) + fig, ax = self.plt.subplots() + df.plot(fontsize=2, ax=ax) for label in (ax.get_xticklabels() + ax.get_yticklabels()): - self.assertEqual(label.get_fontsize(), 2) + assert label.get_fontsize() == 2 - @slow + @pytest.mark.slow def test_frame_inferred(self): # inferred freq - import matplotlib.pyplot as plt # noqa idx = date_range('1/1/1987', freq='MS', periods=100) idx = DatetimeIndex(idx.values, freq=None) @@ -89,26 +89,38 @@ def test_is_error_nozeroindex(self): _check_plot_works(a.plot, yerr=a) def test_nonnumeric_exclude(self): - import matplotlib.pyplot as plt - idx = date_range('1/1/1987', freq='A', periods=3) df = DataFrame({'A': ["x", "y", "z"], 'B': [1, 2, 3]}, idx) - ax = df.plot() # it works - self.assertEqual(len(ax.get_lines()), 1) # B was plotted - plt.close(plt.gcf()) + fig, ax = self.plt.subplots() + df.plot(ax=ax) # it works + assert len(ax.get_lines()) == 1 # B was plotted + self.plt.close(fig) + + msg = "Empty 'DataFrame': no numeric data to plot" + with pytest.raises(TypeError, match=msg): + df['A'].plot() + + def test_tsplot_deprecated(self): + from pandas.tseries.plotting import tsplot + + _, ax = self.plt.subplots() + ts = tm.makeTimeSeries() - self.assertRaises(TypeError, df['A'].plot) + with tm.assert_produces_warning(FutureWarning): + tsplot(ts, self.plt.Axes.plot, ax=ax) - @slow + @pytest.mark.slow def test_tsplot(self): + from pandas.tseries.plotting import tsplot - import matplotlib.pyplot as plt - ax = plt.gca() + _, ax = self.plt.subplots() ts = tm.makeTimeSeries() - f = lambda *args, **kwds: tsplot(s, plt.Axes.plot, *args, **kwds) + def f(*args, **kwds): + with tm.assert_produces_warning(FutureWarning): + return tsplot(s, self.plt.Axes.plot, *args, **kwds) for s in self.period_ser: _check_plot_works(f, s.index.freq, ax=ax, series=s) @@ -122,90 +134,100 @@ def test_tsplot(self): for s in self.datetime_ser: _check_plot_works(s.plot, ax=ax) - ax = ts.plot(style='k') - color = (0., 0., 0., 1) if self.mpl_ge_2_0_0 else (0., 0., 0.) - self.assertEqual(color, ax.get_lines()[0].get_color()) + _, ax = self.plt.subplots() + ts.plot(style='k', ax=ax) + color = (0., 0., 0., 1) + assert color == ax.get_lines()[0].get_color() def test_both_style_and_color(self): - import matplotlib.pyplot as plt # noqa ts = tm.makeTimeSeries() - self.assertRaises(ValueError, ts.plot, style='b-', color='#000099') + msg = ("Cannot pass 'style' string with a color symbol and 'color' " + "keyword argument. Please use one or the other or pass 'style'" + " without a color symbol") + with pytest.raises(ValueError, match=msg): + ts.plot(style='b-', color='#000099') s = ts.reset_index(drop=True) - self.assertRaises(ValueError, s.plot, style='b-', color='#000099') + with pytest.raises(ValueError, match=msg): + s.plot(style='b-', color='#000099') - @slow + @pytest.mark.slow def test_high_freq(self): freaks = ['ms', 'us'] for freq in freaks: - rng = date_range('1/1/2012', periods=100000, freq=freq) + _, ax = self.plt.subplots() + rng = date_range('1/1/2012', periods=100, freq=freq) ser = Series(np.random.randn(len(rng)), rng) - _check_plot_works(ser.plot) + _check_plot_works(ser.plot, ax=ax) def test_get_datevalue(self): - from pandas.tseries.converter import get_datevalue - self.assertIsNone(get_datevalue(None, 'D')) - self.assertEqual(get_datevalue(1987, 'A'), 1987) - self.assertEqual(get_datevalue(Period(1987, 'A'), 'M'), - Period('1987-12', 'M').ordinal) - self.assertEqual(get_datevalue('1/1/1987', 'D'), - Period('1987-1-1', 'D').ordinal) - - @slow + from pandas.plotting._converter import get_datevalue + assert get_datevalue(None, 'D') is None + assert get_datevalue(1987, 'A') == 1987 + assert (get_datevalue(Period(1987, 'A'), 'M') == + Period('1987-12', 'M').ordinal) + assert (get_datevalue('1/1/1987', 'D') == + Period('1987-1-1', 'D').ordinal) + + @pytest.mark.slow def test_ts_plot_format_coord(self): def check_format_of_first_point(ax, expected_string): first_line = ax.get_lines()[0] first_x = first_line.get_xdata()[0].ordinal first_y = first_line.get_ydata()[0] try: - self.assertEqual(expected_string, - ax.format_coord(first_x, first_y)) + assert expected_string == ax.format_coord(first_x, first_y) except (ValueError): pytest.skip("skipping test because issue forming " "test comparison GH7664") annual = Series(1, index=date_range('2014-01-01', periods=3, freq='A-DEC')) - check_format_of_first_point(annual.plot(), 't = 2014 y = 1.000000') + _, ax = self.plt.subplots() + annual.plot(ax=ax) + check_format_of_first_point(ax, 't = 2014 y = 1.000000') # note this is added to the annual plot already in existence, and # changes its freq field daily = Series(1, index=date_range('2014-01-01', periods=3, freq='D')) - check_format_of_first_point(daily.plot(), + daily.plot(ax=ax) + check_format_of_first_point(ax, 't = 2014-01-01 y = 1.000000') tm.close() # tsplot - import matplotlib.pyplot as plt from pandas.tseries.plotting import tsplot - tsplot(annual, plt.Axes.plot) - check_format_of_first_point(plt.gca(), 't = 2014 y = 1.000000') - tsplot(daily, plt.Axes.plot) - check_format_of_first_point(plt.gca(), 't = 2014-01-01 y = 1.000000') - - @slow + _, ax = self.plt.subplots() + with tm.assert_produces_warning(FutureWarning): + tsplot(annual, self.plt.Axes.plot, ax=ax) + check_format_of_first_point(ax, 't = 2014 y = 1.000000') + with tm.assert_produces_warning(FutureWarning): + tsplot(daily, self.plt.Axes.plot, ax=ax) + check_format_of_first_point(ax, 't = 2014-01-01 y = 1.000000') + + @pytest.mark.slow def test_line_plot_period_series(self): for s in self.period_ser: _check_plot_works(s.plot, s.index.freq) - @slow + @pytest.mark.slow def test_line_plot_datetime_series(self): for s in self.datetime_ser: _check_plot_works(s.plot, s.index.freq.rule_code) - @slow + @pytest.mark.slow def test_line_plot_period_frame(self): for df in self.period_df: _check_plot_works(df.plot, df.index.freq) - @slow + @pytest.mark.slow def test_line_plot_datetime_frame(self): for df in self.datetime_df: freq = df.index.to_period(df.index.freq.rule_code).freq _check_plot_works(df.plot, freq) - @slow + @pytest.mark.slow def test_line_plot_inferred_freq(self): for ser in self.datetime_ser: ser = Series(ser.values, Index(np.asarray(ser.index))) @@ -215,17 +237,14 @@ def test_line_plot_inferred_freq(self): _check_plot_works(ser.plot) def test_fake_inferred_business(self): - import matplotlib.pyplot as plt - fig = plt.gcf() - plt.clf() - fig.add_subplot(111) + _, ax = self.plt.subplots() rng = date_range('2001-1-1', '2001-1-10') ts = Series(lrange(len(rng)), rng) ts = ts[:3].append(ts[5:]) - ax = ts.plot() - self.assertFalse(hasattr(ax, 'freq')) + ts.plot(ax=ax) + assert not hasattr(ax, 'freq') - @slow + @pytest.mark.slow def test_plot_offset_freq(self): ser = tm.makeTimeSeries() _check_plot_works(ser.plot) @@ -234,25 +253,21 @@ def test_plot_offset_freq(self): ser = Series(np.random.randn(len(dr)), dr) _check_plot_works(ser.plot) - @slow + @pytest.mark.slow def test_plot_multiple_inferred_freq(self): dr = Index([datetime(2000, 1, 1), datetime(2000, 1, 6), datetime( 2000, 1, 11)]) ser = Series(np.random.randn(len(dr)), dr) _check_plot_works(ser.plot) - @slow + @pytest.mark.slow def test_uhf(self): - import pandas.tseries.converter as conv - import matplotlib.pyplot as plt - fig = plt.gcf() - plt.clf() - fig.add_subplot(111) - + import pandas.plotting._converter as conv idx = date_range('2012-6-22 21:59:51.960928', freq='L', periods=500) df = DataFrame(np.random.randn(len(idx), 2), idx) - ax = df.plot() + _, ax = self.plt.subplots() + df.plot(ax=ax) axis = ax.get_xaxis() tlocs = axis.get_ticklocs() @@ -261,96 +276,85 @@ def test_uhf(self): xp = conv._from_ordinal(loc).strftime('%H:%M:%S.%f') rs = str(label.get_text()) if len(rs): - self.assertEqual(xp, rs) + assert xp == rs - @slow + @pytest.mark.slow def test_irreg_hf(self): - import matplotlib.pyplot as plt - fig = plt.gcf() - plt.clf() - fig.add_subplot(111) - idx = date_range('2012-6-22 21:59:51', freq='S', periods=100) df = DataFrame(np.random.randn(len(idx), 2), idx) irreg = df.iloc[[0, 1, 3, 4]] - ax = irreg.plot() + _, ax = self.plt.subplots() + irreg.plot(ax=ax) diffs = Series(ax.get_lines()[0].get_xydata()[:, 0]).diff() sec = 1. / 24 / 60 / 60 - self.assertTrue((np.fabs(diffs[1:] - [sec, sec * 2, sec]) < 1e-8).all( - )) + assert (np.fabs(diffs[1:] - [sec, sec * 2, sec]) < 1e-8).all() - plt.clf() - fig.add_subplot(111) + _, ax = self.plt.subplots() df2 = df.copy() - df2.index = df.index.asobject - ax = df2.plot() + df2.index = df.index.astype(object) + df2.plot(ax=ax) diffs = Series(ax.get_lines()[0].get_xydata()[:, 0]).diff() - self.assertTrue((np.fabs(diffs[1:] - sec) < 1e-8).all()) + assert (np.fabs(diffs[1:] - sec) < 1e-8).all() def test_irregular_datetime64_repr_bug(self): - import matplotlib.pyplot as plt ser = tm.makeTimeSeries() ser = ser[[0, 1, 2, 7]] - fig = plt.gcf() - plt.clf() - ax = fig.add_subplot(211) - ret = ser.plot() - self.assertIsNotNone(ret) + _, ax = self.plt.subplots() + + ret = ser.plot(ax=ax) + assert ret is not None for rs, xp in zip(ax.get_lines()[0].get_xdata(), ser.index): - self.assertEqual(rs, xp) + assert rs == xp def test_business_freq(self): - import matplotlib.pyplot as plt # noqa bts = tm.makePeriodSeries() - ax = bts.plot() - self.assertEqual(ax.get_lines()[0].get_xydata()[0, 0], - bts.index[0].ordinal) + _, ax = self.plt.subplots() + bts.plot(ax=ax) + assert ax.get_lines()[0].get_xydata()[0, 0] == bts.index[0].ordinal idx = ax.get_lines()[0].get_xdata() - self.assertEqual(PeriodIndex(data=idx).freqstr, 'B') + assert PeriodIndex(data=idx).freqstr == 'B' - @slow + @pytest.mark.slow def test_business_freq_convert(self): - n = tm.N - tm.N = 300 - bts = tm.makeTimeSeries().asfreq('BM') - tm.N = n + bts = tm.makeTimeSeries(300).asfreq('BM') ts = bts.to_period('M') - ax = bts.plot() - self.assertEqual(ax.get_lines()[0].get_xydata()[0, 0], - ts.index[0].ordinal) + _, ax = self.plt.subplots() + bts.plot(ax=ax) + assert ax.get_lines()[0].get_xydata()[0, 0] == ts.index[0].ordinal idx = ax.get_lines()[0].get_xdata() - self.assertEqual(PeriodIndex(data=idx).freqstr, 'M') + assert PeriodIndex(data=idx).freqstr == 'M' def test_nonzero_base(self): # GH2571 idx = (date_range('2012-12-20', periods=24, freq='H') + timedelta( minutes=30)) df = DataFrame(np.arange(24), index=idx) - ax = df.plot() + _, ax = self.plt.subplots() + df.plot(ax=ax) rs = ax.get_lines()[0].get_xdata() - self.assertFalse(Index(rs).is_normalized) + assert not Index(rs).is_normalized def test_dataframe(self): bts = DataFrame({'a': tm.makeTimeSeries()}) - ax = bts.plot() + _, ax = self.plt.subplots() + bts.plot(ax=ax) idx = ax.get_lines()[0].get_xdata() tm.assert_index_equal(bts.index.to_period(), PeriodIndex(idx)) - @slow + @pytest.mark.slow def test_axis_limits(self): - import matplotlib.pyplot as plt def _test(ax): xlim = ax.get_xlim() ax.set_xlim(xlim[0] - 5, xlim[1] + 10) ax.get_figure().canvas.draw() result = ax.get_xlim() - self.assertEqual(result[0], xlim[0] - 5) - self.assertEqual(result[1], xlim[1] + 10) + assert result[0] == xlim[0] - 5 + assert result[1] == xlim[1] + 10 # string expected = (Period('1/1/2000', ax.freq), @@ -358,26 +362,28 @@ def _test(ax): ax.set_xlim('1/1/2000', '4/1/2000') ax.get_figure().canvas.draw() result = ax.get_xlim() - self.assertEqual(int(result[0]), expected[0].ordinal) - self.assertEqual(int(result[1]), expected[1].ordinal) + assert int(result[0]) == expected[0].ordinal + assert int(result[1]) == expected[1].ordinal - # datetim + # datetime expected = (Period('1/1/2000', ax.freq), Period('4/1/2000', ax.freq)) ax.set_xlim(datetime(2000, 1, 1), datetime(2000, 4, 1)) ax.get_figure().canvas.draw() result = ax.get_xlim() - self.assertEqual(int(result[0]), expected[0].ordinal) - self.assertEqual(int(result[1]), expected[1].ordinal) + assert int(result[0]) == expected[0].ordinal + assert int(result[1]) == expected[1].ordinal fig = ax.get_figure() - plt.close(fig) + self.plt.close(fig) ser = tm.makeTimeSeries() - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) _test(ax) + _, ax = self.plt.subplots() df = DataFrame({'a': ser, 'b': ser + 1}) - ax = df.plot() + df.plot(ax=ax) _test(ax) df = DataFrame({'a': ser, 'b': ser + 1}) @@ -387,351 +393,427 @@ def _test(ax): _test(ax) def test_get_finder(self): - import pandas.tseries.converter as conv + import pandas.plotting._converter as conv - self.assertEqual(conv.get_finder('B'), conv._daily_finder) - self.assertEqual(conv.get_finder('D'), conv._daily_finder) - self.assertEqual(conv.get_finder('M'), conv._monthly_finder) - self.assertEqual(conv.get_finder('Q'), conv._quarterly_finder) - self.assertEqual(conv.get_finder('A'), conv._annual_finder) - self.assertEqual(conv.get_finder('W'), conv._daily_finder) + assert conv.get_finder('B') == conv._daily_finder + assert conv.get_finder('D') == conv._daily_finder + assert conv.get_finder('M') == conv._monthly_finder + assert conv.get_finder('Q') == conv._quarterly_finder + assert conv.get_finder('A') == conv._annual_finder + assert conv.get_finder('W') == conv._daily_finder - @slow + @pytest.mark.slow def test_finder_daily(self): - import matplotlib.pyplot as plt - xp = Period('1999-1-1', freq='B').ordinal day_lst = [10, 40, 252, 400, 950, 2750, 10000] - for n in day_lst: + + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + xpl1 = xpl2 = [Period('1999-1-1', freq='B').ordinal] * len(day_lst) + else: # 2.0.1, 2.1.0, 2.2.2, 2.2.3 + xpl1 = [7565, 7564, 7553, 7546, 7518, 7428, 7066] + xpl2 = [7566, 7564, 7554, 7546, 7519, 7429, 7066] + + rs1 = [] + rs2 = [] + for i, n in enumerate(day_lst): rng = bdate_range('1999-1-1', periods=n) ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(xp, rs) + rs1.append(xaxis.get_majorticklocs()[0]) + vmin, vmax = ax.get_xlim() ax.set_xlim(vmin + 0.9, vmax) - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(xp, rs) - plt.close(ax.get_figure()) + rs2.append(xaxis.get_majorticklocs()[0]) + self.plt.close(ax.get_figure()) - @slow + assert rs1 == xpl1 + assert rs2 == xpl2 + + @pytest.mark.slow def test_finder_quarterly(self): - import matplotlib.pyplot as plt - xp = Period('1988Q1').ordinal yrs = [3.5, 11] - for n in yrs: + + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + xpl1 = xpl2 = [Period('1988Q1').ordinal] * len(yrs) + else: # 2.0.1, 2.1.0, 2.2.2, 2.2.3 + xpl1 = [68, 68] + xpl2 = [72, 68] + + rs1 = [] + rs2 = [] + for i, n in enumerate(yrs): rng = period_range('1987Q2', periods=int(n * 4), freq='Q') ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(rs, xp) + rs1.append(xaxis.get_majorticklocs()[0]) + (vmin, vmax) = ax.get_xlim() ax.set_xlim(vmin + 0.9, vmax) - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(xp, rs) - plt.close(ax.get_figure()) + rs2.append(xaxis.get_majorticklocs()[0]) + self.plt.close(ax.get_figure()) - @slow + assert rs1 == xpl1 + assert rs2 == xpl2 + + @pytest.mark.slow def test_finder_monthly(self): - import matplotlib.pyplot as plt - xp = Period('Jan 1988').ordinal yrs = [1.15, 2.5, 4, 11] - for n in yrs: + + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + xpl1 = xpl2 = [Period('Jan 1988').ordinal] * len(yrs) + else: # 2.0.1, 2.1.0, 2.2.2, 2.2.3 + xpl1 = [216, 216, 204, 204] + xpl2 = [216, 216, 216, 204] + + rs1 = [] + rs2 = [] + for i, n in enumerate(yrs): rng = period_range('1987Q2', periods=int(n * 12), freq='M') ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(rs, xp) + rs1.append(xaxis.get_majorticklocs()[0]) + vmin, vmax = ax.get_xlim() ax.set_xlim(vmin + 0.9, vmax) - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(xp, rs) - plt.close(ax.get_figure()) + rs2.append(xaxis.get_majorticklocs()[0]) + self.plt.close(ax.get_figure()) + + assert rs1 == xpl1 + assert rs2 == xpl2 def test_finder_monthly_long(self): rng = period_range('1988Q1', periods=24 * 12, freq='M') ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() rs = xaxis.get_majorticklocs()[0] xp = Period('1989Q1', 'M').ordinal - self.assertEqual(rs, xp) + assert rs == xp - @slow + @pytest.mark.slow def test_finder_annual(self): - import matplotlib.pyplot as plt - xp = [1987, 1988, 1990, 1990, 1995, 2020, 2070, 2170] + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + xp = [1987, 1988, 1990, 1990, 1995, 2020, 2070, 2170] + else: # 2.0.1, 2.1.0, 2.2.2, 2.2.3 + xp = [1986, 1986, 1990, 1990, 1995, 2020, 1970, 1970] + + xp = [Period(x, freq='A').ordinal for x in xp] + rs = [] for i, nyears in enumerate([5, 10, 19, 49, 99, 199, 599, 1001]): rng = period_range('1987', periods=nyears, freq='A') ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() - rs = xaxis.get_majorticklocs()[0] - self.assertEqual(rs, Period(xp[i], freq='A').ordinal) - plt.close(ax.get_figure()) + rs.append(xaxis.get_majorticklocs()[0]) + self.plt.close(ax.get_figure()) + + assert rs == xp - @slow + @pytest.mark.slow def test_finder_minutely(self): nminutes = 50 * 24 * 60 rng = date_range('1/1/1999', freq='Min', periods=nminutes) ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() rs = xaxis.get_majorticklocs()[0] xp = Period('1/1/1999', freq='Min').ordinal - self.assertEqual(rs, xp) + + assert rs == xp def test_finder_hourly(self): nhours = 23 rng = date_range('1/1/1999', freq='H', periods=nhours) ser = Series(np.random.randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) xaxis = ax.get_xaxis() rs = xaxis.get_majorticklocs()[0] - xp = Period('1/1/1999', freq='H').ordinal - self.assertEqual(rs, xp) + if self.mpl_ge_2_0_1: + xp = Period('1/1/1999', freq='H').ordinal + else: # 2.0.0 + xp = Period('1998-12-31 22:00', freq='H').ordinal - @slow - def test_gaps(self): - import matplotlib.pyplot as plt + assert rs == xp + @pytest.mark.slow + def test_gaps(self): ts = tm.makeTimeSeries() ts[5:25] = np.nan - ax = ts.plot() + _, ax = self.plt.subplots() + ts.plot(ax=ax) lines = ax.get_lines() - tm._skip_if_mpl_1_5() - self.assertEqual(len(lines), 1) - l = lines[0] - data = l.get_xydata() - tm.assertIsInstance(data, np.ma.core.MaskedArray) + assert len(lines) == 1 + line = lines[0] + data = line.get_xydata() + + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + data = np.ma.MaskedArray(data, mask=isna(data), fill_value=np.nan) + + assert isinstance(data, np.ma.core.MaskedArray) mask = data.mask - self.assertTrue(mask[5:25, 1].all()) - plt.close(ax.get_figure()) + assert mask[5:25, 1].all() + self.plt.close(ax.get_figure()) # irregular ts = tm.makeTimeSeries() ts = ts[[0, 1, 2, 5, 7, 9, 12, 15, 20]] ts[2:5] = np.nan - ax = ts.plot() + _, ax = self.plt.subplots() + ax = ts.plot(ax=ax) lines = ax.get_lines() - self.assertEqual(len(lines), 1) - l = lines[0] - data = l.get_xydata() - tm.assertIsInstance(data, np.ma.core.MaskedArray) + assert len(lines) == 1 + line = lines[0] + data = line.get_xydata() + + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + data = np.ma.MaskedArray(data, mask=isna(data), fill_value=np.nan) + + assert isinstance(data, np.ma.core.MaskedArray) mask = data.mask - self.assertTrue(mask[2:5, 1].all()) - plt.close(ax.get_figure()) + assert mask[2:5, 1].all() + self.plt.close(ax.get_figure()) # non-ts idx = [0, 1, 2, 5, 7, 9, 12, 15, 20] ser = Series(np.random.randn(len(idx)), idx) ser[2:5] = np.nan - ax = ser.plot() + _, ax = self.plt.subplots() + ser.plot(ax=ax) lines = ax.get_lines() - self.assertEqual(len(lines), 1) - l = lines[0] - data = l.get_xydata() - tm.assertIsInstance(data, np.ma.core.MaskedArray) + assert len(lines) == 1 + line = lines[0] + data = line.get_xydata() + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + data = np.ma.MaskedArray(data, mask=isna(data), fill_value=np.nan) + + assert isinstance(data, np.ma.core.MaskedArray) mask = data.mask - self.assertTrue(mask[2:5, 1].all()) + assert mask[2:5, 1].all() - @slow + @pytest.mark.slow def test_gap_upsample(self): low = tm.makeTimeSeries() low[5:25] = np.nan - ax = low.plot() + _, ax = self.plt.subplots() + low.plot(ax=ax) idxh = date_range(low.index[0], low.index[-1], freq='12h') s = Series(np.random.randn(len(idxh)), idxh) s.plot(secondary_y=True) lines = ax.get_lines() - self.assertEqual(len(lines), 1) - self.assertEqual(len(ax.right_ax.get_lines()), 1) - l = lines[0] - data = l.get_xydata() + assert len(lines) == 1 + assert len(ax.right_ax.get_lines()) == 1 - tm._skip_if_mpl_1_5() + line = lines[0] + data = line.get_xydata() + if (self.mpl_ge_3_0_0 or not self.mpl_ge_2_0_1 + or (self.mpl_ge_2_1_0 and not self.mpl_ge_2_2_2)): + # 2.0.0, 2.2.0 (exactly) or >= 3.0.0 + data = np.ma.MaskedArray(data, mask=isna(data), fill_value=np.nan) - tm.assertIsInstance(data, np.ma.core.MaskedArray) + assert isinstance(data, np.ma.core.MaskedArray) mask = data.mask - self.assertTrue(mask[5:25, 1].all()) + assert mask[5:25, 1].all() - @slow + @pytest.mark.slow def test_secondary_y(self): - import matplotlib.pyplot as plt - ser = Series(np.random.randn(10)) ser2 = Series(np.random.randn(10)) + fig, _ = self.plt.subplots() ax = ser.plot(secondary_y=True) - self.assertTrue(hasattr(ax, 'left_ax')) - self.assertFalse(hasattr(ax, 'right_ax')) - fig = ax.get_figure() + assert hasattr(ax, 'left_ax') + assert not hasattr(ax, 'right_ax') axes = fig.get_axes() - l = ax.get_lines()[0] - xp = Series(l.get_ydata(), l.get_xdata()) + line = ax.get_lines()[0] + xp = Series(line.get_ydata(), line.get_xdata()) assert_series_equal(ser, xp) - self.assertEqual(ax.get_yaxis().get_ticks_position(), 'right') - self.assertFalse(axes[0].get_yaxis().get_visible()) - plt.close(fig) + assert ax.get_yaxis().get_ticks_position() == 'right' + assert not axes[0].get_yaxis().get_visible() + self.plt.close(fig) - ax2 = ser2.plot() - self.assertEqual(ax2.get_yaxis().get_ticks_position(), - self.default_tick_position) - plt.close(ax2.get_figure()) + _, ax2 = self.plt.subplots() + ser2.plot(ax=ax2) + assert (ax2.get_yaxis().get_ticks_position() == + self.default_tick_position) + self.plt.close(ax2.get_figure()) ax = ser2.plot() ax2 = ser.plot(secondary_y=True) - self.assertTrue(ax.get_yaxis().get_visible()) - self.assertFalse(hasattr(ax, 'left_ax')) - self.assertTrue(hasattr(ax, 'right_ax')) - self.assertTrue(hasattr(ax2, 'left_ax')) - self.assertFalse(hasattr(ax2, 'right_ax')) + assert ax.get_yaxis().get_visible() + assert not hasattr(ax, 'left_ax') + assert hasattr(ax, 'right_ax') + assert hasattr(ax2, 'left_ax') + assert not hasattr(ax2, 'right_ax') - @slow + @pytest.mark.slow def test_secondary_y_ts(self): - import matplotlib.pyplot as plt idx = date_range('1/1/2000', periods=10) ser = Series(np.random.randn(10), idx) ser2 = Series(np.random.randn(10), idx) + fig, _ = self.plt.subplots() ax = ser.plot(secondary_y=True) - self.assertTrue(hasattr(ax, 'left_ax')) - self.assertFalse(hasattr(ax, 'right_ax')) - fig = ax.get_figure() + assert hasattr(ax, 'left_ax') + assert not hasattr(ax, 'right_ax') axes = fig.get_axes() - l = ax.get_lines()[0] - xp = Series(l.get_ydata(), l.get_xdata()).to_timestamp() + line = ax.get_lines()[0] + xp = Series(line.get_ydata(), line.get_xdata()).to_timestamp() assert_series_equal(ser, xp) - self.assertEqual(ax.get_yaxis().get_ticks_position(), 'right') - self.assertFalse(axes[0].get_yaxis().get_visible()) - plt.close(fig) + assert ax.get_yaxis().get_ticks_position() == 'right' + assert not axes[0].get_yaxis().get_visible() + self.plt.close(fig) - ax2 = ser2.plot() - self.assertEqual(ax2.get_yaxis().get_ticks_position(), - self.default_tick_position) - plt.close(ax2.get_figure()) + _, ax2 = self.plt.subplots() + ser2.plot(ax=ax2) + assert (ax2.get_yaxis().get_ticks_position() == + self.default_tick_position) + self.plt.close(ax2.get_figure()) ax = ser2.plot() ax2 = ser.plot(secondary_y=True) - self.assertTrue(ax.get_yaxis().get_visible()) + assert ax.get_yaxis().get_visible() - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_secondary_kde(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() - import matplotlib.pyplot as plt # noqa ser = Series(np.random.randn(10)) - ax = ser.plot(secondary_y=True, kind='density') - self.assertTrue(hasattr(ax, 'left_ax')) - self.assertFalse(hasattr(ax, 'right_ax')) - fig = ax.get_figure() + fig, ax = self.plt.subplots() + ax = ser.plot(secondary_y=True, kind='density', ax=ax) + assert hasattr(ax, 'left_ax') + assert not hasattr(ax, 'right_ax') axes = fig.get_axes() - self.assertEqual(axes[1].get_yaxis().get_ticks_position(), 'right') + assert axes[1].get_yaxis().get_ticks_position() == 'right' - @slow + @pytest.mark.slow def test_secondary_bar(self): ser = Series(np.random.randn(10)) - ax = ser.plot(secondary_y=True, kind='bar') - fig = ax.get_figure() + fig, ax = self.plt.subplots() + ser.plot(secondary_y=True, kind='bar', ax=ax) axes = fig.get_axes() - self.assertEqual(axes[1].get_yaxis().get_ticks_position(), 'right') + assert axes[1].get_yaxis().get_ticks_position() == 'right' - @slow + @pytest.mark.slow def test_secondary_frame(self): df = DataFrame(np.random.randn(5, 3), columns=['a', 'b', 'c']) axes = df.plot(secondary_y=['a', 'c'], subplots=True) - self.assertEqual(axes[0].get_yaxis().get_ticks_position(), 'right') - self.assertEqual(axes[1].get_yaxis().get_ticks_position(), - self.default_tick_position) - self.assertEqual(axes[2].get_yaxis().get_ticks_position(), 'right') + assert axes[0].get_yaxis().get_ticks_position() == 'right' + assert (axes[1].get_yaxis().get_ticks_position() == + self.default_tick_position) + assert axes[2].get_yaxis().get_ticks_position() == 'right' - @slow + @pytest.mark.slow def test_secondary_bar_frame(self): df = DataFrame(np.random.randn(5, 3), columns=['a', 'b', 'c']) axes = df.plot(kind='bar', secondary_y=['a', 'c'], subplots=True) - self.assertEqual(axes[0].get_yaxis().get_ticks_position(), 'right') - self.assertEqual(axes[1].get_yaxis().get_ticks_position(), - self.default_tick_position) - self.assertEqual(axes[2].get_yaxis().get_ticks_position(), 'right') + assert axes[0].get_yaxis().get_ticks_position() == 'right' + assert (axes[1].get_yaxis().get_ticks_position() == + self.default_tick_position) + assert axes[2].get_yaxis().get_ticks_position() == 'right' def test_mixed_freq_regular_first(self): - import matplotlib.pyplot as plt # noqa + # TODO s1 = tm.makeTimeSeries() s2 = s1[[0, 5, 10, 11, 12, 13, 14, 15]] # it works! - s1.plot() + _, ax = self.plt.subplots() + s1.plot(ax=ax) - ax2 = s2.plot(style='g') + ax2 = s2.plot(style='g', ax=ax) lines = ax2.get_lines() idx1 = PeriodIndex(lines[0].get_xdata()) idx2 = PeriodIndex(lines[1].get_xdata()) - self.assertTrue(idx1.equals(s1.index.to_period('B'))) - self.assertTrue(idx2.equals(s2.index.to_period('B'))) + + tm.assert_index_equal(idx1, s1.index.to_period('B')) + tm.assert_index_equal(idx2, s2.index.to_period('B')) + left, right = ax2.get_xlim() pidx = s1.index.to_period() - self.assertEqual(left, pidx[0].ordinal) - self.assertEqual(right, pidx[-1].ordinal) + assert left <= pidx[0].ordinal + assert right >= pidx[-1].ordinal - @slow + @pytest.mark.slow def test_mixed_freq_irregular_first(self): - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries() s2 = s1[[0, 5, 10, 11, 12, 13, 14, 15]] - s2.plot(style='g') - ax = s1.plot() - self.assertFalse(hasattr(ax, 'freq')) + _, ax = self.plt.subplots() + s2.plot(style='g', ax=ax) + s1.plot(ax=ax) + assert not hasattr(ax, 'freq') lines = ax.get_lines() x1 = lines[0].get_xdata() - tm.assert_numpy_array_equal(x1, s2.index.asobject.values) + tm.assert_numpy_array_equal(x1, s2.index.astype(object).values) x2 = lines[1].get_xdata() - tm.assert_numpy_array_equal(x2, s1.index.asobject.values) + tm.assert_numpy_array_equal(x2, s1.index.astype(object).values) def test_mixed_freq_regular_first_df(self): # GH 9852 - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries().to_frame() s2 = s1.iloc[[0, 5, 10, 11, 12, 13, 14, 15], :] - ax = s1.plot() + _, ax = self.plt.subplots() + s1.plot(ax=ax) ax2 = s2.plot(style='g', ax=ax) lines = ax2.get_lines() idx1 = PeriodIndex(lines[0].get_xdata()) idx2 = PeriodIndex(lines[1].get_xdata()) - self.assertTrue(idx1.equals(s1.index.to_period('B'))) - self.assertTrue(idx2.equals(s2.index.to_period('B'))) + assert idx1.equals(s1.index.to_period('B')) + assert idx2.equals(s2.index.to_period('B')) left, right = ax2.get_xlim() pidx = s1.index.to_period() - self.assertEqual(left, pidx[0].ordinal) - self.assertEqual(right, pidx[-1].ordinal) + assert left <= pidx[0].ordinal + assert right >= pidx[-1].ordinal - @slow + @pytest.mark.slow def test_mixed_freq_irregular_first_df(self): # GH 9852 - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries().to_frame() s2 = s1.iloc[[0, 5, 10, 11, 12, 13, 14, 15], :] - ax = s2.plot(style='g') - ax = s1.plot(ax=ax) - self.assertFalse(hasattr(ax, 'freq')) + _, ax = self.plt.subplots() + s2.plot(style='g', ax=ax) + s1.plot(ax=ax) + assert not hasattr(ax, 'freq') lines = ax.get_lines() x1 = lines[0].get_xdata() - tm.assert_numpy_array_equal(x1, s2.index.asobject.values) + tm.assert_numpy_array_equal(x1, s2.index.astype(object).values) x2 = lines[1].get_xdata() - tm.assert_numpy_array_equal(x2, s1.index.asobject.values) + tm.assert_numpy_array_equal(x2, s1.index.astype(object).values) def test_mixed_freq_hf_first(self): idxh = date_range('1/1/1999', periods=365, freq='D') idxl = date_range('1/1/1999', periods=12, freq='M') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - high.plot() - ax = low.plot() + _, ax = self.plt.subplots() + high.plot(ax=ax) + low.plot(ax=ax) for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, 'D') + assert PeriodIndex(data=l.get_xdata()).freq == 'D' - @slow + @pytest.mark.slow def test_mixed_freq_alignment(self): ts_ind = date_range('2012-01-01 13:00', '2012-01-02', freq='H') ts_data = np.random.randn(12) @@ -739,44 +821,46 @@ def test_mixed_freq_alignment(self): ts = Series(ts_data, index=ts_ind) ts2 = ts.asfreq('T').interpolate() - ax = ts.plot() - ts2.plot(style='r') + _, ax = self.plt.subplots() + ax = ts.plot(ax=ax) + ts2.plot(style='r', ax=ax) - self.assertEqual(ax.lines[0].get_xdata()[0], - ax.lines[1].get_xdata()[0]) + assert ax.lines[0].get_xdata()[0] == ax.lines[1].get_xdata()[0] - @slow + @pytest.mark.slow def test_mixed_freq_lf_first(self): - import matplotlib.pyplot as plt idxh = date_range('1/1/1999', periods=365, freq='D') idxl = date_range('1/1/1999', periods=12, freq='M') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - low.plot(legend=True) - ax = high.plot(legend=True) + _, ax = self.plt.subplots() + low.plot(legend=True, ax=ax) + high.plot(legend=True, ax=ax) for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, 'D') + assert PeriodIndex(data=l.get_xdata()).freq == 'D' leg = ax.get_legend() - self.assertEqual(len(leg.texts), 2) - plt.close(ax.get_figure()) + assert len(leg.texts) == 2 + self.plt.close(ax.get_figure()) idxh = date_range('1/1/1999', periods=240, freq='T') idxl = date_range('1/1/1999', periods=4, freq='H') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - low.plot() - ax = high.plot() + _, ax = self.plt.subplots() + low.plot(ax=ax) + high.plot(ax=ax) for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, 'T') + assert PeriodIndex(data=l.get_xdata()).freq == 'T' def test_mixed_freq_irreg_period(self): ts = tm.makeTimeSeries() irreg = ts[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 16, 17, 18, 29]] rng = period_range('1/3/2000', periods=30, freq='B') ps = Series(np.random.randn(len(rng)), rng) - irreg.plot() - ps.plot() + _, ax = self.plt.subplots() + irreg.plot(ax=ax) + ps.plot(ax=ax) def test_mixed_freq_shared_ax(self): @@ -790,10 +874,10 @@ def test_mixed_freq_shared_ax(self): s1.plot(ax=ax1) s2.plot(ax=ax2) - self.assertEqual(ax1.freq, 'M') - self.assertEqual(ax2.freq, 'M') - self.assertEqual(ax1.lines[0].get_xydata()[0, 0], - ax2.lines[0].get_xydata()[0, 0]) + assert ax1.freq == 'M' + assert ax2.freq == 'M' + assert (ax1.lines[0].get_xydata()[0, 0] == + ax2.lines[0].get_xydata()[0, 0]) # using twinx fig, ax1 = self.plt.subplots() @@ -801,8 +885,8 @@ def test_mixed_freq_shared_ax(self): s1.plot(ax=ax1) s2.plot(ax=ax2) - self.assertEqual(ax1.lines[0].get_xydata()[0, 0], - ax2.lines[0].get_xydata()[0, 0]) + assert (ax1.lines[0].get_xydata()[0, 0] == + ax2.lines[0].get_xydata()[0, 0]) # TODO (GH14330, GH14322) # plotting the irregular first does not yet work @@ -810,65 +894,79 @@ def test_mixed_freq_shared_ax(self): # ax2 = ax1.twinx() # s2.plot(ax=ax1) # s1.plot(ax=ax2) - # self.assertEqual(ax1.lines[0].get_xydata()[0, 0], - # ax2.lines[0].get_xydata()[0, 0]) + # assert (ax1.lines[0].get_xydata()[0, 0] == + # ax2.lines[0].get_xydata()[0, 0]) + + def test_nat_handling(self): + + _, ax = self.plt.subplots() + + dti = DatetimeIndex(['2015-01-01', NaT, '2015-01-03']) + s = Series(range(len(dti)), dti) + s.plot(ax=ax) + xdata = ax.get_lines()[0].get_xdata() + # plot x data is bounded by index values + assert s.index.min() <= Series(xdata).min() + assert Series(xdata).max() <= s.index.max() - @slow + @pytest.mark.slow def test_to_weekly_resampling(self): idxh = date_range('1/1/1999', periods=52, freq='W') idxl = date_range('1/1/1999', periods=12, freq='M') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - high.plot() - ax = low.plot() + _, ax = self.plt.subplots() + high.plot(ax=ax) + low.plot(ax=ax) for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, idxh.freq) + assert PeriodIndex(data=l.get_xdata()).freq == idxh.freq - # tsplot + _, ax = self.plt.subplots() from pandas.tseries.plotting import tsplot - import matplotlib.pyplot as plt - - tsplot(high, plt.Axes.plot) - lines = tsplot(low, plt.Axes.plot) + with tm.assert_produces_warning(FutureWarning): + tsplot(high, self.plt.Axes.plot, ax=ax) + with tm.assert_produces_warning(FutureWarning): + lines = tsplot(low, self.plt.Axes.plot, ax=ax) for l in lines: - self.assertTrue(PeriodIndex(data=l.get_xdata()).freq, idxh.freq) + assert PeriodIndex(data=l.get_xdata()).freq == idxh.freq - @slow + @pytest.mark.slow def test_from_weekly_resampling(self): idxh = date_range('1/1/1999', periods=52, freq='W') idxl = date_range('1/1/1999', periods=12, freq='M') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - low.plot() - ax = high.plot() + _, ax = self.plt.subplots() + low.plot(ax=ax) + high.plot(ax=ax) expected_h = idxh.to_period().asi8.astype(np.float64) expected_l = np.array([1514, 1519, 1523, 1527, 1531, 1536, 1540, 1544, 1549, 1553, 1558, 1562], dtype=np.float64) for l in ax.get_lines(): - self.assertTrue(PeriodIndex(data=l.get_xdata()).freq, idxh.freq) + assert PeriodIndex(data=l.get_xdata()).freq == idxh.freq xdata = l.get_xdata(orig=False) if len(xdata) == 12: # idxl lines - self.assert_numpy_array_equal(xdata, expected_l) + tm.assert_numpy_array_equal(xdata, expected_l) else: - self.assert_numpy_array_equal(xdata, expected_h) + tm.assert_numpy_array_equal(xdata, expected_h) tm.close() - # tsplot + _, ax = self.plt.subplots() from pandas.tseries.plotting import tsplot - import matplotlib.pyplot as plt - - tsplot(low, plt.Axes.plot) - lines = tsplot(high, plt.Axes.plot) + with tm.assert_produces_warning(FutureWarning): + tsplot(low, self.plt.Axes.plot, ax=ax) + with tm.assert_produces_warning(FutureWarning): + lines = tsplot(high, self.plt.Axes.plot, ax=ax) for l in lines: - self.assertTrue(PeriodIndex(data=l.get_xdata()).freq, idxh.freq) + assert PeriodIndex(data=l.get_xdata()).freq == idxh.freq xdata = l.get_xdata(orig=False) if len(xdata) == 12: # idxl lines - self.assert_numpy_array_equal(xdata, expected_l) + tm.assert_numpy_array_equal(xdata, expected_l) else: - self.assert_numpy_array_equal(xdata, expected_h) + tm.assert_numpy_array_equal(xdata, expected_h) - @slow + @pytest.mark.slow def test_from_resampling_area_line_mixed(self): idxh = date_range('1/1/1999', periods=52, freq='W') idxl = date_range('1/1/1999', periods=12, freq='M') @@ -879,8 +977,9 @@ def test_from_resampling_area_line_mixed(self): # low to high for kind1, kind2 in [('line', 'area'), ('area', 'line')]: - ax = low.plot(kind=kind1, stacked=True) - ax = high.plot(kind=kind2, stacked=True, ax=ax) + _, ax = self.plt.subplots() + low.plot(kind=kind1, stacked=True, ax=ax) + high.plot(kind=kind2, stacked=True, ax=ax) # check low dataframe result expected_x = np.array([1514, 1519, 1523, 1527, 1531, 1536, 1540, @@ -888,45 +987,44 @@ def test_from_resampling_area_line_mixed(self): dtype=np.float64) expected_y = np.zeros(len(expected_x), dtype=np.float64) for i in range(3): - l = ax.lines[i] - self.assertEqual(PeriodIndex(l.get_xdata()).freq, idxh.freq) - self.assert_numpy_array_equal(l.get_xdata(orig=False), - expected_x) + line = ax.lines[i] + assert PeriodIndex(line.get_xdata()).freq == idxh.freq + tm.assert_numpy_array_equal(line.get_xdata(orig=False), + expected_x) # check stacked values are correct expected_y += low[i].values - self.assert_numpy_array_equal( - l.get_ydata(orig=False), expected_y) + tm.assert_numpy_array_equal(line.get_ydata(orig=False), + expected_y) # check high dataframe result expected_x = idxh.to_period().asi8.astype(np.float64) expected_y = np.zeros(len(expected_x), dtype=np.float64) for i in range(3): - l = ax.lines[3 + i] - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, - idxh.freq) - self.assert_numpy_array_equal(l.get_xdata(orig=False), - expected_x) + line = ax.lines[3 + i] + assert PeriodIndex(data=line.get_xdata()).freq == idxh.freq + tm.assert_numpy_array_equal(line.get_xdata(orig=False), + expected_x) expected_y += high[i].values - self.assert_numpy_array_equal(l.get_ydata(orig=False), - expected_y) + tm.assert_numpy_array_equal(line.get_ydata(orig=False), + expected_y) # high to low for kind1, kind2 in [('line', 'area'), ('area', 'line')]: - ax = high.plot(kind=kind1, stacked=True) - ax = low.plot(kind=kind2, stacked=True, ax=ax) + _, ax = self.plt.subplots() + high.plot(kind=kind1, stacked=True, ax=ax) + low.plot(kind=kind2, stacked=True, ax=ax) # check high dataframe result expected_x = idxh.to_period().asi8.astype(np.float64) expected_y = np.zeros(len(expected_x), dtype=np.float64) for i in range(3): - l = ax.lines[i] - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, - idxh.freq) - self.assert_numpy_array_equal( - l.get_xdata(orig=False), expected_x) + line = ax.lines[i] + assert PeriodIndex(data=line.get_xdata()).freq == idxh.freq + tm.assert_numpy_array_equal(line.get_xdata(orig=False), + expected_x) expected_y += high[i].values - self.assert_numpy_array_equal( - l.get_ydata(orig=False), expected_y) + tm.assert_numpy_array_equal(line.get_ydata(orig=False), + expected_y) # check low dataframe result expected_x = np.array([1514, 1519, 1523, 1527, 1531, 1536, 1540, @@ -934,16 +1032,15 @@ def test_from_resampling_area_line_mixed(self): dtype=np.float64) expected_y = np.zeros(len(expected_x), dtype=np.float64) for i in range(3): - l = ax.lines[3 + i] - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, - idxh.freq) - self.assert_numpy_array_equal(l.get_xdata(orig=False), - expected_x) + lines = ax.lines[3 + i] + assert PeriodIndex(data=lines.get_xdata()).freq == idxh.freq + tm.assert_numpy_array_equal(lines.get_xdata(orig=False), + expected_x) expected_y += low[i].values - self.assert_numpy_array_equal(l.get_ydata(orig=False), - expected_y) + tm.assert_numpy_array_equal(lines.get_ydata(orig=False), + expected_y) - @slow + @pytest.mark.slow def test_mixed_freq_second_millisecond(self): # GH 7772, GH 7760 idxh = date_range('2014-07-01 09:00', freq='S', periods=50) @@ -951,21 +1048,23 @@ def test_mixed_freq_second_millisecond(self): high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) # high to low - high.plot() - ax = low.plot() - self.assertEqual(len(ax.get_lines()), 2) + _, ax = self.plt.subplots() + high.plot(ax=ax) + low.plot(ax=ax) + assert len(ax.get_lines()) == 2 for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, 'L') + assert PeriodIndex(data=l.get_xdata()).freq == 'L' tm.close() # low to high - low.plot() - ax = high.plot() - self.assertEqual(len(ax.get_lines()), 2) + _, ax = self.plt.subplots() + low.plot(ax=ax) + high.plot(ax=ax) + assert len(ax.get_lines()) == 2 for l in ax.get_lines(): - self.assertEqual(PeriodIndex(data=l.get_xdata()).freq, 'L') + assert PeriodIndex(data=l.get_xdata()).freq == 'L' - @slow + @pytest.mark.slow def test_irreg_dtypes(self): # date idx = [date(2000, 1, 1), date(2000, 1, 5), date(2000, 1, 20)] @@ -974,11 +1073,13 @@ def test_irreg_dtypes(self): # np.datetime64 idx = date_range('1/1/2000', periods=10) - idx = idx[[0, 2, 5, 9]].asobject + idx = idx[[0, 2, 5, 9]].astype(object) df = DataFrame(np.random.randn(len(idx), 3), idx) - _check_plot_works(df.plot) + _, ax = self.plt.subplots() + _check_plot_works(df.plot, ax=ax) - @slow + @pytest.mark.xfail(reason="fails with py2.7.15", strict=False) + @pytest.mark.slow def test_time(self): t = datetime(1, 1, 1, 3, 30, 0) deltas = np.random.randint(1, 20, 3).cumsum() @@ -986,34 +1087,43 @@ def test_time(self): df = DataFrame({'a': np.random.randn(len(ts)), 'b': np.random.randn(len(ts))}, index=ts) - ax = df.plot() + fig, ax = self.plt.subplots() + df.plot(ax=ax) # verify tick labels + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S') - self.assertEqual(xp, rs) + rs = l.get_text() + if len(rs) > 0: + if s != 0: + xp = time(h, m, s).strftime('%H:%M:%S') + else: + xp = time(h, m, s).strftime('%H:%M') + assert xp == rs # change xlim ax.set_xlim('1:30', '5:00') # check tick labels again + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S') - self.assertEqual(xp, rs) - - @slow + rs = l.get_text() + if len(rs) > 0: + if s != 0: + xp = time(h, m, s).strftime('%H:%M:%S') + else: + xp = time(h, m, s).strftime('%H:%M') + assert xp == rs + + @pytest.mark.slow def test_time_musec(self): t = datetime(1, 1, 1, 3, 30, 0) deltas = np.random.randint(1, 20, 3).cumsum() @@ -1022,159 +1132,166 @@ def test_time_musec(self): df = DataFrame({'a': np.random.randn(len(ts)), 'b': np.random.randn(len(ts))}, index=ts) - ax = df.plot() + fig, ax = self.plt.subplots() + ax = df.plot(ax=ax) # verify tick labels + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) - # TODO: unused? - # us = int((t - int(t)) * 1e6) + us = int(round((t - int(t)) * 1e6)) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S.%f') - self.assertEqual(xp, rs) - - @slow + rs = l.get_text() + if len(rs) > 0: + if (us % 1000) != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S.%f') + elif (us // 1000) != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S.%f')[:-3] + elif s != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S') + else: + xp = time(h, m, s, us).strftime('%H:%M') + assert xp == rs + + @pytest.mark.slow def test_secondary_upsample(self): idxh = date_range('1/1/1999', periods=365, freq='D') idxl = date_range('1/1/1999', periods=12, freq='M') high = Series(np.random.randn(len(idxh)), idxh) low = Series(np.random.randn(len(idxl)), idxl) - low.plot() - ax = high.plot(secondary_y=True) + _, ax = self.plt.subplots() + low.plot(ax=ax) + ax = high.plot(secondary_y=True, ax=ax) for l in ax.get_lines(): - self.assertEqual(PeriodIndex(l.get_xdata()).freq, 'D') - self.assertTrue(hasattr(ax, 'left_ax')) - self.assertFalse(hasattr(ax, 'right_ax')) + assert PeriodIndex(l.get_xdata()).freq == 'D' + assert hasattr(ax, 'left_ax') + assert not hasattr(ax, 'right_ax') for l in ax.left_ax.get_lines(): - self.assertEqual(PeriodIndex(l.get_xdata()).freq, 'D') + assert PeriodIndex(l.get_xdata()).freq == 'D' - @slow + @pytest.mark.slow def test_secondary_legend(self): - import matplotlib.pyplot as plt - fig = plt.gcf() - plt.clf() + fig = self.plt.figure() ax = fig.add_subplot(211) # ts df = tm.makeTimeDataFrame() - ax = df.plot(secondary_y=['A', 'B']) + df.plot(secondary_y=['A', 'B'], ax=ax) leg = ax.get_legend() - self.assertEqual(len(leg.get_lines()), 4) - self.assertEqual(leg.get_texts()[0].get_text(), 'A (right)') - self.assertEqual(leg.get_texts()[1].get_text(), 'B (right)') - self.assertEqual(leg.get_texts()[2].get_text(), 'C') - self.assertEqual(leg.get_texts()[3].get_text(), 'D') - self.assertIsNone(ax.right_ax.get_legend()) + assert len(leg.get_lines()) == 4 + assert leg.get_texts()[0].get_text() == 'A (right)' + assert leg.get_texts()[1].get_text() == 'B (right)' + assert leg.get_texts()[2].get_text() == 'C' + assert leg.get_texts()[3].get_text() == 'D' + assert ax.right_ax.get_legend() is None colors = set() for line in leg.get_lines(): colors.add(line.get_color()) # TODO: color cycle problems - self.assertEqual(len(colors), 4) + assert len(colors) == 4 + self.plt.close(fig) - plt.clf() + fig = self.plt.figure() ax = fig.add_subplot(211) - ax = df.plot(secondary_y=['A', 'C'], mark_right=False) + df.plot(secondary_y=['A', 'C'], mark_right=False, ax=ax) leg = ax.get_legend() - self.assertEqual(len(leg.get_lines()), 4) - self.assertEqual(leg.get_texts()[0].get_text(), 'A') - self.assertEqual(leg.get_texts()[1].get_text(), 'B') - self.assertEqual(leg.get_texts()[2].get_text(), 'C') - self.assertEqual(leg.get_texts()[3].get_text(), 'D') - - plt.clf() - ax = df.plot(kind='bar', secondary_y=['A']) + assert len(leg.get_lines()) == 4 + assert leg.get_texts()[0].get_text() == 'A' + assert leg.get_texts()[1].get_text() == 'B' + assert leg.get_texts()[2].get_text() == 'C' + assert leg.get_texts()[3].get_text() == 'D' + self.plt.close(fig) + + fig, ax = self.plt.subplots() + df.plot(kind='bar', secondary_y=['A'], ax=ax) leg = ax.get_legend() - self.assertEqual(leg.get_texts()[0].get_text(), 'A (right)') - self.assertEqual(leg.get_texts()[1].get_text(), 'B') + assert leg.get_texts()[0].get_text() == 'A (right)' + assert leg.get_texts()[1].get_text() == 'B' + self.plt.close(fig) - plt.clf() - ax = df.plot(kind='bar', secondary_y=['A'], mark_right=False) + fig, ax = self.plt.subplots() + df.plot(kind='bar', secondary_y=['A'], mark_right=False, ax=ax) leg = ax.get_legend() - self.assertEqual(leg.get_texts()[0].get_text(), 'A') - self.assertEqual(leg.get_texts()[1].get_text(), 'B') + assert leg.get_texts()[0].get_text() == 'A' + assert leg.get_texts()[1].get_text() == 'B' + self.plt.close(fig) - plt.clf() + fig = self.plt.figure() ax = fig.add_subplot(211) df = tm.makeTimeDataFrame() - ax = df.plot(secondary_y=['C', 'D']) + ax = df.plot(secondary_y=['C', 'D'], ax=ax) leg = ax.get_legend() - self.assertEqual(len(leg.get_lines()), 4) - self.assertIsNone(ax.right_ax.get_legend()) + assert len(leg.get_lines()) == 4 + assert ax.right_ax.get_legend() is None colors = set() for line in leg.get_lines(): colors.add(line.get_color()) # TODO: color cycle problems - self.assertEqual(len(colors), 4) + assert len(colors) == 4 + self.plt.close(fig) # non-ts df = tm.makeDataFrame() - plt.clf() + fig = self.plt.figure() ax = fig.add_subplot(211) - ax = df.plot(secondary_y=['A', 'B']) + ax = df.plot(secondary_y=['A', 'B'], ax=ax) leg = ax.get_legend() - self.assertEqual(len(leg.get_lines()), 4) - self.assertIsNone(ax.right_ax.get_legend()) + assert len(leg.get_lines()) == 4 + assert ax.right_ax.get_legend() is None colors = set() for line in leg.get_lines(): colors.add(line.get_color()) # TODO: color cycle problems - self.assertEqual(len(colors), 4) + assert len(colors) == 4 + self.plt.close() - plt.clf() + fig = self.plt.figure() ax = fig.add_subplot(211) - ax = df.plot(secondary_y=['C', 'D']) + ax = df.plot(secondary_y=['C', 'D'], ax=ax) leg = ax.get_legend() - self.assertEqual(len(leg.get_lines()), 4) - self.assertIsNone(ax.right_ax.get_legend()) + assert len(leg.get_lines()) == 4 + assert ax.right_ax.get_legend() is None colors = set() for line in leg.get_lines(): colors.add(line.get_color()) # TODO: color cycle problems - self.assertEqual(len(colors), 4) + assert len(colors) == 4 def test_format_date_axis(self): rng = date_range('1/1/2012', periods=12, freq='M') df = DataFrame(np.random.randn(len(rng), 3), rng) - ax = df.plot() + _, ax = self.plt.subplots() + ax = df.plot(ax=ax) xaxis = ax.get_xaxis() for l in xaxis.get_ticklabels(): if len(l.get_text()) > 0: - self.assertEqual(l.get_rotation(), 30) + assert l.get_rotation() == 30 - @slow + @pytest.mark.slow def test_ax_plot(self): - import matplotlib.pyplot as plt - - x = DatetimeIndex(start='2012-01-02', periods=10, freq='D') + x = date_range(start='2012-01-02', periods=10, freq='D') y = lrange(len(x)) - fig = plt.figure() - ax = fig.add_subplot(111) + _, ax = self.plt.subplots() lines = ax.plot(x, y, label='Y') tm.assert_index_equal(DatetimeIndex(lines[0].get_xdata()), x) - @slow + @pytest.mark.slow def test_mpl_nopandas(self): - import matplotlib.pyplot as plt - dates = [date(2008, 12, 31), date(2009, 1, 31)] values1 = np.arange(10.0, 11.0, 0.5) values2 = np.arange(11.0, 12.0, 0.5) kw = dict(fmt='-', lw=4) - plt.close('all') - fig = plt.figure() - ax = fig.add_subplot(111) + _, ax = self.plt.subplots() ax.plot_date([x.toordinal() for x in dates], values1, **kw) ax.plot_date([x.toordinal() for x in dates], values2, **kw) @@ -1185,22 +1302,23 @@ def test_mpl_nopandas(self): exp = np.array([x.toordinal() for x in dates], dtype=np.float64) tm.assert_numpy_array_equal(line2.get_xydata()[:, 0], exp) - @slow + @pytest.mark.slow def test_irregular_ts_shared_ax_xlim(self): # GH 2960 ts = tm.makeTimeSeries()[:20] ts_irregular = ts[[1, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 17, 18]] # plot the left section of the irregular series, then the right section - ax = ts_irregular[:5].plot() + _, ax = self.plt.subplots() + ts_irregular[:5].plot(ax=ax) ts_irregular[5:].plot(ax=ax) # check that axis limits are correct left, right = ax.get_xlim() - self.assertEqual(left, ts_irregular.index.min().toordinal()) - self.assertEqual(right, ts_irregular.index.max().toordinal()) + assert left <= ts_irregular.index.min().toordinal() + assert right >= ts_irregular.index.max().toordinal() - @slow + @pytest.mark.slow def test_secondary_y_non_ts_xlim(self): # GH 3490 - non-timeseries with secondary y index_1 = [1, 2, 3, 4] @@ -1208,15 +1326,16 @@ def test_secondary_y_non_ts_xlim(self): s1 = Series(1, index=index_1) s2 = Series(2, index=index_2) - ax = s1.plot() + _, ax = self.plt.subplots() + s1.plot(ax=ax) left_before, right_before = ax.get_xlim() s2.plot(secondary_y=True, ax=ax) left_after, right_after = ax.get_xlim() - self.assertEqual(left_before, left_after) - self.assertTrue(right_before < right_after) + assert left_before >= left_after + assert right_before < right_after - @slow + @pytest.mark.slow def test_secondary_y_regular_ts_xlim(self): # GH 3490 - regular-timeseries with secondary y index_1 = date_range(start='2000-01-01', periods=4, freq='D') @@ -1224,76 +1343,81 @@ def test_secondary_y_regular_ts_xlim(self): s1 = Series(1, index=index_1) s2 = Series(2, index=index_2) - ax = s1.plot() + _, ax = self.plt.subplots() + s1.plot(ax=ax) left_before, right_before = ax.get_xlim() s2.plot(secondary_y=True, ax=ax) left_after, right_after = ax.get_xlim() - self.assertEqual(left_before, left_after) - self.assertTrue(right_before < right_after) + assert left_before >= left_after + assert right_before < right_after - @slow + @pytest.mark.slow def test_secondary_y_mixed_freq_ts_xlim(self): # GH 3490 - mixed frequency timeseries with secondary y rng = date_range('2000-01-01', periods=10000, freq='min') ts = Series(1, index=rng) - ax = ts.plot() + _, ax = self.plt.subplots() + ts.plot(ax=ax) left_before, right_before = ax.get_xlim() ts.resample('D').mean().plot(secondary_y=True, ax=ax) left_after, right_after = ax.get_xlim() # a downsample should not have changed either limit - self.assertEqual(left_before, left_after) - self.assertEqual(right_before, right_after) + assert left_before == left_after + assert right_before == right_after - @slow + @pytest.mark.slow def test_secondary_y_irregular_ts_xlim(self): # GH 3490 - irregular-timeseries with secondary y ts = tm.makeTimeSeries()[:20] ts_irregular = ts[[1, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 17, 18]] - ax = ts_irregular[:5].plot() + _, ax = self.plt.subplots() + ts_irregular[:5].plot(ax=ax) # plot higher-x values on secondary axis ts_irregular[5:].plot(secondary_y=True, ax=ax) # ensure secondary limits aren't overwritten by plot on primary ts_irregular[:5].plot(ax=ax) left, right = ax.get_xlim() - self.assertEqual(left, ts_irregular.index.min().toordinal()) - self.assertEqual(right, ts_irregular.index.max().toordinal()) + assert left <= ts_irregular.index.min().toordinal() + assert right >= ts_irregular.index.max().toordinal() def test_plot_outofbounds_datetime(self): # 2579 - checking this does not raise values = [date(1677, 1, 1), date(1677, 1, 2)] - self.plt.plot(values) + _, ax = self.plt.subplots() + ax.plot(values) values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)] - self.plt.plot(values) + ax.plot(values) def test_format_timedelta_ticks_narrow(self): - if is_platform_mac(): - pytest.skip("skip on mac for precision display issue on older mpl") - expected_labels = [ - '00:00:00.00000000{:d}'.format(i) - for i in range(10)] + if self.mpl_ge_2_0_1: + expected_labels = (['00:00:00.0000000{:0>2d}'.format(i) + for i in range(10)]) + else: # 2.0.0 + expected_labels = [''] + [ + '00:00:00.00000000{:d}'.format(2 * i) + for i in range(5)] + [''] rng = timedelta_range('0', periods=10, freq='ns') df = DataFrame(np.random.randn(len(rng), 3), rng) - ax = df.plot(fontsize=2) - fig = ax.get_figure() + fig, ax = self.plt.subplots() + df.plot(fontsize=2, ax=ax) fig.canvas.draw() labels = ax.get_xticklabels() - self.assertEqual(len(labels), len(expected_labels)) - for l, l_expected in zip(labels, expected_labels): - self.assertEqual(l.get_text(), l_expected) - def test_format_timedelta_ticks_wide(self): - if is_platform_mac(): - pytest.skip("skip on mac for precision display issue on older mpl") + result_labels = [x.get_text() for x in labels] + assert len(result_labels) == len(expected_labels) + assert result_labels == expected_labels + def test_format_timedelta_ticks_wide(self): expected_labels = [ + '', '00:00:00', '1 days 03:46:40', '2 days 07:33:20', @@ -1302,35 +1426,99 @@ def test_format_timedelta_ticks_wide(self): '5 days 18:53:20', '6 days 22:40:00', '8 days 02:26:40', + '9 days 06:13:20', '' ] + if self.mpl_ge_2_2_0: + expected_labels = expected_labels[1:-1] + elif self.mpl_ge_2_0_1: + expected_labels = expected_labels[1:-1] + expected_labels[-1] = '' rng = timedelta_range('0', periods=10, freq='1 d') df = DataFrame(np.random.randn(len(rng), 3), rng) - ax = df.plot(fontsize=2) - fig = ax.get_figure() + fig, ax = self.plt.subplots() + ax = df.plot(fontsize=2, ax=ax) fig.canvas.draw() labels = ax.get_xticklabels() - self.assertEqual(len(labels), len(expected_labels)) - for l, l_expected in zip(labels, expected_labels): - self.assertEqual(l.get_text(), l_expected) + + result_labels = [x.get_text() for x in labels] + assert len(result_labels) == len(expected_labels) + assert result_labels == expected_labels def test_timedelta_plot(self): # test issue #8711 s = Series(range(5), timedelta_range('1day', periods=5)) - _check_plot_works(s.plot) + _, ax = self.plt.subplots() + _check_plot_works(s.plot, ax=ax) # test long period index = timedelta_range('1 day 2 hr 30 min 10 s', periods=10, freq='1 d') s = Series(np.random.randn(len(index)), index) - _check_plot_works(s.plot) + _, ax = self.plt.subplots() + _check_plot_works(s.plot, ax=ax) # test short period index = timedelta_range('1 day 2 hr 30 min 10 s', periods=10, freq='1 ns') s = Series(np.random.randn(len(index)), index) - _check_plot_works(s.plot) + _, ax = self.plt.subplots() + _check_plot_works(s.plot, ax=ax) + + def test_hist(self): + # https://github.com/matplotlib/matplotlib/issues/8459 + rng = date_range('1/1/2011', periods=10, freq='H') + x = rng + w1 = np.arange(0, 1, .1) + w2 = np.arange(0, 1, .1)[::-1] + _, ax = self.plt.subplots() + ax.hist([x, x], weights=[w1, w2]) + + @pytest.mark.slow + def test_overlapping_datetime(self): + # GB 6608 + s1 = Series([1, 2, 3], index=[datetime(1995, 12, 31), + datetime(2000, 12, 31), + datetime(2005, 12, 31)]) + s2 = Series([1, 2, 3], index=[datetime(1997, 12, 31), + datetime(2003, 12, 31), + datetime(2008, 12, 31)]) + + # plot first series, then add the second series to those axes, + # then try adding the first series again + _, ax = self.plt.subplots() + s1.plot(ax=ax) + s2.plot(ax=ax) + s1.plot(ax=ax) + + @pytest.mark.xfail(reason="GH9053 matplotlib does not use" + " ax.xaxis.converter") + def test_add_matplotlib_datetime64(self): + # GH9053 - ensure that a plot with PeriodConverter still understands + # datetime64 data. This still fails because matplotlib overrides the + # ax.xaxis.converter with a DatetimeConverter + s = Series(np.random.randn(10), + index=date_range('1970-01-02', periods=10)) + ax = s.plot() + ax.plot(s.index, s.values, color='g') + l1, l2 = ax.lines + tm.assert_numpy_array_equal(l1.get_xydata(), l2.get_xydata()) + + def test_matplotlib_scatter_datetime64(self): + # https://github.com/matplotlib/matplotlib/issues/11391 + df = DataFrame(np.random.RandomState(0).rand(10, 2), + columns=["x", "y"]) + df["time"] = date_range("2018-01-01", periods=10, freq="D") + fig, ax = self.plt.subplots() + ax.scatter(x="time", y="y", data=df) + fig.canvas.draw() + label = ax.get_xticklabels()[0] + if self.mpl_ge_3_0_0: + expected = "2017-12-08" + else: + expected = "2017-12-12" + assert label.get_text() == expected def _check_plot_works(f, freq=None, series=None, *args, **kwargs): @@ -1368,5 +1556,15 @@ def _check_plot_works(f, freq=None, series=None, *args, **kwargs): with ensure_clean(return_filelike=True) as path: plt.savefig(path) + + # GH18439 + # this is supported only in Python 3 pickle since + # pickle in Python2 doesn't support instancemethod pickling + # TODO(statsmodels 0.10.0): Remove the statsmodels check + # https://github.com/pandas-dev/pandas/issues/24088 + # https://github.com/statsmodels/statsmodels/issues/4772 + if PY3 and 'statsmodels' not in sys.modules: + with ensure_clean(return_filelike=True) as path: + pickle.dump(fig, path) finally: plt.close(fig) diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index 48af366f24ea4..98b241f5c8206 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -2,37 +2,36 @@ """ Test cases for DataFrame.plot """ -import pytest +from datetime import date, datetime import string import warnings -from datetime import datetime, date +import numpy as np +from numpy.random import rand, randn +import pytest -import pandas as pd -from pandas import (Series, DataFrame, MultiIndex, PeriodIndex, date_range, - bdate_range) -from pandas.types.api import is_list_like -from pandas.compat import (range, lrange, StringIO, lmap, lzip, u, zip, PY3) -from pandas.formats.printing import pprint_thing -import pandas.util.testing as tm -from pandas.util.testing import slow +from pandas.compat import PY3, lmap, lrange, lzip, range, u, zip +import pandas.util._test_decorators as td -from pandas.core.config import set_option +from pandas.core.dtypes.api import is_list_like -import numpy as np -from numpy.random import rand, randn +import pandas as pd +from pandas import ( + DataFrame, MultiIndex, PeriodIndex, Series, bdate_range, date_range) +from pandas.tests.plotting.common import ( + TestPlotBase, _check_plot_works, _ok_for_gaussian_kde, + _skip_if_no_scipy_gaussian_kde) +import pandas.util.testing as tm -import pandas.tools.plotting as plotting -from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works, - _skip_if_no_scipy_gaussian_kde, - _ok_for_gaussian_kde) +from pandas.io.formats.printing import pprint_thing +import pandas.plotting as plotting -@tm.mplskip +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): - def setUp(self): - TestPlotBase.setUp(self) + def setup_method(self, method): + TestPlotBase.setup_method(self, method) import matplotlib as mpl mpl.rcdefaults() @@ -42,7 +41,15 @@ def setUp(self): "C": np.arange(20) + np.random.uniform( size=20)}) - @slow + def _assert_ytickslabels_visibility(self, axes, expected): + for ax, exp in zip(axes, expected): + self._check_visible(ax.get_yticklabels(), visible=exp) + + def _assert_xtickslabels_visibility(self, axes, expected): + for ax, exp in zip(axes, expected): + self._check_visible(ax.get_xticklabels(), visible=exp) + + @pytest.mark.slow def test_plot(self): df = self.tdf _check_plot_works(df.plot, grid=False) @@ -63,8 +70,7 @@ def test_plot(self): self._check_axes_shape(axes, axes_num=4, layout=(4, 1)) df = DataFrame({'x': [1, 2], 'y': [3, 4]}) - # mpl >= 1.5.2 (or slightly below) throw AttributError - with tm.assertRaises((TypeError, AttributeError)): + with pytest.raises(AttributeError, match='Unknown property blarg'): df.plot.line(blarg=True) df = DataFrame(np.random.rand(10, 3), @@ -134,12 +140,32 @@ def test_plot(self): # passed ax should be used: fig, ax = self.plt.subplots() axes = df.plot.bar(subplots=True, ax=ax) - self.assertEqual(len(axes), 1) - if self.mpl_ge_1_5_0: - result = ax.axes - else: - result = ax.get_axes() # deprecated - self.assertIs(result, axes[0]) + assert len(axes) == 1 + result = ax.axes + assert result is axes[0] + + # GH 15516 + def test_mpl2_color_cycle_str(self): + colors = ['C' + str(x) for x in range(10)] + df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) + for c in colors: + _check_plot_works(df.plot, color=c) + + def test_color_single_series_list(self): + # GH 3486 + df = DataFrame({"A": [1, 2, 3]}) + _check_plot_works(df.plot, color=['red']) + + def test_rgb_tuple_color(self): + # GH 16695 + df = DataFrame({'x': [1, 2], 'y': [3, 4]}) + _check_plot_works(df.plot, x='x', y='y', color=(1, 0, 0)) + _check_plot_works(df.plot, x='x', y='y', color=(1, 0, 0, 0.5)) + + def test_color_empty_string(self): + df = DataFrame(randn(10, 2)) + with pytest.raises(ValueError): + df.plot(color='') def test_color_and_style_arguments(self): df = DataFrame({'x': [1, 2], 'y': [3, 4]}) @@ -148,35 +174,35 @@ def test_color_and_style_arguments(self): ax = df.plot(color=['red', 'black'], style=['-', '--']) # check that the linestyles are correctly set: linestyle = [line.get_linestyle() for line in ax.lines] - self.assertEqual(linestyle, ['-', '--']) + assert linestyle == ['-', '--'] # check that the colors are correctly set: color = [line.get_color() for line in ax.lines] - self.assertEqual(color, ['red', 'black']) + assert color == ['red', 'black'] # passing both 'color' and 'style' arguments should not be allowed # if there is a color symbol in the style strings: - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot(color=['red', 'black'], style=['k-', 'r--']) def test_nonnumeric_exclude(self): df = DataFrame({'A': ["x", "y", "z"], 'B': [1, 2, 3]}) ax = df.plot() - self.assertEqual(len(ax.get_lines()), 1) # B was plotted + assert len(ax.get_lines()) == 1 # B was plotted - @slow + @pytest.mark.slow def test_implicit_label(self): df = DataFrame(randn(10, 3), columns=['a', 'b', 'c']) ax = df.plot(x='a', y='b') self._check_text_labels(ax.xaxis.get_label(), 'a') - @slow + @pytest.mark.slow def test_donot_overwrite_index_name(self): # GH 8494 df = DataFrame(randn(2, 2), columns=['a', 'b']) df.index.name = 'NAME' df.plot(y='b', label='LABEL') - self.assertEqual(df.index.name, 'NAME') + assert df.index.name == 'NAME' - @slow + @pytest.mark.slow def test_plot_xy(self): # columns.inferred_type == 'string' df = self.tdf @@ -202,7 +228,7 @@ def test_plot_xy(self): # columns.inferred_type == 'mixed' # TODO add MultiIndex test - @slow + @pytest.mark.slow def test_logscales(self): df = DataFrame({'a': np.arange(100)}, index=np.arange(100)) ax = df.plot(logy=True) @@ -214,40 +240,41 @@ def test_logscales(self): ax = df.plot(loglog=True) self._check_ax_scales(ax, xaxis='log', yaxis='log') - @slow + @pytest.mark.slow def test_xcompat(self): import pandas as pd df = self.tdf ax = df.plot(x_compat=True) lines = ax.get_lines() - self.assertNotIsInstance(lines[0].get_xdata(), PeriodIndex) + assert not isinstance(lines[0].get_xdata(), PeriodIndex) tm.close() - pd.plot_params['xaxis.compat'] = True + pd.plotting.plot_params['xaxis.compat'] = True ax = df.plot() lines = ax.get_lines() - self.assertNotIsInstance(lines[0].get_xdata(), PeriodIndex) + assert not isinstance(lines[0].get_xdata(), PeriodIndex) tm.close() - pd.plot_params['x_compat'] = False + pd.plotting.plot_params['x_compat'] = False + ax = df.plot() lines = ax.get_lines() - self.assertNotIsInstance(lines[0].get_xdata(), PeriodIndex) - self.assertIsInstance(PeriodIndex(lines[0].get_xdata()), PeriodIndex) + assert not isinstance(lines[0].get_xdata(), PeriodIndex) + assert isinstance(PeriodIndex(lines[0].get_xdata()), PeriodIndex) tm.close() # useful if you're plotting a bunch together - with pd.plot_params.use('x_compat', True): + with pd.plotting.plot_params.use('x_compat', True): ax = df.plot() lines = ax.get_lines() - self.assertNotIsInstance(lines[0].get_xdata(), PeriodIndex) + assert not isinstance(lines[0].get_xdata(), PeriodIndex) tm.close() ax = df.plot() lines = ax.get_lines() - self.assertNotIsInstance(lines[0].get_xdata(), PeriodIndex) - self.assertIsInstance(PeriodIndex(lines[0].get_xdata()), PeriodIndex) + assert not isinstance(lines[0].get_xdata(), PeriodIndex) + assert isinstance(PeriodIndex(lines[0].get_xdata()), PeriodIndex) def test_period_compat(self): # GH 9012 @@ -265,20 +292,43 @@ def test_unsorted_index(self): df = DataFrame({'y': np.arange(100)}, index=np.arange(99, -1, -1), dtype=np.int64) ax = df.plot() - l = ax.get_lines()[0] - rs = l.get_xydata() + lines = ax.get_lines()[0] + rs = lines.get_xydata() rs = Series(rs[:, 1], rs[:, 0], dtype=np.int64, name='y') tm.assert_series_equal(rs, df.y, check_index_type=False) tm.close() df.index = pd.Index(np.arange(99, -1, -1), dtype=np.float64) ax = df.plot() - l = ax.get_lines()[0] - rs = l.get_xydata() + lines = ax.get_lines()[0] + rs = lines.get_xydata() rs = Series(rs[:, 1], rs[:, 0], dtype=np.int64, name='y') tm.assert_series_equal(rs, df.y) - @slow + def test_unsorted_index_lims(self): + df = DataFrame({'y': [0., 1., 2., 3.]}, index=[1., 0., 3., 2.]) + ax = df.plot() + xmin, xmax = ax.get_xlim() + lines = ax.get_lines() + assert xmin <= np.nanmin(lines[0].get_data()[0]) + assert xmax >= np.nanmax(lines[0].get_data()[0]) + + df = DataFrame({'y': [0., 1., np.nan, 3., 4., 5., 6.]}, + index=[1., 0., 3., 2., np.nan, 3., 2.]) + ax = df.plot() + xmin, xmax = ax.get_xlim() + lines = ax.get_lines() + assert xmin <= np.nanmin(lines[0].get_data()[0]) + assert xmax >= np.nanmax(lines[0].get_data()[0]) + + df = DataFrame({'y': [0., 1., 2., 3.], 'z': [91., 90., 93., 92.]}) + ax = df.plot(x='z', y='y') + xmin, xmax = ax.get_xlim() + lines = ax.get_lines() + assert xmin <= np.nanmin(lines[0].get_data()[0]) + assert xmax >= np.nanmax(lines[0].get_data()[0]) + + @pytest.mark.slow def test_subplots(self): df = DataFrame(np.random.rand(10, 3), index=list(string.ascii_letters[:10])) @@ -286,7 +336,7 @@ def test_subplots(self): for kind in ['bar', 'barh', 'line', 'area']: axes = df.plot(kind=kind, subplots=True, sharex=True, legend=True) self._check_axes_shape(axes, axes_num=3, layout=(3, 1)) - self.assertEqual(axes.shape, (3, )) + assert axes.shape == (3, ) for ax, column in zip(axes, df.columns): self._check_legend_labels(ax, @@ -316,9 +366,60 @@ def test_subplots(self): axes = df.plot(kind=kind, subplots=True, legend=False) for ax in axes: - self.assertTrue(ax.get_legend() is None) - - @slow + assert ax.get_legend() is None + + def test_groupby_boxplot_sharey(self): + # https://github.com/pandas-dev/pandas/issues/20968 + # sharey can now be switched check whether the right + # pair of axes is turned on or off + + df = DataFrame({'a': [-1.43, -0.15, -3.70, -1.43, -0.14], + 'b': [0.56, 0.84, 0.29, 0.56, 0.85], + 'c': [0, 1, 2, 3, 1]}, + index=[0, 1, 2, 3, 4]) + + # behavior without keyword + axes = df.groupby('c').boxplot() + expected = [True, False, True, False] + self._assert_ytickslabels_visibility(axes, expected) + + # set sharey=True should be identical + axes = df.groupby('c').boxplot(sharey=True) + expected = [True, False, True, False] + self._assert_ytickslabels_visibility(axes, expected) + + # sharey=False, all yticklabels should be visible + axes = df.groupby('c').boxplot(sharey=False) + expected = [True, True, True, True] + self._assert_ytickslabels_visibility(axes, expected) + + def test_groupby_boxplot_sharex(self): + # https://github.com/pandas-dev/pandas/issues/20968 + # sharex can now be switched check whether the right + # pair of axes is turned on or off + + df = DataFrame({'a': [-1.43, -0.15, -3.70, -1.43, -0.14], + 'b': [0.56, 0.84, 0.29, 0.56, 0.85], + 'c': [0, 1, 2, 3, 1]}, + index=[0, 1, 2, 3, 4]) + + # behavior without keyword + axes = df.groupby('c').boxplot() + expected = [True, True, True, True] + self._assert_xtickslabels_visibility(axes, expected) + + # set sharex=False should be identical + axes = df.groupby('c').boxplot(sharex=False) + expected = [True, True, True, True] + self._assert_xtickslabels_visibility(axes, expected) + + # sharex=True, yticklabels should be visible + # only for bottom plots + axes = df.groupby('c').boxplot(sharex=True) + expected = [False, False, True, True] + self._assert_xtickslabels_visibility(axes, expected) + + @pytest.mark.slow def test_subplots_timeseries(self): idx = date_range(start='2014-07-01', freq='M', periods=10) df = DataFrame(np.random.rand(10, 3), index=idx) @@ -354,7 +455,83 @@ def test_subplots_timeseries(self): self._check_ticks_props(ax, xlabelsize=7, xrot=45, ylabelsize=7) - @slow + def test_subplots_timeseries_y_axis(self): + # GH16953 + data = {"numeric": np.array([1, 2, 5]), + "timedelta": [pd.Timedelta(-10, unit="s"), + pd.Timedelta(10, unit="m"), + pd.Timedelta(10, unit="h")], + "datetime_no_tz": [pd.to_datetime("2017-08-01 00:00:00"), + pd.to_datetime("2017-08-01 02:00:00"), + pd.to_datetime("2017-08-02 00:00:00")], + "datetime_all_tz": [pd.to_datetime("2017-08-01 00:00:00", + utc=True), + pd.to_datetime("2017-08-01 02:00:00", + utc=True), + pd.to_datetime("2017-08-02 00:00:00", + utc=True)], + "text": ["This", "should", "fail"]} + testdata = DataFrame(data) + + ax_numeric = testdata.plot(y="numeric") + assert (ax_numeric.get_lines()[0].get_data()[1] == + testdata["numeric"].values).all() + ax_timedelta = testdata.plot(y="timedelta") + assert (ax_timedelta.get_lines()[0].get_data()[1] == + testdata["timedelta"].values).all() + ax_datetime_no_tz = testdata.plot(y="datetime_no_tz") + assert (ax_datetime_no_tz.get_lines()[0].get_data()[1] == + testdata["datetime_no_tz"].values).all() + ax_datetime_all_tz = testdata.plot(y="datetime_all_tz") + assert (ax_datetime_all_tz.get_lines()[0].get_data()[1] == + testdata["datetime_all_tz"].values).all() + with pytest.raises(TypeError): + testdata.plot(y="text") + + @pytest.mark.xfail(reason='not support for period, categorical, ' + 'datetime_mixed_tz') + def test_subplots_timeseries_y_axis_not_supported(self): + """ + This test will fail for: + period: + since period isn't yet implemented in ``select_dtypes`` + and because it will need a custom value converter + + tick formater (as was done for x-axis plots) + + categorical: + because it will need a custom value converter + + tick formater (also doesn't work for x-axis, as of now) + + datetime_mixed_tz: + because of the way how pandas handels ``Series`` of + ``datetime`` objects with different timezone, + generally converting ``datetime`` objects in a tz-aware + form could help with this problem + """ + data = {"numeric": np.array([1, 2, 5]), + "period": [pd.Period('2017-08-01 00:00:00', freq='H'), + pd.Period('2017-08-01 02:00', freq='H'), + pd.Period('2017-08-02 00:00:00', freq='H')], + "categorical": pd.Categorical(["c", "b", "a"], + categories=["a", "b", "c"], + ordered=False), + "datetime_mixed_tz": [pd.to_datetime("2017-08-01 00:00:00", + utc=True), + pd.to_datetime("2017-08-01 02:00:00"), + pd.to_datetime("2017-08-02 00:00:00")]} + testdata = pd.DataFrame(data) + ax_period = testdata.plot(x="numeric", y="period") + assert (ax_period.get_lines()[0].get_data()[1] == + testdata["period"].values).all() + ax_categorical = testdata.plot(x="numeric", y="categorical") + assert (ax_categorical.get_lines()[0].get_data()[1] == + testdata["categorical"].values).all() + ax_datetime_mixed_tz = testdata.plot(x="numeric", + y="datetime_mixed_tz") + assert (ax_datetime_mixed_tz.get_lines()[0].get_data()[1] == + testdata["datetime_mixed_tz"].values).all() + + @pytest.mark.slow def test_subplots_layout(self): # GH 6667 df = DataFrame(np.random.rand(10, 3), @@ -362,60 +539,56 @@ def test_subplots_layout(self): axes = df.plot(subplots=True, layout=(2, 2)) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) - self.assertEqual(axes.shape, (2, 2)) + assert axes.shape == (2, 2) axes = df.plot(subplots=True, layout=(-1, 2)) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) - self.assertEqual(axes.shape, (2, 2)) + assert axes.shape == (2, 2) axes = df.plot(subplots=True, layout=(2, -1)) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) - self.assertEqual(axes.shape, (2, 2)) + assert axes.shape == (2, 2) axes = df.plot(subplots=True, layout=(1, 4)) self._check_axes_shape(axes, axes_num=3, layout=(1, 4)) - self.assertEqual(axes.shape, (1, 4)) + assert axes.shape == (1, 4) axes = df.plot(subplots=True, layout=(-1, 4)) self._check_axes_shape(axes, axes_num=3, layout=(1, 4)) - self.assertEqual(axes.shape, (1, 4)) + assert axes.shape == (1, 4) axes = df.plot(subplots=True, layout=(4, -1)) self._check_axes_shape(axes, axes_num=3, layout=(4, 1)) - self.assertEqual(axes.shape, (4, 1)) + assert axes.shape == (4, 1) - with tm.assertRaises(ValueError): - axes = df.plot(subplots=True, layout=(1, 1)) - with tm.assertRaises(ValueError): - axes = df.plot(subplots=True, layout=(-1, -1)) + with pytest.raises(ValueError): + df.plot(subplots=True, layout=(1, 1)) + with pytest.raises(ValueError): + df.plot(subplots=True, layout=(-1, -1)) # single column df = DataFrame(np.random.rand(10, 1), index=list(string.ascii_letters[:10])) axes = df.plot(subplots=True) self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) - self.assertEqual(axes.shape, (1, )) + assert axes.shape == (1, ) axes = df.plot(subplots=True, layout=(3, 3)) self._check_axes_shape(axes, axes_num=1, layout=(3, 3)) - self.assertEqual(axes.shape, (3, 3)) + assert axes.shape == (3, 3) - @slow + @pytest.mark.slow def test_subplots_warnings(self): # GH 9464 - warnings.simplefilter('error') - try: + with tm.assert_produces_warning(None): df = DataFrame(np.random.randn(100, 4)) df.plot(subplots=True, layout=(3, 2)) df = DataFrame(np.random.randn(100, 4), index=date_range('1/1/2000', periods=100)) df.plot(subplots=True, layout=(3, 2)) - except Warning as w: - self.fail(w) - warnings.simplefilter('default') - @slow + @pytest.mark.slow def test_subplots_multiple_axes(self): # GH 5353, 6970, GH 7069 fig, axes = self.plt.subplots(2, 3) @@ -425,18 +598,18 @@ def test_subplots_multiple_axes(self): returned = df.plot(subplots=True, ax=axes[0], sharex=False, sharey=False) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assertEqual(returned.shape, (3, )) - self.assertIs(returned[0].figure, fig) + assert returned.shape == (3, ) + assert returned[0].figure is fig # draw on second row returned = df.plot(subplots=True, ax=axes[1], sharex=False, sharey=False) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assertEqual(returned.shape, (3, )) - self.assertIs(returned[0].figure, fig) + assert returned.shape == (3, ) + assert returned[0].figure is fig self._check_axes_shape(axes, axes_num=6, layout=(2, 3)) tm.close() - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): fig, axes = self.plt.subplots(2, 3) # pass different number of axes from required df.plot(subplots=True, ax=axes) @@ -447,24 +620,24 @@ def test_subplots_multiple_axes(self): # TestDataFrameGroupByPlots.test_grouped_box_multiple_axes fig, axes = self.plt.subplots(2, 2) with warnings.catch_warnings(): - warnings.simplefilter('ignore') + warnings.simplefilter("ignore", UserWarning) df = DataFrame(np.random.rand(10, 4), index=list(string.ascii_letters[:10])) returned = df.plot(subplots=True, ax=axes, layout=(2, 1), sharex=False, sharey=False) self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) - self.assertEqual(returned.shape, (4, )) + assert returned.shape == (4, ) returned = df.plot(subplots=True, ax=axes, layout=(2, -1), sharex=False, sharey=False) self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) - self.assertEqual(returned.shape, (4, )) + assert returned.shape == (4, ) returned = df.plot(subplots=True, ax=axes, layout=(-1, 2), sharex=False, sharey=False) self._check_axes_shape(returned, axes_num=4, layout=(2, 2)) - self.assertEqual(returned.shape, (4, )) + assert returned.shape == (4, ) # single column fig, axes = self.plt.subplots(1, 1) @@ -473,7 +646,7 @@ def test_subplots_multiple_axes(self): axes = df.plot(subplots=True, ax=[axes], sharex=False, sharey=False) self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) - self.assertEqual(axes.shape, (1, )) + assert axes.shape == (1, ) def test_subplots_ts_share_axes(self): # GH 3964 @@ -516,44 +689,44 @@ def test_subplots_sharex_axes_existing_axes(self): for ax in axes.ravel(): self._check_visible(ax.get_yticklabels(), visible=True) - @slow + @pytest.mark.slow def test_subplots_dup_columns(self): # GH 10962 df = DataFrame(np.random.rand(5, 5), columns=list('aaaaa')) axes = df.plot(subplots=True) for ax in axes: self._check_legend_labels(ax, labels=['a']) - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 tm.close() axes = df.plot(subplots=True, secondary_y='a') for ax in axes: # (right) is only attached when subplots=False self._check_legend_labels(ax, labels=['a']) - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 tm.close() ax = df.plot(secondary_y='a') self._check_legend_labels(ax, labels=['a (right)'] * 5) - self.assertEqual(len(ax.lines), 0) - self.assertEqual(len(ax.right_ax.lines), 5) + assert len(ax.lines) == 0 + assert len(ax.right_ax.lines) == 5 def test_negative_log(self): df = - DataFrame(rand(6, 4), index=list(string.ascii_letters[:6]), columns=['x', 'y', 'z', 'four']) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot.area(logy=True) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot.area(loglog=True) def _compare_stacked_y_cood(self, normal_lines, stacked_lines): base = np.zeros(len(normal_lines[0].get_data()[1])) for nl, sl in zip(normal_lines, stacked_lines): - base += nl.get_data()[1] # get y coodinates + base += nl.get_data()[1] # get y coordinates sy = sl.get_data()[1] - self.assert_numpy_array_equal(base, sy) + tm.assert_numpy_array_equal(base, sy) def test_line_area_stacked(self): with tm.RNGContext(42): @@ -584,7 +757,7 @@ def test_line_area_stacked(self): self._compare_stacked_y_cood(ax1.lines[2:], ax2.lines[2:]) _check_plot_works(mixed_df.plot, stacked=False) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): mixed_df.plot(stacked=True) _check_plot_works(df.plot, kind=kind, logx=True, stacked=True) @@ -603,55 +776,55 @@ def test_line_area_nan_df(self): # remove nan for comparison purpose exp = np.array([1, 2, 3], dtype=np.float64) - self.assert_numpy_array_equal(np.delete(masked1.data, 2), exp) + tm.assert_numpy_array_equal(np.delete(masked1.data, 2), exp) exp = np.array([3, 2, 1], dtype=np.float64) - self.assert_numpy_array_equal(np.delete(masked2.data, 1), exp) - self.assert_numpy_array_equal( + tm.assert_numpy_array_equal(np.delete(masked2.data, 1), exp) + tm.assert_numpy_array_equal( masked1.mask, np.array([False, False, True, False])) - self.assert_numpy_array_equal( + tm.assert_numpy_array_equal( masked2.mask, np.array([False, True, False, False])) expected1 = np.array([1, 2, 0, 3], dtype=np.float64) expected2 = np.array([3, 0, 2, 1], dtype=np.float64) ax = _check_plot_works(d.plot, stacked=True) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) - self.assert_numpy_array_equal(ax.lines[1].get_ydata(), - expected1 + expected2) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) + tm.assert_numpy_array_equal(ax.lines[1].get_ydata(), + expected1 + expected2) ax = _check_plot_works(d.plot.area) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) - self.assert_numpy_array_equal(ax.lines[1].get_ydata(), - expected1 + expected2) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) + tm.assert_numpy_array_equal(ax.lines[1].get_ydata(), + expected1 + expected2) ax = _check_plot_works(d.plot.area, stacked=False) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) - self.assert_numpy_array_equal(ax.lines[1].get_ydata(), expected2) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected1) + tm.assert_numpy_array_equal(ax.lines[1].get_ydata(), expected2) def test_line_lim(self): df = DataFrame(rand(6, 3), columns=['x', 'y', 'z']) ax = df.plot() xmin, xmax = ax.get_xlim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data()[0][0]) - self.assertEqual(xmax, lines[0].get_data()[0][-1]) + assert xmin <= lines[0].get_data()[0][0] + assert xmax >= lines[0].get_data()[0][-1] ax = df.plot(secondary_y=True) xmin, xmax = ax.get_xlim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data()[0][0]) - self.assertEqual(xmax, lines[0].get_data()[0][-1]) + assert xmin <= lines[0].get_data()[0][0] + assert xmax >= lines[0].get_data()[0][-1] axes = df.plot(secondary_y=True, subplots=True) self._check_axes_shape(axes, axes_num=3, layout=(3, 1)) for ax in axes: - self.assertTrue(hasattr(ax, 'left_ax')) - self.assertFalse(hasattr(ax, 'right_ax')) + assert hasattr(ax, 'left_ax') + assert not hasattr(ax, 'right_ax') xmin, xmax = ax.get_xlim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data()[0][0]) - self.assertEqual(xmax, lines[0].get_data()[0][-1]) + assert xmin <= lines[0].get_data()[0][0] + assert xmax >= lines[0].get_data()[0][-1] def test_area_lim(self): df = DataFrame(rand(6, 4), columns=['x', 'y', 'z', 'four']) @@ -662,18 +835,18 @@ def test_area_lim(self): xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data()[0][0]) - self.assertEqual(xmax, lines[0].get_data()[0][-1]) - self.assertEqual(ymin, 0) + assert xmin <= lines[0].get_data()[0][0] + assert xmax >= lines[0].get_data()[0][-1] + assert ymin == 0 ax = _check_plot_works(neg_df.plot.area, stacked=stacked) ymin, ymax = ax.get_ylim() - self.assertEqual(ymax, 0) + assert ymax == 0 - @slow + @pytest.mark.slow def test_bar_colors(self): import matplotlib.pyplot as plt - default_colors = self._maybe_unpack_cycler(plt.rcParams) + default_colors = self._unpack_cycler(plt.rcParams) df = DataFrame(randn(5, 5)) ax = df.plot.bar() @@ -706,28 +879,42 @@ def test_bar_colors(self): self._check_colors(ax.patches[::5], facecolors=['green'] * 5) tm.close() - @slow + def test_bar_user_colors(self): + df = pd.DataFrame({"A": range(4), + "B": range(1, 5), + "color": ['red', 'blue', 'blue', 'red']}) + # This should *only* work when `y` is specified, else + # we use one color per column + ax = df.plot.bar(y='A', color=df['color']) + result = [p.get_facecolor() for p in ax.patches] + expected = [(1., 0., 0., 1.), + (0., 0., 1., 1.), + (0., 0., 1., 1.), + (1., 0., 0., 1.)] + assert result == expected + + @pytest.mark.slow def test_bar_linewidth(self): df = DataFrame(randn(5, 5)) # regular ax = df.plot.bar(linewidth=2) for r in ax.patches: - self.assertEqual(r.get_linewidth(), 2) + assert r.get_linewidth() == 2 # stacked ax = df.plot.bar(stacked=True, linewidth=2) for r in ax.patches: - self.assertEqual(r.get_linewidth(), 2) + assert r.get_linewidth() == 2 # subplots axes = df.plot.bar(linewidth=2, subplots=True) self._check_axes_shape(axes, axes_num=5, layout=(5, 1)) for ax in axes: for r in ax.patches: - self.assertEqual(r.get_linewidth(), 2) + assert r.get_linewidth() == 2 - @slow + @pytest.mark.slow def test_bar_barwidth(self): df = DataFrame(randn(5, 5)) @@ -736,36 +923,36 @@ def test_bar_barwidth(self): # regular ax = df.plot.bar(width=width) for r in ax.patches: - self.assertEqual(r.get_width(), width / len(df.columns)) + assert r.get_width() == width / len(df.columns) # stacked ax = df.plot.bar(stacked=True, width=width) for r in ax.patches: - self.assertEqual(r.get_width(), width) + assert r.get_width() == width # horizontal regular ax = df.plot.barh(width=width) for r in ax.patches: - self.assertEqual(r.get_height(), width / len(df.columns)) + assert r.get_height() == width / len(df.columns) # horizontal stacked ax = df.plot.barh(stacked=True, width=width) for r in ax.patches: - self.assertEqual(r.get_height(), width) + assert r.get_height() == width # subplots axes = df.plot.bar(width=width, subplots=True) for ax in axes: for r in ax.patches: - self.assertEqual(r.get_width(), width) + assert r.get_width() == width # horizontal subplots axes = df.plot.barh(width=width, subplots=True) for ax in axes: for r in ax.patches: - self.assertEqual(r.get_height(), width) + assert r.get_height() == width - @slow + @pytest.mark.slow def test_bar_barwidth_position(self): df = DataFrame(randn(5, 5)) self._check_bar_alignment(df, kind='bar', stacked=False, width=0.9, @@ -781,7 +968,7 @@ def test_bar_barwidth_position(self): self._check_bar_alignment(df, kind='barh', subplots=True, width=0.9, position=0.2) - @slow + @pytest.mark.slow def test_bar_barwidth_position_int(self): # GH 12979 df = DataFrame(randn(5, 5)) @@ -790,10 +977,10 @@ def test_bar_barwidth_position_int(self): ax = df.plot.bar(stacked=True, width=w) ticks = ax.xaxis.get_ticklocs() tm.assert_numpy_array_equal(ticks, np.array([0, 1, 2, 3, 4])) - self.assertEqual(ax.get_xlim(), (-0.75, 4.75)) + assert ax.get_xlim() == (-0.75, 4.75) # check left-edge of bars - self.assertEqual(ax.patches[0].get_x(), -0.5) - self.assertEqual(ax.patches[-1].get_x(), 3.5) + assert ax.patches[0].get_x() == -0.5 + assert ax.patches[-1].get_x() == 3.5 self._check_bar_alignment(df, kind='bar', stacked=True, width=1) self._check_bar_alignment(df, kind='barh', stacked=False, width=1) @@ -801,36 +988,36 @@ def test_bar_barwidth_position_int(self): self._check_bar_alignment(df, kind='bar', subplots=True, width=1) self._check_bar_alignment(df, kind='barh', subplots=True, width=1) - @slow + @pytest.mark.slow def test_bar_bottom_left(self): df = DataFrame(rand(5, 5)) ax = df.plot.bar(stacked=False, bottom=1) result = [p.get_y() for p in ax.patches] - self.assertEqual(result, [1] * 25) + assert result == [1] * 25 ax = df.plot.bar(stacked=True, bottom=[-1, -2, -3, -4, -5]) result = [p.get_y() for p in ax.patches[:5]] - self.assertEqual(result, [-1, -2, -3, -4, -5]) + assert result == [-1, -2, -3, -4, -5] ax = df.plot.barh(stacked=False, left=np.array([1, 1, 1, 1, 1])) result = [p.get_x() for p in ax.patches] - self.assertEqual(result, [1] * 25) + assert result == [1] * 25 ax = df.plot.barh(stacked=True, left=[1, 2, 3, 4, 5]) result = [p.get_x() for p in ax.patches[:5]] - self.assertEqual(result, [1, 2, 3, 4, 5]) + assert result == [1, 2, 3, 4, 5] axes = df.plot.bar(subplots=True, bottom=-1) for ax in axes: result = [p.get_y() for p in ax.patches] - self.assertEqual(result, [-1] * 5) + assert result == [-1] * 5 axes = df.plot.barh(subplots=True, left=np.array([1, 1, 1, 1, 1])) for ax in axes: result = [p.get_x() for p in ax.patches] - self.assertEqual(result, [1] * 5) + assert result == [1] * 5 - @slow + @pytest.mark.slow def test_bar_nan(self): df = DataFrame({'A': [10, np.nan, 20], 'B': [5, 10, 20], @@ -838,17 +1025,17 @@ def test_bar_nan(self): ax = df.plot.bar() expected = [10, 0, 20, 5, 10, 20, 1, 2, 3] result = [p.get_height() for p in ax.patches] - self.assertEqual(result, expected) + assert result == expected ax = df.plot.bar(stacked=True) result = [p.get_height() for p in ax.patches] - self.assertEqual(result, expected) + assert result == expected result = [p.get_y() for p in ax.patches] expected = [0.0, 0.0, 0.0, 10.0, 0.0, 20.0, 15.0, 10.0, 40.0] - self.assertEqual(result, expected) + assert result == expected - @slow + @pytest.mark.slow def test_bar_categorical(self): # GH 13019 df1 = pd.DataFrame(np.random.randn(6, 5), @@ -863,18 +1050,18 @@ def test_bar_categorical(self): ax = df.plot.bar() ticks = ax.xaxis.get_ticklocs() tm.assert_numpy_array_equal(ticks, np.array([0, 1, 2, 3, 4, 5])) - self.assertEqual(ax.get_xlim(), (-0.5, 5.5)) + assert ax.get_xlim() == (-0.5, 5.5) # check left-edge of bars - self.assertEqual(ax.patches[0].get_x(), -0.25) - self.assertEqual(ax.patches[-1].get_x(), 5.15) + assert ax.patches[0].get_x() == -0.25 + assert ax.patches[-1].get_x() == 5.15 ax = df.plot.bar(stacked=True) tm.assert_numpy_array_equal(ticks, np.array([0, 1, 2, 3, 4, 5])) - self.assertEqual(ax.get_xlim(), (-0.5, 5.5)) - self.assertEqual(ax.patches[0].get_x(), -0.25) - self.assertEqual(ax.patches[-1].get_x(), 4.75) + assert ax.get_xlim() == (-0.5, 5.5) + assert ax.patches[0].get_x() == -0.25 + assert ax.patches[-1].get_x() == 4.75 - @slow + @pytest.mark.slow def test_plot_scatter(self): df = DataFrame(randn(6, 4), index=list(string.ascii_letters[:6]), @@ -883,16 +1070,97 @@ def test_plot_scatter(self): _check_plot_works(df.plot.scatter, x='x', y='y') _check_plot_works(df.plot.scatter, x=1, y=2) - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot.scatter(x='x') - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot.scatter(y='y') # GH 6951 axes = df.plot(x='x', y='y', kind='scatter', subplots=True) self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) - @slow + @pytest.mark.slow + def test_if_scatterplot_colorbar_affects_xaxis_visibility(self): + # addressing issue #10611, to ensure colobar does not + # interfere with x-axis label and ticklabels with + # ipython inline backend. + random_array = np.random.random((1000, 3)) + df = pd.DataFrame(random_array, + columns=['A label', 'B label', 'C label']) + + ax1 = df.plot.scatter(x='A label', y='B label') + ax2 = df.plot.scatter(x='A label', y='B label', c='C label') + + vis1 = [vis.get_visible() for vis in + ax1.xaxis.get_minorticklabels()] + vis2 = [vis.get_visible() for vis in + ax2.xaxis.get_minorticklabels()] + assert vis1 == vis2 + + vis1 = [vis.get_visible() for vis in + ax1.xaxis.get_majorticklabels()] + vis2 = [vis.get_visible() for vis in + ax2.xaxis.get_majorticklabels()] + assert vis1 == vis2 + + assert (ax1.xaxis.get_label().get_visible() == + ax2.xaxis.get_label().get_visible()) + + @pytest.mark.slow + def test_if_hexbin_xaxis_label_is_visible(self): + # addressing issue #10678, to ensure colobar does not + # interfere with x-axis label and ticklabels with + # ipython inline backend. + random_array = np.random.random((1000, 3)) + df = pd.DataFrame(random_array, + columns=['A label', 'B label', 'C label']) + + ax = df.plot.hexbin('A label', 'B label', gridsize=12) + assert all(vis.get_visible() for vis in + ax.xaxis.get_minorticklabels()) + assert all(vis.get_visible() for vis in + ax.xaxis.get_majorticklabels()) + assert ax.xaxis.get_label().get_visible() + + @pytest.mark.slow + def test_if_scatterplot_colorbars_are_next_to_parent_axes(self): + import matplotlib.pyplot as plt + random_array = np.random.random((1000, 3)) + df = pd.DataFrame(random_array, + columns=['A label', 'B label', 'C label']) + + fig, axes = plt.subplots(1, 2) + df.plot.scatter('A label', 'B label', c='C label', ax=axes[0]) + df.plot.scatter('A label', 'B label', c='C label', ax=axes[1]) + plt.tight_layout() + + points = np.array([ax.get_position().get_points() + for ax in fig.axes]) + axes_x_coords = points[:, :, 0] + parent_distance = axes_x_coords[1, :] - axes_x_coords[0, :] + colorbar_distance = axes_x_coords[3, :] - axes_x_coords[2, :] + assert np.isclose(parent_distance, + colorbar_distance, atol=1e-7).all() + + @pytest.mark.slow + def test_plot_scatter_with_categorical_data(self): + # GH 16199 + df = pd.DataFrame({'x': [1, 2, 3, 4], + 'y': pd.Categorical(['a', 'b', 'a', 'c'])}) + + with pytest.raises(ValueError) as ve: + df.plot(x='x', y='y', kind='scatter') + ve.match('requires y column to be numeric') + + with pytest.raises(ValueError) as ve: + df.plot(x='y', y='x', kind='scatter') + ve.match('requires x column to be numeric') + + with pytest.raises(ValueError) as ve: + df.plot(x='y', y='y', kind='scatter') + ve.match('requires x column to be numeric') + + @pytest.mark.slow def test_plot_scatter_with_c(self): df = DataFrame(randn(6, 4), index=list(string.ascii_letters[:6]), @@ -902,25 +1170,23 @@ def test_plot_scatter_with_c(self): df.plot.scatter(x=0, y=1, c=2)] for ax in axes: # default to Greys - self.assertEqual(ax.collections[0].cmap.name, 'Greys') - - if self.mpl_ge_1_3_1: + assert ax.collections[0].cmap.name == 'Greys' - # n.b. there appears to be no public method to get the colorbar - # label - self.assertEqual(ax.collections[0].colorbar._label, 'z') + # n.b. there appears to be no public method + # to get the colorbar label + assert ax.collections[0].colorbar._label == 'z' cm = 'cubehelix' ax = df.plot.scatter(x='x', y='y', c='z', colormap=cm) - self.assertEqual(ax.collections[0].cmap.name, cm) + assert ax.collections[0].cmap.name == cm # verify turning off colorbar works ax = df.plot.scatter(x='x', y='y', c='z', colorbar=False) - self.assertIs(ax.collections[0].colorbar, None) + assert ax.collections[0].colorbar is None # verify that we can still plot a solid color ax = df.plot.scatter(x=0, y=1, c='red') - self.assertIs(ax.collections[0].colorbar, None) + assert ax.collections[0].colorbar is None self._check_colors(ax.collections, facecolors=['r']) # Ensure that we can pass an np.array straight through to matplotlib, @@ -938,8 +1204,8 @@ def test_plot_scatter_with_c(self): # identical to the values we supplied, normally we'd be on shaky ground # comparing floats for equality but here we expect them to be # identical. - self.assertTrue(np.array_equal(ax.collections[0].get_facecolor(), - rgba_array)) + tm.assert_numpy_array_equal(ax.collections[0] + .get_facecolor(), rgba_array) # we don't test the colors of the faces in this next plot because they # are dependent on the spring colormap, which may change its colors # later. @@ -948,10 +1214,10 @@ def test_plot_scatter_with_c(self): def test_scatter_colors(self): df = DataFrame({'a': [1, 2, 3], 'b': [1, 2, 3], 'c': [1, 2, 3]}) - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot.scatter(x='a', y='b', c='c', color='green') - default_colors = self._maybe_unpack_cycler(self.plt.rcParams) + default_colors = self._unpack_cycler(self.plt.rcParams) ax = df.plot.scatter(x='a', y='b', c='c') tm.assert_numpy_array_equal( @@ -962,7 +1228,7 @@ def test_scatter_colors(self): tm.assert_numpy_array_equal(ax.collections[0].get_facecolor()[0], np.array([1, 1, 1, 1], dtype=np.float64)) - @slow + @pytest.mark.slow def test_plot_bar(self): df = DataFrame(randn(6, 4), index=list(string.ascii_letters[:6]), @@ -1006,21 +1272,20 @@ def _check_bar_alignment(self, df, kind='bar', stacked=False, if kind == 'bar': axis = ax.xaxis ax_min, ax_max = ax.get_xlim() - min_edge = min([p.get_x() for p in ax.patches]) - max_edge = max([p.get_x() + p.get_width() for p in ax.patches]) + min_edge = min(p.get_x() for p in ax.patches) + max_edge = max(p.get_x() + p.get_width() for p in ax.patches) elif kind == 'barh': axis = ax.yaxis ax_min, ax_max = ax.get_ylim() - min_edge = min([p.get_y() for p in ax.patches]) - max_edge = max([p.get_y() + p.get_height() for p in ax.patches - ]) + min_edge = min(p.get_y() for p in ax.patches) + max_edge = max(p.get_y() + p.get_height() for p in ax.patches) else: raise ValueError # GH 7498 # compare margins between lim and bar edges - self.assertAlmostEqual(ax_min, min_edge - 0.25) - self.assertAlmostEqual(ax_max, max_edge + 0.25) + tm.assert_almost_equal(ax_min, min_edge - 0.25) + tm.assert_almost_equal(ax_max, max_edge + 0.25) p = ax.patches[0] if kind == 'bar' and (stacked is True or subplots is True): @@ -1040,20 +1305,20 @@ def _check_bar_alignment(self, df, kind='bar', stacked=False, raise ValueError # Check the ticks locates on integer - self.assertTrue((axis.get_ticklocs() == np.arange(len(df))).all()) + assert (axis.get_ticklocs() == np.arange(len(df))).all() if align == 'center': # Check whether the bar locates on center - self.assertAlmostEqual(axis.get_ticklocs()[0], center) + tm.assert_almost_equal(axis.get_ticklocs()[0], center) elif align == 'edge': # Check whether the bar's edge starts from the tick - self.assertAlmostEqual(axis.get_ticklocs()[0], edge) + tm.assert_almost_equal(axis.get_ticklocs()[0], edge) else: raise ValueError return axes - @slow + @pytest.mark.slow def test_bar_stacked_center(self): # GH2157 df = DataFrame({'A': [3] * 5, 'B': lrange(5)}, index=lrange(5)) @@ -1062,7 +1327,7 @@ def test_bar_stacked_center(self): self._check_bar_alignment(df, kind='barh', stacked=True) self._check_bar_alignment(df, kind='barh', stacked=True, width=0.9) - @slow + @pytest.mark.slow def test_bar_center(self): df = DataFrame({'A': [3] * 5, 'B': lrange(5)}, index=lrange(5)) self._check_bar_alignment(df, kind='bar', stacked=False) @@ -1070,7 +1335,7 @@ def test_bar_center(self): self._check_bar_alignment(df, kind='barh', stacked=False) self._check_bar_alignment(df, kind='barh', stacked=False, width=0.9) - @slow + @pytest.mark.slow def test_bar_subplots_center(self): df = DataFrame({'A': [3] * 5, 'B': lrange(5)}, index=lrange(5)) self._check_bar_alignment(df, kind='bar', subplots=True) @@ -1078,7 +1343,7 @@ def test_bar_subplots_center(self): self._check_bar_alignment(df, kind='barh', subplots=True) self._check_bar_alignment(df, kind='barh', subplots=True, width=0.9) - @slow + @pytest.mark.slow def test_bar_align_single_column(self): df = DataFrame(randn(5)) self._check_bar_alignment(df, kind='bar', stacked=False) @@ -1088,7 +1353,7 @@ def test_bar_align_single_column(self): self._check_bar_alignment(df, kind='bar', subplots=True) self._check_bar_alignment(df, kind='barh', subplots=True) - @slow + @pytest.mark.slow def test_bar_edge(self): df = DataFrame({'A': [3] * 5, 'B': lrange(5)}, index=lrange(5)) @@ -1113,25 +1378,20 @@ def test_bar_edge(self): self._check_bar_alignment(df, kind='barh', subplots=True, width=0.9, align='edge') - @slow + @pytest.mark.slow def test_bar_log_no_subplots(self): # GH3254, GH3298 matplotlib/matplotlib#1882, #1892 # regressions in 1.2.1 - expected = np.array([1., 10.]) - - if not self.mpl_le_1_2_1: - expected = np.hstack((.1, expected, 100)) + expected = np.array([.1, 1., 10., 100]) # no subplots df = DataFrame({'A': [3] * 5, 'B': lrange(1, 6)}, index=lrange(5)) ax = df.plot.bar(grid=True, log=True) tm.assert_numpy_array_equal(ax.yaxis.get_ticklocs(), expected) - @slow + @pytest.mark.slow def test_bar_log_subplots(self): - expected = np.array([1., 10., 100., 1000.]) - if not self.mpl_le_1_2_1: - expected = np.hstack((.1, expected, 1e4)) + expected = np.array([.1, 1., 10., 100., 1000., 1e4]) ax = DataFrame([Series([200, 300]), Series([300, 500])]).plot.bar( log=True, subplots=True) @@ -1139,7 +1399,7 @@ def test_bar_log_subplots(self): tm.assert_numpy_array_equal(ax[0].yaxis.get_ticklocs(), expected) tm.assert_numpy_array_equal(ax[1].yaxis.get_ticklocs(), expected) - @slow + @pytest.mark.slow def test_boxplot(self): df = self.hist_df series = df['height'] @@ -1150,7 +1410,7 @@ def test_boxplot(self): self._check_text_labels(ax.get_xticklabels(), labels) tm.assert_numpy_array_equal(ax.xaxis.get_ticklocs(), np.arange(1, len(numeric_cols) + 1)) - self.assertEqual(len(ax.lines), self.bp_n_objects * len(numeric_cols)) + assert len(ax.lines) == self.bp_n_objects * len(numeric_cols) # different warning on py3 if not PY3: @@ -1161,7 +1421,7 @@ def test_boxplot(self): self._check_ax_scales(axes, yaxis='log') for ax, label in zip(axes, labels): self._check_text_labels(ax.get_xticklabels(), [label]) - self.assertEqual(len(ax.lines), self.bp_n_objects) + assert len(ax.lines) == self.bp_n_objects axes = series.plot.box(rot=40) self._check_ticks_props(axes, xrot=40, yrot=0) @@ -1175,9 +1435,9 @@ def test_boxplot(self): labels = [pprint_thing(c) for c in numeric_cols] self._check_text_labels(ax.get_xticklabels(), labels) tm.assert_numpy_array_equal(ax.xaxis.get_ticklocs(), positions) - self.assertEqual(len(ax.lines), self.bp_n_objects * len(numeric_cols)) + assert len(ax.lines) == self.bp_n_objects * len(numeric_cols) - @slow + @pytest.mark.slow def test_boxplot_vertical(self): df = self.hist_df numeric_cols = df._get_numeric_data().columns @@ -1187,7 +1447,7 @@ def test_boxplot_vertical(self): ax = df.plot.box(rot=50, fontsize=8, vert=False) self._check_ticks_props(ax, xrot=0, yrot=50, ylabelsize=8) self._check_text_labels(ax.get_yticklabels(), labels) - self.assertEqual(len(ax.lines), self.bp_n_objects * len(numeric_cols)) + assert len(ax.lines) == self.bp_n_objects * len(numeric_cols) # _check_plot_works adds an ax so catch warning. see GH #13188 with tm.assert_produces_warning(UserWarning): @@ -1197,20 +1457,20 @@ def test_boxplot_vertical(self): self._check_ax_scales(axes, xaxis='log') for ax, label in zip(axes, labels): self._check_text_labels(ax.get_yticklabels(), [label]) - self.assertEqual(len(ax.lines), self.bp_n_objects) + assert len(ax.lines) == self.bp_n_objects positions = np.array([3, 2, 8]) ax = df.plot.box(positions=positions, vert=False) self._check_text_labels(ax.get_yticklabels(), labels) tm.assert_numpy_array_equal(ax.yaxis.get_ticklocs(), positions) - self.assertEqual(len(ax.lines), self.bp_n_objects * len(numeric_cols)) + assert len(ax.lines) == self.bp_n_objects * len(numeric_cols) - @slow + @pytest.mark.slow def test_boxplot_return_type(self): df = DataFrame(randn(6, 4), index=list(string.ascii_letters[:6]), columns=['one', 'two', 'three', 'four']) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot.box(return_type='NOTATYPE') result = df.plot.box(return_type='dict') @@ -1225,13 +1485,13 @@ def test_boxplot_return_type(self): result = df.plot.box(return_type='both') self._check_box_return_type(result, 'both') - @slow + @pytest.mark.slow def test_boxplot_subplots_return_type(self): df = self.hist_df # normal style: return_type=None result = df.plot.box(subplots=True) - self.assertIsInstance(result, Series) + assert isinstance(result, Series) self._check_box_return_type(result, None, expected_keys=[ 'height', 'weight', 'category']) @@ -1242,10 +1502,11 @@ def test_boxplot_subplots_return_type(self): expected_keys=['height', 'weight', 'category'], check_ax_title=False) - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_df(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() + df = DataFrame(randn(100, 4)) ax = _check_plot_works(df.plot, kind='kde') expected = [pprint_thing(c) for c in df.columns] @@ -1263,19 +1524,18 @@ def test_kde_df(self): axes = df.plot(kind='kde', logy=True, subplots=True) self._check_ax_scales(axes, yaxis='log') - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_missing_vals(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() + df = DataFrame(np.random.uniform(size=(100, 4))) df.loc[0, 0] = np.nan _check_plot_works(df.plot, kind='kde') - @slow + @pytest.mark.slow def test_hist_df(self): from matplotlib.patches import Rectangle - if self.mpl_le_1_2_1: - pytest.skip("not supported in matplotlib <= 1.2.x") df = DataFrame(randn(100, 4)) series = df[0] @@ -1294,16 +1554,20 @@ def test_hist_df(self): self._check_ticks_props(axes, xrot=40, yrot=0) tm.close() - ax = series.plot.hist(normed=True, cumulative=True, bins=4) + if plotting._compat._mpl_ge_2_2_0(): + kwargs = {"density": True} + else: + kwargs = {"normed": True} + ax = series.plot.hist(cumulative=True, bins=4, **kwargs) # height of last bin (index 5) must be 1.0 rects = [x for x in ax.get_children() if isinstance(x, Rectangle)] - self.assertAlmostEqual(rects[-1].get_height(), 1.0) + tm.assert_almost_equal(rects[-1].get_height(), 1.0) tm.close() ax = series.plot.hist(cumulative=True, bins=4) rects = [x for x in ax.get_children() if isinstance(x, Rectangle)] - self.assertAlmostEqual(rects[-2].get_height(), 100.0) + tm.assert_almost_equal(rects[-2].get_height(), 100.0) tm.close() # if horizontal, yticklabels are rotated @@ -1319,19 +1583,19 @@ def _check_box_coord(self, patches, expected_y=None, expected_h=None, # dtype is depending on above values, no need to check if expected_y is not None: - self.assert_numpy_array_equal(result_y, expected_y, - check_dtype=False) + tm.assert_numpy_array_equal(result_y, expected_y, + check_dtype=False) if expected_h is not None: - self.assert_numpy_array_equal(result_height, expected_h, - check_dtype=False) + tm.assert_numpy_array_equal(result_height, expected_h, + check_dtype=False) if expected_x is not None: - self.assert_numpy_array_equal(result_x, expected_x, - check_dtype=False) + tm.assert_numpy_array_equal(result_x, expected_x, + check_dtype=False) if expected_w is not None: - self.assert_numpy_array_equal(result_width, expected_w, - check_dtype=False) + tm.assert_numpy_array_equal(result_width, expected_w, + check_dtype=False) - @slow + @pytest.mark.slow def test_hist_df_coord(self): normal_df = DataFrame({'A': np.repeat(np.array([1, 2, 3, 4, 5]), np.array([10, 9, 8, 7, 6])), @@ -1383,51 +1647,49 @@ def test_hist_df_coord(self): expected_y=np.array([0, 0, 0, 0, 0]), expected_h=np.array([6, 7, 8, 9, 10])) - if self.mpl_ge_1_3_1: - - # horizontal - ax = df.plot.hist(bins=5, orientation='horizontal') - self._check_box_coord(ax.patches[:5], - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([10, 9, 8, 7, 6])) - self._check_box_coord(ax.patches[5:10], - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([8, 8, 8, 8, 8])) - self._check_box_coord(ax.patches[10:], - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([6, 7, 8, 9, 10])) - - ax = df.plot.hist(bins=5, stacked=True, - orientation='horizontal') - self._check_box_coord(ax.patches[:5], - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([10, 9, 8, 7, 6])) - self._check_box_coord(ax.patches[5:10], - expected_x=np.array([10, 9, 8, 7, 6]), - expected_w=np.array([8, 8, 8, 8, 8])) - self._check_box_coord( - ax.patches[10:], - expected_x=np.array([18, 17, 16, 15, 14]), - expected_w=np.array([6, 7, 8, 9, 10])) - - axes = df.plot.hist(bins=5, stacked=True, subplots=True, - orientation='horizontal') - self._check_box_coord(axes[0].patches, - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([10, 9, 8, 7, 6])) - self._check_box_coord(axes[1].patches, - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([8, 8, 8, 8, 8])) - self._check_box_coord(axes[2].patches, - expected_x=np.array([0, 0, 0, 0, 0]), - expected_w=np.array([6, 7, 8, 9, 10])) - - @slow + # horizontal + ax = df.plot.hist(bins=5, orientation='horizontal') + self._check_box_coord(ax.patches[:5], + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([10, 9, 8, 7, 6])) + self._check_box_coord(ax.patches[5:10], + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([8, 8, 8, 8, 8])) + self._check_box_coord(ax.patches[10:], + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([6, 7, 8, 9, 10])) + + ax = df.plot.hist(bins=5, stacked=True, + orientation='horizontal') + self._check_box_coord(ax.patches[:5], + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([10, 9, 8, 7, 6])) + self._check_box_coord(ax.patches[5:10], + expected_x=np.array([10, 9, 8, 7, 6]), + expected_w=np.array([8, 8, 8, 8, 8])) + self._check_box_coord( + ax.patches[10:], + expected_x=np.array([18, 17, 16, 15, 14]), + expected_w=np.array([6, 7, 8, 9, 10])) + + axes = df.plot.hist(bins=5, stacked=True, subplots=True, + orientation='horizontal') + self._check_box_coord(axes[0].patches, + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([10, 9, 8, 7, 6])) + self._check_box_coord(axes[1].patches, + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([8, 8, 8, 8, 8])) + self._check_box_coord(axes[2].patches, + expected_x=np.array([0, 0, 0, 0, 0]), + expected_w=np.array([6, 7, 8, 9, 10])) + + @pytest.mark.slow def test_plot_int_columns(self): df = DataFrame(randn(100, 4)).cumsum() _check_plot_works(df.plot, legend=True) - @slow + @pytest.mark.slow def test_df_legend_labels(self): kinds = ['line', 'bar', 'barh', 'kde', 'area', 'hist'] df = DataFrame(rand(3, 3), columns=['a', 'b', 'c']) @@ -1494,7 +1756,7 @@ def test_df_legend_labels(self): self._check_text_labels(ax.xaxis.get_label(), 'a') ax = df5.plot(y='c', label='LABEL_c', ax=ax) self._check_legend_labels(ax, labels=['LABEL_b', 'LABEL_c']) - self.assertTrue(df5.columns.tolist() == ['b', 'c']) + assert df5.columns.tolist() == ['b', 'c'] def test_legend_name(self): multi = DataFrame(randn(4, 4), @@ -1520,7 +1782,7 @@ def test_legend_name(self): leg_title = ax.legend_.get_title() self._check_text_labels(leg_title, 'new') - @slow + @pytest.mark.slow def test_no_legend(self): kinds = ['line', 'bar', 'barh', 'kde', 'area', 'hist'] df = DataFrame(rand(3, 3), columns=['a', 'b', 'c']) @@ -1532,7 +1794,7 @@ def test_no_legend(self): ax = df.plot(kind=kind, legend=False) self._check_legend_labels(ax, visible=False) - @slow + @pytest.mark.slow def test_style_by_column(self): import matplotlib.pyplot as plt fig = plt.gcf() @@ -1546,20 +1808,19 @@ def test_style_by_column(self): fig.add_subplot(111) ax = df.plot(style=markers) for i, l in enumerate(ax.get_lines()[:len(markers)]): - self.assertEqual(l.get_marker(), markers[i]) + assert l.get_marker() == markers[i] - @slow + @pytest.mark.slow def test_line_label_none(self): s = Series([1, 2]) ax = s.plot() - self.assertEqual(ax.get_legend(), None) + assert ax.get_legend() is None ax = s.plot(legend=True) - self.assertEqual(ax.get_legend().get_texts()[0].get_text(), 'None') + assert ax.get_legend().get_texts()[0].get_text() == 'None' - @slow + @pytest.mark.slow def test_line_colors(self): - import sys from matplotlib import cm custom_colors = 'rgcby' @@ -1568,16 +1829,13 @@ def test_line_colors(self): ax = df.plot(color=custom_colors) self._check_colors(ax.get_lines(), linecolors=custom_colors) - tmp = sys.stderr - sys.stderr = StringIO() - try: - tm.close() - ax2 = df.plot(colors=custom_colors) - lines2 = ax2.get_lines() - for l1, l2 in zip(ax.get_lines(), lines2): - self.assertEqual(l1.get_color(), l2.get_color()) - finally: - sys.stderr = tmp + tm.close() + + ax2 = df.plot(color=custom_colors) + lines2 = ax2.get_lines() + + for l1, l2 in zip(ax.get_lines(), lines2): + assert l1.get_color() == l2.get_color() tm.close() @@ -1606,30 +1864,29 @@ def test_line_colors(self): self._check_colors(ax.get_lines(), linecolors=custom_colors) tm.close() - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): # Color contains shorthand hex value results in ValueError custom_colors = ['#F00', '#00F', '#FF0', '#000', '#FFF'] # Forced show plot _check_plot_works(df.plot, color=custom_colors) - @slow + @pytest.mark.slow def test_dont_modify_colors(self): colors = ['r', 'g', 'b'] pd.DataFrame(np.random.rand(10, 2)).plot(color=colors) - self.assertEqual(len(colors), 3) + assert len(colors) == 3 - @slow + @pytest.mark.slow def test_line_colors_and_styles_subplots(self): # GH 9894 from matplotlib import cm - default_colors = self._maybe_unpack_cycler(self.plt.rcParams) + default_colors = self._unpack_cycler(self.plt.rcParams) df = DataFrame(randn(5, 5)) axes = df.plot(subplots=True) for ax, c in zip(axes, list(default_colors)): - if self.mpl_ge_2_0_0: - c = [c] + c = [c] self._check_colors(ax.get_lines(), linecolors=c) tm.close() @@ -1663,7 +1920,7 @@ def test_line_colors_and_styles_subplots(self): self._check_colors(ax.get_lines(), linecolors=[c]) tm.close() - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): # Color contains shorthand hex value results in ValueError custom_colors = ['#F00', '#00F', '#FF0', '#000', '#FFF'] # Forced show plot @@ -1696,7 +1953,7 @@ def test_line_colors_and_styles_subplots(self): self._check_colors(ax.get_lines(), linecolors=[c]) tm.close() - @slow + @pytest.mark.slow def test_area_colors(self): from matplotlib import cm from matplotlib.collections import PolyCollection @@ -1710,16 +1967,10 @@ def test_area_colors(self): self._check_colors(poly, facecolors=custom_colors) handles, labels = ax.get_legend_handles_labels() - if self.mpl_ge_1_5_0: - self._check_colors(handles, facecolors=custom_colors) - else: - # legend is stored as Line2D, thus check linecolors - linehandles = [x for x in handles - if not isinstance(x, PolyCollection)] - self._check_colors(linehandles, linecolors=custom_colors) + self._check_colors(handles, facecolors=custom_colors) for h in handles: - self.assertTrue(h.get_alpha() is None) + assert h.get_alpha() is None tm.close() ax = df.plot.area(colormap='jet') @@ -1729,14 +1980,9 @@ def test_area_colors(self): self._check_colors(poly, facecolors=jet_colors) handles, labels = ax.get_legend_handles_labels() - if self.mpl_ge_1_5_0: - self._check_colors(handles, facecolors=jet_colors) - else: - linehandles = [x for x in handles - if not isinstance(x, PolyCollection)] - self._check_colors(linehandles, linecolors=jet_colors) + self._check_colors(handles, facecolors=jet_colors) for h in handles: - self.assertTrue(h.get_alpha() is None) + assert h.get_alpha() is None tm.close() # When stacked=False, alpha is set to 0.5 @@ -1747,18 +1993,14 @@ def test_area_colors(self): self._check_colors(poly, facecolors=jet_with_alpha) handles, labels = ax.get_legend_handles_labels() - if self.mpl_ge_1_5_0: - linecolors = jet_with_alpha - else: - # Line2D can't have alpha in its linecolor - linecolors = jet_colors + linecolors = jet_with_alpha self._check_colors(handles[:len(jet_colors)], linecolors=linecolors) for h in handles: - self.assertEqual(h.get_alpha(), 0.5) + assert h.get_alpha() == 0.5 - @slow + @pytest.mark.slow def test_hist_colors(self): - default_colors = self._maybe_unpack_cycler(self.plt.rcParams) + default_colors = self._unpack_cycler(self.plt.rcParams) df = DataFrame(randn(5, 5)) ax = df.plot.hist() @@ -1790,9 +2032,9 @@ def test_hist_colors(self): self._check_colors(ax.patches[::10], facecolors=['green'] * 5) tm.close() - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_colors(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() from matplotlib import cm @@ -1813,13 +2055,13 @@ def test_kde_colors(self): rgba_colors = lmap(cm.jet, np.linspace(0, 1, len(df))) self._check_colors(ax.get_lines(), linecolors=rgba_colors) - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_colors_and_styles_subplots(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() from matplotlib import cm - default_colors = self._maybe_unpack_cycler(self.plt.rcParams) + default_colors = self._unpack_cycler(self.plt.rcParams) df = DataFrame(randn(5, 5)) @@ -1872,13 +2114,13 @@ def test_kde_colors_and_styles_subplots(self): self._check_colors(ax.get_lines(), linecolors=[c]) tm.close() - @slow + @pytest.mark.slow def test_boxplot_colors(self): def _check_colors(bp, box_c, whiskers_c, medians_c, caps_c='k', fliers_c=None): # TODO: outside this func? if fliers_c is None: - fliers_c = 'k' if self.mpl_ge_2_0_0 else 'b' + fliers_c = 'k' self._check_colors(bp['boxes'], linecolors=[box_c] * len(bp['boxes'])) self._check_colors(bp['whiskers'], @@ -1890,7 +2132,7 @@ def _check_colors(bp, box_c, whiskers_c, medians_c, caps_c='k', self._check_colors(bp['caps'], linecolors=[caps_c] * len(bp['caps'])) - default_colors = self._maybe_unpack_cycler(self.plt.rcParams) + default_colors = self._unpack_cycler(self.plt.rcParams) df = DataFrame(randn(5, 5)) bp = df.plot.box(return_type='dict') @@ -1933,23 +2175,20 @@ def _check_colors(bp, box_c, whiskers_c, medians_c, caps_c='k', _check_colors(bp, (0, 1, 0), (0, 1, 0), (0, 1, 0), (0, 1, 0), '#123456') - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): # Color contains invalid key results in ValueError df.plot.box(color=dict(boxes='red', xxxx='blue')) def test_default_color_cycle(self): import matplotlib.pyplot as plt + import cycler colors = list('rgbk') - if self.mpl_ge_1_5_0: - import cycler - plt.rcParams['axes.prop_cycle'] = cycler.cycler('color', colors) - else: - plt.rcParams['axes.color_cycle'] = colors + plt.rcParams['axes.prop_cycle'] = cycler.cycler('color', colors) df = DataFrame(randn(5, 3)) ax = df.plot() - expected = self._maybe_unpack_cycler(plt.rcParams)[:3] + expected = self._unpack_cycler(plt.rcParams)[:3] self._check_colors(ax.get_lines(), linecolors=expected) def test_unordered_ts(self): @@ -1960,13 +2199,13 @@ def test_unordered_ts(self): columns=['test']) ax = df.plot() xticks = ax.lines[0].get_xdata() - self.assertTrue(xticks[0] < xticks[1]) + assert xticks[0] < xticks[1] ydata = ax.lines[0].get_ydata() tm.assert_numpy_array_equal(ydata, np.array([1.0, 2.0, 3.0])) def test_kind_both_ways(self): df = DataFrame({'x': [1, 2, 3]}) - for kind in plotting._common_kinds: + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue df.plot(kind=kind) @@ -1977,21 +2216,21 @@ def test_kind_both_ways(self): def test_all_invalid_plot_data(self): df = DataFrame(list('abcd')) - for kind in plotting._common_kinds: + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot(kind=kind) - @slow + @pytest.mark.slow def test_partially_invalid_plot_data(self): with tm.RNGContext(42): df = DataFrame(randn(10, 2), dtype=object) df[np.random.rand(df.shape[0]) > 0.5] = 'a' - for kind in plotting._common_kinds: + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot(kind=kind) with tm.RNGContext(42): @@ -2000,74 +2239,119 @@ def test_partially_invalid_plot_data(self): df = DataFrame(rand(10, 2), dtype=object) df[np.random.rand(df.shape[0]) > 0.5] = 'a' for kind in kinds: - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot(kind=kind) def test_invalid_kind(self): df = DataFrame(randn(10, 2)) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot(kind='aasdf') - @slow + @pytest.mark.parametrize("x,y,lbl", [ + (['B', 'C'], 'A', 'a'), + (['A'], ['B', 'C'], ['b', 'c']), + ('A', ['B', 'C'], 'badlabel') + ]) + def test_invalid_xy_args(self, x, y, lbl): + # GH 18671, 19699 allows y to be list-like but not x + df = DataFrame({"A": [1, 2], 'B': [3, 4], 'C': [5, 6]}) + with pytest.raises(ValueError): + df.plot(x=x, y=y, label=lbl) + + @pytest.mark.parametrize("x,y", [ + ('A', 'B'), + (['A'], 'B') + ]) + def test_invalid_xy_args_dup_cols(self, x, y): + # GH 18671, 19699 allows y to be list-like but not x + df = DataFrame([[1, 3, 5], [2, 4, 6]], columns=list('AAB')) + with pytest.raises(ValueError): + df.plot(x=x, y=y) + + @pytest.mark.parametrize("x,y,lbl,colors", [ + ('A', ['B'], ['b'], ['red']), + ('A', ['B', 'C'], ['b', 'c'], ['red', 'blue']), + (0, [1, 2], ['bokeh', 'cython'], ['green', 'yellow']) + ]) + def test_y_listlike(self, x, y, lbl, colors): + # GH 19699: tests list-like y and verifies lbls & colors + df = DataFrame({"A": [1, 2], 'B': [3, 4], 'C': [5, 6]}) + _check_plot_works(df.plot, x='A', y=y, label=lbl) + + ax = df.plot(x=x, y=y, label=lbl, color=colors) + assert len(ax.lines) == len(y) + self._check_colors(ax.get_lines(), linecolors=colors) + + @pytest.mark.parametrize("x,y,colnames", [ + (0, 1, ['A', 'B']), + (1, 0, [0, 1]) + ]) + def test_xy_args_integer(self, x, y, colnames): + # GH 20056: tests integer args for xy and checks col names + df = DataFrame({"A": [1, 2], 'B': [3, 4]}) + df.columns = colnames + _check_plot_works(df.plot, x=x, y=y) + + @pytest.mark.slow def test_hexbin_basic(self): df = self.hexbin_df ax = df.plot.hexbin(x='A', y='B', gridsize=10) # TODO: need better way to test. This just does existence. - self.assertEqual(len(ax.collections), 1) + assert len(ax.collections) == 1 # GH 6951 axes = df.plot.hexbin(x='A', y='B', subplots=True) # hexbin should have 2 axes in the figure, 1 for plotting and another # is colorbar - self.assertEqual(len(axes[0].figure.axes), 2) + assert len(axes[0].figure.axes) == 2 # return value is single axes self._check_axes_shape(axes, axes_num=1, layout=(1, 1)) - @slow + @pytest.mark.slow def test_hexbin_with_c(self): df = self.hexbin_df ax = df.plot.hexbin(x='A', y='B', C='C') - self.assertEqual(len(ax.collections), 1) + assert len(ax.collections) == 1 ax = df.plot.hexbin(x='A', y='B', C='C', reduce_C_function=np.std) - self.assertEqual(len(ax.collections), 1) + assert len(ax.collections) == 1 - @slow + @pytest.mark.slow def test_hexbin_cmap(self): df = self.hexbin_df # Default to BuGn ax = df.plot.hexbin(x='A', y='B') - self.assertEqual(ax.collections[0].cmap.name, 'BuGn') + assert ax.collections[0].cmap.name == 'BuGn' cm = 'cubehelix' ax = df.plot.hexbin(x='A', y='B', colormap=cm) - self.assertEqual(ax.collections[0].cmap.name, cm) + assert ax.collections[0].cmap.name == cm - @slow + @pytest.mark.slow def test_no_color_bar(self): df = self.hexbin_df ax = df.plot.hexbin(x='A', y='B', colorbar=None) - self.assertIs(ax.collections[0].colorbar, None) + assert ax.collections[0].colorbar is None - @slow + @pytest.mark.slow def test_allow_cmap(self): df = self.hexbin_df ax = df.plot.hexbin(x='A', y='B', cmap='YlGn') - self.assertEqual(ax.collections[0].cmap.name, 'YlGn') + assert ax.collections[0].cmap.name == 'YlGn' - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): df.plot.hexbin(x='A', y='B', cmap='YlGn', colormap='BuGn') - @slow + @pytest.mark.slow def test_pie_df(self): df = DataFrame(np.random.rand(5, 3), columns=['X', 'Y', 'Z'], index=['a', 'b', 'c', 'd', 'e']) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot.pie() ax = _check_plot_works(df.plot.pie, y='Y') @@ -2080,11 +2364,11 @@ def test_pie_df(self): with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(df.plot.pie, subplots=True) - self.assertEqual(len(axes), len(df.columns)) + assert len(axes) == len(df.columns) for ax in axes: self._check_text_labels(ax.texts, df.index) for ax, ylabel in zip(axes, df.columns): - self.assertEqual(ax.get_ylabel(), ylabel) + assert ax.get_ylabel() == ylabel labels = ['A', 'B', 'C', 'D', 'E'] color_args = ['r', 'g', 'b', 'c', 'm'] @@ -2092,7 +2376,7 @@ def test_pie_df(self): axes = _check_plot_works(df.plot.pie, subplots=True, labels=labels, colors=color_args) - self.assertEqual(len(axes), len(df.columns)) + assert len(axes) == len(df.columns) for ax in axes: self._check_text_labels(ax.texts, labels) @@ -2110,83 +2394,85 @@ def test_pie_df_nan(self): expected = list(base_expected) # force copy expected[i] = '' result = [x.get_text() for x in ax.texts] - self.assertEqual(result, expected) + assert result == expected # legend labels # NaN's not included in legend with subplots # see https://github.com/pandas-dev/pandas/issues/8390 - self.assertEqual([x.get_text() for x in - ax.get_legend().get_texts()], - base_expected[:i] + base_expected[i + 1:]) + assert ([x.get_text() for x in ax.get_legend().get_texts()] == + base_expected[:i] + base_expected[i + 1:]) - @slow + @pytest.mark.slow def test_errorbar_plot(self): - d = {'x': np.arange(12), 'y': np.arange(12, 0, -1)} - df = DataFrame(d) - d_err = {'x': np.ones(12) * 0.2, 'y': np.ones(12) * 0.4} - df_err = DataFrame(d_err) - - # check line plots - ax = _check_plot_works(df.plot, yerr=df_err, logy=True) - self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(df.plot, yerr=df_err, logx=True, logy=True) - self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(df.plot, yerr=df_err, loglog=True) - self._check_has_errorbars(ax, xerr=0, yerr=2) + with warnings.catch_warnings(): + d = {'x': np.arange(12), 'y': np.arange(12, 0, -1)} + df = DataFrame(d) + d_err = {'x': np.ones(12) * 0.2, 'y': np.ones(12) * 0.4} + df_err = DataFrame(d_err) - kinds = ['line', 'bar', 'barh'] - for kind in kinds: - ax = _check_plot_works(df.plot, yerr=df_err['x'], kind=kind) + # check line plots + ax = _check_plot_works(df.plot, yerr=df_err, logy=True) self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(df.plot, yerr=d_err, kind=kind) + ax = _check_plot_works(df.plot, yerr=df_err, logx=True, logy=True) self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(df.plot, yerr=df_err, xerr=df_err, - kind=kind) - self._check_has_errorbars(ax, xerr=2, yerr=2) - ax = _check_plot_works(df.plot, yerr=df_err['x'], xerr=df_err['x'], - kind=kind) - self._check_has_errorbars(ax, xerr=2, yerr=2) - ax = _check_plot_works(df.plot, xerr=0.2, yerr=0.2, kind=kind) - self._check_has_errorbars(ax, xerr=2, yerr=2) - # _check_plot_works adds an ax so catch warning. see GH #13188 - with tm.assert_produces_warning(UserWarning): + ax = _check_plot_works(df.plot, yerr=df_err, loglog=True) + self._check_has_errorbars(ax, xerr=0, yerr=2) + + kinds = ['line', 'bar', 'barh'] + for kind in kinds: + ax = _check_plot_works(df.plot, yerr=df_err['x'], kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works(df.plot, yerr=d_err, kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works(df.plot, yerr=df_err, xerr=df_err, + kind=kind) + self._check_has_errorbars(ax, xerr=2, yerr=2) + ax = _check_plot_works(df.plot, yerr=df_err['x'], + xerr=df_err['x'], + kind=kind) + self._check_has_errorbars(ax, xerr=2, yerr=2) + ax = _check_plot_works(df.plot, xerr=0.2, yerr=0.2, kind=kind) + self._check_has_errorbars(ax, xerr=2, yerr=2) + + # _check_plot_works adds an ax so catch warning. see GH #13188 axes = _check_plot_works(df.plot, yerr=df_err, xerr=df_err, subplots=True, kind=kind) - self._check_has_errorbars(axes, xerr=1, yerr=1) - - ax = _check_plot_works((df + 1).plot, yerr=df_err, - xerr=df_err, kind='bar', log=True) - self._check_has_errorbars(ax, xerr=2, yerr=2) + self._check_has_errorbars(axes, xerr=1, yerr=1) - # yerr is raw error values - ax = _check_plot_works(df['y'].plot, yerr=np.ones(12) * 0.4) - self._check_has_errorbars(ax, xerr=0, yerr=1) - ax = _check_plot_works(df.plot, yerr=np.ones((2, 12)) * 0.4) - self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works((df + 1).plot, yerr=df_err, + xerr=df_err, kind='bar', log=True) + self._check_has_errorbars(ax, xerr=2, yerr=2) - # yerr is iterator - import itertools - ax = _check_plot_works(df.plot, yerr=itertools.repeat(0.1, len(df))) - self._check_has_errorbars(ax, xerr=0, yerr=2) + # yerr is raw error values + ax = _check_plot_works(df['y'].plot, yerr=np.ones(12) * 0.4) + self._check_has_errorbars(ax, xerr=0, yerr=1) + ax = _check_plot_works(df.plot, yerr=np.ones((2, 12)) * 0.4) + self._check_has_errorbars(ax, xerr=0, yerr=2) - # yerr is column name - for yerr in ['yerr', u('誤差')]: - s_df = df.copy() - s_df[yerr] = np.ones(12) * 0.2 - ax = _check_plot_works(s_df.plot, yerr=yerr) + # yerr is iterator + import itertools + ax = _check_plot_works(df.plot, + yerr=itertools.repeat(0.1, len(df))) self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(s_df.plot, y='y', x='x', yerr=yerr) - self._check_has_errorbars(ax, xerr=0, yerr=1) - with tm.assertRaises(ValueError): - df.plot(yerr=np.random.randn(11)) + # yerr is column name + for yerr in ['yerr', u('誤差')]: + s_df = df.copy() + s_df[yerr] = np.ones(12) * 0.2 + ax = _check_plot_works(s_df.plot, yerr=yerr) + self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works(s_df.plot, y='y', x='x', yerr=yerr) + self._check_has_errorbars(ax, xerr=0, yerr=1) + + with pytest.raises(ValueError): + df.plot(yerr=np.random.randn(11)) - df_err = DataFrame({'x': ['zzz'] * 12, 'y': ['zzz'] * 12}) - with tm.assertRaises((ValueError, TypeError)): - df.plot(yerr=df_err) + df_err = DataFrame({'x': ['zzz'] * 12, 'y': ['zzz'] * 12}) + with pytest.raises((ValueError, TypeError)): + df.plot(yerr=df_err) - @slow + @pytest.mark.slow def test_errorbar_with_integer_column_names(self): # test with integer column names df = DataFrame(np.random.randn(10, 2)) @@ -2196,7 +2482,7 @@ def test_errorbar_with_integer_column_names(self): ax = _check_plot_works(df.plot, y=0, yerr=1) self._check_has_errorbars(ax, xerr=0, yerr=1) - @slow + @pytest.mark.slow def test_errorbar_with_partial_columns(self): df = DataFrame(np.random.randn(10, 3)) df_err = DataFrame(np.random.randn(10, 2), columns=[0, 2]) @@ -2219,36 +2505,37 @@ def test_errorbar_with_partial_columns(self): ax = _check_plot_works(df.plot, yerr=err) self._check_has_errorbars(ax, xerr=0, yerr=1) - @slow + @pytest.mark.slow def test_errorbar_timeseries(self): - d = {'x': np.arange(12), 'y': np.arange(12, 0, -1)} - d_err = {'x': np.ones(12) * 0.2, 'y': np.ones(12) * 0.4} + with warnings.catch_warnings(): + d = {'x': np.arange(12), 'y': np.arange(12, 0, -1)} + d_err = {'x': np.ones(12) * 0.2, 'y': np.ones(12) * 0.4} - # check time-series plots - ix = date_range('1/1/2000', '1/1/2001', freq='M') - tdf = DataFrame(d, index=ix) - tdf_err = DataFrame(d_err, index=ix) + # check time-series plots + ix = date_range('1/1/2000', '1/1/2001', freq='M') + tdf = DataFrame(d, index=ix) + tdf_err = DataFrame(d_err, index=ix) - kinds = ['line', 'bar', 'barh'] - for kind in kinds: - ax = _check_plot_works(tdf.plot, yerr=tdf_err, kind=kind) - self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(tdf.plot, yerr=d_err, kind=kind) - self._check_has_errorbars(ax, xerr=0, yerr=2) - ax = _check_plot_works(tdf.plot, y='y', yerr=tdf_err['x'], - kind=kind) - self._check_has_errorbars(ax, xerr=0, yerr=1) - ax = _check_plot_works(tdf.plot, y='y', yerr='x', kind=kind) - self._check_has_errorbars(ax, xerr=0, yerr=1) - ax = _check_plot_works(tdf.plot, yerr=tdf_err, kind=kind) - self._check_has_errorbars(ax, xerr=0, yerr=2) - # _check_plot_works adds an ax so catch warning. see GH #13188 - with tm.assert_produces_warning(UserWarning): + kinds = ['line', 'bar', 'barh'] + for kind in kinds: + ax = _check_plot_works(tdf.plot, yerr=tdf_err, kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works(tdf.plot, yerr=d_err, kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=2) + ax = _check_plot_works(tdf.plot, y='y', yerr=tdf_err['x'], + kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=1) + ax = _check_plot_works(tdf.plot, y='y', yerr='x', kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=1) + ax = _check_plot_works(tdf.plot, yerr=tdf_err, kind=kind) + self._check_has_errorbars(ax, xerr=0, yerr=2) + + # _check_plot_works adds an ax so catch warning. see GH #13188 axes = _check_plot_works(tdf.plot, kind=kind, yerr=tdf_err, subplots=True) - self._check_has_errorbars(axes, xerr=0, yerr=1) + self._check_has_errorbars(axes, xerr=0, yerr=1) def test_errorbar_asymmetrical(self): @@ -2257,28 +2544,20 @@ def test_errorbar_asymmetrical(self): # each column is [0, 1, 2, 3, 4], [3, 4, 5, 6, 7]... df = DataFrame(np.arange(15).reshape(3, 5)).T - data = df.values ax = df.plot(yerr=err, xerr=err / 2) - if self.mpl_ge_2_0_0: - yerr_0_0 = ax.collections[1].get_paths()[0].vertices[:, 1] - expected_0_0 = err[0, :, 0] * np.array([-1, 1]) - tm.assert_almost_equal(yerr_0_0, expected_0_0) - else: - self.assertEqual(ax.lines[7].get_ydata()[0], - data[0, 1] - err[1, 0, 0]) - self.assertEqual(ax.lines[8].get_ydata()[0], - data[0, 1] + err[1, 1, 0]) - - self.assertEqual(ax.lines[5].get_xdata()[0], -err[1, 0, 0] / 2) - self.assertEqual(ax.lines[6].get_xdata()[0], err[1, 1, 0] / 2) + yerr_0_0 = ax.collections[1].get_paths()[0].vertices[:, 1] + expected_0_0 = err[0, :, 0] * np.array([-1, 1]) + tm.assert_almost_equal(yerr_0_0, expected_0_0) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot(yerr=err.T) tm.close() + # This XPASSES when tested with mpl == 3.0.1 + @td.xfail_if_mpl_2_2 def test_table(self): df = DataFrame(np.random.rand(10, 3), index=list(string.ascii_letters[:10])) @@ -2286,9 +2565,9 @@ def test_table(self): _check_plot_works(df.plot, table=df) ax = df.plot() - self.assertTrue(len(ax.tables) == 0) + assert len(ax.tables) == 0 plotting.table(ax, df.T) - self.assertTrue(len(ax.tables) == 1) + assert len(ax.tables) == 1 def test_errorbar_scatter(self): df = DataFrame( @@ -2332,7 +2611,7 @@ def _check_errorbar_color(containers, expected, has_err='has_xerr'): self._check_has_errorbars(ax, xerr=0, yerr=1) _check_errorbar_color(ax.containers, 'green', has_err='has_yerr') - @slow + @pytest.mark.slow def test_sharex_and_ax(self): # https://github.com/pandas-dev/pandas/issues/9737 using gridspec, # the axis in fig.get_axis() are sorted differently than pandas @@ -2348,7 +2627,7 @@ def test_sharex_and_ax(self): def _check(axes): for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) for ax in [axes[0], axes[2]]: self._check_visible(ax.get_xticklabels(), visible=False) @@ -2378,13 +2657,13 @@ def _check(axes): gs.tight_layout(plt.gcf()) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) self._check_visible(ax.get_xticklabels(minor=True), visible=True) tm.close() - @slow + @pytest.mark.slow def test_sharey_and_ax(self): # https://github.com/pandas-dev/pandas/issues/9737 using gridspec, # the axis in fig.get_axis() are sorted differently than pandas @@ -2400,7 +2679,7 @@ def test_sharey_and_ax(self): def _check(axes): for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_xticklabels(), visible=True) self._check_visible( ax.get_xticklabels(minor=True), visible=True) @@ -2430,7 +2709,7 @@ def _check(axes): gs.tight_layout(plt.gcf()) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) self._check_visible(ax.get_xticklabels(minor=True), visible=True) @@ -2441,7 +2720,7 @@ def test_memory_leak(self): import gc results = {} - for kind in plotting._plot_klass.keys(): + for kind in plotting._core._plot_klass.keys(): if not _ok_for_gaussian_kde(kind): continue args = {} @@ -2463,11 +2742,11 @@ def test_memory_leak(self): gc.collect() for key in results: # check that every plot was collected - with tm.assertRaises(ReferenceError): + with pytest.raises(ReferenceError): # need to actually access something to get an error results[key].lines - @slow + @pytest.mark.slow def test_df_subplots_patterns_minorticks(self): # GH 10657 import matplotlib.pyplot as plt @@ -2480,7 +2759,7 @@ def test_df_subplots_patterns_minorticks(self): fig, axes = plt.subplots(2, 1, sharex=True) axes = df.plot(subplots=True, ax=axes) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) # xaxis of 1st ax must be hidden self._check_visible(axes[0].get_xticklabels(), visible=False) @@ -2493,7 +2772,7 @@ def test_df_subplots_patterns_minorticks(self): with tm.assert_produces_warning(UserWarning): axes = df.plot(subplots=True, ax=axes, sharex=True) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) # xaxis of 1st ax must be hidden self._check_visible(axes[0].get_xticklabels(), visible=False) @@ -2506,13 +2785,13 @@ def test_df_subplots_patterns_minorticks(self): fig, axes = plt.subplots(2, 1) axes = df.plot(subplots=True, ax=axes) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) self._check_visible(ax.get_xticklabels(minor=True), visible=True) tm.close() - @slow + @pytest.mark.slow def test_df_gridspec_patterns(self): # GH 10819 import matplotlib.pyplot as plt @@ -2540,9 +2819,9 @@ def _get_horizontal_grid(): for ax1, ax2 in [_get_vertical_grid(), _get_horizontal_grid()]: ax1 = ts.plot(ax=ax1) - self.assertEqual(len(ax1.lines), 1) + assert len(ax1.lines) == 1 ax2 = df.plot(ax=ax2) - self.assertEqual(len(ax2.lines), 2) + assert len(ax2.lines) == 2 for ax in [ax1, ax2]: self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) @@ -2553,8 +2832,8 @@ def _get_horizontal_grid(): # subplots=True for ax1, ax2 in [_get_vertical_grid(), _get_horizontal_grid()]: axes = df.plot(subplots=True, ax=[ax1, ax2]) - self.assertEqual(len(ax1.lines), 1) - self.assertEqual(len(ax2.lines), 1) + assert len(ax1.lines) == 1 + assert len(ax2.lines) == 1 for ax in axes: self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) @@ -2567,8 +2846,8 @@ def _get_horizontal_grid(): with tm.assert_produces_warning(UserWarning): axes = df.plot(subplots=True, ax=[ax1, ax2], sharex=True, sharey=True) - self.assertEqual(len(axes[0].lines), 1) - self.assertEqual(len(axes[1].lines), 1) + assert len(axes[0].lines) == 1 + assert len(axes[1].lines) == 1 for ax in [ax1, ax2]: # yaxis are visible because there is only one column self._check_visible(ax.get_yticklabels(), visible=True) @@ -2584,8 +2863,8 @@ def _get_horizontal_grid(): with tm.assert_produces_warning(UserWarning): axes = df.plot(subplots=True, ax=[ax1, ax2], sharex=True, sharey=True) - self.assertEqual(len(axes[0].lines), 1) - self.assertEqual(len(axes[1].lines), 1) + assert len(axes[0].lines) == 1 + assert len(axes[1].lines) == 1 self._check_visible(axes[0].get_yticklabels(), visible=True) # yaxis of axes1 (right) are hidden self._check_visible(axes[1].get_yticklabels(), visible=False) @@ -2610,7 +2889,7 @@ def _get_boxed_grid(): index=ts.index, columns=list('ABCD')) axes = df.plot(subplots=True, ax=axes) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 # axis are visible because these are not shared self._check_visible(ax.get_yticklabels(), visible=True) self._check_visible(ax.get_xticklabels(), visible=True) @@ -2622,7 +2901,7 @@ def _get_boxed_grid(): with tm.assert_produces_warning(UserWarning): axes = df.plot(subplots=True, ax=axes, sharex=True, sharey=True) for ax in axes: - self.assertEqual(len(ax.lines), 1) + assert len(ax.lines) == 1 for ax in [axes[0], axes[2]]: # left column self._check_visible(ax.get_yticklabels(), visible=True) for ax in [axes[1], axes[3]]: # right column @@ -2635,31 +2914,17 @@ def _get_boxed_grid(): self._check_visible(ax.get_xticklabels(minor=True), visible=True) tm.close() - @slow + @pytest.mark.slow def test_df_grid_settings(self): # Make sure plot defaults to rcParams['axes.grid'] setting, GH 9792 self._check_grid_settings( DataFrame({'a': [1, 2, 3], 'b': [2, 3, 4]}), - plotting._dataframe_kinds, kws={'x': 'a', 'y': 'b'}) - - def test_option_mpl_style(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - set_option('display.mpl_style', 'default') - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - set_option('display.mpl_style', None) - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - set_option('display.mpl_style', False) - - with tm.assertRaises(ValueError): - set_option('display.mpl_style', 'default2') + plotting._core._dataframe_kinds, kws={'x': 'a', 'y': 'b'}) def test_invalid_colormap(self): df = DataFrame(randn(3, 2), columns=['A', 'B']) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.plot(colormap='invalid_colormap') def test_plain_axes(self): @@ -2686,7 +2951,7 @@ def test_plain_axes(self): Series(rand(10)).plot(ax=cax) fig, ax = self.plt.subplots() - from mpl_toolkits.axes_grid.inset_locator import inset_axes + from mpl_toolkits.axes_grid1.inset_locator import inset_axes iax = inset_axes(ax, width="30%", height=1., loc=3) Series(rand(10)).plot(ax=ax) Series(rand(10)).plot(ax=iax) @@ -2696,21 +2961,32 @@ def test_passed_bar_colors(self): color_tuples = [(0.9, 0, 0, 1), (0, 0.9, 0, 1), (0, 0, 0.9, 1)] colormap = mpl.colors.ListedColormap(color_tuples) barplot = pd.DataFrame([[1, 2, 3]]).plot(kind="bar", cmap=colormap) - self.assertEqual(color_tuples, [c.get_facecolor() - for c in barplot.patches]) + assert color_tuples == [c.get_facecolor() for c in barplot.patches] def test_rcParams_bar_colors(self): import matplotlib as mpl color_tuples = [(0.9, 0, 0, 1), (0, 0.9, 0, 1), (0, 0, 0.9, 1)] - try: # mpl 1.5 - with mpl.rc_context( - rc={'axes.prop_cycle': mpl.cycler("color", color_tuples)}): - barplot = pd.DataFrame([[1, 2, 3]]).plot(kind="bar") - except (AttributeError, KeyError): # mpl 1.4 - with mpl.rc_context(rc={'axes.color_cycle': color_tuples}): - barplot = pd.DataFrame([[1, 2, 3]]).plot(kind="bar") - self.assertEqual(color_tuples, [c.get_facecolor() - for c in barplot.patches]) + with mpl.rc_context( + rc={'axes.prop_cycle': mpl.cycler("color", color_tuples)}): + barplot = pd.DataFrame([[1, 2, 3]]).plot(kind="bar") + assert color_tuples == [c.get_facecolor() for c in barplot.patches] + + @pytest.mark.parametrize('method', ['line', 'barh', 'bar']) + def test_secondary_axis_font_size(self, method): + # GH: 12565 + df = (pd.DataFrame(np.random.randn(15, 2), + columns=list('AB')) + .assign(C=lambda df: df.B.cumsum()) + .assign(D=lambda df: df.C * 1.1)) + + fontsize = 20 + sy = ['C', 'D'] + + kwargs = dict(secondary_y=sy, fontsize=fontsize, + mark_right=True) + ax = getattr(df.plot, method)(**kwargs) + self._check_ticks_props(axes=ax.right_ax, + ylabelsize=fontsize) def _generate_4_axes_via_gridspec(): diff --git a/pandas/tests/plotting/test_groupby.py b/pandas/tests/plotting/test_groupby.py index 93efb3f994c38..5a5ee75928c97 100644 --- a/pandas/tests/plotting/test_groupby.py +++ b/pandas/tests/plotting/test_groupby.py @@ -3,15 +3,16 @@ """ Test cases for GroupBy.plot """ -from pandas import Series, DataFrame -import pandas.util.testing as tm - import numpy as np +import pandas.util._test_decorators as td + +from pandas import DataFrame, Series from pandas.tests.plotting.common import TestPlotBase +import pandas.util.testing as tm -@tm.mplskip +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): def test_series_groupby_plotting_nominally_works(self): @@ -68,7 +69,7 @@ def test_plot_kwargs(self): res = df.groupby('z').plot(kind='scatter', x='x', y='y') # check that a scatter plot is effectively plotted: the axes should # contain a PathCollection from the scatter plot (GH11805) - self.assertEqual(len(res['a'].collections), 1) + assert len(res['a'].collections) == 1 res = df.groupby('z').plot.scatter(x='x', y='y') - self.assertEqual(len(res['a'].collections), 1) + assert len(res['a'].collections) == 1 diff --git a/pandas/tests/plotting/test_hist_method.py b/pandas/tests/plotting/test_hist_method.py index 380bdc12abce4..4f0bef52b5e15 100644 --- a/pandas/tests/plotting/test_hist_method.py +++ b/pandas/tests/plotting/test_hist_method.py @@ -2,29 +2,32 @@ """ Test cases for .hist method """ -from pandas import Series, DataFrame -import pandas.util.testing as tm -from pandas.util.testing import slow - import numpy as np from numpy.random import randn +import pytest + +import pandas.util._test_decorators as td + +from pandas import DataFrame, Series +from pandas.tests.plotting.common import TestPlotBase, _check_plot_works +import pandas.util.testing as tm -import pandas.tools.plotting as plotting -from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works) +from pandas.plotting._compat import _mpl_ge_2_2_0 +from pandas.plotting._core import grouped_hist -@tm.mplskip +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): - def setUp(self): - TestPlotBase.setUp(self) + def setup_method(self, method): + TestPlotBase.setup_method(self, method) import matplotlib as mpl mpl.rcdefaults() self.ts = tm.makeTimeSeries() self.ts.name = 'ts' - @slow + @pytest.mark.slow def test_hist_legacy(self): _check_plot_works(self.ts.hist) _check_plot_works(self.ts.hist, grid=False) @@ -45,25 +48,25 @@ def test_hist_legacy(self): _check_plot_works(self.ts.hist, figure=fig, ax=ax1) _check_plot_works(self.ts.hist, figure=fig, ax=ax2) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): self.ts.hist(by=self.ts.index, figure=fig) - @slow + @pytest.mark.slow def test_hist_bins_legacy(self): df = DataFrame(np.random.randn(10, 2)) ax = df.hist(bins=2)[0][0] - self.assertEqual(len(ax.patches), 2) + assert len(ax.patches) == 2 - @slow + @pytest.mark.slow def test_hist_layout(self): df = self.hist_df - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.height.hist(layout=(1, 1)) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.height.hist(layout=[1, 1]) - @slow + @pytest.mark.slow def test_hist_layout_with_by(self): df = self.hist_df @@ -109,7 +112,7 @@ def test_hist_layout_with_by(self): self._check_axes_shape( axes, axes_num=4, layout=(4, 2), figsize=(12, 7)) - @slow + @pytest.mark.slow def test_hist_no_overlap(self): from matplotlib.pyplot import subplot, gcf x = Series(randn(2)) @@ -119,29 +122,29 @@ def test_hist_no_overlap(self): subplot(122) y.hist() fig = gcf() - axes = fig.axes if self.mpl_ge_1_5_0 else fig.get_axes() - self.assertEqual(len(axes), 2) + axes = fig.axes + assert len(axes) == 2 - @slow + @pytest.mark.slow def test_hist_by_no_extra_plots(self): df = self.hist_df axes = df.height.hist(by=df.gender) # noqa - self.assertEqual(len(self.plt.get_fignums()), 1) + assert len(self.plt.get_fignums()) == 1 - @slow + @pytest.mark.slow def test_plot_fails_when_ax_differs_from_figure(self): from pylab import figure fig1 = figure() fig2 = figure() ax1 = fig1.add_subplot(111) - with tm.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.ts.hist(ax=ax1, figure=fig2) -@tm.mplskip +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): - @slow + @pytest.mark.slow def test_hist_df_legacy(self): from matplotlib.patches import Rectangle with tm.assert_produces_warning(UserWarning): @@ -152,7 +155,7 @@ def test_hist_df_legacy(self): with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(df.hist, grid=False) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) - self.assertFalse(axes[1, 1].get_visible()) + assert not axes[1, 1].get_visible() df = DataFrame(randn(100, 1)) _check_plot_works(df.hist) @@ -191,10 +194,14 @@ def test_hist_df_legacy(self): tm.close() # make sure kwargs to hist are handled - ax = ser.hist(normed=True, cumulative=True, bins=4) + if _mpl_ge_2_2_0(): + kwargs = {"density": True} + else: + kwargs = {"normed": True} + ax = ser.hist(cumulative=True, bins=4, **kwargs) # height of last bin (index 5) must be 1.0 rects = [x for x in ax.get_children() if isinstance(x, Rectangle)] - self.assertAlmostEqual(rects[-1].get_height(), 1.0) + tm.assert_almost_equal(rects[-1].get_height(), 1.0) tm.close() ax = ser.hist(log=True) @@ -204,10 +211,10 @@ def test_hist_df_legacy(self): tm.close() # propagate attr exception from matplotlib.Axes.hist - with tm.assertRaises(AttributeError): + with pytest.raises(AttributeError): ser.hist(foo='bar') - @slow + @pytest.mark.slow def test_hist_layout(self): df = DataFrame(randn(100, 3)) @@ -229,16 +236,16 @@ def test_hist_layout(self): self._check_axes_shape(axes, axes_num=3, layout=expected) # layout too small for all 4 plots - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.hist(layout=(1, 1)) # invalid format for layout - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.hist(layout=(1,)) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.hist(layout=(-1, -1)) - @slow + @pytest.mark.slow # GH 9351 def test_tight_layout(self): if self.mpl_ge_2_0_1: @@ -249,10 +256,10 @@ def test_tight_layout(self): tm.close() -@tm.mplskip +@td.skip_if_no_mpl class TestDataFrameGroupByPlots(TestPlotBase): - @slow + @pytest.mark.slow def test_grouped_hist_legacy(self): from matplotlib.patches import Rectangle @@ -260,7 +267,7 @@ def test_grouped_hist_legacy(self): df['C'] = np.random.randint(0, 4, 500) df['D'] = ['X'] * 500 - axes = plotting.grouped_hist(df.A, by=df.C) + axes = grouped_hist(df.A, by=df.C) self._check_axes_shape(axes, axes_num=4, layout=(2, 2)) tm.close() @@ -277,32 +284,37 @@ def test_grouped_hist_legacy(self): # make sure kwargs to hist are handled xf, yf = 20, 18 xrot, yrot = 30, 40 - axes = plotting.grouped_hist(df.A, by=df.C, normed=True, - cumulative=True, bins=4, - xlabelsize=xf, xrot=xrot, - ylabelsize=yf, yrot=yrot) + + if _mpl_ge_2_2_0(): + kwargs = {"density": True} + else: + kwargs = {"normed": True} + + axes = grouped_hist(df.A, by=df.C, cumulative=True, + bins=4, xlabelsize=xf, xrot=xrot, + ylabelsize=yf, yrot=yrot, **kwargs) # height of last bin (index 5) must be 1.0 for ax in axes.ravel(): rects = [x for x in ax.get_children() if isinstance(x, Rectangle)] height = rects[-1].get_height() - self.assertAlmostEqual(height, 1.0) + tm.assert_almost_equal(height, 1.0) self._check_ticks_props(axes, xlabelsize=xf, xrot=xrot, ylabelsize=yf, yrot=yrot) tm.close() - axes = plotting.grouped_hist(df.A, by=df.C, log=True) + axes = grouped_hist(df.A, by=df.C, log=True) # scale of y must be 'log' self._check_ax_scales(axes, yaxis='log') tm.close() # propagate attr exception from matplotlib.Axes.hist - with tm.assertRaises(AttributeError): - plotting.grouped_hist(df.A, by=df.C, foo='bar') + with pytest.raises(AttributeError): + grouped_hist(df.A, by=df.C, foo='bar') with tm.assert_produces_warning(FutureWarning): df.hist(by='C', figsize='default') - @slow + @pytest.mark.slow def test_grouped_hist_legacy2(self): n = 10 weight = Series(np.random.normal(166, 20, size=n)) @@ -313,19 +325,24 @@ def test_grouped_hist_legacy2(self): 'gender': gender_int}) gb = df_int.groupby('gender') axes = gb.hist() - self.assertEqual(len(axes), 2) - self.assertEqual(len(self.plt.get_fignums()), 2) + assert len(axes) == 2 + assert len(self.plt.get_fignums()) == 2 tm.close() - @slow + @pytest.mark.slow def test_grouped_hist_layout(self): df = self.hist_df - self.assertRaises(ValueError, df.hist, column='weight', by=df.gender, - layout=(1, 1)) - self.assertRaises(ValueError, df.hist, column='height', by=df.category, - layout=(1, 3)) - self.assertRaises(ValueError, df.hist, column='height', by=df.category, - layout=(-1, -1)) + msg = "Layout of 1x1 must be larger than required size 2" + with pytest.raises(ValueError, match=msg): + df.hist(column='weight', by=df.gender, layout=(1, 1)) + + msg = "Layout of 1x3 must be larger than required size 4" + with pytest.raises(ValueError, match=msg): + df.hist(column='height', by=df.category, layout=(1, 3)) + + msg = "At least one dimension of layout must be positive" + with pytest.raises(ValueError, match=msg): + df.hist(column='height', by=df.category, layout=(-1, -1)) with tm.assert_produces_warning(UserWarning): axes = _check_plot_works(df.hist, column='height', by=df.gender, @@ -366,7 +383,7 @@ def test_grouped_hist_layout(self): axes = df.hist(column=['height', 'weight', 'category']) self._check_axes_shape(axes, axes_num=3, layout=(2, 2)) - @slow + @pytest.mark.slow def test_grouped_hist_multiple_axes(self): # GH 6970, GH 7069 df = self.hist_df @@ -374,54 +391,54 @@ def test_grouped_hist_multiple_axes(self): fig, axes = self.plt.subplots(2, 3) returned = df.hist(column=['height', 'weight', 'category'], ax=axes[0]) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assert_numpy_array_equal(returned, axes[0]) - self.assertIs(returned[0].figure, fig) + tm.assert_numpy_array_equal(returned, axes[0]) + assert returned[0].figure is fig returned = df.hist(by='classroom', ax=axes[1]) self._check_axes_shape(returned, axes_num=3, layout=(1, 3)) - self.assert_numpy_array_equal(returned, axes[1]) - self.assertIs(returned[0].figure, fig) + tm.assert_numpy_array_equal(returned, axes[1]) + assert returned[0].figure is fig - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): fig, axes = self.plt.subplots(2, 3) # pass different number of axes from required axes = df.hist(column='height', ax=axes) - @slow + @pytest.mark.slow def test_axis_share_x(self): df = self.hist_df # GH4089 ax1, ax2 = df.hist(column='height', by=df.gender, sharex=True) # share x - self.assertTrue(ax1._shared_x_axes.joined(ax1, ax2)) - self.assertTrue(ax2._shared_x_axes.joined(ax1, ax2)) + assert ax1._shared_x_axes.joined(ax1, ax2) + assert ax2._shared_x_axes.joined(ax1, ax2) # don't share y - self.assertFalse(ax1._shared_y_axes.joined(ax1, ax2)) - self.assertFalse(ax2._shared_y_axes.joined(ax1, ax2)) + assert not ax1._shared_y_axes.joined(ax1, ax2) + assert not ax2._shared_y_axes.joined(ax1, ax2) - @slow + @pytest.mark.slow def test_axis_share_y(self): df = self.hist_df ax1, ax2 = df.hist(column='height', by=df.gender, sharey=True) # share y - self.assertTrue(ax1._shared_y_axes.joined(ax1, ax2)) - self.assertTrue(ax2._shared_y_axes.joined(ax1, ax2)) + assert ax1._shared_y_axes.joined(ax1, ax2) + assert ax2._shared_y_axes.joined(ax1, ax2) # don't share x - self.assertFalse(ax1._shared_x_axes.joined(ax1, ax2)) - self.assertFalse(ax2._shared_x_axes.joined(ax1, ax2)) + assert not ax1._shared_x_axes.joined(ax1, ax2) + assert not ax2._shared_x_axes.joined(ax1, ax2) - @slow + @pytest.mark.slow def test_axis_share_xy(self): df = self.hist_df ax1, ax2 = df.hist(column='height', by=df.gender, sharex=True, sharey=True) # share both x and y - self.assertTrue(ax1._shared_x_axes.joined(ax1, ax2)) - self.assertTrue(ax2._shared_x_axes.joined(ax1, ax2)) + assert ax1._shared_x_axes.joined(ax1, ax2) + assert ax2._shared_x_axes.joined(ax1, ax2) - self.assertTrue(ax1._shared_y_axes.joined(ax1, ax2)) - self.assertTrue(ax2._shared_y_axes.joined(ax1, ax2)) + assert ax1._shared_y_axes.joined(ax1, ax2) + assert ax2._shared_y_axes.joined(ax1, ax2) diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index 11f00386ec592..98248586f3d27 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -2,91 +2,69 @@ """ Test cases for misc plot functions """ -from pandas import Series, DataFrame -from pandas.compat import lmap -import pandas.util.testing as tm -from pandas.util.testing import slow - import numpy as np from numpy import random from numpy.random import randn +import pytest + +from pandas.compat import lmap +import pandas.util._test_decorators as td + +from pandas import DataFrame +from pandas.tests.plotting.common import TestPlotBase, _check_plot_works +import pandas.util.testing as tm -import pandas.tools.plotting as plotting -from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works, - _ok_for_gaussian_kde) +import pandas.plotting as plotting -@tm.mplskip +@td.skip_if_mpl +def test_import_error_message(): + # GH-19810 + df = DataFrame({"A": [1, 2]}) + + with pytest.raises(ImportError, match='matplotlib is required'): + df.plot() + + +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): - def setUp(self): - TestPlotBase.setUp(self) + def setup_method(self, method): + TestPlotBase.setup_method(self, method) import matplotlib as mpl mpl.rcdefaults() self.ts = tm.makeTimeSeries() self.ts.name = 'ts' - @slow + @pytest.mark.slow def test_autocorrelation_plot(self): - from pandas.tools.plotting import autocorrelation_plot + from pandas.plotting import autocorrelation_plot _check_plot_works(autocorrelation_plot, series=self.ts) _check_plot_works(autocorrelation_plot, series=self.ts.values) ax = autocorrelation_plot(self.ts, label='Test') self._check_legend_labels(ax, labels=['Test']) - @slow + @pytest.mark.slow def test_lag_plot(self): - from pandas.tools.plotting import lag_plot + from pandas.plotting import lag_plot _check_plot_works(lag_plot, series=self.ts) _check_plot_works(lag_plot, series=self.ts, lag=5) - @slow + @pytest.mark.slow def test_bootstrap_plot(self): - from pandas.tools.plotting import bootstrap_plot + from pandas.plotting import bootstrap_plot _check_plot_works(bootstrap_plot, series=self.ts, size=10) -@tm.mplskip +@td.skip_if_no_mpl class TestDataFramePlots(TestPlotBase): - @slow - def test_scatter_plot_legacy(self): - tm._skip_if_no_scipy() - - df = DataFrame(randn(100, 2)) - - def scat(**kwds): - return plotting.scatter_matrix(df, **kwds) - - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat) - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, marker='+') - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, vmin=0) - if _ok_for_gaussian_kde('kde'): - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, diagonal='kde') - if _ok_for_gaussian_kde('density'): - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, diagonal='density') - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, diagonal='hist') - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat, range_padding=.1) - - def scat2(x, y, by=None, ax=None, figsize=None): - return plotting.scatter_plot(df, x, y, by, ax, figsize=None) - - _check_plot_works(scat2, x=0, y=1) - grouper = Series(np.repeat([1, 2, 3, 4, 5], 20), df.index) - with tm.assert_produces_warning(UserWarning): - _check_plot_works(scat2, x=0, y=1, by=grouper) - + # This XPASSES when tested with mpl == 3.0.1 + @td.xfail_if_mpl_2_2 + @td.skip_if_no_scipy def test_scatter_matrix_axis(self): - tm._skip_if_no_scipy() scatter_matrix = plotting.scatter_matrix with tm.RNGContext(42): @@ -99,10 +77,7 @@ def test_scatter_matrix_axis(self): axes0_labels = axes[0][0].yaxis.get_majorticklabels() # GH 5662 - if self.mpl_ge_2_0_0: - expected = ['-2', '0', '2'] - else: - expected = ['-2', '-1', '0', '1', '2'] + expected = ['-2', '0', '2'] self._check_text_labels(axes0_labels, expected) self._check_ticks_props( axes, xlabelsize=8, xrot=90, ylabelsize=8, yrot=0) @@ -114,20 +89,17 @@ def test_scatter_matrix_axis(self): axes = _check_plot_works(scatter_matrix, filterwarnings='always', frame=df, range_padding=.1) axes0_labels = axes[0][0].yaxis.get_majorticklabels() - if self.mpl_ge_2_0_0: - expected = ['-1.0', '-0.5', '0.0'] - else: - expected = ['-1.2', '-1.0', '-0.8', '-0.6', '-0.4', '-0.2', '0.0'] + expected = ['-1.0', '-0.5', '0.0'] self._check_text_labels(axes0_labels, expected) self._check_ticks_props( axes, xlabelsize=8, xrot=90, ylabelsize=8, yrot=0) - @slow - def test_andrews_curves(self): - from pandas.tools.plotting import andrews_curves + @pytest.mark.slow + def test_andrews_curves(self, iris): + from pandas.plotting import andrews_curves from matplotlib import cm - df = self.iris + df = iris _check_plot_works(andrews_curves, frame=df, class_column='Name') @@ -187,12 +159,12 @@ def test_andrews_curves(self): with tm.assert_produces_warning(FutureWarning): andrews_curves(data=df, class_column='Name') - @slow - def test_parallel_coordinates(self): - from pandas.tools.plotting import parallel_coordinates + @pytest.mark.slow + def test_parallel_coordinates(self, iris): + from pandas.plotting import parallel_coordinates from matplotlib import cm - df = self.iris + df = iris ax = _check_plot_works(parallel_coordinates, frame=df, class_column='Name') @@ -235,12 +207,34 @@ def test_parallel_coordinates(self): with tm.assert_produces_warning(FutureWarning): parallel_coordinates(df, 'Name', colors=colors) - @slow - def test_radviz(self): - from pandas.tools.plotting import radviz + # not sure if this is indicative of a problem + @pytest.mark.filterwarnings("ignore:Attempting to set:UserWarning") + def test_parallel_coordinates_with_sorted_labels(self): + """ For #15908 """ + from pandas.plotting import parallel_coordinates + + df = DataFrame({"feat": [i for i in range(30)], + "class": [2 for _ in range(10)] + + [3 for _ in range(10)] + + [1 for _ in range(10)]}) + ax = parallel_coordinates(df, 'class', sort_labels=True) + polylines, labels = ax.get_legend_handles_labels() + color_label_tuples = \ + zip([polyline.get_color() for polyline in polylines], labels) + ordered_color_label_tuples = sorted(color_label_tuples, + key=lambda x: x[1]) + prev_next_tupels = zip([i for i in ordered_color_label_tuples[0:-1]], + [i for i in ordered_color_label_tuples[1:]]) + for prev, nxt in prev_next_tupels: + # labels and colors are ordered strictly increasing + assert prev[1] < nxt[1] and prev[0] < nxt[0] + + @pytest.mark.slow + def test_radviz(self, iris): + from pandas.plotting import radviz from matplotlib import cm - df = self.iris + df = iris _check_plot_works(radviz, frame=df, class_column='Name') rgba = ('#556270', '#4ECDC4', '#C7F464') @@ -273,28 +267,96 @@ def test_radviz(self): handles, labels = ax.get_legend_handles_labels() self._check_colors(handles, facecolors=colors) - @slow - def test_subplot_titles(self): - df = self.iris.drop('Name', axis=1).head() + @pytest.mark.slow + def test_subplot_titles(self, iris): + df = iris.drop('Name', axis=1).head() # Use the column names as the subplot titles title = list(df.columns) # Case len(title) == len(df) plot = df.plot(subplots=True, title=title) - self.assertEqual([p.get_title() for p in plot], title) + assert [p.get_title() for p in plot] == title # Case len(title) > len(df) - self.assertRaises(ValueError, df.plot, subplots=True, - title=title + ["kittens > puppies"]) + msg = ("The length of `title` must equal the number of columns if" + " using `title` of type `list` and `subplots=True`") + with pytest.raises(ValueError, match=msg): + df.plot(subplots=True, title=title + ["kittens > puppies"]) # Case len(title) < len(df) - self.assertRaises(ValueError, df.plot, subplots=True, title=title[:2]) + with pytest.raises(ValueError, match=msg): + df.plot(subplots=True, title=title[:2]) # Case subplots=False and title is of type list - self.assertRaises(ValueError, df.plot, subplots=False, title=title) + msg = ("Using `title` of type `list` is not supported unless" + " `subplots=True` is passed") + with pytest.raises(ValueError, match=msg): + df.plot(subplots=False, title=title) # Case df with 3 numeric columns but layout of (2,2) plot = df.drop('SepalWidth', axis=1).plot(subplots=True, layout=(2, 2), title=title[:-1]) title_list = [ax.get_title() for sublist in plot for ax in sublist] - self.assertEqual(title_list, title[:3] + ['']) + assert title_list == title[:3] + [''] + + def test_get_standard_colors_random_seed(self): + # GH17525 + df = DataFrame(np.zeros((10, 10))) + + # Make sure that the random seed isn't reset by _get_standard_colors + plotting.parallel_coordinates(df, 0) + rand1 = random.random() + plotting.parallel_coordinates(df, 0) + rand2 = random.random() + assert rand1 != rand2 + + # Make sure it produces the same colors every time it's called + from pandas.plotting._style import _get_standard_colors + color1 = _get_standard_colors(1, color_type='random') + color2 = _get_standard_colors(1, color_type='random') + assert color1 == color2 + + def test_get_standard_colors_default_num_colors(self): + from pandas.plotting._style import _get_standard_colors + + # Make sure the default color_types returns the specified amount + color1 = _get_standard_colors(1, color_type='default') + color2 = _get_standard_colors(9, color_type='default') + color3 = _get_standard_colors(20, color_type='default') + assert len(color1) == 1 + assert len(color2) == 9 + assert len(color3) == 20 + + def test_plot_single_color(self): + # Example from #20585. All 3 bars should have the same color + df = DataFrame({'account-start': ['2017-02-03', '2017-03-03', + '2017-01-01'], + 'client': ['Alice Anders', 'Bob Baker', + 'Charlie Chaplin'], + 'balance': [-1432.32, 10.43, 30000.00], + 'db-id': [1234, 2424, 251], + 'proxy-id': [525, 1525, 2542], + 'rank': [52, 525, 32], + }) + ax = df.client.value_counts().plot.bar() + colors = lmap(lambda rect: rect.get_facecolor(), + ax.get_children()[0:3]) + assert all(color == colors[0] for color in colors) + + def test_get_standard_colors_no_appending(self): + # GH20726 + + # Make sure not to add more colors so that matplotlib can cycle + # correctly. + from matplotlib import cm + color_before = cm.gnuplot(range(5)) + color_after = plotting._style._get_standard_colors( + 1, color=color_before) + assert len(color_after) == len(color_before) + + df = DataFrame(np.random.randn(48, 4), columns=list("ABCD")) + + color_list = cm.gnuplot(np.linspace(0, 1, 16)) + p = df.A.plot.bar(figsize=(16, 7), color=color_list) + assert (p.patches[1].get_facecolor() + == p.patches[17].get_facecolor()) diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py index 8c00d606059a4..a234ea8f9416b 100644 --- a/pandas/tests/plotting/test_series.py +++ b/pandas/tests/plotting/test_series.py @@ -3,30 +3,31 @@ """ Test cases for Series.plot """ -import itertools - from datetime import datetime - -import pandas as pd -from pandas import Series, DataFrame, date_range -from pandas.compat import range, lrange -import pandas.util.testing as tm -from pandas.util.testing import slow +from itertools import chain import numpy as np from numpy.random import randn +import pytest -import pandas.tools.plotting as plotting -from pandas.tests.plotting.common import (TestPlotBase, _check_plot_works, - _skip_if_no_scipy_gaussian_kde, - _ok_for_gaussian_kde) +from pandas.compat import lrange, range +import pandas.util._test_decorators as td + +import pandas as pd +from pandas import DataFrame, Series, date_range +from pandas.tests.plotting.common import ( + TestPlotBase, _check_plot_works, _ok_for_gaussian_kde, + _skip_if_no_scipy_gaussian_kde) +import pandas.util.testing as tm +import pandas.plotting as plotting -@tm.mplskip + +@td.skip_if_no_mpl class TestSeriesPlots(TestPlotBase): - def setUp(self): - TestPlotBase.setUp(self) + def setup_method(self, method): + TestPlotBase.setup_method(self, method) import matplotlib as mpl mpl.rcdefaults() @@ -39,7 +40,7 @@ def setUp(self): self.iseries = tm.makePeriodSeries() self.iseries.name = 'iseries' - @slow + @pytest.mark.slow def test_plot(self): _check_plot_works(self.ts.plot, label='foo') _check_plot_works(self.ts.plot, use_index=False) @@ -77,88 +78,97 @@ def test_plot(self): ax = _check_plot_works(self.ts.plot, subplots=True, layout=(1, -1)) self._check_axes_shape(ax, axes_num=1, layout=(1, 1)) - @slow + @pytest.mark.slow def test_plot_figsize_and_title(self): # figsize and title - ax = self.series.plot(title='Test', figsize=(16, 8)) + _, ax = self.plt.subplots() + ax = self.series.plot(title='Test', figsize=(16, 8), ax=ax) self._check_text_labels(ax.title, 'Test') self._check_axes_shape(ax, axes_num=1, layout=(1, 1), figsize=(16, 8)) def test_dont_modify_rcParams(self): # GH 8242 - if self.mpl_ge_1_5_0: - key = 'axes.prop_cycle' - else: - key = 'axes.color_cycle' + key = 'axes.prop_cycle' colors = self.plt.rcParams[key] - Series([1, 2, 3]).plot() - self.assertEqual(colors, self.plt.rcParams[key]) + _, ax = self.plt.subplots() + Series([1, 2, 3]).plot(ax=ax) + assert colors == self.plt.rcParams[key] def test_ts_line_lim(self): - ax = self.ts.plot() + fig, ax = self.plt.subplots() + ax = self.ts.plot(ax=ax) xmin, xmax = ax.get_xlim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data(orig=False)[0][0]) - self.assertEqual(xmax, lines[0].get_data(orig=False)[0][-1]) + assert xmin <= lines[0].get_data(orig=False)[0][0] + assert xmax >= lines[0].get_data(orig=False)[0][-1] tm.close() - ax = self.ts.plot(secondary_y=True) + ax = self.ts.plot(secondary_y=True, ax=ax) xmin, xmax = ax.get_xlim() lines = ax.get_lines() - self.assertEqual(xmin, lines[0].get_data(orig=False)[0][0]) - self.assertEqual(xmax, lines[0].get_data(orig=False)[0][-1]) + assert xmin <= lines[0].get_data(orig=False)[0][0] + assert xmax >= lines[0].get_data(orig=False)[0][-1] def test_ts_area_lim(self): - ax = self.ts.plot.area(stacked=False) + _, ax = self.plt.subplots() + ax = self.ts.plot.area(stacked=False, ax=ax) xmin, xmax = ax.get_xlim() line = ax.get_lines()[0].get_data(orig=False)[0] - self.assertEqual(xmin, line[0]) - self.assertEqual(xmax, line[-1]) + assert xmin <= line[0] + assert xmax >= line[-1] tm.close() # GH 7471 - ax = self.ts.plot.area(stacked=False, x_compat=True) + _, ax = self.plt.subplots() + ax = self.ts.plot.area(stacked=False, x_compat=True, ax=ax) xmin, xmax = ax.get_xlim() line = ax.get_lines()[0].get_data(orig=False)[0] - self.assertEqual(xmin, line[0]) - self.assertEqual(xmax, line[-1]) + assert xmin <= line[0] + assert xmax >= line[-1] tm.close() tz_ts = self.ts.copy() tz_ts.index = tz_ts.tz_localize('GMT').tz_convert('CET') - ax = tz_ts.plot.area(stacked=False, x_compat=True) + _, ax = self.plt.subplots() + ax = tz_ts.plot.area(stacked=False, x_compat=True, ax=ax) xmin, xmax = ax.get_xlim() line = ax.get_lines()[0].get_data(orig=False)[0] - self.assertEqual(xmin, line[0]) - self.assertEqual(xmax, line[-1]) + assert xmin <= line[0] + assert xmax >= line[-1] tm.close() - ax = tz_ts.plot.area(stacked=False, secondary_y=True) + _, ax = self.plt.subplots() + ax = tz_ts.plot.area(stacked=False, secondary_y=True, ax=ax) xmin, xmax = ax.get_xlim() line = ax.get_lines()[0].get_data(orig=False)[0] - self.assertEqual(xmin, line[0]) - self.assertEqual(xmax, line[-1]) + assert xmin <= line[0] + assert xmax >= line[-1] def test_label(self): s = Series([1, 2]) - ax = s.plot(label='LABEL', legend=True) + _, ax = self.plt.subplots() + ax = s.plot(label='LABEL', legend=True, ax=ax) self._check_legend_labels(ax, labels=['LABEL']) self.plt.close() - ax = s.plot(legend=True) + _, ax = self.plt.subplots() + ax = s.plot(legend=True, ax=ax) self._check_legend_labels(ax, labels=['None']) self.plt.close() # get name from index s.name = 'NAME' - ax = s.plot(legend=True) + _, ax = self.plt.subplots() + ax = s.plot(legend=True, ax=ax) self._check_legend_labels(ax, labels=['NAME']) self.plt.close() # override the default - ax = s.plot(legend=True, label='LABEL') + _, ax = self.plt.subplots() + ax = s.plot(legend=True, label='LABEL', ax=ax) self._check_legend_labels(ax, labels=['LABEL']) self.plt.close() # Add lebel info, but don't draw - ax = s.plot(legend=False, label='LABEL') - self.assertEqual(ax.get_legend(), None) # Hasn't been drawn + _, ax = self.plt.subplots() + ax = s.plot(legend=False, label='LABEL', ax=ax) + assert ax.get_legend() is None # Hasn't been drawn ax.legend() # draw it self._check_legend_labels(ax, labels=['LABEL']) @@ -172,91 +182,113 @@ def test_line_area_nan_series(self): masked = ax.lines[0].get_ydata() # remove nan for comparison purpose exp = np.array([1, 2, 3], dtype=np.float64) - self.assert_numpy_array_equal(np.delete(masked.data, 2), exp) - self.assert_numpy_array_equal( + tm.assert_numpy_array_equal(np.delete(masked.data, 2), exp) + tm.assert_numpy_array_equal( masked.mask, np.array([False, False, True, False])) expected = np.array([1, 2, 0, 3], dtype=np.float64) ax = _check_plot_works(d.plot, stacked=True) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) ax = _check_plot_works(d.plot.area) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) ax = _check_plot_works(d.plot.area, stacked=False) - self.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) + tm.assert_numpy_array_equal(ax.lines[0].get_ydata(), expected) def test_line_use_index_false(self): s = Series([1, 2, 3], index=['a', 'b', 'c']) s.index.name = 'The Index' - ax = s.plot(use_index=False) + _, ax = self.plt.subplots() + ax = s.plot(use_index=False, ax=ax) label = ax.get_xlabel() - self.assertEqual(label, '') - ax2 = s.plot.bar(use_index=False) + assert label == '' + _, ax = self.plt.subplots() + ax2 = s.plot.bar(use_index=False, ax=ax) label2 = ax2.get_xlabel() - self.assertEqual(label2, '') + assert label2 == '' - @slow + @pytest.mark.slow def test_bar_log(self): - expected = np.array([1., 10., 100., 1000.]) - - if not self.mpl_le_1_2_1: - expected = np.hstack((.1, expected, 1e4)) + expected = np.array([1e-1, 1e0, 1e1, 1e2, 1e3, 1e4]) - ax = Series([200, 500]).plot.bar(log=True) + _, ax = self.plt.subplots() + ax = Series([200, 500]).plot.bar(log=True, ax=ax) tm.assert_numpy_array_equal(ax.yaxis.get_ticklocs(), expected) tm.close() - ax = Series([200, 500]).plot.barh(log=True) + _, ax = self.plt.subplots() + ax = Series([200, 500]).plot.barh(log=True, ax=ax) tm.assert_numpy_array_equal(ax.xaxis.get_ticklocs(), expected) tm.close() # GH 9905 - expected = np.array([1.0e-03, 1.0e-02, 1.0e-01, 1.0e+00]) - - if not self.mpl_le_1_2_1: - expected = np.hstack((1.0e-04, expected, 1.0e+01)) - if self.mpl_ge_2_0_0: - expected = np.hstack((1.0e-05, expected)) + expected = np.array([1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e0, 1e1]) - ax = Series([0.1, 0.01, 0.001]).plot(log=True, kind='bar') - ymin = 0.0007943282347242822 if self.mpl_ge_2_0_0 else 0.001 - ymax = 0.12589254117941673 if self.mpl_ge_2_0_0 else .10000000000000001 + _, ax = self.plt.subplots() + ax = Series([0.1, 0.01, 0.001]).plot(log=True, kind='bar', ax=ax) + ymin = 0.0007943282347242822 + ymax = 0.12589254117941673 res = ax.get_ylim() - self.assertAlmostEqual(res[0], ymin) - self.assertAlmostEqual(res[1], ymax) + tm.assert_almost_equal(res[0], ymin) + tm.assert_almost_equal(res[1], ymax) tm.assert_numpy_array_equal(ax.yaxis.get_ticklocs(), expected) tm.close() - ax = Series([0.1, 0.01, 0.001]).plot(log=True, kind='barh') + _, ax = self.plt.subplots() + ax = Series([0.1, 0.01, 0.001]).plot(log=True, kind='barh', ax=ax) res = ax.get_xlim() - self.assertAlmostEqual(res[0], ymin) - self.assertAlmostEqual(res[1], ymax) + tm.assert_almost_equal(res[0], ymin) + tm.assert_almost_equal(res[1], ymax) tm.assert_numpy_array_equal(ax.xaxis.get_ticklocs(), expected) - @slow + @pytest.mark.slow def test_bar_ignore_index(self): df = Series([1, 2, 3, 4], index=['a', 'b', 'c', 'd']) - ax = df.plot.bar(use_index=False) + _, ax = self.plt.subplots() + ax = df.plot.bar(use_index=False, ax=ax) self._check_text_labels(ax.get_xticklabels(), ['0', '1', '2', '3']) + def test_bar_user_colors(self): + s = Series([1, 2, 3, 4]) + ax = s.plot.bar(color=['red', 'blue', 'blue', 'red']) + result = [p.get_facecolor() for p in ax.patches] + expected = [(1., 0., 0., 1.), + (0., 0., 1., 1.), + (0., 0., 1., 1.), + (1., 0., 0., 1.)] + assert result == expected + def test_rotation(self): df = DataFrame(randn(5, 5)) # Default rot 0 - axes = df.plot() + _, ax = self.plt.subplots() + axes = df.plot(ax=ax) self._check_ticks_props(axes, xrot=0) - axes = df.plot(rot=30) + _, ax = self.plt.subplots() + axes = df.plot(rot=30, ax=ax) self._check_ticks_props(axes, xrot=30) def test_irregular_datetime(self): rng = date_range('1/1/2000', '3/1/2000') rng = rng[[0, 1, 2, 3, 5, 9, 10, 11, 12]] ser = Series(randn(len(rng)), rng) - ax = ser.plot() + _, ax = self.plt.subplots() + ax = ser.plot(ax=ax) xp = datetime(1999, 1, 1).toordinal() ax.set_xlim('1/1/1999', '1/1/2001') - self.assertEqual(xp, ax.get_xlim()[0]) + assert xp == ax.get_xlim()[0] + + def test_unsorted_index_xlim(self): + ser = Series([0., 1., np.nan, 3., 4., 5., 6.], + index=[1., 0., 3., 2., np.nan, 3., 2.]) + _, ax = self.plt.subplots() + ax = ser.plot(ax=ax) + xmin, xmax = ax.get_xlim() + lines = ax.get_lines() + assert xmin <= np.nanmin(lines[0].get_data(orig=False)[0]) + assert xmax >= np.nanmax(lines[0].get_data(orig=False)[0]) - @slow + @pytest.mark.slow def test_pie_series(self): # if sum of values is less than 1.0, pie handle them as rate and draw # semicircle. @@ -264,7 +296,7 @@ def test_pie_series(self): index=['a', 'b', 'c', 'd', 'e'], name='YLABEL') ax = _check_plot_works(series.plot.pie) self._check_text_labels(ax.texts, series.index) - self.assertEqual(ax.get_ylabel(), 'YLABEL') + assert ax.get_ylabel() == 'YLABEL' # without wedge labels ax = _check_plot_works(series.plot.pie, labels=None) @@ -290,14 +322,13 @@ def test_pie_series(self): autopct='%.2f', fontsize=7) pcts = ['{0:.2f}'.format(s * 100) for s in series.values / float(series.sum())] - iters = [iter(series.index), iter(pcts)] - expected_texts = list(next(it) for it in itertools.cycle(iters)) + expected_texts = list(chain.from_iterable(zip(series.index, pcts))) self._check_text_labels(ax.texts, expected_texts) for t in ax.texts: - self.assertEqual(t.get_fontsize(), 7) + assert t.get_fontsize() == 7 # includes negative value - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): series = Series([1, 2, 0, 4, -1], index=['a', 'b', 'c', 'd', 'e']) series.plot.pie() @@ -309,31 +340,35 @@ def test_pie_series(self): def test_pie_nan(self): s = Series([1, np.nan, 1, 1]) - ax = s.plot.pie(legend=True) + _, ax = self.plt.subplots() + ax = s.plot.pie(legend=True, ax=ax) expected = ['0', '', '2', '3'] result = [x.get_text() for x in ax.texts] - self.assertEqual(result, expected) + assert result == expected - @slow + @pytest.mark.slow def test_hist_df_kwargs(self): df = DataFrame(np.random.randn(10, 2)) - ax = df.plot.hist(bins=5) - self.assertEqual(len(ax.patches), 10) + _, ax = self.plt.subplots() + ax = df.plot.hist(bins=5, ax=ax) + assert len(ax.patches) == 10 - @slow + @pytest.mark.slow def test_hist_df_with_nonnumerics(self): # GH 9853 with tm.RNGContext(1): df = DataFrame( np.random.randn(10, 4), columns=['A', 'B', 'C', 'D']) df['E'] = ['x', 'y'] * 5 - ax = df.plot.hist(bins=5) - self.assertEqual(len(ax.patches), 20) + _, ax = self.plt.subplots() + ax = df.plot.hist(bins=5, ax=ax) + assert len(ax.patches) == 20 - ax = df.plot.hist() # bins=10 - self.assertEqual(len(ax.patches), 40) + _, ax = self.plt.subplots() + ax = df.plot.hist(ax=ax) # bins=10 + assert len(ax.patches) == 40 - @slow + @pytest.mark.slow def test_hist_legacy(self): _check_plot_works(self.ts.hist) _check_plot_works(self.ts.hist, grid=False) @@ -356,25 +391,25 @@ def test_hist_legacy(self): _check_plot_works(self.ts.hist, figure=fig, ax=ax1) _check_plot_works(self.ts.hist, figure=fig, ax=ax2) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): self.ts.hist(by=self.ts.index, figure=fig) - @slow + @pytest.mark.slow def test_hist_bins_legacy(self): df = DataFrame(np.random.randn(10, 2)) ax = df.hist(bins=2)[0][0] - self.assertEqual(len(ax.patches), 2) + assert len(ax.patches) == 2 - @slow + @pytest.mark.slow def test_hist_layout(self): df = self.hist_df - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.height.hist(layout=(1, 1)) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): df.height.hist(layout=[1, 1]) - @slow + @pytest.mark.slow def test_hist_layout_with_by(self): df = self.hist_df @@ -418,7 +453,7 @@ def test_hist_layout_with_by(self): self._check_axes_shape(axes, axes_num=4, layout=(4, 2), figsize=(12, 7)) - @slow + @pytest.mark.slow def test_hist_no_overlap(self): from matplotlib.pyplot import subplot, gcf x = Series(randn(2)) @@ -428,114 +463,138 @@ def test_hist_no_overlap(self): subplot(122) y.hist() fig = gcf() - axes = fig.axes if self.mpl_ge_1_5_0 else fig.get_axes() - self.assertEqual(len(axes), 2) + axes = fig.axes + assert len(axes) == 2 - @slow + @pytest.mark.slow def test_hist_secondary_legend(self): # GH 9610 df = DataFrame(np.random.randn(30, 4), columns=list('abcd')) # primary -> secondary - ax = df['a'].plot.hist(legend=True) + _, ax = self.plt.subplots() + ax = df['a'].plot.hist(legend=True, ax=ax) df['b'].plot.hist(ax=ax, legend=True, secondary_y=True) # both legends are dran on left ax # left and right axis must be visible self._check_legend_labels(ax, labels=['a', 'b (right)']) - self.assertTrue(ax.get_yaxis().get_visible()) - self.assertTrue(ax.right_ax.get_yaxis().get_visible()) + assert ax.get_yaxis().get_visible() + assert ax.right_ax.get_yaxis().get_visible() tm.close() # secondary -> secondary - ax = df['a'].plot.hist(legend=True, secondary_y=True) + _, ax = self.plt.subplots() + ax = df['a'].plot.hist(legend=True, secondary_y=True, ax=ax) df['b'].plot.hist(ax=ax, legend=True, secondary_y=True) # both legends are draw on left ax # left axis must be invisible, right axis must be visible self._check_legend_labels(ax.left_ax, labels=['a (right)', 'b (right)']) - self.assertFalse(ax.left_ax.get_yaxis().get_visible()) - self.assertTrue(ax.get_yaxis().get_visible()) + assert not ax.left_ax.get_yaxis().get_visible() + assert ax.get_yaxis().get_visible() tm.close() # secondary -> primary - ax = df['a'].plot.hist(legend=True, secondary_y=True) + _, ax = self.plt.subplots() + ax = df['a'].plot.hist(legend=True, secondary_y=True, ax=ax) # right axes is returned df['b'].plot.hist(ax=ax, legend=True) # both legends are draw on left ax # left and right axis must be visible self._check_legend_labels(ax.left_ax, labels=['a (right)', 'b']) - self.assertTrue(ax.left_ax.get_yaxis().get_visible()) - self.assertTrue(ax.get_yaxis().get_visible()) + assert ax.left_ax.get_yaxis().get_visible() + assert ax.get_yaxis().get_visible() tm.close() - @slow + @pytest.mark.slow def test_df_series_secondary_legend(self): # GH 9779 df = DataFrame(np.random.randn(30, 3), columns=list('abc')) s = Series(np.random.randn(30), name='x') # primary -> secondary (without passing ax) - ax = df.plot() - s.plot(legend=True, secondary_y=True) + _, ax = self.plt.subplots() + ax = df.plot(ax=ax) + s.plot(legend=True, secondary_y=True, ax=ax) # both legends are dran on left ax # left and right axis must be visible self._check_legend_labels(ax, labels=['a', 'b', 'c', 'x (right)']) - self.assertTrue(ax.get_yaxis().get_visible()) - self.assertTrue(ax.right_ax.get_yaxis().get_visible()) + assert ax.get_yaxis().get_visible() + assert ax.right_ax.get_yaxis().get_visible() tm.close() # primary -> secondary (with passing ax) - ax = df.plot() + _, ax = self.plt.subplots() + ax = df.plot(ax=ax) s.plot(ax=ax, legend=True, secondary_y=True) # both legends are dran on left ax # left and right axis must be visible self._check_legend_labels(ax, labels=['a', 'b', 'c', 'x (right)']) - self.assertTrue(ax.get_yaxis().get_visible()) - self.assertTrue(ax.right_ax.get_yaxis().get_visible()) + assert ax.get_yaxis().get_visible() + assert ax.right_ax.get_yaxis().get_visible() tm.close() # seconcary -> secondary (without passing ax) - ax = df.plot(secondary_y=True) - s.plot(legend=True, secondary_y=True) + _, ax = self.plt.subplots() + ax = df.plot(secondary_y=True, ax=ax) + s.plot(legend=True, secondary_y=True, ax=ax) # both legends are dran on left ax # left axis must be invisible and right axis must be visible expected = ['a (right)', 'b (right)', 'c (right)', 'x (right)'] self._check_legend_labels(ax.left_ax, labels=expected) - self.assertFalse(ax.left_ax.get_yaxis().get_visible()) - self.assertTrue(ax.get_yaxis().get_visible()) + assert not ax.left_ax.get_yaxis().get_visible() + assert ax.get_yaxis().get_visible() tm.close() # secondary -> secondary (with passing ax) - ax = df.plot(secondary_y=True) + _, ax = self.plt.subplots() + ax = df.plot(secondary_y=True, ax=ax) s.plot(ax=ax, legend=True, secondary_y=True) # both legends are dran on left ax # left axis must be invisible and right axis must be visible expected = ['a (right)', 'b (right)', 'c (right)', 'x (right)'] self._check_legend_labels(ax.left_ax, expected) - self.assertFalse(ax.left_ax.get_yaxis().get_visible()) - self.assertTrue(ax.get_yaxis().get_visible()) + assert not ax.left_ax.get_yaxis().get_visible() + assert ax.get_yaxis().get_visible() tm.close() # secondary -> secondary (with passing ax) - ax = df.plot(secondary_y=True, mark_right=False) + _, ax = self.plt.subplots() + ax = df.plot(secondary_y=True, mark_right=False, ax=ax) s.plot(ax=ax, legend=True, secondary_y=True) # both legends are dran on left ax # left axis must be invisible and right axis must be visible expected = ['a', 'b', 'c', 'x (right)'] self._check_legend_labels(ax.left_ax, expected) - self.assertFalse(ax.left_ax.get_yaxis().get_visible()) - self.assertTrue(ax.get_yaxis().get_visible()) + assert not ax.left_ax.get_yaxis().get_visible() + assert ax.get_yaxis().get_visible() tm.close() - @slow + @pytest.mark.slow + def test_secondary_logy(self): + # GH 25545 + s1 = Series(np.random.randn(30)) + s2 = Series(np.random.randn(30)) + + ax1 = s1.plot(logy=True) + ax2 = s2.plot(secondary_y=True, logy=True) + + assert ax1.get_yscale() == 'log' + assert ax2.get_yscale() == 'log' + + @pytest.mark.slow def test_plot_fails_with_dupe_color_and_style(self): x = Series(randn(2)) - with tm.assertRaises(ValueError): - x.plot(style='k--', color='k') + with pytest.raises(ValueError): + _, ax = self.plt.subplots() + x.plot(style='k--', color='k', ax=ax) - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_hist_kde(self): - ax = self.ts.plot.hist(logy=True) + + _, ax = self.plt.subplots() + ax = self.ts.plot.hist(logy=True, ax=ax) self._check_ax_scales(ax, yaxis='log') xlabels = ax.get_xticklabels() # ticks are values, thus ticklabels are blank @@ -543,122 +602,136 @@ def test_hist_kde(self): ylabels = ax.get_yticklabels() self._check_text_labels(ylabels, [''] * len(ylabels)) - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() _check_plot_works(self.ts.plot.kde) _check_plot_works(self.ts.plot.density) - ax = self.ts.plot.kde(logy=True) + _, ax = self.plt.subplots() + ax = self.ts.plot.kde(logy=True, ax=ax) self._check_ax_scales(ax, yaxis='log') xlabels = ax.get_xticklabels() self._check_text_labels(xlabels, [''] * len(xlabels)) ylabels = ax.get_yticklabels() self._check_text_labels(ylabels, [''] * len(ylabels)) - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_kwargs(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() - from numpy import linspace - _check_plot_works(self.ts.plot.kde, bw_method=.5, - ind=linspace(-100, 100, 20)) + + sample_points = np.linspace(-100, 100, 20) + _check_plot_works(self.ts.plot.kde, bw_method='scott', ind=20) + _check_plot_works(self.ts.plot.kde, bw_method=None, ind=20) + _check_plot_works(self.ts.plot.kde, bw_method=None, ind=np.int(20)) + _check_plot_works(self.ts.plot.kde, bw_method=.5, ind=sample_points) _check_plot_works(self.ts.plot.density, bw_method=.5, - ind=linspace(-100, 100, 20)) - ax = self.ts.plot.kde(logy=True, bw_method=.5, - ind=linspace(-100, 100, 20)) + ind=sample_points) + _, ax = self.plt.subplots() + ax = self.ts.plot.kde(logy=True, bw_method=.5, ind=sample_points, + ax=ax) self._check_ax_scales(ax, yaxis='log') self._check_text_labels(ax.yaxis.get_label(), 'Density') - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_kde_missing_vals(self): - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() + s = Series(np.random.uniform(size=50)) s[0] = np.nan axes = _check_plot_works(s.plot.kde) - # check if the values have any missing values - # GH14821 - self.assertTrue(any(~np.isnan(axes.lines[0].get_xdata())), - msg='Missing Values not dropped') - @slow + # gh-14821: check if the values have any missing values + assert any(~np.isnan(axes.lines[0].get_xdata())) + + @pytest.mark.slow def test_hist_kwargs(self): - ax = self.ts.plot.hist(bins=5) - self.assertEqual(len(ax.patches), 5) + _, ax = self.plt.subplots() + ax = self.ts.plot.hist(bins=5, ax=ax) + assert len(ax.patches) == 5 self._check_text_labels(ax.yaxis.get_label(), 'Frequency') tm.close() - if self.mpl_ge_1_3_1: - ax = self.ts.plot.hist(orientation='horizontal') - self._check_text_labels(ax.xaxis.get_label(), 'Frequency') - tm.close() + _, ax = self.plt.subplots() + ax = self.ts.plot.hist(orientation='horizontal', ax=ax) + self._check_text_labels(ax.xaxis.get_label(), 'Frequency') + tm.close() - ax = self.ts.plot.hist(align='left', stacked=True) - tm.close() + _, ax = self.plt.subplots() + ax = self.ts.plot.hist(align='left', stacked=True, ax=ax) + tm.close() - @slow + @pytest.mark.slow + @td.skip_if_no_scipy def test_hist_kde_color(self): - ax = self.ts.plot.hist(logy=True, bins=10, color='b') + _, ax = self.plt.subplots() + ax = self.ts.plot.hist(logy=True, bins=10, color='b', ax=ax) self._check_ax_scales(ax, yaxis='log') - self.assertEqual(len(ax.patches), 10) + assert len(ax.patches) == 10 self._check_colors(ax.patches, facecolors=['b'] * 10) - tm._skip_if_no_scipy() _skip_if_no_scipy_gaussian_kde() - ax = self.ts.plot.kde(logy=True, color='r') + _, ax = self.plt.subplots() + ax = self.ts.plot.kde(logy=True, color='r', ax=ax) self._check_ax_scales(ax, yaxis='log') lines = ax.get_lines() - self.assertEqual(len(lines), 1) + assert len(lines) == 1 self._check_colors(lines, ['r']) - @slow + @pytest.mark.slow def test_boxplot_series(self): - ax = self.ts.plot.box(logy=True) + _, ax = self.plt.subplots() + ax = self.ts.plot.box(logy=True, ax=ax) self._check_ax_scales(ax, yaxis='log') xlabels = ax.get_xticklabels() self._check_text_labels(xlabels, [self.ts.name]) ylabels = ax.get_yticklabels() self._check_text_labels(ylabels, [''] * len(ylabels)) - @slow + @pytest.mark.slow def test_kind_both_ways(self): s = Series(range(3)) - for kind in plotting._common_kinds + plotting._series_kinds: + kinds = (plotting._core._common_kinds + + plotting._core._series_kinds) + _, ax = self.plt.subplots() + for kind in kinds: if not _ok_for_gaussian_kde(kind): continue - s.plot(kind=kind) + s.plot(kind=kind, ax=ax) getattr(s.plot, kind)() - @slow + @pytest.mark.slow def test_invalid_plot_data(self): s = Series(list('abcd')) - for kind in plotting._common_kinds: + _, ax = self.plt.subplots() + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue - with tm.assertRaises(TypeError): - s.plot(kind=kind) + with pytest.raises(TypeError): + s.plot(kind=kind, ax=ax) - @slow + @pytest.mark.slow def test_valid_object_plot(self): s = Series(lrange(10), dtype=object) - for kind in plotting._common_kinds: + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue _check_plot_works(s.plot, kind=kind) def test_partially_invalid_plot_data(self): s = Series(['a', 'b', 1.0, 2]) - for kind in plotting._common_kinds: + _, ax = self.plt.subplots() + for kind in plotting._core._common_kinds: if not _ok_for_gaussian_kde(kind): continue - with tm.assertRaises(TypeError): - s.plot(kind=kind) + with pytest.raises(TypeError): + s.plot(kind=kind, ax=ax) def test_invalid_kind(self): s = Series([1, 2]) - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): s.plot(kind='aasdf') - @slow + @pytest.mark.slow def test_dup_datetime_index_plot(self): dr1 = date_range('1/1/2009', periods=4) dr2 = date_range('1/2/2009', periods=4) @@ -667,7 +740,7 @@ def test_dup_datetime_index_plot(self): s = Series(values, index=index) _check_plot_works(s.plot) - @slow + @pytest.mark.slow def test_errorbar_plot(self): s = Series(np.arange(10), name='x') @@ -702,103 +775,109 @@ def test_errorbar_plot(self): self._check_has_errorbars(ax, xerr=0, yerr=1) # check incorrect lengths and types - with tm.assertRaises(ValueError): + with pytest.raises(ValueError): s.plot(yerr=np.arange(11)) s_err = ['zzz'] * 10 - # in mpl 1.5+ this is a TypeError - with tm.assertRaises((ValueError, TypeError)): + # MPL > 2.0.0 will most likely use TypeError here + with pytest.raises((TypeError, ValueError)): s.plot(yerr=s_err) + # This XPASSES when tested with mpl == 3.0.1 + @td.xfail_if_mpl_2_2 def test_table(self): _check_plot_works(self.series.plot, table=True) _check_plot_works(self.series.plot, table=self.series) - @slow + @pytest.mark.slow def test_series_grid_settings(self): # Make sure plot defaults to rcParams['axes.grid'] setting, GH 9792 self._check_grid_settings(Series([1, 2, 3]), - plotting._series_kinds + - plotting._common_kinds) + plotting._core._series_kinds + + plotting._core._common_kinds) - @slow + @pytest.mark.slow def test_standard_colors(self): + from pandas.plotting._style import _get_standard_colors + for c in ['r', 'red', 'green', '#FF0000']: - result = plotting._get_standard_colors(1, color=c) - self.assertEqual(result, [c]) + result = _get_standard_colors(1, color=c) + assert result == [c] - result = plotting._get_standard_colors(1, color=[c]) - self.assertEqual(result, [c]) + result = _get_standard_colors(1, color=[c]) + assert result == [c] - result = plotting._get_standard_colors(3, color=c) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(3, color=c) + assert result == [c] * 3 - result = plotting._get_standard_colors(3, color=[c]) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(3, color=[c]) + assert result == [c] * 3 - @slow + @pytest.mark.slow def test_standard_colors_all(self): import matplotlib.colors as colors + from pandas.plotting._style import _get_standard_colors # multiple colors like mediumaquamarine for c in colors.cnames: - result = plotting._get_standard_colors(num_colors=1, color=c) - self.assertEqual(result, [c]) + result = _get_standard_colors(num_colors=1, color=c) + assert result == [c] - result = plotting._get_standard_colors(num_colors=1, color=[c]) - self.assertEqual(result, [c]) + result = _get_standard_colors(num_colors=1, color=[c]) + assert result == [c] - result = plotting._get_standard_colors(num_colors=3, color=c) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(num_colors=3, color=c) + assert result == [c] * 3 - result = plotting._get_standard_colors(num_colors=3, color=[c]) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(num_colors=3, color=[c]) + assert result == [c] * 3 # single letter colors like k for c in colors.ColorConverter.colors: - result = plotting._get_standard_colors(num_colors=1, color=c) - self.assertEqual(result, [c]) + result = _get_standard_colors(num_colors=1, color=c) + assert result == [c] - result = plotting._get_standard_colors(num_colors=1, color=[c]) - self.assertEqual(result, [c]) + result = _get_standard_colors(num_colors=1, color=[c]) + assert result == [c] - result = plotting._get_standard_colors(num_colors=3, color=c) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(num_colors=3, color=c) + assert result == [c] * 3 - result = plotting._get_standard_colors(num_colors=3, color=[c]) - self.assertEqual(result, [c] * 3) + result = _get_standard_colors(num_colors=3, color=[c]) + assert result == [c] * 3 def test_series_plot_color_kwargs(self): # GH1890 - ax = Series(np.arange(12) + 1).plot(color='green') + _, ax = self.plt.subplots() + ax = Series(np.arange(12) + 1).plot(color='green', ax=ax) self._check_colors(ax.get_lines(), linecolors=['green']) def test_time_series_plot_color_kwargs(self): # #1890 + _, ax = self.plt.subplots() ax = Series(np.arange(12) + 1, index=date_range( - '1/1/2000', periods=12)).plot(color='green') + '1/1/2000', periods=12)).plot(color='green', ax=ax) self._check_colors(ax.get_lines(), linecolors=['green']) def test_time_series_plot_color_with_empty_kwargs(self): import matplotlib as mpl - if self.mpl_ge_1_5_0: - def_colors = self._maybe_unpack_cycler(mpl.rcParams) - else: - def_colors = mpl.rcParams['axes.color_cycle'] + def_colors = self._unpack_cycler(mpl.rcParams) index = date_range('1/1/2000', periods=12) s = Series(np.arange(1, 13), index=index) ncolors = 3 + _, ax = self.plt.subplots() for i in range(ncolors): - ax = s.plot() + ax = s.plot(ax=ax) self._check_colors(ax.get_lines(), linecolors=def_colors[:ncolors]) def test_xticklabels(self): # GH11529 s = Series(np.arange(10), index=['P%02d' % i for i in range(10)]) - ax = s.plot(xticks=[0, 3, 5, 9]) + _, ax = self.plt.subplots() + ax = s.plot(xticks=[0, 3, 5, 9], ax=ax) exp = ['P%02d' % i for i in [0, 3, 5, 9]] self._check_text_labels(ax.get_xticklabels(), exp) @@ -810,3 +889,15 @@ def test_custom_business_day_freq(self): freq=CustomBusinessDay(holidays=['2014-05-26']))) _check_plot_works(s.plot) + + @pytest.mark.xfail + def test_plot_accessor_updates_on_inplace(self): + s = Series([1, 2, 3, 4]) + _, ax = self.plt.subplots() + ax = s.plot(ax=ax) + before = ax.xaxis.get_ticklocs() + + s.drop([0, 1], inplace=True) + _, ax = self.plt.subplots() + after = ax.xaxis.get_ticklocs() + tm.assert_numpy_array_equal(before, after) diff --git a/pandas/tests/reductions/__init__.py b/pandas/tests/reductions/__init__.py new file mode 100644 index 0000000000000..e3851753b6742 --- /dev/null +++ b/pandas/tests/reductions/__init__.py @@ -0,0 +1,4 @@ +""" +Tests for reductions where we want to test for matching behavior across +Array, Index, Series, and DataFrame methods. +""" diff --git a/pandas/tests/reductions/test_reductions.py b/pandas/tests/reductions/test_reductions.py new file mode 100644 index 0000000000000..fbf7f610688ba --- /dev/null +++ b/pandas/tests/reductions/test_reductions.py @@ -0,0 +1,1161 @@ +# -*- coding: utf-8 -*- +from datetime import datetime, timedelta + +import numpy as np +import pytest + +import pandas as pd +from pandas import ( + Categorical, DataFrame, DatetimeIndex, Index, NaT, Period, PeriodIndex, + RangeIndex, Series, Timedelta, TimedeltaIndex, Timestamp, compat, isna, + timedelta_range, to_timedelta) +from pandas.core import nanops +import pandas.util.testing as tm + + +def get_objs(): + indexes = [ + tm.makeBoolIndex(10, name='a'), + tm.makeIntIndex(10, name='a'), + tm.makeFloatIndex(10, name='a'), + tm.makeDateIndex(10, name='a'), + tm.makeDateIndex(10, name='a').tz_localize(tz='US/Eastern'), + tm.makePeriodIndex(10, name='a'), + tm.makeStringIndex(10, name='a'), + tm.makeUnicodeIndex(10, name='a') + ] + + arr = np.random.randn(10) + series = [Series(arr, index=idx, name='a') for idx in indexes] + + objs = indexes + series + return objs + + +objs = get_objs() + + +class TestReductions(object): + + @pytest.mark.parametrize('opname', ['max', 'min']) + @pytest.mark.parametrize('obj', objs) + def test_ops(self, opname, obj): + result = getattr(obj, opname)() + if not isinstance(obj, PeriodIndex): + expected = getattr(obj.values, opname)() + else: + expected = pd.Period( + ordinal=getattr(obj._ndarray_values, opname)(), + freq=obj.freq) + try: + assert result == expected + except TypeError: + # comparing tz-aware series with np.array results in + # TypeError + expected = expected.astype('M8[ns]').astype('int64') + assert result.value == expected + + def test_nanops(self): + # GH#7261 + for opname in ['max', 'min']: + for klass in [Index, Series]: + arg_op = 'arg' + opname if klass is Index else 'idx' + opname + + obj = klass([np.nan, 2.0]) + assert getattr(obj, opname)() == 2.0 + + obj = klass([np.nan]) + assert pd.isna(getattr(obj, opname)()) + assert pd.isna(getattr(obj, opname)(skipna=False)) + + obj = klass([]) + assert pd.isna(getattr(obj, opname)()) + assert pd.isna(getattr(obj, opname)(skipna=False)) + + obj = klass([pd.NaT, datetime(2011, 11, 1)]) + # check DatetimeIndex monotonic path + assert getattr(obj, opname)() == datetime(2011, 11, 1) + assert getattr(obj, opname)(skipna=False) is pd.NaT + + assert getattr(obj, arg_op)() == 1 + result = getattr(obj, arg_op)(skipna=False) + if klass is Series: + assert np.isnan(result) + else: + assert result == -1 + + obj = klass([pd.NaT, datetime(2011, 11, 1), pd.NaT]) + # check DatetimeIndex non-monotonic path + assert getattr(obj, opname)(), datetime(2011, 11, 1) + assert getattr(obj, opname)(skipna=False) is pd.NaT + + assert getattr(obj, arg_op)() == 1 + result = getattr(obj, arg_op)(skipna=False) + if klass is Series: + assert np.isnan(result) + else: + assert result == -1 + + for dtype in ["M8[ns]", "datetime64[ns, UTC]"]: + # cases with empty Series/DatetimeIndex + obj = klass([], dtype=dtype) + + assert getattr(obj, opname)() is pd.NaT + assert getattr(obj, opname)(skipna=False) is pd.NaT + + with pytest.raises(ValueError, match="empty sequence"): + getattr(obj, arg_op)() + with pytest.raises(ValueError, match="empty sequence"): + getattr(obj, arg_op)(skipna=False) + + # argmin/max + obj = Index(np.arange(5, dtype='int64')) + assert obj.argmin() == 0 + assert obj.argmax() == 4 + + obj = Index([np.nan, 1, np.nan, 2]) + assert obj.argmin() == 1 + assert obj.argmax() == 3 + assert obj.argmin(skipna=False) == -1 + assert obj.argmax(skipna=False) == -1 + + obj = Index([np.nan]) + assert obj.argmin() == -1 + assert obj.argmax() == -1 + assert obj.argmin(skipna=False) == -1 + assert obj.argmax(skipna=False) == -1 + + obj = Index([pd.NaT, datetime(2011, 11, 1), datetime(2011, 11, 2), + pd.NaT]) + assert obj.argmin() == 1 + assert obj.argmax() == 2 + assert obj.argmin(skipna=False) == -1 + assert obj.argmax(skipna=False) == -1 + + obj = Index([pd.NaT]) + assert obj.argmin() == -1 + assert obj.argmax() == -1 + assert obj.argmin(skipna=False) == -1 + assert obj.argmax(skipna=False) == -1 + + @pytest.mark.parametrize('op, expected_col', [ + ['max', 'a'], ['min', 'b'] + ]) + def test_same_tz_min_max_axis_1(self, op, expected_col): + # GH 10390 + df = DataFrame(pd.date_range('2016-01-01 00:00:00', periods=3, + tz='UTC'), + columns=['a']) + df['b'] = df.a.subtract(pd.Timedelta(seconds=3600)) + result = getattr(df, op)(axis=1) + expected = df[expected_col] + tm.assert_series_equal(result, expected) + + +class TestIndexReductions(object): + # Note: the name TestIndexReductions indicates these tests + # were moved from a Index-specific test file, _not_ that these tests are + # intended long-term to be Index-specific + + @pytest.mark.parametrize('start,stop,step', + [(0, 400, 3), (500, 0, -6), (-10**6, 10**6, 4), + (10**6, -10**6, -4), (0, 10, 20)]) + def test_max_min_range(self, start, stop, step): + # GH#17607 + idx = RangeIndex(start, stop, step) + expected = idx._int64index.max() + result = idx.max() + assert result == expected + + # skipna should be irrelevant since RangeIndex should never have NAs + result2 = idx.max(skipna=False) + assert result2 == expected + + expected = idx._int64index.min() + result = idx.min() + assert result == expected + + # skipna should be irrelevant since RangeIndex should never have NAs + result2 = idx.min(skipna=False) + assert result2 == expected + + # empty + idx = RangeIndex(start, stop, -step) + assert isna(idx.max()) + assert isna(idx.min()) + + def test_minmax_timedelta64(self): + + # monotonic + idx1 = TimedeltaIndex(['1 days', '2 days', '3 days']) + assert idx1.is_monotonic + + # non-monotonic + idx2 = TimedeltaIndex(['1 days', np.nan, '3 days', 'NaT']) + assert not idx2.is_monotonic + + for idx in [idx1, idx2]: + assert idx.min() == Timedelta('1 days') + assert idx.max() == Timedelta('3 days') + assert idx.argmin() == 0 + assert idx.argmax() == 2 + + for op in ['min', 'max']: + # Return NaT + obj = TimedeltaIndex([]) + assert pd.isna(getattr(obj, op)()) + + obj = TimedeltaIndex([pd.NaT]) + assert pd.isna(getattr(obj, op)()) + + obj = TimedeltaIndex([pd.NaT, pd.NaT, pd.NaT]) + assert pd.isna(getattr(obj, op)()) + + def test_numpy_minmax_timedelta64(self): + td = timedelta_range('16815 days', '16820 days', freq='D') + + assert np.min(td) == Timedelta('16815 days') + assert np.max(td) == Timedelta('16820 days') + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.min(td, out=0) + with pytest.raises(ValueError, match=errmsg): + np.max(td, out=0) + + assert np.argmin(td) == 0 + assert np.argmax(td) == 5 + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.argmin(td, out=0) + with pytest.raises(ValueError, match=errmsg): + np.argmax(td, out=0) + + def test_timedelta_ops(self): + # GH#4984 + # make sure ops return Timedelta + s = Series([Timestamp('20130101') + timedelta(seconds=i * i) + for i in range(10)]) + td = s.diff() + + result = td.mean() + expected = to_timedelta(timedelta(seconds=9)) + assert result == expected + + result = td.to_frame().mean() + assert result[0] == expected + + result = td.quantile(.1) + expected = Timedelta(np.timedelta64(2600, 'ms')) + assert result == expected + + result = td.median() + expected = to_timedelta('00:00:09') + assert result == expected + + result = td.to_frame().median() + assert result[0] == expected + + # GH#6462 + # consistency in returned values for sum + result = td.sum() + expected = to_timedelta('00:01:21') + assert result == expected + + result = td.to_frame().sum() + assert result[0] == expected + + # std + result = td.std() + expected = to_timedelta(Series(td.dropna().values).std()) + assert result == expected + + result = td.to_frame().std() + assert result[0] == expected + + # invalid ops + for op in ['skew', 'kurt', 'sem', 'prod']: + msg = "reduction operation '{}' not allowed for this dtype" + with pytest.raises(TypeError, match=msg.format(op)): + getattr(td, op)() + + # GH#10040 + # make sure NaT is properly handled by median() + s = Series([Timestamp('2015-02-03'), Timestamp('2015-02-07')]) + assert s.diff().median() == timedelta(days=4) + + s = Series([Timestamp('2015-02-03'), Timestamp('2015-02-07'), + Timestamp('2015-02-15')]) + assert s.diff().median() == timedelta(days=6) + + def test_minmax_tz(self, tz_naive_fixture): + tz = tz_naive_fixture + # monotonic + idx1 = pd.DatetimeIndex(['2011-01-01', '2011-01-02', + '2011-01-03'], tz=tz) + assert idx1.is_monotonic + + # non-monotonic + idx2 = pd.DatetimeIndex(['2011-01-01', pd.NaT, '2011-01-03', + '2011-01-02', pd.NaT], tz=tz) + assert not idx2.is_monotonic + + for idx in [idx1, idx2]: + assert idx.min() == Timestamp('2011-01-01', tz=tz) + assert idx.max() == Timestamp('2011-01-03', tz=tz) + assert idx.argmin() == 0 + assert idx.argmax() == 2 + + @pytest.mark.parametrize('op', ['min', 'max']) + def test_minmax_nat_datetime64(self, op): + # Return NaT + obj = DatetimeIndex([]) + assert pd.isna(getattr(obj, op)()) + + obj = DatetimeIndex([pd.NaT]) + assert pd.isna(getattr(obj, op)()) + + obj = DatetimeIndex([pd.NaT, pd.NaT, pd.NaT]) + assert pd.isna(getattr(obj, op)()) + + def test_numpy_minmax_datetime64(self): + dr = pd.date_range(start='2016-01-15', end='2016-01-20') + + assert np.min(dr) == Timestamp('2016-01-15 00:00:00', freq='D') + assert np.max(dr) == Timestamp('2016-01-20 00:00:00', freq='D') + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.min(dr, out=0) + + with pytest.raises(ValueError, match=errmsg): + np.max(dr, out=0) + + assert np.argmin(dr) == 0 + assert np.argmax(dr) == 5 + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.argmin(dr, out=0) + + with pytest.raises(ValueError, match=errmsg): + np.argmax(dr, out=0) + + def test_minmax_period(self): + + # monotonic + idx1 = pd.PeriodIndex([NaT, '2011-01-01', '2011-01-02', + '2011-01-03'], freq='D') + assert idx1.is_monotonic + + # non-monotonic + idx2 = pd.PeriodIndex(['2011-01-01', NaT, '2011-01-03', + '2011-01-02', NaT], freq='D') + assert not idx2.is_monotonic + + for idx in [idx1, idx2]: + assert idx.min() == pd.Period('2011-01-01', freq='D') + assert idx.max() == pd.Period('2011-01-03', freq='D') + assert idx1.argmin() == 1 + assert idx2.argmin() == 0 + assert idx1.argmax() == 3 + assert idx2.argmax() == 2 + + for op in ['min', 'max']: + # Return NaT + obj = PeriodIndex([], freq='M') + result = getattr(obj, op)() + assert result is NaT + + obj = PeriodIndex([NaT], freq='M') + result = getattr(obj, op)() + assert result is NaT + + obj = PeriodIndex([NaT, NaT, NaT], freq='M') + result = getattr(obj, op)() + assert result is NaT + + def test_numpy_minmax_period(self): + pr = pd.period_range(start='2016-01-15', end='2016-01-20') + + assert np.min(pr) == Period('2016-01-15', freq='D') + assert np.max(pr) == Period('2016-01-20', freq='D') + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.min(pr, out=0) + with pytest.raises(ValueError, match=errmsg): + np.max(pr, out=0) + + assert np.argmin(pr) == 0 + assert np.argmax(pr) == 5 + + errmsg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=errmsg): + np.argmin(pr, out=0) + with pytest.raises(ValueError, match=errmsg): + np.argmax(pr, out=0) + + def test_min_max_categorical(self): + + ci = pd.CategoricalIndex(list('aabbca'), + categories=list('cab'), + ordered=False) + with pytest.raises(TypeError): + ci.min() + with pytest.raises(TypeError): + ci.max() + + ci = pd.CategoricalIndex(list('aabbca'), + categories=list('cab'), + ordered=True) + assert ci.min() == 'c' + assert ci.max() == 'b' + + +class TestSeriesReductions(object): + # Note: the name TestSeriesReductions indicates these tests + # were moved from a series-specific test file, _not_ that these tests are + # intended long-term to be series-specific + + def test_sum_inf(self): + s = Series(np.random.randn(10)) + s2 = s.copy() + + s[5:8] = np.inf + s2[5:8] = np.nan + + assert np.isinf(s.sum()) + + arr = np.random.randn(100, 100).astype('f4') + arr[:, 2] = np.inf + + with pd.option_context("mode.use_inf_as_na", True): + tm.assert_almost_equal(s.sum(), s2.sum()) + + res = nanops.nansum(arr, axis=1) + assert np.isinf(res).all() + + @pytest.mark.parametrize("use_bottleneck", [True, False]) + @pytest.mark.parametrize("method, unit", [ + ("sum", 0.0), + ("prod", 1.0) + ]) + def test_empty(self, method, unit, use_bottleneck): + with pd.option_context("use_bottleneck", use_bottleneck): + # GH#9422 / GH#18921 + # Entirely empty + s = Series([]) + # NA by default + result = getattr(s, method)() + assert result == unit + + # Explicit + result = getattr(s, method)(min_count=0) + assert result == unit + + result = getattr(s, method)(min_count=1) + assert pd.isna(result) + + # Skipna, default + result = getattr(s, method)(skipna=True) + result == unit + + # Skipna, explicit + result = getattr(s, method)(skipna=True, min_count=0) + assert result == unit + + result = getattr(s, method)(skipna=True, min_count=1) + assert pd.isna(result) + + # All-NA + s = Series([np.nan]) + # NA by default + result = getattr(s, method)() + assert result == unit + + # Explicit + result = getattr(s, method)(min_count=0) + assert result == unit + + result = getattr(s, method)(min_count=1) + assert pd.isna(result) + + # Skipna, default + result = getattr(s, method)(skipna=True) + result == unit + + # skipna, explicit + result = getattr(s, method)(skipna=True, min_count=0) + assert result == unit + + result = getattr(s, method)(skipna=True, min_count=1) + assert pd.isna(result) + + # Mix of valid, empty + s = Series([np.nan, 1]) + # Default + result = getattr(s, method)() + assert result == 1.0 + + # Explicit + result = getattr(s, method)(min_count=0) + assert result == 1.0 + + result = getattr(s, method)(min_count=1) + assert result == 1.0 + + # Skipna + result = getattr(s, method)(skipna=True) + assert result == 1.0 + + result = getattr(s, method)(skipna=True, min_count=0) + assert result == 1.0 + + result = getattr(s, method)(skipna=True, min_count=1) + assert result == 1.0 + + # GH#844 (changed in GH#9422) + df = DataFrame(np.empty((10, 0))) + assert (getattr(df, method)(1) == unit).all() + + s = pd.Series([1]) + result = getattr(s, method)(min_count=2) + assert pd.isna(result) + + s = pd.Series([np.nan]) + result = getattr(s, method)(min_count=2) + assert pd.isna(result) + + s = pd.Series([np.nan, 1]) + result = getattr(s, method)(min_count=2) + assert pd.isna(result) + + @pytest.mark.parametrize('method, unit', [ + ('sum', 0.0), + ('prod', 1.0), + ]) + def test_empty_multi(self, method, unit): + s = pd.Series([1, np.nan, np.nan, np.nan], + index=pd.MultiIndex.from_product([('a', 'b'), (0, 1)])) + # 1 / 0 by default + result = getattr(s, method)(level=0) + expected = pd.Series([1, unit], index=['a', 'b']) + tm.assert_series_equal(result, expected) + + # min_count=0 + result = getattr(s, method)(level=0, min_count=0) + expected = pd.Series([1, unit], index=['a', 'b']) + tm.assert_series_equal(result, expected) + + # min_count=1 + result = getattr(s, method)(level=0, min_count=1) + expected = pd.Series([1, np.nan], index=['a', 'b']) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "method", ['mean', 'median', 'std', 'var']) + def test_ops_consistency_on_empty(self, method): + + # GH#7869 + # consistency on empty + + # float + result = getattr(Series(dtype=float), method)() + assert pd.isna(result) + + # timedelta64[ns] + result = getattr(Series(dtype='m8[ns]'), method)() + assert result is pd.NaT + + def test_nansum_buglet(self): + ser = Series([1.0, np.nan], index=[0, 1]) + result = np.nansum(ser) + tm.assert_almost_equal(result, 1) + + @pytest.mark.parametrize("use_bottleneck", [True, False]) + def test_sum_overflow(self, use_bottleneck): + + with pd.option_context('use_bottleneck', use_bottleneck): + # GH#6915 + # overflowing on the smaller int dtypes + for dtype in ['int32', 'int64']: + v = np.arange(5000000, dtype=dtype) + s = Series(v) + + result = s.sum(skipna=False) + assert int(result) == v.sum(dtype='int64') + result = s.min(skipna=False) + assert int(result) == 0 + result = s.max(skipna=False) + assert int(result) == v[-1] + + for dtype in ['float32', 'float64']: + v = np.arange(5000000, dtype=dtype) + s = Series(v) + + result = s.sum(skipna=False) + assert result == v.sum(dtype=dtype) + result = s.min(skipna=False) + assert np.allclose(float(result), 0.0) + result = s.max(skipna=False) + assert np.allclose(float(result), v[-1]) + + def test_empty_timeseries_reductions_return_nat(self): + # covers GH#11245 + for dtype in ('m8[ns]', 'm8[ns]', 'M8[ns]', 'M8[ns, UTC]'): + assert Series([], dtype=dtype).min() is pd.NaT + assert Series([], dtype=dtype).max() is pd.NaT + assert Series([], dtype=dtype).min(skipna=False) is pd.NaT + assert Series([], dtype=dtype).max(skipna=False) is pd.NaT + + def test_numpy_argmin_deprecated(self): + # See GH#16830 + data = np.arange(1, 11) + + s = Series(data, index=data) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # The deprecation of Series.argmin also causes a deprecation + # warning when calling np.argmin. This behavior is temporary + # until the implementation of Series.argmin is corrected. + result = np.argmin(s) + + assert result == 1 + + with tm.assert_produces_warning(FutureWarning): + # argmin is aliased to idxmin + result = s.argmin() + + assert result == 1 + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + msg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.argmin(s, out=data) + + def test_numpy_argmax_deprecated(self): + # See GH#16830 + data = np.arange(1, 11) + + s = Series(data, index=data) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # The deprecation of Series.argmax also causes a deprecation + # warning when calling np.argmax. This behavior is temporary + # until the implementation of Series.argmax is corrected. + result = np.argmax(s) + assert result == 10 + + with tm.assert_produces_warning(FutureWarning): + # argmax is aliased to idxmax + result = s.argmax() + + assert result == 10 + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + msg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.argmax(s, out=data) + + def test_idxmin(self): + # test idxmin + # _check_stat_op approach can not be used here because of isna check. + string_series = tm.makeStringSeries().rename('series') + + # add some NaNs + string_series[5:15] = np.NaN + + # skipna or no + assert string_series[string_series.idxmin()] == string_series.min() + assert pd.isna(string_series.idxmin(skipna=False)) + + # no NaNs + nona = string_series.dropna() + assert nona[nona.idxmin()] == nona.min() + assert (nona.index.values.tolist().index(nona.idxmin()) == + nona.values.argmin()) + + # all NaNs + allna = string_series * np.nan + assert pd.isna(allna.idxmin()) + + # datetime64[ns] + s = Series(pd.date_range('20130102', periods=6)) + result = s.idxmin() + assert result == 0 + + s[0] = np.nan + result = s.idxmin() + assert result == 1 + + def test_idxmax(self): + # test idxmax + # _check_stat_op approach can not be used here because of isna check. + string_series = tm.makeStringSeries().rename('series') + + # add some NaNs + string_series[5:15] = np.NaN + + # skipna or no + assert string_series[string_series.idxmax()] == string_series.max() + assert pd.isna(string_series.idxmax(skipna=False)) + + # no NaNs + nona = string_series.dropna() + assert nona[nona.idxmax()] == nona.max() + assert (nona.index.values.tolist().index(nona.idxmax()) == + nona.values.argmax()) + + # all NaNs + allna = string_series * np.nan + assert pd.isna(allna.idxmax()) + + from pandas import date_range + s = Series(date_range('20130102', periods=6)) + result = s.idxmax() + assert result == 5 + + s[5] = np.nan + result = s.idxmax() + assert result == 4 + + # Float64Index + # GH#5914 + s = pd.Series([1, 2, 3], [1.1, 2.1, 3.1]) + result = s.idxmax() + assert result == 3.1 + result = s.idxmin() + assert result == 1.1 + + s = pd.Series(s.index, s.index) + result = s.idxmax() + assert result == 3.1 + result = s.idxmin() + assert result == 1.1 + + def test_all_any(self): + ts = tm.makeTimeSeries() + bool_series = ts > 0 + assert not bool_series.all() + assert bool_series.any() + + # Alternative types, with implicit 'object' dtype. + s = Series(['abc', True]) + assert 'abc' == s.any() # 'abc' || True => 'abc' + + def test_all_any_params(self): + # Check skipna, with implicit 'object' dtype. + s1 = Series([np.nan, True]) + s2 = Series([np.nan, False]) + assert s1.all(skipna=False) # nan && True => True + assert s1.all(skipna=True) + assert np.isnan(s2.any(skipna=False)) # nan || False => nan + assert not s2.any(skipna=True) + + # Check level. + s = pd.Series([False, False, True, True, False, True], + index=[0, 0, 1, 1, 2, 2]) + tm.assert_series_equal(s.all(level=0), Series([False, True, False])) + tm.assert_series_equal(s.any(level=0), Series([False, True, True])) + + # bool_only is not implemented with level option. + with pytest.raises(NotImplementedError): + s.any(bool_only=True, level=0) + with pytest.raises(NotImplementedError): + s.all(bool_only=True, level=0) + + # bool_only is not implemented alone. + with pytest.raises(NotImplementedError): + s.any(bool_only=True,) + with pytest.raises(NotImplementedError): + s.all(bool_only=True) + + def test_timedelta64_analytics(self): + + # index min/max + dti = pd.date_range('2012-1-1', periods=3, freq='D') + td = Series(dti) - pd.Timestamp('20120101') + + result = td.idxmin() + assert result == 0 + + result = td.idxmax() + assert result == 2 + + # GH#2982 + # with NaT + td[0] = np.nan + + result = td.idxmin() + assert result == 1 + + result = td.idxmax() + assert result == 2 + + # abs + s1 = Series(pd.date_range('20120101', periods=3)) + s2 = Series(pd.date_range('20120102', periods=3)) + expected = Series(s2 - s1) + + # FIXME: don't leave commented-out code + # this fails as numpy returns timedelta64[us] + # result = np.abs(s1-s2) + # assert_frame_equal(result,expected) + + result = (s1 - s2).abs() + tm.assert_series_equal(result, expected) + + # max/min + result = td.max() + expected = pd.Timedelta('2 days') + assert result == expected + + result = td.min() + expected = pd.Timedelta('1 days') + assert result == expected + + @pytest.mark.parametrize( + "test_input,error_type", + [ + (pd.Series([]), ValueError), + + # For strings, or any Series with dtype 'O' + (pd.Series(['foo', 'bar', 'baz']), TypeError), + (pd.Series([(1,), (2,)]), TypeError), + + # For mixed data types + ( + pd.Series(['foo', 'foo', 'bar', 'bar', None, np.nan, 'baz']), + TypeError + ), + ] + ) + def test_assert_idxminmax_raises(self, test_input, error_type): + """ + Cases where ``Series.argmax`` and related should raise an exception + """ + with pytest.raises(error_type): + test_input.idxmin() + with pytest.raises(error_type): + test_input.idxmin(skipna=False) + with pytest.raises(error_type): + test_input.idxmax() + with pytest.raises(error_type): + test_input.idxmax(skipna=False) + + def test_idxminmax_with_inf(self): + # For numeric data with NA and Inf (GH #13595) + s = pd.Series([0, -np.inf, np.inf, np.nan]) + + assert s.idxmin() == 1 + assert np.isnan(s.idxmin(skipna=False)) + + assert s.idxmax() == 2 + assert np.isnan(s.idxmax(skipna=False)) + + # Using old-style behavior that treats floating point nan, -inf, and + # +inf as missing + with pd.option_context('mode.use_inf_as_na', True): + assert s.idxmin() == 0 + assert np.isnan(s.idxmin(skipna=False)) + assert s.idxmax() == 0 + np.isnan(s.idxmax(skipna=False)) + + +class TestDatetime64SeriesReductions(object): + # Note: the name TestDatetime64SeriesReductions indicates these tests + # were moved from a series-specific test file, _not_ that these tests are + # intended long-term to be series-specific + + @pytest.mark.parametrize('nat_ser', [ + Series([pd.NaT, pd.NaT]), + Series([pd.NaT, pd.Timedelta('nat')]), + Series([pd.Timedelta('nat'), pd.Timedelta('nat')])]) + def test_minmax_nat_series(self, nat_ser): + # GH#23282 + assert nat_ser.min() is pd.NaT + assert nat_ser.max() is pd.NaT + assert nat_ser.min(skipna=False) is pd.NaT + assert nat_ser.max(skipna=False) is pd.NaT + + @pytest.mark.parametrize('nat_df', [ + pd.DataFrame([pd.NaT, pd.NaT]), + pd.DataFrame([pd.NaT, pd.Timedelta('nat')]), + pd.DataFrame([pd.Timedelta('nat'), pd.Timedelta('nat')])]) + def test_minmax_nat_dataframe(self, nat_df): + # GH#23282 + assert nat_df.min()[0] is pd.NaT + assert nat_df.max()[0] is pd.NaT + assert nat_df.min(skipna=False)[0] is pd.NaT + assert nat_df.max(skipna=False)[0] is pd.NaT + + def test_min_max(self): + rng = pd.date_range('1/1/2000', '12/31/2000') + rng2 = rng.take(np.random.permutation(len(rng))) + + the_min = rng2.min() + the_max = rng2.max() + assert isinstance(the_min, pd.Timestamp) + assert isinstance(the_max, pd.Timestamp) + assert the_min == rng[0] + assert the_max == rng[-1] + + assert rng.min() == rng[0] + assert rng.max() == rng[-1] + + def test_min_max_series(self): + rng = pd.date_range('1/1/2000', periods=10, freq='4h') + lvls = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C'] + df = DataFrame({'TS': rng, 'V': np.random.randn(len(rng)), 'L': lvls}) + + result = df.TS.max() + exp = pd.Timestamp(df.TS.iat[-1]) + assert isinstance(result, pd.Timestamp) + assert result == exp + + result = df.TS.min() + exp = pd.Timestamp(df.TS.iat[0]) + assert isinstance(result, pd.Timestamp) + assert result == exp + + +class TestCategoricalSeriesReductions(object): + # Note: the name TestCategoricalSeriesReductions indicates these tests + # were moved from a series-specific test file, _not_ that these tests are + # intended long-term to be series-specific + + def test_min_max(self): + # unordered cats have no min/max + cat = Series(Categorical(["a", "b", "c", "d"], ordered=False)) + with pytest.raises(TypeError): + cat.min() + with pytest.raises(TypeError): + cat.max() + + cat = Series(Categorical(["a", "b", "c", "d"], ordered=True)) + _min = cat.min() + _max = cat.max() + assert _min == "a" + assert _max == "d" + + cat = Series(Categorical(["a", "b", "c", "d"], categories=[ + 'd', 'c', 'b', 'a'], ordered=True)) + _min = cat.min() + _max = cat.max() + assert _min == "d" + assert _max == "a" + + cat = Series(Categorical( + [np.nan, "b", "c", np.nan], categories=['d', 'c', 'b', 'a' + ], ordered=True)) + _min = cat.min() + _max = cat.max() + assert np.isnan(_min) + assert _max == "b" + + cat = Series(Categorical( + [np.nan, 1, 2, np.nan], categories=[5, 4, 3, 2, 1], ordered=True)) + _min = cat.min() + _max = cat.max() + assert np.isnan(_min) + assert _max == 1 + + def test_min_max_numeric_only(self): + # TODO deprecate numeric_only argument for Categorical and use + # skipna as well, see GH25303 + cat = Series(Categorical( + ["a", "b", np.nan, "a"], categories=['b', 'a'], ordered=True)) + + _min = cat.min() + _max = cat.max() + assert np.isnan(_min) + assert _max == "a" + + _min = cat.min(numeric_only=True) + _max = cat.max(numeric_only=True) + assert _min == "b" + assert _max == "a" + + _min = cat.min(numeric_only=False) + _max = cat.max(numeric_only=False) + assert np.isnan(_min) + assert _max == "a" + + +class TestSeriesMode(object): + # Note: the name TestSeriesMode indicates these tests + # were moved from a series-specific test file, _not_ that these tests are + # intended long-term to be series-specific + + @pytest.mark.parametrize('dropna, expected', [ + (True, Series([], dtype=np.float64)), + (False, Series([], dtype=np.float64)) + ]) + def test_mode_empty(self, dropna, expected): + s = Series([], dtype=np.float64) + result = s.mode(dropna) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('dropna, data, expected', [ + (True, [1, 1, 1, 2], [1]), + (True, [1, 1, 1, 2, 3, 3, 3], [1, 3]), + (False, [1, 1, 1, 2], [1]), + (False, [1, 1, 1, 2, 3, 3, 3], [1, 3]), + ]) + @pytest.mark.parametrize( + 'dt', + list(np.typecodes['AllInteger'] + np.typecodes['Float']) + ) + def test_mode_numerical(self, dropna, data, expected, dt): + s = Series(data, dtype=dt) + result = s.mode(dropna) + expected = Series(expected, dtype=dt) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('dropna, expected', [ + (True, [1.0]), + (False, [1, np.nan]), + ]) + def test_mode_numerical_nan(self, dropna, expected): + s = Series([1, 1, 2, np.nan, np.nan]) + result = s.mode(dropna) + expected = Series(expected) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('dropna, expected1, expected2, expected3', [ + (True, ['b'], ['bar'], ['nan']), + (False, ['b'], [np.nan], ['nan']) + ]) + def test_mode_str_obj(self, dropna, expected1, expected2, expected3): + # Test string and object types. + data = ['a'] * 2 + ['b'] * 3 + + s = Series(data, dtype='c') + result = s.mode(dropna) + expected1 = Series(expected1, dtype='c') + tm.assert_series_equal(result, expected1) + + data = ['foo', 'bar', 'bar', np.nan, np.nan, np.nan] + + s = Series(data, dtype=object) + result = s.mode(dropna) + expected2 = Series(expected2, dtype=object) + tm.assert_series_equal(result, expected2) + + data = ['foo', 'bar', 'bar', np.nan, np.nan, np.nan] + + s = Series(data, dtype=object).astype(str) + result = s.mode(dropna) + expected3 = Series(expected3, dtype=str) + tm.assert_series_equal(result, expected3) + + @pytest.mark.parametrize('dropna, expected1, expected2', [ + (True, ['foo'], ['foo']), + (False, ['foo'], [np.nan]) + ]) + def test_mode_mixeddtype(self, dropna, expected1, expected2): + s = Series([1, 'foo', 'foo']) + result = s.mode(dropna) + expected = Series(expected1) + tm.assert_series_equal(result, expected) + + s = Series([1, 'foo', 'foo', np.nan, np.nan, np.nan]) + result = s.mode(dropna) + expected = Series(expected2, dtype=object) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('dropna, expected1, expected2', [ + (True, ['1900-05-03', '2011-01-03', '2013-01-02'], + ['2011-01-03', '2013-01-02']), + (False, [np.nan], [np.nan, '2011-01-03', '2013-01-02']), + ]) + def test_mode_datetime(self, dropna, expected1, expected2): + s = Series(['2011-01-03', '2013-01-02', + '1900-05-03', 'nan', 'nan'], dtype='M8[ns]') + result = s.mode(dropna) + expected1 = Series(expected1, dtype='M8[ns]') + tm.assert_series_equal(result, expected1) + + s = Series(['2011-01-03', '2013-01-02', '1900-05-03', + '2011-01-03', '2013-01-02', 'nan', 'nan'], + dtype='M8[ns]') + result = s.mode(dropna) + expected2 = Series(expected2, dtype='M8[ns]') + tm.assert_series_equal(result, expected2) + + @pytest.mark.parametrize('dropna, expected1, expected2', [ + (True, ['-1 days', '0 days', '1 days'], ['2 min', '1 day']), + (False, [np.nan], [np.nan, '2 min', '1 day']), + ]) + def test_mode_timedelta(self, dropna, expected1, expected2): + # gh-5986: Test timedelta types. + + s = Series(['1 days', '-1 days', '0 days', 'nan', 'nan'], + dtype='timedelta64[ns]') + result = s.mode(dropna) + expected1 = Series(expected1, dtype='timedelta64[ns]') + tm.assert_series_equal(result, expected1) + + s = Series(['1 day', '1 day', '-1 day', '-1 day 2 min', + '2 min', '2 min', 'nan', 'nan'], + dtype='timedelta64[ns]') + result = s.mode(dropna) + expected2 = Series(expected2, dtype='timedelta64[ns]') + tm.assert_series_equal(result, expected2) + + @pytest.mark.parametrize('dropna, expected1, expected2, expected3', [ + (True, Categorical([1, 2], categories=[1, 2]), + Categorical(['a'], categories=[1, 'a']), + Categorical([3, 1], categories=[3, 2, 1], ordered=True)), + (False, Categorical([np.nan], categories=[1, 2]), + Categorical([np.nan, 'a'], categories=[1, 'a']), + Categorical([np.nan, 3, 1], categories=[3, 2, 1], ordered=True)), + ]) + def test_mode_category(self, dropna, expected1, expected2, expected3): + s = Series(Categorical([1, 2, np.nan, np.nan])) + result = s.mode(dropna) + expected1 = Series(expected1, dtype='category') + tm.assert_series_equal(result, expected1) + + s = Series(Categorical([1, 'a', 'a', np.nan, np.nan])) + result = s.mode(dropna) + expected2 = Series(expected2, dtype='category') + tm.assert_series_equal(result, expected2) + + s = Series(Categorical([1, 1, 2, 3, 3, np.nan, np.nan], + categories=[3, 2, 1], ordered=True)) + result = s.mode(dropna) + expected3 = Series(expected3, dtype='category') + tm.assert_series_equal(result, expected3) + + @pytest.mark.parametrize('dropna, expected1, expected2', [ + (True, [2**63], [1, 2**63]), + (False, [2**63], [1, 2**63]) + ]) + def test_mode_intoverflow(self, dropna, expected1, expected2): + # Test for uint64 overflow. + s = Series([1, 2**63, 2**63], dtype=np.uint64) + result = s.mode(dropna) + expected1 = Series(expected1, dtype=np.uint64) + tm.assert_series_equal(result, expected1) + + s = Series([1, 2**63], dtype=np.uint64) + result = s.mode(dropna) + expected2 = Series(expected2, dtype=np.uint64) + tm.assert_series_equal(result, expected2) + + @pytest.mark.skipif(not compat.PY3, reason="only PY3") + def test_mode_sortwarning(self): + # Check for the warning that is raised when the mode + # results cannot be sorted + + expected = Series(['foo', np.nan]) + s = Series([1, 'foo', 'foo', np.nan, np.nan]) + + with tm.assert_produces_warning(UserWarning, check_stacklevel=False): + result = s.mode(dropna=False) + result = result.sort_values().reset_index(drop=True) + + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/reductions/test_stat_reductions.py b/pandas/tests/reductions/test_stat_reductions.py new file mode 100644 index 0000000000000..11ecd03f6c7e1 --- /dev/null +++ b/pandas/tests/reductions/test_stat_reductions.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +""" +Tests for statistical reductions of 2nd moment or higher: var, skew, kurt, ... +""" + +import numpy as np +import pytest + +from pandas.compat import lrange +import pandas.util._test_decorators as td + +import pandas as pd +from pandas import DataFrame, Series, compat +import pandas.util.testing as tm + + +class TestSeriesStatReductions(object): + # Note: the name TestSeriesStatReductions indicates these tests + # were moved from a series-specific test file, _not_ that these tests are + # intended long-term to be series-specific + + def _check_stat_op(self, name, alternate, string_series_, + check_objects=False, check_allna=False): + + with pd.option_context('use_bottleneck', False): + f = getattr(Series, name) + + # add some NaNs + string_series_[5:15] = np.NaN + + # mean, idxmax, idxmin, min, and max are valid for dates + if name not in ['max', 'min', 'mean']: + ds = Series(pd.date_range('1/1/2001', periods=10)) + with pytest.raises(TypeError): + f(ds) + + # skipna or no + assert pd.notna(f(string_series_)) + assert pd.isna(f(string_series_, skipna=False)) + + # check the result is correct + nona = string_series_.dropna() + tm.assert_almost_equal(f(nona), alternate(nona.values)) + tm.assert_almost_equal(f(string_series_), alternate(nona.values)) + + allna = string_series_ * np.nan + + if check_allna: + assert np.isnan(f(allna)) + + # dtype=object with None, it works! + s = Series([1, 2, 3, None, 5]) + f(s) + + # GH#2888 + items = [0] + items.extend(lrange(2 ** 40, 2 ** 40 + 1000)) + s = Series(items, dtype='int64') + tm.assert_almost_equal(float(f(s)), float(alternate(s.values))) + + # check date range + if check_objects: + s = Series(pd.bdate_range('1/1/2000', periods=10)) + res = f(s) + exp = alternate(s) + assert res == exp + + # check on string data + if name not in ['sum', 'min', 'max']: + with pytest.raises(TypeError): + f(Series(list('abc'))) + + # Invalid axis. + with pytest.raises(ValueError): + f(string_series_, axis=1) + + # Unimplemented numeric_only parameter. + if 'numeric_only' in compat.signature(f).args: + with pytest.raises(NotImplementedError, match=name): + f(string_series_, numeric_only=True) + + def test_sum(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('sum', np.sum, string_series, check_allna=False) + + def test_mean(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('mean', np.mean, string_series) + + def test_median(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('median', np.median, string_series) + + # test with integers, test failure + int_ts = Series(np.ones(10, dtype=int), index=lrange(10)) + tm.assert_almost_equal(np.median(int_ts), int_ts.median()) + + def test_prod(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('prod', np.prod, string_series) + + def test_min(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('min', np.min, string_series, check_objects=True) + + def test_max(self): + string_series = tm.makeStringSeries().rename('series') + self._check_stat_op('max', np.max, string_series, check_objects=True) + + def test_var_std(self): + string_series = tm.makeStringSeries().rename('series') + datetime_series = tm.makeTimeSeries().rename('ts') + + alt = lambda x: np.std(x, ddof=1) + self._check_stat_op('std', alt, string_series) + + alt = lambda x: np.var(x, ddof=1) + self._check_stat_op('var', alt, string_series) + + result = datetime_series.std(ddof=4) + expected = np.std(datetime_series.values, ddof=4) + tm.assert_almost_equal(result, expected) + + result = datetime_series.var(ddof=4) + expected = np.var(datetime_series.values, ddof=4) + tm.assert_almost_equal(result, expected) + + # 1 - element series with ddof=1 + s = datetime_series.iloc[[0]] + result = s.var(ddof=1) + assert pd.isna(result) + + result = s.std(ddof=1) + assert pd.isna(result) + + def test_sem(self): + string_series = tm.makeStringSeries().rename('series') + datetime_series = tm.makeTimeSeries().rename('ts') + + alt = lambda x: np.std(x, ddof=1) / np.sqrt(len(x)) + self._check_stat_op('sem', alt, string_series) + + result = datetime_series.sem(ddof=4) + expected = np.std(datetime_series.values, + ddof=4) / np.sqrt(len(datetime_series.values)) + tm.assert_almost_equal(result, expected) + + # 1 - element series with ddof=1 + s = datetime_series.iloc[[0]] + result = s.sem(ddof=1) + assert pd.isna(result) + + @td.skip_if_no_scipy + def test_skew(self): + from scipy.stats import skew + + string_series = tm.makeStringSeries().rename('series') + + alt = lambda x: skew(x, bias=False) + self._check_stat_op('skew', alt, string_series) + + # test corner cases, skew() returns NaN unless there's at least 3 + # values + min_N = 3 + for i in range(1, min_N + 1): + s = Series(np.ones(i)) + df = DataFrame(np.ones((i, i))) + if i < min_N: + assert np.isnan(s.skew()) + assert np.isnan(df.skew()).all() + else: + assert 0 == s.skew() + assert (df.skew() == 0).all() + + @td.skip_if_no_scipy + def test_kurt(self): + from scipy.stats import kurtosis + + string_series = tm.makeStringSeries().rename('series') + + alt = lambda x: kurtosis(x, bias=False) + self._check_stat_op('kurt', alt, string_series) + + index = pd.MultiIndex( + levels=[['bar'], ['one', 'two', 'three'], [0, 1]], + codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1]] + ) + s = Series(np.random.randn(6), index=index) + tm.assert_almost_equal(s.kurt(), s.kurt(level=0)['bar']) + + # test corner cases, kurt() returns NaN unless there's at least 4 + # values + min_N = 4 + for i in range(1, min_N + 1): + s = Series(np.ones(i)) + df = DataFrame(np.ones((i, i))) + if i < min_N: + assert np.isnan(s.kurt()) + assert np.isnan(df.kurt()).all() + else: + assert 0 == s.kurt() + assert (df.kurt() == 0).all() diff --git a/pandas/tests/resample/__init__.py b/pandas/tests/resample/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/resample/conftest.py b/pandas/tests/resample/conftest.py new file mode 100644 index 0000000000000..d0f78f6d5b439 --- /dev/null +++ b/pandas/tests/resample/conftest.py @@ -0,0 +1,142 @@ +from datetime import datetime + +import numpy as np +import pytest + +from pandas import DataFrame, Series +from pandas.core.indexes.datetimes import date_range +from pandas.core.indexes.period import period_range + +# The various methods we support +downsample_methods = ['min', 'max', 'first', 'last', 'sum', 'mean', 'sem', + 'median', 'prod', 'var', 'std', 'ohlc', 'quantile'] +upsample_methods = ['count', 'size'] +series_methods = ['nunique'] +resample_methods = downsample_methods + upsample_methods + series_methods + + +@pytest.fixture(params=downsample_methods) +def downsample_method(request): + """Fixture for parametrization of Grouper downsample methods.""" + return request.param + + +@pytest.fixture(params=upsample_methods) +def upsample_method(request): + """Fixture for parametrization of Grouper upsample methods.""" + return request.param + + +@pytest.fixture(params=resample_methods) +def resample_method(request): + """Fixture for parametrization of Grouper resample methods.""" + return request.param + + +@pytest.fixture +def simple_date_range_series(): + """ + Series with date range index and random data for test purposes. + """ + def _simple_date_range_series(start, end, freq='D'): + rng = date_range(start, end, freq=freq) + return Series(np.random.randn(len(rng)), index=rng) + return _simple_date_range_series + + +@pytest.fixture +def simple_period_range_series(): + """ + Series with period range index and random data for test purposes. + """ + def _simple_period_range_series(start, end, freq='D'): + rng = period_range(start, end, freq=freq) + return Series(np.random.randn(len(rng)), index=rng) + return _simple_period_range_series + + +@pytest.fixture +def _index_start(): + """Fixture for parametrization of index, series and frame.""" + return datetime(2005, 1, 1) + + +@pytest.fixture +def _index_end(): + """Fixture for parametrization of index, series and frame.""" + return datetime(2005, 1, 10) + + +@pytest.fixture +def _index_freq(): + """Fixture for parametrization of index, series and frame.""" + return 'D' + + +@pytest.fixture +def _index_name(): + """Fixture for parametrization of index, series and frame.""" + return None + + +@pytest.fixture +def index(_index_factory, _index_start, _index_end, _index_freq, _index_name): + """Fixture for parametrization of date_range, period_range and + timedelta_range indexes""" + return _index_factory( + _index_start, _index_end, freq=_index_freq, name=_index_name) + + +@pytest.fixture +def _static_values(index): + """Fixture for parametrization of values used in parametrization of + Series and DataFrames with date_range, period_range and + timedelta_range indexes""" + return np.arange(len(index)) + + +@pytest.fixture +def _series_name(): + """Fixture for parametrization of Series name for Series used with + date_range, period_range and timedelta_range indexes""" + return None + + +@pytest.fixture +def series(index, _series_name, _static_values): + """Fixture for parametrization of Series with date_range, period_range and + timedelta_range indexes""" + return Series(_static_values, index=index, name=_series_name) + + +@pytest.fixture +def empty_series(series): + """Fixture for parametrization of empty Series with date_range, + period_range and timedelta_range indexes""" + return series[:0] + + +@pytest.fixture +def frame(index, _series_name, _static_values): + """Fixture for parametrization of DataFrame with date_range, period_range + and timedelta_range indexes""" + # _series_name is intentionally unused + return DataFrame({'value': _static_values}, index=index) + + +@pytest.fixture +def empty_frame(series): + """Fixture for parametrization of empty DataFrame with date_range, + period_range and timedelta_range indexes""" + index = series.index[:0] + return DataFrame(index=index) + + +@pytest.fixture(params=[Series, DataFrame]) +def series_and_frame(request, series, frame): + """Fixture for parametrization of Series and DataFrame with date_range, + period_range and timedelta_range indexes""" + if request.param == Series: + return series + if request.param == DataFrame: + return frame diff --git a/pandas/tests/resample/test_base.py b/pandas/tests/resample/test_base.py new file mode 100644 index 0000000000000..8f912ea5c524a --- /dev/null +++ b/pandas/tests/resample/test_base.py @@ -0,0 +1,235 @@ +from datetime import datetime, timedelta + +import numpy as np +import pytest + +from pandas.compat import range, zip + +import pandas as pd +from pandas import DataFrame, Series +from pandas.core.groupby.groupby import DataError +from pandas.core.indexes.datetimes import date_range +from pandas.core.indexes.period import PeriodIndex, period_range +from pandas.core.indexes.timedeltas import TimedeltaIndex, timedelta_range +from pandas.core.resample import TimeGrouper +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_index_equal, + assert_series_equal) + +# a fixture value can be overridden by the test parameter value. Note that the +# value of the fixture can be overridden this way even if the test doesn't use +# it directly (doesn't mention it in the function prototype). +# see https://docs.pytest.org/en/latest/fixture.html#override-a-fixture-with-direct-test-parametrization # noqa +# in this module we override the fixture values defined in conftest.py +# tuples of '_index_factory,_series_name,_index_start,_index_end' +DATE_RANGE = (date_range, 'dti', datetime(2005, 1, 1), datetime(2005, 1, 10)) +PERIOD_RANGE = ( + period_range, 'pi', datetime(2005, 1, 1), datetime(2005, 1, 10)) +TIMEDELTA_RANGE = (timedelta_range, 'tdi', '1 day', '10 day') + +all_ts = pytest.mark.parametrize( + '_index_factory,_series_name,_index_start,_index_end', + [DATE_RANGE, PERIOD_RANGE, TIMEDELTA_RANGE] +) + + +@pytest.fixture +def create_index(_index_factory): + def _create_index(*args, **kwargs): + """ return the _index_factory created using the args, kwargs """ + return _index_factory(*args, **kwargs) + return _create_index + + +@pytest.mark.parametrize('freq', ['2D', '1H']) +@pytest.mark.parametrize( + '_index_factory,_series_name,_index_start,_index_end', + [DATE_RANGE, TIMEDELTA_RANGE] +) +def test_asfreq(series_and_frame, freq, create_index): + obj = series_and_frame + + result = obj.resample(freq).asfreq() + new_index = create_index(obj.index[0], obj.index[-1], freq=freq) + expected = obj.reindex(new_index) + assert_almost_equal(result, expected) + + +@pytest.mark.parametrize( + '_index_factory,_series_name,_index_start,_index_end', + [DATE_RANGE, TIMEDELTA_RANGE] +) +def test_asfreq_fill_value(series, create_index): + # test for fill value during resampling, issue 3715 + + s = series + + result = s.resample('1H').asfreq() + new_index = create_index(s.index[0], s.index[-1], freq='1H') + expected = s.reindex(new_index) + assert_series_equal(result, expected) + + frame = s.to_frame('value') + frame.iloc[1] = None + result = frame.resample('1H').asfreq(fill_value=4.0) + new_index = create_index(frame.index[0], + frame.index[-1], freq='1H') + expected = frame.reindex(new_index, fill_value=4.0) + assert_frame_equal(result, expected) + + +@all_ts +def test_resample_interpolate(frame): + # # 12925 + df = frame + assert_frame_equal( + df.resample('1T').asfreq().interpolate(), + df.resample('1T').interpolate()) + + +def test_raises_on_non_datetimelike_index(): + # this is a non datetimelike index + xp = DataFrame() + msg = ("Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex," + " but got an instance of 'Index'") + with pytest.raises(TypeError, match=msg): + xp.resample('A').mean() + + +@all_ts +@pytest.mark.parametrize('freq', ['M', 'D', 'H']) +def test_resample_empty_series(freq, empty_series, resample_method): + # GH12771 & GH12868 + + if resample_method == 'ohlc': + pytest.skip('need to test for ohlc from GH13083') + + s = empty_series + result = getattr(s.resample(freq), resample_method)() + + expected = s.copy() + if isinstance(s.index, PeriodIndex): + expected.index = s.index.asfreq(freq=freq) + else: + expected.index = s.index._shallow_copy(freq=freq) + assert_index_equal(result.index, expected.index) + assert result.index.freq == expected.index.freq + assert_series_equal(result, expected, check_dtype=False) + + +@all_ts +@pytest.mark.parametrize('freq', ['M', 'D', 'H']) +def test_resample_empty_dataframe(empty_frame, freq, resample_method): + # GH13212 + df = empty_frame + # count retains dimensions too + result = getattr(df.resample(freq), resample_method)() + if resample_method != 'size': + expected = df.copy() + else: + # GH14962 + expected = Series([]) + + if isinstance(df.index, PeriodIndex): + expected.index = df.index.asfreq(freq=freq) + else: + expected.index = df.index._shallow_copy(freq=freq) + assert_index_equal(result.index, expected.index) + assert result.index.freq == expected.index.freq + assert_almost_equal(result, expected, check_dtype=False) + + # test size for GH13212 (currently stays as df) + + +@pytest.mark.parametrize("index", tm.all_timeseries_index_generator(0)) +@pytest.mark.parametrize( + "dtype", + [np.float, np.int, np.object, 'datetime64[ns]']) +def test_resample_empty_dtypes(index, dtype, resample_method): + + # Empty series were sometimes causing a segfault (for the functions + # with Cython bounds-checking disabled) or an IndexError. We just run + # them to ensure they no longer do. (GH #10228) + empty_series = Series([], index, dtype) + try: + getattr(empty_series.resample('d'), resample_method)() + except DataError: + # Ignore these since some combinations are invalid + # (ex: doing mean with dtype of np.object) + pass + + +@all_ts +def test_resample_loffset_arg_type(frame, create_index): + # GH 13218, 15002 + df = frame + expected_means = [df.values[i:i + 2].mean() + for i in range(0, len(df.values), 2)] + expected_index = create_index(df.index[0], + periods=len(df.index) / 2, + freq='2D') + + # loffset coerces PeriodIndex to DateTimeIndex + if isinstance(expected_index, PeriodIndex): + expected_index = expected_index.to_timestamp() + + expected_index += timedelta(hours=2) + expected = DataFrame({'value': expected_means}, index=expected_index) + + for arg in ['mean', {'value': 'mean'}, ['mean']]: + + result_agg = df.resample('2D', loffset='2H').agg(arg) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result_how = df.resample('2D', how=arg, loffset='2H') + + if isinstance(arg, list): + expected.columns = pd.MultiIndex.from_tuples([('value', + 'mean')]) + + # GH 13022, 7687 - TODO: fix resample w/ TimedeltaIndex + if isinstance(expected.index, TimedeltaIndex): + msg = "DataFrame are different" + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(result_agg, expected) + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(result_how, expected) + else: + assert_frame_equal(result_agg, expected) + assert_frame_equal(result_how, expected) + + +@all_ts +def test_apply_to_empty_series(empty_series): + # GH 14313 + s = empty_series + for freq in ['M', 'D', 'H']: + result = s.resample(freq).apply(lambda x: 1) + expected = s.resample(freq).apply(np.sum) + + assert_series_equal(result, expected, check_dtype=False) + + +@all_ts +def test_resampler_is_iterable(series): + # GH 15314 + freq = 'H' + tg = TimeGrouper(freq, convention='start') + grouped = series.groupby(tg) + resampled = series.resample(freq) + for (rk, rv), (gk, gv) in zip(resampled, grouped): + assert rk == gk + assert_series_equal(rv, gv) + + +@all_ts +def test_resample_quantile(series): + # GH 15023 + s = series + q = 0.75 + freq = 'H' + result = s.resample(freq).quantile(q) + expected = s.resample(freq).agg(lambda x: x.quantile(q)).rename(s.name) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/resample/test_datetime_index.py b/pandas/tests/resample/test_datetime_index.py new file mode 100644 index 0000000000000..ce675893d9907 --- /dev/null +++ b/pandas/tests/resample/test_datetime_index.py @@ -0,0 +1,1478 @@ +from datetime import datetime, timedelta +from functools import partial + +import numpy as np +import pytest +import pytz + +from pandas.compat import StringIO, range +from pandas.errors import UnsupportedFunctionCall + +import pandas as pd +from pandas import DataFrame, Series, Timedelta, Timestamp, isna, notna +from pandas.core.indexes.datetimes import date_range +from pandas.core.indexes.period import Period, period_range +from pandas.core.resample import ( + DatetimeIndex, TimeGrouper, _get_timestamp_range_edges) +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) + +import pandas.tseries.offsets as offsets +from pandas.tseries.offsets import BDay, Minute + + +@pytest.fixture() +def _index_factory(): + return date_range + + +@pytest.fixture +def _index_freq(): + return 'Min' + + +@pytest.fixture +def _static_values(index): + return np.random.rand(len(index)) + + +def test_custom_grouper(index): + + dti = index + s = Series(np.array([1] * len(dti)), index=dti, dtype='int64') + + b = TimeGrouper(Minute(5)) + g = s.groupby(b) + + # check all cython functions work + funcs = ['add', 'mean', 'prod', 'ohlc', 'min', 'max', 'var'] + for f in funcs: + g._cython_agg_general(f) + + b = TimeGrouper(Minute(5), closed='right', label='right') + g = s.groupby(b) + # check all cython functions work + funcs = ['add', 'mean', 'prod', 'ohlc', 'min', 'max', 'var'] + for f in funcs: + g._cython_agg_general(f) + + assert g.ngroups == 2593 + assert notna(g.mean()).all() + + # construct expected val + arr = [1] + [5] * 2592 + idx = dti[0:-1:5] + idx = idx.append(dti[-1:]) + expect = Series(arr, index=idx) + + # GH2763 - return in put dtype if we can + result = g.agg(np.sum) + assert_series_equal(result, expect) + + df = DataFrame(np.random.rand(len(dti), 10), + index=dti, dtype='float64') + r = df.groupby(b).agg(np.sum) + + assert len(r.columns) == 10 + assert len(r.index) == 2593 + + +@pytest.mark.parametrize( + '_index_start,_index_end,_index_name', + [('1/1/2000 00:00:00', '1/1/2000 00:13:00', 'index')]) +@pytest.mark.parametrize('closed, expected', [ + ('right', + lambda s: Series( + [s[0], s[1:6].mean(), s[6:11].mean(), s[11:].mean()], + index=date_range( + '1/1/2000', periods=4, freq='5min', name='index'))), + ('left', + lambda s: Series( + [s[:5].mean(), s[5:10].mean(), s[10:].mean()], + index=date_range( + '1/1/2000 00:05', periods=3, freq='5min', name='index')) + ) +]) +def test_resample_basic(series, closed, expected): + s = series + expected = expected(s) + result = s.resample('5min', closed=closed, label='right').mean() + assert_series_equal(result, expected) + + +def test_resample_basic_grouper(series): + s = series + result = s.resample('5Min').last() + grouper = TimeGrouper(Minute(5), closed='left', label='left') + expected = s.groupby(grouper).agg(lambda x: x[-1]) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + '_index_start,_index_end,_index_name', + [('1/1/2000 00:00:00', '1/1/2000 00:13:00', 'index')]) +@pytest.mark.parametrize('keyword,value', [ + ('label', 'righttt'), + ('closed', 'righttt'), + ('convention', 'starttt') +]) +def test_resample_string_kwargs(series, keyword, value): + # see gh-19303 + # Check that wrong keyword argument strings raise an error + msg = "Unsupported value {value} for `{keyword}`".format( + value=value, keyword=keyword) + with pytest.raises(ValueError, match=msg): + series.resample('5min', **({keyword: value})) + + +@pytest.mark.parametrize( + '_index_start,_index_end,_index_name', + [('1/1/2000 00:00:00', '1/1/2000 00:13:00', 'index')]) +def test_resample_how(series, downsample_method): + if downsample_method == 'ohlc': + pytest.skip('covered by test_resample_how_ohlc') + + s = series + grouplist = np.ones_like(s) + grouplist[0] = 0 + grouplist[1:6] = 1 + grouplist[6:11] = 2 + grouplist[11:] = 3 + expected = s.groupby(grouplist).agg(downsample_method) + expected.index = date_range( + '1/1/2000', periods=4, freq='5min', name='index') + + result = getattr(s.resample( + '5min', closed='right', label='right'), downsample_method)() + assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + '_index_start,_index_end,_index_name', + [('1/1/2000 00:00:00', '1/1/2000 00:13:00', 'index')]) +def test_resample_how_ohlc(series): + s = series + grouplist = np.ones_like(s) + grouplist[0] = 0 + grouplist[1:6] = 1 + grouplist[6:11] = 2 + grouplist[11:] = 3 + + def _ohlc(group): + if isna(group).all(): + return np.repeat(np.nan, 4) + return [group[0], group.max(), group.min(), group[-1]] + + expected = DataFrame( + s.groupby(grouplist).agg(_ohlc).values.tolist(), + index=date_range('1/1/2000', periods=4, freq='5min', name='index'), + columns=['open', 'high', 'low', 'close']) + + result = s.resample('5min', closed='right', label='right').ohlc() + assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + 'func', ['min', 'max', 'sum', 'prod', 'mean', 'var', 'std']) +def test_numpy_compat(func): + # see gh-12811 + s = Series([1, 2, 3, 4, 5], index=date_range( + '20130101', periods=5, freq='s')) + r = s.resample('2s') + + msg = "numpy operations are not valid with resample" + + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(r, func)(func, 1, 2, 3) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(r, func)(axis=1) + + +def test_resample_how_callables(): + # GH#7929 + data = np.arange(5, dtype=np.int64) + ind = date_range(start='2014-01-01', periods=len(data), freq='d') + df = DataFrame({"A": data, "B": data}, index=ind) + + def fn(x, a=1): + return str(type(x)) + + class FnClass(object): + + def __call__(self, x): + return str(type(x)) + + df_standard = df.resample("M").apply(fn) + df_lambda = df.resample("M").apply(lambda x: str(type(x))) + df_partial = df.resample("M").apply(partial(fn)) + df_partial2 = df.resample("M").apply(partial(fn, a=2)) + df_class = df.resample("M").apply(FnClass()) + + assert_frame_equal(df_standard, df_lambda) + assert_frame_equal(df_standard, df_partial) + assert_frame_equal(df_standard, df_partial2) + assert_frame_equal(df_standard, df_class) + + +def test_resample_rounding(): + # GH 8371 + # odd results when rounding is needed + + data = """date,time,value +11-08-2014,00:00:01.093,1 +11-08-2014,00:00:02.159,1 +11-08-2014,00:00:02.667,1 +11-08-2014,00:00:03.175,1 +11-08-2014,00:00:07.058,1 +11-08-2014,00:00:07.362,1 +11-08-2014,00:00:08.324,1 +11-08-2014,00:00:08.830,1 +11-08-2014,00:00:08.982,1 +11-08-2014,00:00:09.815,1 +11-08-2014,00:00:10.540,1 +11-08-2014,00:00:11.061,1 +11-08-2014,00:00:11.617,1 +11-08-2014,00:00:13.607,1 +11-08-2014,00:00:14.535,1 +11-08-2014,00:00:15.525,1 +11-08-2014,00:00:17.960,1 +11-08-2014,00:00:20.674,1 +11-08-2014,00:00:21.191,1""" + + df = pd.read_csv(StringIO(data), parse_dates={'timestamp': [ + 'date', 'time']}, index_col='timestamp') + df.index.name = None + result = df.resample('6s').sum() + expected = DataFrame({'value': [ + 4, 9, 4, 2 + ]}, index=date_range('2014-11-08', freq='6s', periods=4)) + assert_frame_equal(result, expected) + + result = df.resample('7s').sum() + expected = DataFrame({'value': [ + 4, 10, 4, 1 + ]}, index=date_range('2014-11-08', freq='7s', periods=4)) + assert_frame_equal(result, expected) + + result = df.resample('11s').sum() + expected = DataFrame({'value': [ + 11, 8 + ]}, index=date_range('2014-11-08', freq='11s', periods=2)) + assert_frame_equal(result, expected) + + result = df.resample('13s').sum() + expected = DataFrame({'value': [ + 13, 6 + ]}, index=date_range('2014-11-08', freq='13s', periods=2)) + assert_frame_equal(result, expected) + + result = df.resample('17s').sum() + expected = DataFrame({'value': [ + 16, 3 + ]}, index=date_range('2014-11-08', freq='17s', periods=2)) + assert_frame_equal(result, expected) + + +def test_resample_basic_from_daily(): + # from daily + dti = date_range(start=datetime(2005, 1, 1), + end=datetime(2005, 1, 10), freq='D', name='index') + + s = Series(np.random.rand(len(dti)), dti) + + # to weekly + result = s.resample('w-sun').last() + + assert len(result) == 3 + assert (result.index.dayofweek == [6, 6, 6]).all() + assert result.iloc[0] == s['1/2/2005'] + assert result.iloc[1] == s['1/9/2005'] + assert result.iloc[2] == s.iloc[-1] + + result = s.resample('W-MON').last() + assert len(result) == 2 + assert (result.index.dayofweek == [0, 0]).all() + assert result.iloc[0] == s['1/3/2005'] + assert result.iloc[1] == s['1/10/2005'] + + result = s.resample('W-TUE').last() + assert len(result) == 2 + assert (result.index.dayofweek == [1, 1]).all() + assert result.iloc[0] == s['1/4/2005'] + assert result.iloc[1] == s['1/10/2005'] + + result = s.resample('W-WED').last() + assert len(result) == 2 + assert (result.index.dayofweek == [2, 2]).all() + assert result.iloc[0] == s['1/5/2005'] + assert result.iloc[1] == s['1/10/2005'] + + result = s.resample('W-THU').last() + assert len(result) == 2 + assert (result.index.dayofweek == [3, 3]).all() + assert result.iloc[0] == s['1/6/2005'] + assert result.iloc[1] == s['1/10/2005'] + + result = s.resample('W-FRI').last() + assert len(result) == 2 + assert (result.index.dayofweek == [4, 4]).all() + assert result.iloc[0] == s['1/7/2005'] + assert result.iloc[1] == s['1/10/2005'] + + # to biz day + result = s.resample('B').last() + assert len(result) == 7 + assert (result.index.dayofweek == [4, 0, 1, 2, 3, 4, 0]).all() + + assert result.iloc[0] == s['1/2/2005'] + assert result.iloc[1] == s['1/3/2005'] + assert result.iloc[5] == s['1/9/2005'] + assert result.index.name == 'index' + + +def test_resample_upsampling_picked_but_not_correct(): + + # Test for issue #3020 + dates = date_range('01-Jan-2014', '05-Jan-2014', freq='D') + series = Series(1, index=dates) + + result = series.resample('D').mean() + assert result.index[0] == dates[0] + + # GH 5955 + # incorrect deciding to upsample when the axis frequency matches the + # resample frequency + + s = Series(np.arange(1., 6), index=[datetime( + 1975, 1, i, 12, 0) for i in range(1, 6)]) + expected = Series(np.arange(1., 6), index=date_range( + '19750101', periods=5, freq='D')) + + result = s.resample('D').count() + assert_series_equal(result, Series(1, index=expected.index)) + + result1 = s.resample('D').sum() + result2 = s.resample('D').mean() + assert_series_equal(result1, expected) + assert_series_equal(result2, expected) + + +def test_resample_frame_basic(): + df = tm.makeTimeDataFrame() + + b = TimeGrouper('M') + g = df.groupby(b) + + # check all cython functions work + funcs = ['add', 'mean', 'prod', 'min', 'max', 'var'] + for f in funcs: + g._cython_agg_general(f) + + result = df.resample('A').mean() + assert_series_equal(result['A'], df['A'].resample('A').mean()) + + result = df.resample('M').mean() + assert_series_equal(result['A'], df['A'].resample('M').mean()) + + df.resample('M', kind='period').mean() + df.resample('W-WED', kind='period').mean() + + +@pytest.mark.parametrize('loffset', [timedelta(minutes=1), + '1min', Minute(1), + np.timedelta64(1, 'm')]) +def test_resample_loffset(loffset): + # GH 7687 + rng = date_range('1/1/2000 00:00:00', '1/1/2000 00:13:00', freq='min') + s = Series(np.random.randn(14), index=rng) + + result = s.resample('5min', closed='right', label='right', + loffset=loffset).mean() + idx = date_range('1/1/2000', periods=4, freq='5min') + expected = Series([s[0], s[1:6].mean(), s[6:11].mean(), s[11:].mean()], + index=idx + timedelta(minutes=1)) + assert_series_equal(result, expected) + assert result.index.freq == Minute(5) + + # from daily + dti = date_range(start=datetime(2005, 1, 1), + end=datetime(2005, 1, 10), freq='D') + ser = Series(np.random.rand(len(dti)), dti) + + # to weekly + result = ser.resample('w-sun').last() + business_day_offset = BDay() + expected = ser.resample('w-sun', loffset=-business_day_offset).last() + assert result.index[0] - business_day_offset == expected.index[0] + + +def test_resample_loffset_upsample(): + # GH 20744 + rng = date_range('1/1/2000 00:00:00', '1/1/2000 00:13:00', freq='min') + s = Series(np.random.randn(14), index=rng) + + result = s.resample('5min', closed='right', label='right', + loffset=timedelta(minutes=1)).ffill() + idx = date_range('1/1/2000', periods=4, freq='5min') + expected = Series([s[0], s[5], s[10], s[-1]], + index=idx + timedelta(minutes=1)) + + assert_series_equal(result, expected) + + +def test_resample_loffset_count(): + # GH 12725 + start_time = '1/1/2000 00:00:00' + rng = date_range(start_time, periods=100, freq='S') + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.resample('10S', loffset='1s').count() + + expected_index = ( + date_range(start_time, periods=10, freq='10S') + + timedelta(seconds=1) + ) + expected = Series(10, index=expected_index) + + assert_series_equal(result, expected) + + # Same issue should apply to .size() since it goes through + # same code path + result = ts.resample('10S', loffset='1s').size() + + assert_series_equal(result, expected) + + +def test_resample_upsample(): + # from daily + dti = date_range(start=datetime(2005, 1, 1), + end=datetime(2005, 1, 10), freq='D', name='index') + + s = Series(np.random.rand(len(dti)), dti) + + # to minutely, by padding + result = s.resample('Min').pad() + assert len(result) == 12961 + assert result[0] == s[0] + assert result[-1] == s[-1] + + assert result.index.name == 'index' + + +def test_resample_how_method(): + # GH9915 + s = Series([11, 22], + index=[Timestamp('2015-03-31 21:48:52.672000'), + Timestamp('2015-03-31 21:49:52.739000')]) + expected = Series([11, np.NaN, np.NaN, np.NaN, np.NaN, np.NaN, 22], + index=[Timestamp('2015-03-31 21:48:50'), + Timestamp('2015-03-31 21:49:00'), + Timestamp('2015-03-31 21:49:10'), + Timestamp('2015-03-31 21:49:20'), + Timestamp('2015-03-31 21:49:30'), + Timestamp('2015-03-31 21:49:40'), + Timestamp('2015-03-31 21:49:50')]) + assert_series_equal(s.resample("10S").mean(), expected) + + +def test_resample_extra_index_point(): + # GH#9756 + index = date_range(start='20150101', end='20150331', freq='BM') + expected = DataFrame({'A': Series([21, 41, 63], index=index)}) + + index = date_range(start='20150101', end='20150331', freq='B') + df = DataFrame( + {'A': Series(range(len(index)), index=index)}, dtype='int64') + result = df.resample('BM').last() + assert_frame_equal(result, expected) + + +def test_upsample_with_limit(): + rng = date_range('1/1/2000', periods=3, freq='5t') + ts = Series(np.random.randn(len(rng)), rng) + + result = ts.resample('t').ffill(limit=2) + expected = ts.reindex(result.index, method='ffill', limit=2) + assert_series_equal(result, expected) + + +def test_nearest_upsample_with_limit(): + rng = date_range('1/1/2000', periods=3, freq='5t') + ts = Series(np.random.randn(len(rng)), rng) + + result = ts.resample('t').nearest(limit=2) + expected = ts.reindex(result.index, method='nearest', limit=2) + assert_series_equal(result, expected) + + +def test_resample_ohlc(series): + s = series + + grouper = TimeGrouper(Minute(5)) + expect = s.groupby(grouper).agg(lambda x: x[-1]) + result = s.resample('5Min').ohlc() + + assert len(result) == len(expect) + assert len(result.columns) == 4 + + xs = result.iloc[-2] + assert xs['open'] == s[-6] + assert xs['high'] == s[-6:-1].max() + assert xs['low'] == s[-6:-1].min() + assert xs['close'] == s[-2] + + xs = result.iloc[0] + assert xs['open'] == s[0] + assert xs['high'] == s[:5].max() + assert xs['low'] == s[:5].min() + assert xs['close'] == s[4] + + +def test_resample_ohlc_result(): + + # GH 12332 + index = pd.date_range('1-1-2000', '2-15-2000', freq='h') + index = index.union(pd.date_range('4-15-2000', '5-15-2000', freq='h')) + s = Series(range(len(index)), index=index) + + a = s.loc[:'4-15-2000'].resample('30T').ohlc() + assert isinstance(a, DataFrame) + + b = s.loc[:'4-14-2000'].resample('30T').ohlc() + assert isinstance(b, DataFrame) + + # GH12348 + # raising on odd period + rng = date_range('2013-12-30', '2014-01-07') + index = rng.drop([Timestamp('2014-01-01'), + Timestamp('2013-12-31'), + Timestamp('2014-01-04'), + Timestamp('2014-01-05')]) + df = DataFrame(data=np.arange(len(index)), index=index) + result = df.resample('B').mean() + expected = df.reindex(index=date_range(rng[0], rng[-1], freq='B')) + assert_frame_equal(result, expected) + + +def test_resample_ohlc_dataframe(): + df = ( + DataFrame({ + 'PRICE': { + Timestamp('2011-01-06 10:59:05', tz=None): 24990, + Timestamp('2011-01-06 12:43:33', tz=None): 25499, + Timestamp('2011-01-06 12:54:09', tz=None): 25499}, + 'VOLUME': { + Timestamp('2011-01-06 10:59:05', tz=None): 1500000000, + Timestamp('2011-01-06 12:43:33', tz=None): 5000000000, + Timestamp('2011-01-06 12:54:09', tz=None): 100000000}}) + ).reindex(['VOLUME', 'PRICE'], axis=1) + res = df.resample('H').ohlc() + exp = pd.concat([df['VOLUME'].resample('H').ohlc(), + df['PRICE'].resample('H').ohlc()], + axis=1, + keys=['VOLUME', 'PRICE']) + assert_frame_equal(exp, res) + + df.columns = [['a', 'b'], ['c', 'd']] + res = df.resample('H').ohlc() + exp.columns = pd.MultiIndex.from_tuples([ + ('a', 'c', 'open'), ('a', 'c', 'high'), ('a', 'c', 'low'), + ('a', 'c', 'close'), ('b', 'd', 'open'), ('b', 'd', 'high'), + ('b', 'd', 'low'), ('b', 'd', 'close')]) + assert_frame_equal(exp, res) + + # dupe columns fail atm + # df.columns = ['PRICE', 'PRICE'] + + +def test_resample_dup_index(): + + # GH 4812 + # dup columns with resample raising + df = DataFrame(np.random.randn(4, 12), index=[2000, 2000, 2000, 2000], + columns=[Period(year=2000, month=i + 1, freq='M') + for i in range(12)]) + df.iloc[3, :] = np.nan + result = df.resample('Q', axis=1).mean() + expected = df.groupby(lambda x: int((x.month - 1) / 3), axis=1).mean() + expected.columns = [ + Period(year=2000, quarter=i + 1, freq='Q') for i in range(4)] + assert_frame_equal(result, expected) + + +def test_resample_reresample(): + dti = date_range(start=datetime(2005, 1, 1), + end=datetime(2005, 1, 10), freq='D') + s = Series(np.random.rand(len(dti)), dti) + bs = s.resample('B', closed='right', label='right').mean() + result = bs.resample('8H').mean() + assert len(result) == 22 + assert isinstance(result.index.freq, offsets.DateOffset) + assert result.index.freq == offsets.Hour(8) + + +def test_resample_timestamp_to_period(simple_date_range_series): + ts = simple_date_range_series('1/1/1990', '1/1/2000') + + result = ts.resample('A-DEC', kind='period').mean() + expected = ts.resample('A-DEC').mean() + expected.index = period_range('1990', '2000', freq='a-dec') + assert_series_equal(result, expected) + + result = ts.resample('A-JUN', kind='period').mean() + expected = ts.resample('A-JUN').mean() + expected.index = period_range('1990', '2000', freq='a-jun') + assert_series_equal(result, expected) + + result = ts.resample('M', kind='period').mean() + expected = ts.resample('M').mean() + expected.index = period_range('1990-01', '2000-01', freq='M') + assert_series_equal(result, expected) + + result = ts.resample('M', kind='period').mean() + expected = ts.resample('M').mean() + expected.index = period_range('1990-01', '2000-01', freq='M') + assert_series_equal(result, expected) + + +def test_ohlc_5min(): + def _ohlc(group): + if isna(group).all(): + return np.repeat(np.nan, 4) + return [group[0], group.max(), group.min(), group[-1]] + + rng = date_range('1/1/2000 00:00:00', '1/1/2000 5:59:50', freq='10s') + ts = Series(np.random.randn(len(rng)), index=rng) + + resampled = ts.resample('5min', closed='right', + label='right').ohlc() + + assert (resampled.loc['1/1/2000 00:00'] == ts[0]).all() + + exp = _ohlc(ts[1:31]) + assert (resampled.loc['1/1/2000 00:05'] == exp).all() + + exp = _ohlc(ts['1/1/2000 5:55:01':]) + assert (resampled.loc['1/1/2000 6:00:00'] == exp).all() + + +def test_downsample_non_unique(): + rng = date_range('1/1/2000', '2/29/2000') + rng2 = rng.repeat(5).values + ts = Series(np.random.randn(len(rng2)), index=rng2) + + result = ts.resample('M').mean() + + expected = ts.groupby(lambda x: x.month).mean() + assert len(result) == 2 + assert_almost_equal(result[0], expected[1]) + assert_almost_equal(result[1], expected[2]) + + +def test_asfreq_non_unique(): + # GH #1077 + rng = date_range('1/1/2000', '2/29/2000') + rng2 = rng.repeat(2).values + ts = Series(np.random.randn(len(rng2)), index=rng2) + + msg = 'cannot reindex from a duplicate axis' + with pytest.raises(ValueError, match=msg): + ts.asfreq('B') + + +def test_resample_axis1(): + rng = date_range('1/1/2000', '2/29/2000') + df = DataFrame(np.random.randn(3, len(rng)), columns=rng, + index=['a', 'b', 'c']) + + result = df.resample('M', axis=1).mean() + expected = df.T.resample('M').mean().T + tm.assert_frame_equal(result, expected) + + +def test_resample_anchored_ticks(): + # If a fixed delta (5 minute, 4 hour) evenly divides a day, we should + # "anchor" the origin at midnight so we get regular intervals rather + # than starting from the first timestamp which might start in the + # middle of a desired interval + + rng = date_range('1/1/2000 04:00:00', periods=86400, freq='s') + ts = Series(np.random.randn(len(rng)), index=rng) + ts[:2] = np.nan # so results are the same + + freqs = ['t', '5t', '15t', '30t', '4h', '12h'] + for freq in freqs: + result = ts[2:].resample(freq, closed='left', label='left').mean() + expected = ts.resample(freq, closed='left', label='left').mean() + assert_series_equal(result, expected) + + +def test_resample_single_group(): + mysum = lambda x: x.sum() + + rng = date_range('2000-1-1', '2000-2-10', freq='D') + ts = Series(np.random.randn(len(rng)), index=rng) + assert_series_equal(ts.resample('M').sum(), + ts.resample('M').apply(mysum)) + + rng = date_range('2000-1-1', '2000-1-10', freq='D') + ts = Series(np.random.randn(len(rng)), index=rng) + assert_series_equal(ts.resample('M').sum(), + ts.resample('M').apply(mysum)) + + # GH 3849 + s = Series([30.1, 31.6], index=[Timestamp('20070915 15:30:00'), + Timestamp('20070915 15:40:00')]) + expected = Series([0.75], index=[Timestamp('20070915')]) + result = s.resample('D').apply(lambda x: np.std(x)) + assert_series_equal(result, expected) + + +def test_resample_base(): + rng = date_range('1/1/2000 00:00:00', '1/1/2000 02:00', freq='s') + ts = Series(np.random.randn(len(rng)), index=rng) + + resampled = ts.resample('5min', base=2).mean() + exp_rng = date_range('12/31/1999 23:57:00', '1/1/2000 01:57', + freq='5min') + tm.assert_index_equal(resampled.index, exp_rng) + + +def test_resample_daily_anchored(): + rng = date_range('1/1/2000 0:00:00', periods=10000, freq='T') + ts = Series(np.random.randn(len(rng)), index=rng) + ts[:2] = np.nan # so results are the same + + result = ts[2:].resample('D', closed='left', label='left').mean() + expected = ts.resample('D', closed='left', label='left').mean() + assert_series_equal(result, expected) + + +def test_resample_to_period_monthly_buglet(): + # GH #1259 + + rng = date_range('1/1/2000', '12/31/2000') + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.resample('M', kind='period').mean() + exp_index = period_range('Jan-2000', 'Dec-2000', freq='M') + tm.assert_index_equal(result.index, exp_index) + + +def test_period_with_agg(): + + # aggregate a period resampler with a lambda + s2 = Series(np.random.randint(0, 5, 50), + index=pd.period_range('2012-01-01', freq='H', periods=50), + dtype='float64') + + expected = s2.to_timestamp().resample('D').mean().to_period() + result = s2.resample('D').agg(lambda x: x.mean()) + assert_series_equal(result, expected) + + +def test_resample_segfault(): + # GH 8573 + # segfaulting in older versions + all_wins_and_wagers = [ + (1, datetime(2013, 10, 1, 16, 20), 1, 0), + (2, datetime(2013, 10, 1, 16, 10), 1, 0), + (2, datetime(2013, 10, 1, 18, 15), 1, 0), + (2, datetime(2013, 10, 1, 16, 10, 31), 1, 0)] + + df = DataFrame.from_records(all_wins_and_wagers, + columns=("ID", "timestamp", "A", "B") + ).set_index("timestamp") + result = df.groupby("ID").resample("5min").sum() + expected = df.groupby("ID").apply(lambda x: x.resample("5min").sum()) + assert_frame_equal(result, expected) + + +def test_resample_dtype_preservation(): + + # GH 12202 + # validation tests for dtype preservation + + df = DataFrame({'date': pd.date_range(start='2016-01-01', + periods=4, freq='W'), + 'group': [1, 1, 2, 2], + 'val': Series([5, 6, 7, 8], + dtype='int32')} + ).set_index('date') + + result = df.resample('1D').ffill() + assert result.val.dtype == np.int32 + + result = df.groupby('group').resample('1D').ffill() + assert result.val.dtype == np.int32 + + +def test_resample_dtype_coerceion(): + + pytest.importorskip('scipy.interpolate') + + # GH 16361 + df = {"a": [1, 3, 1, 4]} + df = DataFrame(df, index=pd.date_range("2017-01-01", "2017-01-04")) + + expected = (df.astype("float64") + .resample("H") + .mean() + ["a"] + .interpolate("cubic") + ) + + result = df.resample("H")["a"].mean().interpolate("cubic") + tm.assert_series_equal(result, expected) + + result = df.resample("H").mean()["a"].interpolate("cubic") + tm.assert_series_equal(result, expected) + + +def test_weekly_resample_buglet(): + # #1327 + rng = date_range('1/1/2000', freq='B', periods=20) + ts = Series(np.random.randn(len(rng)), index=rng) + + resampled = ts.resample('W').mean() + expected = ts.resample('W-SUN').mean() + assert_series_equal(resampled, expected) + + +def test_monthly_resample_error(): + # #1451 + dates = date_range('4/16/2012 20:00', periods=5000, freq='h') + ts = Series(np.random.randn(len(dates)), index=dates) + # it works! + ts.resample('M') + + +def test_nanosecond_resample_error(): + # GH 12307 - Values falls after last bin when + # Resampling using pd.tseries.offsets.Nano as period + start = 1443707890427 + exp_start = 1443707890400 + indx = pd.date_range( + start=pd.to_datetime(start), + periods=10, + freq='100n' + ) + ts = Series(range(len(indx)), index=indx) + r = ts.resample(pd.tseries.offsets.Nano(100)) + result = r.agg('mean') + + exp_indx = pd.date_range( + start=pd.to_datetime(exp_start), + periods=10, + freq='100n' + ) + exp = Series(range(len(exp_indx)), index=exp_indx) + + assert_series_equal(result, exp) + + +def test_resample_anchored_intraday(simple_date_range_series): + # #1471, #1458 + + rng = date_range('1/1/2012', '4/1/2012', freq='100min') + df = DataFrame(rng.month, index=rng) + + result = df.resample('M').mean() + expected = df.resample( + 'M', kind='period').mean().to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') + tm.assert_frame_equal(result, expected) + + result = df.resample('M', closed='left').mean() + exp = df.tshift(1, freq='D').resample('M', kind='period').mean() + exp = exp.to_timestamp(how='end') + + exp.index = exp.index + Timedelta(1, 'ns') - Timedelta(1, 'D') + tm.assert_frame_equal(result, exp) + + rng = date_range('1/1/2012', '4/1/2012', freq='100min') + df = DataFrame(rng.month, index=rng) + + result = df.resample('Q').mean() + expected = df.resample( + 'Q', kind='period').mean().to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') + tm.assert_frame_equal(result, expected) + + result = df.resample('Q', closed='left').mean() + expected = df.tshift(1, freq='D').resample('Q', kind='period', + closed='left').mean() + expected = expected.to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') + tm.assert_frame_equal(result, expected) + + ts = simple_date_range_series('2012-04-29 23:00', '2012-04-30 5:00', + freq='h') + resampled = ts.resample('M').mean() + assert len(resampled) == 1 + + +def test_resample_anchored_monthstart(simple_date_range_series): + ts = simple_date_range_series('1/1/2000', '12/31/2002') + + freqs = ['MS', 'BMS', 'QS-MAR', 'AS-DEC', 'AS-JUN'] + + for freq in freqs: + ts.resample(freq).mean() + + +def test_resample_anchored_multiday(): + # When resampling a range spanning multiple days, ensure that the + # start date gets used to determine the offset. Fixes issue where + # a one day period is not a multiple of the frequency. + # + # See: https://github.com/pandas-dev/pandas/issues/8683 + + index = pd.date_range( + '2014-10-14 23:06:23.206', periods=3, freq='400L' + ) | pd.date_range( + '2014-10-15 23:00:00', periods=2, freq='2200L') + + s = Series(np.random.randn(5), index=index) + + # Ensure left closing works + result = s.resample('2200L').mean() + assert result.index[-1] == Timestamp('2014-10-15 23:00:02.000') + + # Ensure right closing works + result = s.resample('2200L', label='right').mean() + assert result.index[-1] == Timestamp('2014-10-15 23:00:04.200') + + +def test_corner_cases(simple_period_range_series, + simple_date_range_series): + # miscellaneous test coverage + + rng = date_range('1/1/2000', periods=12, freq='t') + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.resample('5t', closed='right', label='left').mean() + ex_index = date_range('1999-12-31 23:55', periods=4, freq='5t') + tm.assert_index_equal(result.index, ex_index) + + len0pts = simple_period_range_series( + '2007-01', '2010-05', freq='M')[:0] + # it works + result = len0pts.resample('A-DEC').mean() + assert len(result) == 0 + + # resample to periods + ts = simple_date_range_series( + '2000-04-28', '2000-04-30 11:00', freq='h') + result = ts.resample('M', kind='period').mean() + assert len(result) == 1 + assert result.index[0] == Period('2000-04', freq='M') + + +def test_anchored_lowercase_buglet(): + dates = date_range('4/16/2012 20:00', periods=50000, freq='s') + ts = Series(np.random.randn(len(dates)), index=dates) + # it works! + ts.resample('d').mean() + + +def test_upsample_apply_functions(): + # #1596 + rng = pd.date_range('2012-06-12', periods=4, freq='h') + + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.resample('20min').aggregate(['mean', 'sum']) + assert isinstance(result, DataFrame) + + +def test_resample_not_monotonic(): + rng = pd.date_range('2012-06-12', periods=200, freq='h') + ts = Series(np.random.randn(len(rng)), index=rng) + + ts = ts.take(np.random.permutation(len(ts))) + + result = ts.resample('D').sum() + exp = ts.sort_index().resample('D').sum() + assert_series_equal(result, exp) + + +def test_resample_median_bug_1688(): + + for dtype in ['int64', 'int32', 'float64', 'float32']: + df = DataFrame([1, 2], index=[datetime(2012, 1, 1, 0, 0, 0), + datetime(2012, 1, 1, 0, 5, 0)], + dtype=dtype) + + result = df.resample("T").apply(lambda x: x.mean()) + exp = df.asfreq('T') + tm.assert_frame_equal(result, exp) + + result = df.resample("T").median() + exp = df.asfreq('T') + tm.assert_frame_equal(result, exp) + + +def test_how_lambda_functions(simple_date_range_series): + + ts = simple_date_range_series('1/1/2000', '4/1/2000') + + result = ts.resample('M').apply(lambda x: x.mean()) + exp = ts.resample('M').mean() + tm.assert_series_equal(result, exp) + + foo_exp = ts.resample('M').mean() + foo_exp.name = 'foo' + bar_exp = ts.resample('M').std() + bar_exp.name = 'bar' + + result = ts.resample('M').apply( + [lambda x: x.mean(), lambda x: x.std(ddof=1)]) + result.columns = ['foo', 'bar'] + tm.assert_series_equal(result['foo'], foo_exp) + tm.assert_series_equal(result['bar'], bar_exp) + + # this is a MI Series, so comparing the names of the results + # doesn't make sense + result = ts.resample('M').aggregate({'foo': lambda x: x.mean(), + 'bar': lambda x: x.std(ddof=1)}) + tm.assert_series_equal(result['foo'], foo_exp, check_names=False) + tm.assert_series_equal(result['bar'], bar_exp, check_names=False) + + +def test_resample_unequal_times(): + # #1772 + start = datetime(1999, 3, 1, 5) + # end hour is less than start + end = datetime(2012, 7, 31, 4) + bad_ind = date_range(start, end, freq="30min") + df = DataFrame({'close': 1}, index=bad_ind) + + # it works! + df.resample('AS').sum() + + +def test_resample_consistency(): + + # GH 6418 + # resample with bfill / limit / reindex consistency + + i30 = pd.date_range('2002-02-02', periods=4, freq='30T') + s = Series(np.arange(4.), index=i30) + s[2] = np.NaN + + # Upsample by factor 3 with reindex() and resample() methods: + i10 = pd.date_range(i30[0], i30[-1], freq='10T') + + s10 = s.reindex(index=i10, method='bfill') + s10_2 = s.reindex(index=i10, method='bfill', limit=2) + rl = s.reindex_like(s10, method='bfill', limit=2) + r10_2 = s.resample('10Min').bfill(limit=2) + r10 = s.resample('10Min').bfill() + + # s10_2, r10, r10_2, rl should all be equal + assert_series_equal(s10_2, r10) + assert_series_equal(s10_2, r10_2) + assert_series_equal(s10_2, rl) + + +def test_resample_timegrouper(): + # GH 7227 + dates1 = [datetime(2014, 10, 1), datetime(2014, 9, 3), + datetime(2014, 11, 5), datetime(2014, 9, 5), + datetime(2014, 10, 8), datetime(2014, 7, 15)] + + dates2 = dates1[:2] + [pd.NaT] + dates1[2:4] + [pd.NaT] + dates1[4:] + dates3 = [pd.NaT] + dates1 + [pd.NaT] + + for dates in [dates1, dates2, dates3]: + df = DataFrame(dict(A=dates, B=np.arange(len(dates)))) + result = df.set_index('A').resample('M').count() + exp_idx = pd.DatetimeIndex(['2014-07-31', '2014-08-31', + '2014-09-30', + '2014-10-31', '2014-11-30'], + freq='M', name='A') + expected = DataFrame({'B': [1, 0, 2, 2, 1]}, index=exp_idx) + assert_frame_equal(result, expected) + + result = df.groupby(pd.Grouper(freq='M', key='A')).count() + assert_frame_equal(result, expected) + + df = DataFrame(dict(A=dates, B=np.arange(len(dates)), C=np.arange( + len(dates)))) + result = df.set_index('A').resample('M').count() + expected = DataFrame({'B': [1, 0, 2, 2, 1], 'C': [1, 0, 2, 2, 1]}, + index=exp_idx, columns=['B', 'C']) + assert_frame_equal(result, expected) + + result = df.groupby(pd.Grouper(freq='M', key='A')).count() + assert_frame_equal(result, expected) + + +def test_resample_nunique(): + + # GH 12352 + df = DataFrame({ + 'ID': {Timestamp('2015-06-05 00:00:00'): '0010100903', + Timestamp('2015-06-08 00:00:00'): '0010150847'}, + 'DATE': {Timestamp('2015-06-05 00:00:00'): '2015-06-05', + Timestamp('2015-06-08 00:00:00'): '2015-06-08'}}) + r = df.resample('D') + g = df.groupby(pd.Grouper(freq='D')) + expected = df.groupby(pd.Grouper(freq='D')).ID.apply(lambda x: + x.nunique()) + assert expected.name == 'ID' + + for t in [r, g]: + result = r.ID.nunique() + assert_series_equal(result, expected) + + result = df.ID.resample('D').nunique() + assert_series_equal(result, expected) + + result = df.ID.groupby(pd.Grouper(freq='D')).nunique() + assert_series_equal(result, expected) + + +def test_resample_nunique_preserves_column_level_names(): + # see gh-23222 + df = tm.makeTimeDataFrame(freq="1D").abs() + df.columns = pd.MultiIndex.from_arrays([df.columns.tolist()] * 2, + names=["lev0", "lev1"]) + result = df.resample("1h").nunique() + tm.assert_index_equal(df.columns, result.columns) + + +def test_resample_nunique_with_date_gap(): + # GH 13453 + index = pd.date_range('1-1-2000', '2-15-2000', freq='h') + index2 = pd.date_range('4-15-2000', '5-15-2000', freq='h') + index3 = index.append(index2) + s = Series(range(len(index3)), index=index3, dtype='int64') + r = s.resample('M') + + # Since all elements are unique, these should all be the same + results = [ + r.count(), + r.nunique(), + r.agg(Series.nunique), + r.agg('nunique') + ] + + assert_series_equal(results[0], results[1]) + assert_series_equal(results[0], results[2]) + assert_series_equal(results[0], results[3]) + + +@pytest.mark.parametrize('n', [10000, 100000]) +@pytest.mark.parametrize('k', [10, 100, 1000]) +def test_resample_group_info(n, k): + # GH10914 + + # use a fixed seed to always have the same uniques + prng = np.random.RandomState(1234) + + dr = date_range(start='2015-08-27', periods=n // 10, freq='T') + ts = Series(prng.randint(0, n // k, n).astype('int64'), + index=prng.choice(dr, n)) + + left = ts.resample('30T').nunique() + ix = date_range(start=ts.index.min(), end=ts.index.max(), + freq='30T') + + vals = ts.values + bins = np.searchsorted(ix.values, ts.index, side='right') + + sorter = np.lexsort((vals, bins)) + vals, bins = vals[sorter], bins[sorter] + + mask = np.r_[True, vals[1:] != vals[:-1]] + mask |= np.r_[True, bins[1:] != bins[:-1]] + + arr = np.bincount(bins[mask] - 1, + minlength=len(ix)).astype('int64', copy=False) + right = Series(arr, index=ix) + + assert_series_equal(left, right) + + +def test_resample_size(): + n = 10000 + dr = date_range('2015-09-19', periods=n, freq='T') + ts = Series(np.random.randn(n), index=np.random.choice(dr, n)) + + left = ts.resample('7T').size() + ix = date_range(start=left.index.min(), end=ts.index.max(), freq='7T') + + bins = np.searchsorted(ix.values, ts.index.values, side='right') + val = np.bincount(bins, minlength=len(ix) + 1)[1:].astype('int64', + copy=False) + + right = Series(val, index=ix) + assert_series_equal(left, right) + + +def test_resample_across_dst(): + # The test resamples a DatetimeIndex with values before and after a + # DST change + # Issue: 14682 + + # The DatetimeIndex we will start with + # (note that DST happens at 03:00+02:00 -> 02:00+01:00) + # 2016-10-30 02:23:00+02:00, 2016-10-30 02:23:00+01:00 + df1 = DataFrame([1477786980, 1477790580], columns=['ts']) + dti1 = DatetimeIndex(pd.to_datetime(df1.ts, unit='s') + .dt.tz_localize('UTC') + .dt.tz_convert('Europe/Madrid')) + + # The expected DatetimeIndex after resampling. + # 2016-10-30 02:00:00+02:00, 2016-10-30 02:00:00+01:00 + df2 = DataFrame([1477785600, 1477789200], columns=['ts']) + dti2 = DatetimeIndex(pd.to_datetime(df2.ts, unit='s') + .dt.tz_localize('UTC') + .dt.tz_convert('Europe/Madrid')) + df = DataFrame([5, 5], index=dti1) + + result = df.resample(rule='H').sum() + expected = DataFrame([5, 5], index=dti2) + + assert_frame_equal(result, expected) + + +def test_groupby_with_dst_time_change(): + # GH 24972 + index = pd.DatetimeIndex([1478064900001000000, 1480037118776792000], + tz='UTC').tz_convert('America/Chicago') + + df = pd.DataFrame([1, 2], index=index) + result = df.groupby(pd.Grouper(freq='1d')).last() + expected_index_values = pd.date_range('2016-11-02', '2016-11-24', + freq='d', tz='America/Chicago') + + index = pd.DatetimeIndex(expected_index_values) + expected = pd.DataFrame([1.0] + ([np.nan] * 21) + [2.0], index=index) + assert_frame_equal(result, expected) + + +def test_resample_dst_anchor(): + # 5172 + dti = DatetimeIndex([datetime(2012, 11, 4, 23)], tz='US/Eastern') + df = DataFrame([5], index=dti) + assert_frame_equal(df.resample(rule='D').sum(), + DataFrame([5], index=df.index.normalize())) + df.resample(rule='MS').sum() + assert_frame_equal( + df.resample(rule='MS').sum(), + DataFrame([5], index=DatetimeIndex([datetime(2012, 11, 1)], + tz='US/Eastern'))) + + dti = date_range('2013-09-30', '2013-11-02', freq='30Min', + tz='Europe/Paris') + values = range(dti.size) + df = DataFrame({"a": values, + "b": values, + "c": values}, index=dti, dtype='int64') + how = {"a": "min", "b": "max", "c": "count"} + + assert_frame_equal( + df.resample("W-MON").agg(how)[["a", "b", "c"]], + DataFrame({"a": [0, 48, 384, 720, 1056, 1394], + "b": [47, 383, 719, 1055, 1393, 1586], + "c": [48, 336, 336, 336, 338, 193]}, + index=date_range('9/30/2013', '11/4/2013', + freq='W-MON', tz='Europe/Paris')), + 'W-MON Frequency') + + assert_frame_equal( + df.resample("2W-MON").agg(how)[["a", "b", "c"]], + DataFrame({"a": [0, 48, 720, 1394], + "b": [47, 719, 1393, 1586], + "c": [48, 672, 674, 193]}, + index=date_range('9/30/2013', '11/11/2013', + freq='2W-MON', tz='Europe/Paris')), + '2W-MON Frequency') + + assert_frame_equal( + df.resample("MS").agg(how)[["a", "b", "c"]], + DataFrame({"a": [0, 48, 1538], + "b": [47, 1537, 1586], + "c": [48, 1490, 49]}, + index=date_range('9/1/2013', '11/1/2013', + freq='MS', tz='Europe/Paris')), + 'MS Frequency') + + assert_frame_equal( + df.resample("2MS").agg(how)[["a", "b", "c"]], + DataFrame({"a": [0, 1538], + "b": [1537, 1586], + "c": [1538, 49]}, + index=date_range('9/1/2013', '11/1/2013', + freq='2MS', tz='Europe/Paris')), + '2MS Frequency') + + df_daily = df['10/26/2013':'10/29/2013'] + assert_frame_equal( + df_daily.resample("D").agg({"a": "min", "b": "max", "c": "count"}) + [["a", "b", "c"]], + DataFrame({"a": [1248, 1296, 1346, 1394], + "b": [1295, 1345, 1393, 1441], + "c": [48, 50, 48, 48]}, + index=date_range('10/26/2013', '10/29/2013', + freq='D', tz='Europe/Paris')), + 'D Frequency') + + +def test_downsample_across_dst(): + # GH 8531 + tz = pytz.timezone('Europe/Berlin') + dt = datetime(2014, 10, 26) + dates = date_range(tz.localize(dt), periods=4, freq='2H') + result = Series(5, index=dates).resample('H').mean() + expected = Series([5., np.nan] * 3 + [5.], + index=date_range(tz.localize(dt), periods=7, + freq='H')) + tm.assert_series_equal(result, expected) + + +def test_downsample_across_dst_weekly(): + # GH 9119, GH 21459 + df = DataFrame(index=DatetimeIndex([ + '2017-03-25', '2017-03-26', '2017-03-27', + '2017-03-28', '2017-03-29' + ], tz='Europe/Amsterdam'), + data=[11, 12, 13, 14, 15]) + result = df.resample('1W').sum() + expected = DataFrame([23, 42], index=pd.DatetimeIndex([ + '2017-03-26', '2017-04-02' + ], tz='Europe/Amsterdam')) + tm.assert_frame_equal(result, expected) + + idx = pd.date_range("2013-04-01", "2013-05-01", tz='Europe/London', + freq='H') + s = Series(index=idx) + result = s.resample('W').mean() + expected = Series(index=pd.date_range( + '2013-04-07', freq='W', periods=5, tz='Europe/London' + )) + tm.assert_series_equal(result, expected) + + +def test_resample_with_nat(): + # GH 13020 + index = DatetimeIndex([pd.NaT, + '1970-01-01 00:00:00', + pd.NaT, + '1970-01-01 00:00:01', + '1970-01-01 00:00:02']) + frame = DataFrame([2, 3, 5, 7, 11], index=index) + + index_1s = DatetimeIndex(['1970-01-01 00:00:00', + '1970-01-01 00:00:01', + '1970-01-01 00:00:02']) + frame_1s = DataFrame([3, 7, 11], index=index_1s) + assert_frame_equal(frame.resample('1s').mean(), frame_1s) + + index_2s = DatetimeIndex(['1970-01-01 00:00:00', + '1970-01-01 00:00:02']) + frame_2s = DataFrame([5, 11], index=index_2s) + assert_frame_equal(frame.resample('2s').mean(), frame_2s) + + index_3s = DatetimeIndex(['1970-01-01 00:00:00']) + frame_3s = DataFrame([7], index=index_3s) + assert_frame_equal(frame.resample('3s').mean(), frame_3s) + + assert_frame_equal(frame.resample('60s').mean(), frame_3s) + + +def test_resample_datetime_values(): + # GH 13119 + # check that datetime dtype is preserved when NaT values are + # introduced by the resampling + + dates = [datetime(2016, 1, 15), datetime(2016, 1, 19)] + df = DataFrame({'timestamp': dates}, index=dates) + + exp = Series([datetime(2016, 1, 15), pd.NaT, datetime(2016, 1, 19)], + index=date_range('2016-01-15', periods=3, freq='2D'), + name='timestamp') + + res = df.resample('2D').first()['timestamp'] + tm.assert_series_equal(res, exp) + res = df['timestamp'].resample('2D').first() + tm.assert_series_equal(res, exp) + + +def test_resample_apply_with_additional_args(series): + # GH 14615 + def f(data, add_arg): + return np.mean(data) * add_arg + + multiplier = 10 + result = series.resample('D').apply(f, multiplier) + expected = series.resample('D').mean().multiply(multiplier) + tm.assert_series_equal(result, expected) + + # Testing as kwarg + result = series.resample('D').apply(f, add_arg=multiplier) + expected = series.resample('D').mean().multiply(multiplier) + tm.assert_series_equal(result, expected) + + # Testing dataframe + df = pd.DataFrame({"A": 1, "B": 2}, + index=pd.date_range('2017', periods=10)) + result = df.groupby("A").resample("D").agg(f, multiplier) + expected = df.groupby("A").resample('D').mean().multiply(multiplier) + assert_frame_equal(result, expected) + + +@pytest.mark.parametrize('k', [1, 2, 3]) +@pytest.mark.parametrize('n1, freq1, n2, freq2', [ + (30, 'S', 0.5, 'Min'), + (60, 'S', 1, 'Min'), + (3600, 'S', 1, 'H'), + (60, 'Min', 1, 'H'), + (21600, 'S', 0.25, 'D'), + (86400, 'S', 1, 'D'), + (43200, 'S', 0.5, 'D'), + (1440, 'Min', 1, 'D'), + (12, 'H', 0.5, 'D'), + (24, 'H', 1, 'D'), +]) +def test_resample_equivalent_offsets(n1, freq1, n2, freq2, k): + # GH 24127 + n1_ = n1 * k + n2_ = n2 * k + s = pd.Series(0, index=pd.date_range('19910905 13:00', + '19911005 07:00', + freq=freq1)) + s = s + range(len(s)) + + result1 = s.resample(str(n1_) + freq1).mean() + result2 = s.resample(str(n2_) + freq2).mean() + assert_series_equal(result1, result2) + + +@pytest.mark.parametrize('first,last,offset,exp_first,exp_last', [ + ('19910905', '19920406', 'D', '19910905', '19920407'), + ('19910905 00:00', '19920406 06:00', 'D', '19910905', '19920407'), + ('19910905 06:00', '19920406 06:00', 'H', '19910905 06:00', + '19920406 07:00'), + ('19910906', '19920406', 'M', '19910831', '19920430'), + ('19910831', '19920430', 'M', '19910831', '19920531'), + ('1991-08', '1992-04', 'M', '19910831', '19920531'), +]) +def test_get_timestamp_range_edges(first, last, offset, + exp_first, exp_last): + first = pd.Period(first) + first = first.to_timestamp(first.freq) + last = pd.Period(last) + last = last.to_timestamp(last.freq) + + exp_first = pd.Timestamp(exp_first, freq=offset) + exp_last = pd.Timestamp(exp_last, freq=offset) + + offset = pd.tseries.frequencies.to_offset(offset) + result = _get_timestamp_range_edges(first, last, offset) + expected = (exp_first, exp_last) + assert result == expected diff --git a/pandas/tests/resample/test_period_index.py b/pandas/tests/resample/test_period_index.py new file mode 100644 index 0000000000000..8abdf9034527b --- /dev/null +++ b/pandas/tests/resample/test_period_index.py @@ -0,0 +1,772 @@ +from datetime import datetime, timedelta + +import dateutil +import numpy as np +import pytest +import pytz + +from pandas._libs.tslibs.ccalendar import DAYS, MONTHS +from pandas._libs.tslibs.period import IncompatibleFrequency +from pandas.compat import lrange, range, zip + +import pandas as pd +from pandas import DataFrame, Series, Timestamp +from pandas.core.indexes.base import InvalidIndexError +from pandas.core.indexes.datetimes import date_range +from pandas.core.indexes.period import Period, PeriodIndex, period_range +from pandas.core.resample import _get_period_range_edges +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) + +import pandas.tseries.offsets as offsets + + +@pytest.fixture() +def _index_factory(): + return period_range + + +@pytest.fixture +def _series_name(): + return 'pi' + + +class TestPeriodIndex(object): + + @pytest.mark.parametrize('freq', ['2D', '1H', '2H']) + @pytest.mark.parametrize('kind', ['period', None, 'timestamp']) + def test_asfreq(self, series_and_frame, freq, kind): + # GH 12884, 15944 + # make sure .asfreq() returns PeriodIndex (except kind='timestamp') + + obj = series_and_frame + if kind == 'timestamp': + expected = obj.to_timestamp().resample(freq).asfreq() + else: + start = obj.index[0].to_timestamp(how='start') + end = (obj.index[-1] + obj.index.freq).to_timestamp(how='start') + new_index = date_range(start=start, end=end, freq=freq, + closed='left') + expected = obj.to_timestamp().reindex(new_index).to_period(freq) + result = obj.resample(freq, kind=kind).asfreq() + assert_almost_equal(result, expected) + + def test_asfreq_fill_value(self, series): + # test for fill value during resampling, issue 3715 + + s = series + new_index = date_range(s.index[0].to_timestamp(how='start'), + (s.index[-1]).to_timestamp(how='start'), + freq='1H') + expected = s.to_timestamp().reindex(new_index, fill_value=4.0) + result = s.resample('1H', kind='timestamp').asfreq(fill_value=4.0) + assert_series_equal(result, expected) + + frame = s.to_frame('value') + new_index = date_range(frame.index[0].to_timestamp(how='start'), + (frame.index[-1]).to_timestamp(how='start'), + freq='1H') + expected = frame.to_timestamp().reindex(new_index, fill_value=3.0) + result = frame.resample('1H', kind='timestamp').asfreq(fill_value=3.0) + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('freq', ['H', '12H', '2D', 'W']) + @pytest.mark.parametrize('kind', [None, 'period', 'timestamp']) + @pytest.mark.parametrize('kwargs', [dict(on='date'), dict(level='d')]) + def test_selection(self, index, freq, kind, kwargs): + # This is a bug, these should be implemented + # GH 14008 + rng = np.arange(len(index), dtype=np.int64) + df = DataFrame({'date': index, 'a': rng}, + index=pd.MultiIndex.from_arrays([rng, index], + names=['v', 'd'])) + msg = ("Resampling from level= or on= selection with a PeriodIndex is" + r" not currently supported, use \.set_index\(\.\.\.\) to" + " explicitly set index") + with pytest.raises(NotImplementedError, match=msg): + df.resample(freq, kind=kind, **kwargs) + + @pytest.mark.parametrize('month', MONTHS) + @pytest.mark.parametrize('meth', ['ffill', 'bfill']) + @pytest.mark.parametrize('conv', ['start', 'end']) + @pytest.mark.parametrize('targ', ['D', 'B', 'M']) + def test_annual_upsample_cases(self, targ, conv, meth, month, + simple_period_range_series): + ts = simple_period_range_series( + '1/1/1990', '12/31/1991', freq='A-%s' % month) + + result = getattr(ts.resample(targ, convention=conv), meth)() + expected = result.to_timestamp(targ, how=conv) + expected = expected.asfreq(targ, meth).to_period() + assert_series_equal(result, expected) + + def test_basic_downsample(self, simple_period_range_series): + ts = simple_period_range_series('1/1/1990', '6/30/1995', freq='M') + result = ts.resample('a-dec').mean() + + expected = ts.groupby(ts.index.year).mean() + expected.index = period_range('1/1/1990', '6/30/1995', freq='a-dec') + assert_series_equal(result, expected) + + # this is ok + assert_series_equal(ts.resample('a-dec').mean(), result) + assert_series_equal(ts.resample('a').mean(), result) + + @pytest.mark.parametrize('rule,expected_error_msg', [ + ('a-dec', ''), + ('q-mar', ''), + ('M', ''), + ('w-thu', '') + ]) + def test_not_subperiod( + self, simple_period_range_series, rule, expected_error_msg): + # These are incompatible period rules for resampling + ts = simple_period_range_series('1/1/1990', '6/30/1995', freq='w-wed') + msg = ("Frequency cannot be resampled to {}, as they" + " are not sub or super periods").format(expected_error_msg) + with pytest.raises(IncompatibleFrequency, match=msg): + ts.resample(rule).mean() + + @pytest.mark.parametrize('freq', ['D', '2D']) + def test_basic_upsample(self, freq, simple_period_range_series): + ts = simple_period_range_series('1/1/1990', '6/30/1995', freq='M') + result = ts.resample('a-dec').mean() + + resampled = result.resample(freq, convention='end').ffill() + expected = result.to_timestamp(freq, how='end') + expected = expected.asfreq(freq, 'ffill').to_period(freq) + assert_series_equal(resampled, expected) + + def test_upsample_with_limit(self): + rng = period_range('1/1/2000', periods=5, freq='A') + ts = Series(np.random.randn(len(rng)), rng) + + result = ts.resample('M', convention='end').ffill(limit=2) + expected = ts.asfreq('M').reindex(result.index, method='ffill', + limit=2) + assert_series_equal(result, expected) + + def test_annual_upsample(self, simple_period_range_series): + ts = simple_period_range_series('1/1/1990', '12/31/1995', freq='A-DEC') + df = DataFrame({'a': ts}) + rdf = df.resample('D').ffill() + exp = df['a'].resample('D').ffill() + assert_series_equal(rdf['a'], exp) + + rng = period_range('2000', '2003', freq='A-DEC') + ts = Series([1, 2, 3, 4], index=rng) + + result = ts.resample('M').ffill() + ex_index = period_range('2000-01', '2003-12', freq='M') + + expected = ts.asfreq('M', how='start').reindex(ex_index, + method='ffill') + assert_series_equal(result, expected) + + @pytest.mark.parametrize('month', MONTHS) + @pytest.mark.parametrize('target', ['D', 'B', 'M']) + @pytest.mark.parametrize('convention', ['start', 'end']) + def test_quarterly_upsample(self, month, target, convention, + simple_period_range_series): + freq = 'Q-{month}'.format(month=month) + ts = simple_period_range_series('1/1/1990', '12/31/1995', freq=freq) + result = ts.resample(target, convention=convention).ffill() + expected = result.to_timestamp(target, how=convention) + expected = expected.asfreq(target, 'ffill').to_period() + assert_series_equal(result, expected) + + @pytest.mark.parametrize('target', ['D', 'B']) + @pytest.mark.parametrize('convention', ['start', 'end']) + def test_monthly_upsample(self, target, convention, + simple_period_range_series): + ts = simple_period_range_series('1/1/1990', '12/31/1995', freq='M') + result = ts.resample(target, convention=convention).ffill() + expected = result.to_timestamp(target, how=convention) + expected = expected.asfreq(target, 'ffill').to_period() + assert_series_equal(result, expected) + + def test_resample_basic(self): + # GH3609 + s = Series(range(100), index=date_range( + '20130101', freq='s', periods=100, name='idx'), dtype='float') + s[10:30] = np.nan + index = PeriodIndex([ + Period('2013-01-01 00:00', 'T'), + Period('2013-01-01 00:01', 'T')], name='idx') + expected = Series([34.5, 79.5], index=index) + result = s.to_period().resample('T', kind='period').mean() + assert_series_equal(result, expected) + result2 = s.resample('T', kind='period').mean() + assert_series_equal(result2, expected) + + @pytest.mark.parametrize('freq,expected_vals', [('M', [31, 29, 31, 9]), + ('2M', [31 + 29, 31 + 9])]) + def test_resample_count(self, freq, expected_vals): + # GH12774 + series = Series(1, index=pd.period_range(start='2000', periods=100)) + result = series.resample(freq).count() + expected_index = pd.period_range(start='2000', freq=freq, + periods=len(expected_vals)) + expected = Series(expected_vals, index=expected_index) + assert_series_equal(result, expected) + + def test_resample_same_freq(self, resample_method): + + # GH12770 + series = Series(range(3), index=pd.period_range( + start='2000', periods=3, freq='M')) + expected = series + + result = getattr(series.resample('M'), resample_method)() + assert_series_equal(result, expected) + + def test_resample_incompat_freq(self): + msg = ("Frequency cannot be resampled to ," + " as they are not sub or super periods") + with pytest.raises(IncompatibleFrequency, match=msg): + Series(range(3), index=pd.period_range( + start='2000', periods=3, freq='M')).resample('W').mean() + + def test_with_local_timezone_pytz(self): + # see gh-5430 + local_timezone = pytz.timezone('America/Los_Angeles') + + start = datetime(year=2013, month=11, day=1, hour=0, minute=0, + tzinfo=pytz.utc) + # 1 day later + end = datetime(year=2013, month=11, day=2, hour=0, minute=0, + tzinfo=pytz.utc) + + index = pd.date_range(start, end, freq='H') + + series = Series(1, index=index) + series = series.tz_convert(local_timezone) + result = series.resample('D', kind='period').mean() + + # Create the expected series + # Index is moved back a day with the timezone conversion from UTC to + # Pacific + expected_index = (pd.period_range(start=start, end=end, freq='D') - + offsets.Day()) + expected = Series(1, index=expected_index) + assert_series_equal(result, expected) + + def test_resample_with_pytz(self): + # GH 13238 + s = Series(2, index=pd.date_range('2017-01-01', periods=48, freq="H", + tz="US/Eastern")) + result = s.resample("D").mean() + expected = Series(2, index=pd.DatetimeIndex(['2017-01-01', + '2017-01-02'], + tz="US/Eastern")) + assert_series_equal(result, expected) + # Especially assert that the timezone is LMT for pytz + assert result.index.tz == pytz.timezone('US/Eastern') + + def test_with_local_timezone_dateutil(self): + # see gh-5430 + local_timezone = 'dateutil/America/Los_Angeles' + + start = datetime(year=2013, month=11, day=1, hour=0, minute=0, + tzinfo=dateutil.tz.tzutc()) + # 1 day later + end = datetime(year=2013, month=11, day=2, hour=0, minute=0, + tzinfo=dateutil.tz.tzutc()) + + index = pd.date_range(start, end, freq='H', name='idx') + + series = Series(1, index=index) + series = series.tz_convert(local_timezone) + result = series.resample('D', kind='period').mean() + + # Create the expected series + # Index is moved back a day with the timezone conversion from UTC to + # Pacific + expected_index = (pd.period_range(start=start, end=end, freq='D', + name='idx') - offsets.Day()) + expected = Series(1, index=expected_index) + assert_series_equal(result, expected) + + def test_resample_nonexistent_time_bin_edge(self): + # GH 19375 + index = date_range('2017-03-12', '2017-03-12 1:45:00', freq='15T') + s = Series(np.zeros(len(index)), index=index) + expected = s.tz_localize('US/Pacific') + result = expected.resample('900S').mean() + tm.assert_series_equal(result, expected) + + # GH 23742 + index = date_range(start='2017-10-10', end='2017-10-20', freq='1H') + index = index.tz_localize('UTC').tz_convert('America/Sao_Paulo') + df = DataFrame(data=list(range(len(index))), index=index) + result = df.groupby(pd.Grouper(freq='1D')).count() + expected = date_range(start='2017-10-09', end='2017-10-20', freq='D', + tz="America/Sao_Paulo", + nonexistent='shift_forward', closed='left') + tm.assert_index_equal(result.index, expected) + + def test_resample_ambiguous_time_bin_edge(self): + # GH 10117 + idx = pd.date_range("2014-10-25 22:00:00", "2014-10-26 00:30:00", + freq="30T", tz="Europe/London") + expected = Series(np.zeros(len(idx)), index=idx) + result = expected.resample('30T').mean() + tm.assert_series_equal(result, expected) + + def test_fill_method_and_how_upsample(self): + # GH2073 + s = Series(np.arange(9, dtype='int64'), + index=date_range('2010-01-01', periods=9, freq='Q')) + last = s.resample('M').ffill() + both = s.resample('M').ffill().resample('M').last().astype('int64') + assert_series_equal(last, both) + + @pytest.mark.parametrize('day', DAYS) + @pytest.mark.parametrize('target', ['D', 'B']) + @pytest.mark.parametrize('convention', ['start', 'end']) + def test_weekly_upsample(self, day, target, convention, + simple_period_range_series): + freq = 'W-{day}'.format(day=day) + ts = simple_period_range_series('1/1/1990', '12/31/1995', freq=freq) + result = ts.resample(target, convention=convention).ffill() + expected = result.to_timestamp(target, how=convention) + expected = expected.asfreq(target, 'ffill').to_period() + assert_series_equal(result, expected) + + def test_resample_to_timestamps(self, simple_period_range_series): + ts = simple_period_range_series('1/1/1990', '12/31/1995', freq='M') + + result = ts.resample('A-DEC', kind='timestamp').mean() + expected = ts.to_timestamp(how='start').resample('A-DEC').mean() + assert_series_equal(result, expected) + + def test_resample_to_quarterly(self, simple_period_range_series): + for month in MONTHS: + ts = simple_period_range_series( + '1990', '1992', freq='A-%s' % month) + quar_ts = ts.resample('Q-%s' % month).ffill() + + stamps = ts.to_timestamp('D', how='start') + qdates = period_range(ts.index[0].asfreq('D', 'start'), + ts.index[-1].asfreq('D', 'end'), + freq='Q-%s' % month) + + expected = stamps.reindex(qdates.to_timestamp('D', 's'), + method='ffill') + expected.index = qdates + + assert_series_equal(quar_ts, expected) + + # conforms, but different month + ts = simple_period_range_series('1990', '1992', freq='A-JUN') + + for how in ['start', 'end']: + result = ts.resample('Q-MAR', convention=how).ffill() + expected = ts.asfreq('Q-MAR', how=how) + expected = expected.reindex(result.index, method='ffill') + + # .to_timestamp('D') + # expected = expected.resample('Q-MAR').ffill() + + assert_series_equal(result, expected) + + def test_resample_fill_missing(self): + rng = PeriodIndex([2000, 2005, 2007, 2009], freq='A') + + s = Series(np.random.randn(4), index=rng) + + stamps = s.to_timestamp() + filled = s.resample('A').ffill() + expected = stamps.resample('A').ffill().to_period('A') + assert_series_equal(filled, expected) + + def test_cant_fill_missing_dups(self): + rng = PeriodIndex([2000, 2005, 2005, 2007, 2007], freq='A') + s = Series(np.random.randn(5), index=rng) + msg = "Reindexing only valid with uniquely valued Index objects" + with pytest.raises(InvalidIndexError, match=msg): + s.resample('A').ffill() + + @pytest.mark.parametrize('freq', ['5min']) + @pytest.mark.parametrize('kind', ['period', None, 'timestamp']) + def test_resample_5minute(self, freq, kind): + rng = period_range('1/1/2000', '1/5/2000', freq='T') + ts = Series(np.random.randn(len(rng)), index=rng) + expected = ts.to_timestamp().resample(freq).mean() + if kind != 'timestamp': + expected = expected.to_period(freq) + result = ts.resample(freq, kind=kind).mean() + assert_series_equal(result, expected) + + def test_upsample_daily_business_daily(self, simple_period_range_series): + ts = simple_period_range_series('1/1/2000', '2/1/2000', freq='B') + + result = ts.resample('D').asfreq() + expected = ts.asfreq('D').reindex(period_range('1/3/2000', '2/1/2000')) + assert_series_equal(result, expected) + + ts = simple_period_range_series('1/1/2000', '2/1/2000') + result = ts.resample('H', convention='s').asfreq() + exp_rng = period_range('1/1/2000', '2/1/2000 23:00', freq='H') + expected = ts.asfreq('H', how='s').reindex(exp_rng) + assert_series_equal(result, expected) + + def test_resample_irregular_sparse(self): + dr = date_range(start='1/1/2012', freq='5min', periods=1000) + s = Series(np.array(100), index=dr) + # subset the data. + subset = s[:'2012-01-04 06:55'] + + result = subset.resample('10min').apply(len) + expected = s.resample('10min').apply(len).loc[result.index] + assert_series_equal(result, expected) + + def test_resample_weekly_all_na(self): + rng = date_range('1/1/2000', periods=10, freq='W-WED') + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts.resample('W-THU').asfreq() + + assert result.isna().all() + + result = ts.resample('W-THU').asfreq().ffill()[:-1] + expected = ts.asfreq('W-THU').ffill() + assert_series_equal(result, expected) + + def test_resample_tz_localized(self): + dr = date_range(start='2012-4-13', end='2012-5-1') + ts = Series(lrange(len(dr)), dr) + + ts_utc = ts.tz_localize('UTC') + ts_local = ts_utc.tz_convert('America/Los_Angeles') + + result = ts_local.resample('W').mean() + + ts_local_naive = ts_local.copy() + ts_local_naive.index = [x.replace(tzinfo=None) + for x in ts_local_naive.index.to_pydatetime()] + + exp = ts_local_naive.resample( + 'W').mean().tz_localize('America/Los_Angeles') + + assert_series_equal(result, exp) + + # it works + result = ts_local.resample('D').mean() + + # #2245 + idx = date_range('2001-09-20 15:59', '2001-09-20 16:00', freq='T', + tz='Australia/Sydney') + s = Series([1, 2], index=idx) + + result = s.resample('D', closed='right', label='right').mean() + ex_index = date_range('2001-09-21', periods=1, freq='D', + tz='Australia/Sydney') + expected = Series([1.5], index=ex_index) + + assert_series_equal(result, expected) + + # for good measure + result = s.resample('D', kind='period').mean() + ex_index = period_range('2001-09-20', periods=1, freq='D') + expected = Series([1.5], index=ex_index) + assert_series_equal(result, expected) + + # GH 6397 + # comparing an offset that doesn't propagate tz's + rng = date_range('1/1/2011', periods=20000, freq='H') + rng = rng.tz_localize('EST') + ts = DataFrame(index=rng) + ts['first'] = np.random.randn(len(rng)) + ts['second'] = np.cumsum(np.random.randn(len(rng))) + expected = DataFrame( + { + 'first': ts.resample('A').sum()['first'], + 'second': ts.resample('A').mean()['second']}, + columns=['first', 'second']) + result = ts.resample( + 'A').agg({'first': np.sum, + 'second': np.mean}).reindex(columns=['first', 'second']) + assert_frame_equal(result, expected) + + def test_closed_left_corner(self): + # #1465 + s = Series(np.random.randn(21), + index=date_range(start='1/1/2012 9:30', + freq='1min', periods=21)) + s[0] = np.nan + + result = s.resample('10min', closed='left', label='right').mean() + exp = s[1:].resample('10min', closed='left', label='right').mean() + assert_series_equal(result, exp) + + result = s.resample('10min', closed='left', label='left').mean() + exp = s[1:].resample('10min', closed='left', label='left').mean() + + ex_index = date_range(start='1/1/2012 9:30', freq='10min', periods=3) + + tm.assert_index_equal(result.index, ex_index) + assert_series_equal(result, exp) + + def test_quarterly_resampling(self): + rng = period_range('2000Q1', periods=10, freq='Q-DEC') + ts = Series(np.arange(10), index=rng) + + result = ts.resample('A').mean() + exp = ts.to_timestamp().resample('A').mean().to_period() + assert_series_equal(result, exp) + + def test_resample_weekly_bug_1726(self): + # 8/6/12 is a Monday + ind = date_range(start="8/6/2012", end="8/26/2012", freq="D") + n = len(ind) + data = [[x] * 5 for x in range(n)] + df = DataFrame(data, columns=['open', 'high', 'low', 'close', 'vol'], + index=ind) + + # it works! + df.resample('W-MON', closed='left', label='left').first() + + def test_resample_with_dst_time_change(self): + # GH 15549 + index = ( + pd.DatetimeIndex([1457537600000000000, 1458059600000000000]) + .tz_localize("UTC").tz_convert('America/Chicago') + ) + df = pd.DataFrame([1, 2], index=index) + result = df.resample('12h', closed='right', + label='right').last().ffill() + + expected_index_values = ['2016-03-09 12:00:00-06:00', + '2016-03-10 00:00:00-06:00', + '2016-03-10 12:00:00-06:00', + '2016-03-11 00:00:00-06:00', + '2016-03-11 12:00:00-06:00', + '2016-03-12 00:00:00-06:00', + '2016-03-12 12:00:00-06:00', + '2016-03-13 00:00:00-06:00', + '2016-03-13 13:00:00-05:00', + '2016-03-14 01:00:00-05:00', + '2016-03-14 13:00:00-05:00', + '2016-03-15 01:00:00-05:00', + '2016-03-15 13:00:00-05:00'] + index = pd.to_datetime(expected_index_values, utc=True).tz_convert( + 'America/Chicago') + expected = pd.DataFrame([1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 2.0], index=index) + assert_frame_equal(result, expected) + + def test_resample_bms_2752(self): + # GH2753 + foo = Series(index=pd.bdate_range('20000101', '20000201')) + res1 = foo.resample("BMS").mean() + res2 = foo.resample("BMS").mean().resample("B").mean() + assert res1.index[0] == Timestamp('20000103') + assert res1.index[0] == res2.index[0] + + # def test_monthly_convention_span(self): + # rng = period_range('2000-01', periods=3, freq='M') + # ts = Series(np.arange(3), index=rng) + + # # hacky way to get same thing + # exp_index = period_range('2000-01-01', '2000-03-31', freq='D') + # expected = ts.asfreq('D', how='end').reindex(exp_index) + # expected = expected.fillna(method='bfill') + + # result = ts.resample('D', convention='span').mean() + + # assert_series_equal(result, expected) + + def test_default_right_closed_label(self): + end_freq = ['D', 'Q', 'M', 'D'] + end_types = ['M', 'A', 'Q', 'W'] + + for from_freq, to_freq in zip(end_freq, end_types): + idx = date_range(start='8/15/2012', periods=100, freq=from_freq) + df = DataFrame(np.random.randn(len(idx), 2), idx) + + resampled = df.resample(to_freq).mean() + assert_frame_equal(resampled, df.resample(to_freq, closed='right', + label='right').mean()) + + def test_default_left_closed_label(self): + others = ['MS', 'AS', 'QS', 'D', 'H'] + others_freq = ['D', 'Q', 'M', 'H', 'T'] + + for from_freq, to_freq in zip(others_freq, others): + idx = date_range(start='8/15/2012', periods=100, freq=from_freq) + df = DataFrame(np.random.randn(len(idx), 2), idx) + + resampled = df.resample(to_freq).mean() + assert_frame_equal(resampled, df.resample(to_freq, closed='left', + label='left').mean()) + + def test_all_values_single_bin(self): + # 2070 + index = period_range(start="2012-01-01", end="2012-12-31", freq="M") + s = Series(np.random.randn(len(index)), index=index) + + result = s.resample("A").mean() + tm.assert_almost_equal(result[0], s.mean()) + + def test_evenly_divisible_with_no_extra_bins(self): + # 4076 + # when the frequency is evenly divisible, sometimes extra bins + + df = DataFrame(np.random.randn(9, 3), + index=date_range('2000-1-1', periods=9)) + result = df.resample('5D').mean() + expected = pd.concat( + [df.iloc[0:5].mean(), df.iloc[5:].mean()], axis=1).T + expected.index = [Timestamp('2000-1-1'), Timestamp('2000-1-6')] + assert_frame_equal(result, expected) + + index = date_range(start='2001-5-4', periods=28) + df = DataFrame( + [{'REST_KEY': 1, 'DLY_TRN_QT': 80, 'DLY_SLS_AMT': 90, + 'COOP_DLY_TRN_QT': 30, 'COOP_DLY_SLS_AMT': 20}] * 28 + + [{'REST_KEY': 2, 'DLY_TRN_QT': 70, 'DLY_SLS_AMT': 10, + 'COOP_DLY_TRN_QT': 50, 'COOP_DLY_SLS_AMT': 20}] * 28, + index=index.append(index)).sort_index() + + index = date_range('2001-5-4', periods=4, freq='7D') + expected = DataFrame( + [{'REST_KEY': 14, 'DLY_TRN_QT': 14, 'DLY_SLS_AMT': 14, + 'COOP_DLY_TRN_QT': 14, 'COOP_DLY_SLS_AMT': 14}] * 4, + index=index) + result = df.resample('7D').count() + assert_frame_equal(result, expected) + + expected = DataFrame( + [{'REST_KEY': 21, 'DLY_TRN_QT': 1050, 'DLY_SLS_AMT': 700, + 'COOP_DLY_TRN_QT': 560, 'COOP_DLY_SLS_AMT': 280}] * 4, + index=index) + result = df.resample('7D').sum() + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('kind', ['period', None, 'timestamp']) + @pytest.mark.parametrize('agg_arg', ['mean', {'value': 'mean'}, ['mean']]) + def test_loffset_returns_datetimeindex(self, frame, kind, agg_arg): + # make sure passing loffset returns DatetimeIndex in all cases + # basic method taken from Base.test_resample_loffset_arg_type() + df = frame + expected_means = [df.values[i:i + 2].mean() + for i in range(0, len(df.values), 2)] + expected_index = period_range( + df.index[0], periods=len(df.index) / 2, freq='2D') + + # loffset coerces PeriodIndex to DateTimeIndex + expected_index = expected_index.to_timestamp() + expected_index += timedelta(hours=2) + expected = DataFrame({'value': expected_means}, index=expected_index) + + result_agg = df.resample('2D', loffset='2H', kind=kind).agg(agg_arg) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result_how = df.resample('2D', how=agg_arg, loffset='2H', + kind=kind) + if isinstance(agg_arg, list): + expected.columns = pd.MultiIndex.from_tuples([('value', 'mean')]) + assert_frame_equal(result_agg, expected) + assert_frame_equal(result_how, expected) + + @pytest.mark.parametrize('freq, period_mult', [('H', 24), ('12H', 2)]) + @pytest.mark.parametrize('kind', [None, 'period']) + def test_upsampling_ohlc(self, freq, period_mult, kind): + # GH 13083 + pi = period_range(start='2000', freq='D', periods=10) + s = Series(range(len(pi)), index=pi) + expected = s.to_timestamp().resample(freq).ohlc().to_period(freq) + + # timestamp-based resampling doesn't include all sub-periods + # of the last original period, so extend accordingly: + new_index = period_range(start='2000', freq=freq, + periods=period_mult * len(pi)) + expected = expected.reindex(new_index) + result = s.resample(freq, kind=kind).ohlc() + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('periods, values', + [([pd.NaT, '1970-01-01 00:00:00', pd.NaT, + '1970-01-01 00:00:02', '1970-01-01 00:00:03'], + [2, 3, 5, 7, 11]), + ([pd.NaT, pd.NaT, '1970-01-01 00:00:00', pd.NaT, + pd.NaT, pd.NaT, '1970-01-01 00:00:02', + '1970-01-01 00:00:03', pd.NaT, pd.NaT], + [1, 2, 3, 5, 6, 8, 7, 11, 12, 13])]) + @pytest.mark.parametrize('freq, expected_values', + [('1s', [3, np.NaN, 7, 11]), + ('2s', [3, int((7 + 11) / 2)]), + ('3s', [int((3 + 7) / 2), 11])]) + def test_resample_with_nat(self, periods, values, freq, expected_values): + # GH 13224 + index = PeriodIndex(periods, freq='S') + frame = DataFrame(values, index=index) + + expected_index = period_range('1970-01-01 00:00:00', + periods=len(expected_values), freq=freq) + expected = DataFrame(expected_values, index=expected_index) + result = frame.resample(freq).mean() + assert_frame_equal(result, expected) + + def test_resample_with_only_nat(self): + # GH 13224 + pi = PeriodIndex([pd.NaT] * 3, freq='S') + frame = DataFrame([2, 3, 5], index=pi) + expected_index = PeriodIndex(data=[], freq=pi.freq) + expected = DataFrame([], index=expected_index) + result = frame.resample('1s').mean() + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('start,end,start_freq,end_freq,base', [ + ('19910905', '19910909 03:00', 'H', '24H', 10), + ('19910905', '19910909 12:00', 'H', '24H', 10), + ('19910905', '19910909 23:00', 'H', '24H', 10), + ('19910905 10:00', '19910909', 'H', '24H', 10), + ('19910905 10:00', '19910909 10:00', 'H', '24H', 10), + ('19910905', '19910909 10:00', 'H', '24H', 10), + ('19910905 12:00', '19910909', 'H', '24H', 10), + ('19910905 12:00', '19910909 03:00', 'H', '24H', 10), + ('19910905 12:00', '19910909 12:00', 'H', '24H', 10), + ('19910905 12:00', '19910909 12:00', 'H', '24H', 34), + ('19910905 12:00', '19910909 12:00', 'H', '17H', 10), + ('19910905 12:00', '19910909 12:00', 'H', '17H', 3), + ('19910905 12:00', '19910909 1:00', 'H', 'M', 3), + ('19910905', '19910913 06:00', '2H', '24H', 10), + ('19910905', '19910905 01:39', 'Min', '5Min', 3), + ('19910905', '19910905 03:18', '2Min', '5Min', 3), + ]) + def test_resample_with_non_zero_base(self, start, end, start_freq, + end_freq, base): + # GH 23882 + s = pd.Series(0, index=pd.period_range(start, end, freq=start_freq)) + s = s + np.arange(len(s)) + result = s.resample(end_freq, base=base).mean() + result = result.to_timestamp(end_freq) + # to_timestamp casts 24H -> D + result = result.asfreq(end_freq) if end_freq == '24H' else result + expected = s.to_timestamp().resample(end_freq, base=base).mean() + assert_series_equal(result, expected) + + @pytest.mark.parametrize('first,last,offset,exp_first,exp_last', [ + ('19910905', '19920406', 'D', '19910905', '19920406'), + ('19910905 00:00', '19920406 06:00', 'D', '19910905', '19920406'), + ('19910905 06:00', '19920406 06:00', 'H', '19910905 06:00', + '19920406 06:00'), + ('19910906', '19920406', 'M', '1991-09', '1992-04'), + ('19910831', '19920430', 'M', '1991-08', '1992-04'), + ('1991-08', '1992-04', 'M', '1991-08', '1992-04'), + ]) + def test_get_period_range_edges(self, first, last, offset, + exp_first, exp_last): + first = pd.Period(first) + last = pd.Period(last) + + exp_first = pd.Period(exp_first, freq=offset) + exp_last = pd.Period(exp_last, freq=offset) + + offset = pd.tseries.frequencies.to_offset(offset) + result = _get_period_range_edges(first, last, offset) + expected = (exp_first, exp_last) + assert result == expected diff --git a/pandas/tests/resample/test_resample_api.py b/pandas/tests/resample/test_resample_api.py new file mode 100644 index 0000000000000..97f1e07380ef9 --- /dev/null +++ b/pandas/tests/resample/test_resample_api.py @@ -0,0 +1,573 @@ +# pylint: disable=E1101 + +from collections import OrderedDict +from datetime import datetime + +import numpy as np +import pytest + +from pandas.compat import range + +import pandas as pd +from pandas import DataFrame, Series +from pandas.core.indexes.datetimes import date_range +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal + +dti = date_range(start=datetime(2005, 1, 1), + end=datetime(2005, 1, 10), freq='Min') + +test_series = Series(np.random.rand(len(dti)), dti) +test_frame = DataFrame( + {'A': test_series, 'B': test_series, 'C': np.arange(len(dti))}) + + +def test_str(): + + r = test_series.resample('H') + assert ('DatetimeIndexResampler [freq=, axis=0, closed=left, ' + 'label=left, convention=start, base=0]' in str(r)) + + +def test_api(): + + r = test_series.resample('H') + result = r.mean() + assert isinstance(result, Series) + assert len(result) == 217 + + r = test_series.to_frame().resample('H') + result = r.mean() + assert isinstance(result, DataFrame) + assert len(result) == 217 + + +def test_groupby_resample_api(): + + # GH 12448 + # .groupby(...).resample(...) hitting warnings + # when appropriate + df = DataFrame({'date': pd.date_range(start='2016-01-01', + periods=4, + freq='W'), + 'group': [1, 1, 2, 2], + 'val': [5, 6, 7, 8]}).set_index('date') + + # replication step + i = pd.date_range('2016-01-03', periods=8).tolist() + \ + pd.date_range('2016-01-17', periods=8).tolist() + index = pd.MultiIndex.from_arrays([[1] * 8 + [2] * 8, i], + names=['group', 'date']) + expected = DataFrame({'val': [5] * 7 + [6] + [7] * 7 + [8]}, + index=index) + result = df.groupby('group').apply( + lambda x: x.resample('1D').ffill())[['val']] + assert_frame_equal(result, expected) + + +def test_groupby_resample_on_api(): + + # GH 15021 + # .groupby(...).resample(on=...) results in an unexpected + # keyword warning. + df = DataFrame({'key': ['A', 'B'] * 5, + 'dates': pd.date_range('2016-01-01', periods=10), + 'values': np.random.randn(10)}) + + expected = df.set_index('dates').groupby('key').resample('D').mean() + + result = df.groupby('key').resample('D', on='dates').mean() + assert_frame_equal(result, expected) + + +def test_pipe(): + # GH17905 + + # series + r = test_series.resample('H') + expected = r.max() - r.mean() + result = r.pipe(lambda x: x.max() - x.mean()) + tm.assert_series_equal(result, expected) + + # dataframe + r = test_frame.resample('H') + expected = r.max() - r.mean() + result = r.pipe(lambda x: x.max() - x.mean()) + tm.assert_frame_equal(result, expected) + + +def test_getitem(): + + r = test_frame.resample('H') + tm.assert_index_equal(r._selected_obj.columns, test_frame.columns) + + r = test_frame.resample('H')['B'] + assert r._selected_obj.name == test_frame.columns[1] + + # technically this is allowed + r = test_frame.resample('H')['A', 'B'] + tm.assert_index_equal(r._selected_obj.columns, + test_frame.columns[[0, 1]]) + + r = test_frame.resample('H')['A', 'B'] + tm.assert_index_equal(r._selected_obj.columns, + test_frame.columns[[0, 1]]) + + +@pytest.mark.parametrize('key', [['D'], ['A', 'D']]) +def test_select_bad_cols(key): + g = test_frame.resample('H') + # 'A' should not be referenced as a bad column... + # will have to rethink regex if you change message! + msg = r"^\"Columns not found: 'D'\"$" + with pytest.raises(KeyError, match=msg): + g[key] + + +def test_attribute_access(): + + r = test_frame.resample('H') + tm.assert_series_equal(r.A.sum(), r['A'].sum()) + + +def test_api_compat_before_use(): + + # make sure that we are setting the binner + # on these attributes + for attr in ['groups', 'ngroups', 'indices']: + rng = pd.date_range('1/1/2012', periods=100, freq='S') + ts = Series(np.arange(len(rng)), index=rng) + rs = ts.resample('30s') + + # before use + getattr(rs, attr) + + # after grouper is initialized is ok + rs.mean() + getattr(rs, attr) + + +def tests_skip_nuisance(): + + df = test_frame + df['D'] = 'foo' + r = df.resample('H') + result = r[['A', 'B']].sum() + expected = pd.concat([r.A.sum(), r.B.sum()], axis=1) + assert_frame_equal(result, expected) + + expected = r[['A', 'B', 'C']].sum() + result = r.sum() + assert_frame_equal(result, expected) + + +def test_downsample_but_actually_upsampling(): + + # this is reindex / asfreq + rng = pd.date_range('1/1/2012', periods=100, freq='S') + ts = Series(np.arange(len(rng), dtype='int64'), index=rng) + result = ts.resample('20s').asfreq() + expected = Series([0, 20, 40, 60, 80], + index=pd.date_range('2012-01-01 00:00:00', + freq='20s', + periods=5)) + assert_series_equal(result, expected) + + +def test_combined_up_downsampling_of_irregular(): + + # since we are reallydoing an operation like this + # ts2.resample('2s').mean().ffill() + # preserve these semantics + + rng = pd.date_range('1/1/2012', periods=100, freq='S') + ts = Series(np.arange(len(rng)), index=rng) + ts2 = ts.iloc[[0, 1, 2, 3, 5, 7, 11, 15, 16, 25, 30]] + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = ts2.resample('2s', how='mean', fill_method='ffill') + expected = ts2.resample('2s').mean().ffill() + assert_series_equal(result, expected) + + +def test_transform(): + + r = test_series.resample('20min') + expected = test_series.groupby( + pd.Grouper(freq='20min')).transform('mean') + result = r.transform('mean') + assert_series_equal(result, expected) + + +def test_fillna(): + + # need to upsample here + rng = pd.date_range('1/1/2012', periods=10, freq='2S') + ts = Series(np.arange(len(rng), dtype='int64'), index=rng) + r = ts.resample('s') + + expected = r.ffill() + result = r.fillna(method='ffill') + assert_series_equal(result, expected) + + expected = r.bfill() + result = r.fillna(method='bfill') + assert_series_equal(result, expected) + + msg = (r"Invalid fill method\. Expecting pad \(ffill\), backfill" + r" \(bfill\) or nearest\. Got 0") + with pytest.raises(ValueError, match=msg): + r.fillna(0) + + +def test_apply_without_aggregation(): + + # both resample and groupby should work w/o aggregation + r = test_series.resample('20min') + g = test_series.groupby(pd.Grouper(freq='20min')) + + for t in [g, r]: + result = t.apply(lambda x: x) + assert_series_equal(result, test_series) + + +def test_agg_consistency(): + + # make sure that we are consistent across + # similar aggregations with and w/o selection list + df = DataFrame(np.random.randn(1000, 3), + index=pd.date_range('1/1/2012', freq='S', periods=1000), + columns=['A', 'B', 'C']) + + r = df.resample('3T') + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + expected = r[['A', 'B', 'C']].agg({'r1': 'mean', 'r2': 'sum'}) + result = r.agg({'r1': 'mean', 'r2': 'sum'}) + assert_frame_equal(result, expected) + +# TODO: once GH 14008 is fixed, move these tests into +# `Base` test class + + +def test_agg(): + # test with all three Resampler apis and TimeGrouper + + np.random.seed(1234) + index = date_range(datetime(2005, 1, 1), + datetime(2005, 1, 10), freq='D') + index.name = 'date' + df = DataFrame(np.random.rand(10, 2), columns=list('AB'), index=index) + df_col = df.reset_index() + df_mult = df_col.copy() + df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], + names=['index', 'date']) + r = df.resample('2D') + cases = [ + r, + df_col.resample('2D', on='date'), + df_mult.resample('2D', level='date'), + df.groupby(pd.Grouper(freq='2D')) + ] + + a_mean = r['A'].mean() + a_std = r['A'].std() + a_sum = r['A'].sum() + b_mean = r['B'].mean() + b_std = r['B'].std() + b_sum = r['B'].sum() + + expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) + expected.columns = pd.MultiIndex.from_product([['A', 'B'], + ['mean', 'std']]) + for t in cases: + result = t.aggregate([np.mean, np.std]) + assert_frame_equal(result, expected) + + expected = pd.concat([a_mean, b_std], axis=1) + for t in cases: + result = t.aggregate({'A': np.mean, + 'B': np.std}) + assert_frame_equal(result, expected, check_like=True) + + expected = pd.concat([a_mean, a_std], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), + ('A', 'std')]) + for t in cases: + result = t.aggregate({'A': ['mean', 'std']}) + assert_frame_equal(result, expected) + + expected = pd.concat([a_mean, a_sum], axis=1) + expected.columns = ['mean', 'sum'] + for t in cases: + result = t['A'].aggregate(['mean', 'sum']) + assert_frame_equal(result, expected) + + expected = pd.concat([a_mean, a_sum], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), + ('A', 'sum')]) + for t in cases: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}}) + assert_frame_equal(result, expected, check_like=True) + + expected = pd.concat([a_mean, a_sum, b_mean, b_sum], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), + ('A', 'sum'), + ('B', 'mean2'), + ('B', 'sum2')]) + for t in cases: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}, + 'B': {'mean2': 'mean', 'sum2': 'sum'}}) + assert_frame_equal(result, expected, check_like=True) + + expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), + ('A', 'std'), + ('B', 'mean'), + ('B', 'std')]) + for t in cases: + result = t.aggregate({'A': ['mean', 'std'], + 'B': ['mean', 'std']}) + assert_frame_equal(result, expected, check_like=True) + + expected = pd.concat([a_mean, a_sum, b_mean, b_sum], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('r1', 'A', 'mean'), + ('r1', 'A', 'sum'), + ('r2', 'B', 'mean'), + ('r2', 'B', 'sum')]) + + +def test_agg_misc(): + # test with all three Resampler apis and TimeGrouper + + np.random.seed(1234) + index = date_range(datetime(2005, 1, 1), + datetime(2005, 1, 10), freq='D') + index.name = 'date' + df = DataFrame(np.random.rand(10, 2), columns=list('AB'), index=index) + df_col = df.reset_index() + df_mult = df_col.copy() + df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], + names=['index', 'date']) + + r = df.resample('2D') + cases = [ + r, + df_col.resample('2D', on='date'), + df_mult.resample('2D', level='date'), + df.groupby(pd.Grouper(freq='2D')) + ] + + # passed lambda + for t in cases: + result = t.agg({'A': np.sum, + 'B': lambda x: np.std(x, ddof=1)}) + rcustom = t['B'].apply(lambda x: np.std(x, ddof=1)) + expected = pd.concat([r['A'].sum(), rcustom], axis=1) + assert_frame_equal(result, expected, check_like=True) + + # agg with renamers + expected = pd.concat([t['A'].sum(), + t['B'].sum(), + t['A'].mean(), + t['B'].mean()], + axis=1) + expected.columns = pd.MultiIndex.from_tuples([('result1', 'A'), + ('result1', 'B'), + ('result2', 'A'), + ('result2', 'B')]) + + for t in cases: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t[['A', 'B']].agg(OrderedDict([('result1', np.sum), + ('result2', np.mean)])) + assert_frame_equal(result, expected, check_like=True) + + # agg with different hows + expected = pd.concat([t['A'].sum(), + t['A'].std(), + t['B'].mean(), + t['B'].std()], + axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), + ('A', 'std'), + ('B', 'mean'), + ('B', 'std')]) + for t in cases: + result = t.agg(OrderedDict([('A', ['sum', 'std']), + ('B', ['mean', 'std'])])) + assert_frame_equal(result, expected, check_like=True) + + # equivalent of using a selection list / or not + for t in cases: + result = t[['A', 'B']].agg({'A': ['sum', 'std'], + 'B': ['mean', 'std']}) + assert_frame_equal(result, expected, check_like=True) + + # series like aggs + for t in cases: + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t['A'].agg({'A': ['sum', 'std']}) + expected = pd.concat([t['A'].sum(), + t['A'].std()], + axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), + ('A', 'std')]) + assert_frame_equal(result, expected, check_like=True) + + expected = pd.concat([t['A'].agg(['sum', 'std']), + t['A'].agg(['mean', 'std'])], + axis=1) + expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), + ('A', 'std'), + ('B', 'mean'), + ('B', 'std')]) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t['A'].agg({'A': ['sum', 'std'], + 'B': ['mean', 'std']}) + assert_frame_equal(result, expected, check_like=True) + + # errors + # invalid names in the agg specification + msg = "\"Column 'B' does not exist!\"" + for t in cases: + with pytest.raises(KeyError, match=msg): + t[['A']].agg({'A': ['sum', 'std'], + 'B': ['mean', 'std']}) + + +def test_agg_nested_dicts(): + + np.random.seed(1234) + index = date_range(datetime(2005, 1, 1), + datetime(2005, 1, 10), freq='D') + index.name = 'date' + df = DataFrame(np.random.rand(10, 2), columns=list('AB'), index=index) + df_col = df.reset_index() + df_mult = df_col.copy() + df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], + names=['index', 'date']) + r = df.resample('2D') + cases = [ + r, + df_col.resample('2D', on='date'), + df_mult.resample('2D', level='date'), + df.groupby(pd.Grouper(freq='2D')) + ] + + msg = r"cannot perform renaming for r(1|2) with a nested dictionary" + for t in cases: + with pytest.raises(pd.core.base.SpecificationError, match=msg): + t.aggregate({'r1': {'A': ['mean', 'sum']}, + 'r2': {'B': ['mean', 'sum']}}) + + for t in cases: + expected = pd.concat([t['A'].mean(), t['A'].std(), t['B'].mean(), + t['B'].std()], axis=1) + expected.columns = pd.MultiIndex.from_tuples([('ra', 'mean'), ( + 'ra', 'std'), ('rb', 'mean'), ('rb', 'std')]) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t[['A', 'B']].agg({'A': {'ra': ['mean', 'std']}, + 'B': {'rb': ['mean', 'std']}}) + assert_frame_equal(result, expected, check_like=True) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = t.agg({'A': {'ra': ['mean', 'std']}, + 'B': {'rb': ['mean', 'std']}}) + assert_frame_equal(result, expected, check_like=True) + + +def test_try_aggregate_non_existing_column(): + # GH 16766 + data = [ + {'dt': datetime(2017, 6, 1, 0), 'x': 1.0, 'y': 2.0}, + {'dt': datetime(2017, 6, 1, 1), 'x': 2.0, 'y': 2.0}, + {'dt': datetime(2017, 6, 1, 2), 'x': 3.0, 'y': 1.5} + ] + df = DataFrame(data).set_index('dt') + + # Error as we don't have 'z' column + msg = "\"Column 'z' does not exist!\"" + with pytest.raises(KeyError, match=msg): + df.resample('30T').agg({'x': ['mean'], + 'y': ['median'], + 'z': ['sum']}) + + +def test_selection_api_validation(): + # GH 13500 + index = date_range(datetime(2005, 1, 1), + datetime(2005, 1, 10), freq='D') + + rng = np.arange(len(index), dtype=np.int64) + df = DataFrame({'date': index, 'a': rng}, + index=pd.MultiIndex.from_arrays([rng, index], + names=['v', 'd'])) + df_exp = DataFrame({'a': rng}, index=index) + + # non DatetimeIndex + msg = ("Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex," + " but got an instance of 'Int64Index'") + with pytest.raises(TypeError, match=msg): + df.resample('2D', level='v') + + msg = "The Grouper cannot specify both a key and a level!" + with pytest.raises(ValueError, match=msg): + df.resample('2D', on='date', level='d') + + msg = "unhashable type: 'list'" + with pytest.raises(TypeError, match=msg): + df.resample('2D', on=['a', 'date']) + + msg = r"\"Level \['a', 'date'\] not found\"" + with pytest.raises(KeyError, match=msg): + df.resample('2D', level=['a', 'date']) + + # upsampling not allowed + msg = ("Upsampling from level= or on= selection is not supported, use" + r" \.set_index\(\.\.\.\) to explicitly set index to datetime-like") + with pytest.raises(ValueError, match=msg): + df.resample('2D', level='d').asfreq() + with pytest.raises(ValueError, match=msg): + df.resample('2D', on='date').asfreq() + + exp = df_exp.resample('2D').sum() + exp.index.name = 'date' + assert_frame_equal(exp, df.resample('2D', on='date').sum()) + + exp.index.name = 'd' + assert_frame_equal(exp, df.resample('2D', level='d').sum()) + + +@pytest.mark.parametrize('col_name', ['t2', 't2x', 't2q', 'T_2M', + 't2p', 't2m', 't2m1', 'T2M']) +def test_agg_with_datetime_index_list_agg_func(col_name): + # GH 22660 + # The parametrized column names would get converted to dates by our + # date parser. Some would result in OutOfBoundsError (ValueError) while + # others would result in OverflowError when passed into Timestamp. + # We catch these errors and move on to the correct branch. + df = pd.DataFrame(list(range(200)), + index=pd.date_range(start='2017-01-01', freq='15min', + periods=200, tz='Europe/Berlin'), + columns=[col_name]) + result = df.resample('1d').aggregate(['mean']) + expected = pd.DataFrame([47.5, 143.5, 195.5], + index=pd.date_range(start='2017-01-01', freq='D', + periods=3, tz='Europe/Berlin'), + columns=pd.MultiIndex(levels=[[col_name], + ['mean']], + codes=[[0], [0]])) + assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_resampler_grouper.py b/pandas/tests/resample/test_resampler_grouper.py new file mode 100644 index 0000000000000..b61acfc3d2c5e --- /dev/null +++ b/pandas/tests/resample/test_resampler_grouper.py @@ -0,0 +1,260 @@ +# pylint: disable=E1101 + +from textwrap import dedent + +import numpy as np + +from pandas.compat import range + +import pandas as pd +from pandas import DataFrame, Series, Timestamp +from pandas.core.indexes.datetimes import date_range +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal + +test_frame = DataFrame({'A': [1] * 20 + [2] * 12 + [3] * 8, + 'B': np.arange(40)}, + index=date_range('1/1/2000', + freq='s', + periods=40)) + + +def test_tab_complete_ipython6_warning(ip): + from IPython.core.completer import provisionalcompleter + code = dedent("""\ + import pandas.util.testing as tm + s = tm.makeTimeSeries() + rs = s.resample("D") + """) + ip.run_code(code) + + with tm.assert_produces_warning(None): + with provisionalcompleter('ignore'): + list(ip.Completer.completions('rs.', 1)) + + +def test_deferred_with_groupby(): + + # GH 12486 + # support deferred resample ops with groupby + data = [['2010-01-01', 'A', 2], ['2010-01-02', 'A', 3], + ['2010-01-05', 'A', 8], ['2010-01-10', 'A', 7], + ['2010-01-13', 'A', 3], ['2010-01-01', 'B', 5], + ['2010-01-03', 'B', 2], ['2010-01-04', 'B', 1], + ['2010-01-11', 'B', 7], ['2010-01-14', 'B', 3]] + + df = DataFrame(data, columns=['date', 'id', 'score']) + df.date = pd.to_datetime(df.date) + + def f(x): + return x.set_index('date').resample('D').asfreq() + expected = df.groupby('id').apply(f) + result = df.set_index('date').groupby('id').resample('D').asfreq() + assert_frame_equal(result, expected) + + df = DataFrame({'date': pd.date_range(start='2016-01-01', + periods=4, + freq='W'), + 'group': [1, 1, 2, 2], + 'val': [5, 6, 7, 8]}).set_index('date') + + def f(x): + return x.resample('1D').ffill() + expected = df.groupby('group').apply(f) + result = df.groupby('group').resample('1D').ffill() + assert_frame_equal(result, expected) + + +def test_getitem(): + g = test_frame.groupby('A') + + expected = g.B.apply(lambda x: x.resample('2s').mean()) + + result = g.resample('2s').B.mean() + assert_series_equal(result, expected) + + result = g.B.resample('2s').mean() + assert_series_equal(result, expected) + + result = g.resample('2s').mean().B + assert_series_equal(result, expected) + + +def test_getitem_multiple(): + + # GH 13174 + # multiple calls after selection causing an issue with aliasing + data = [{'id': 1, 'buyer': 'A'}, {'id': 2, 'buyer': 'B'}] + df = DataFrame(data, index=pd.date_range('2016-01-01', periods=2)) + r = df.groupby('id').resample('1D') + result = r['buyer'].count() + expected = Series([1, 1], + index=pd.MultiIndex.from_tuples( + [(1, Timestamp('2016-01-01')), + (2, Timestamp('2016-01-02'))], + names=['id', None]), + name='buyer') + assert_series_equal(result, expected) + + result = r['buyer'].count() + assert_series_equal(result, expected) + + +def test_groupby_resample_on_api_with_getitem(): + # GH 17813 + df = pd.DataFrame({'id': list('aabbb'), + 'date': pd.date_range('1-1-2016', periods=5), + 'data': 1}) + exp = df.set_index('date').groupby('id').resample('2D')['data'].sum() + result = df.groupby('id').resample('2D', on='date')['data'].sum() + assert_series_equal(result, exp) + + +def test_nearest(): + + # GH 17496 + # Resample nearest + index = pd.date_range('1/1/2000', periods=3, freq='T') + result = Series(range(3), index=index).resample('20s').nearest() + + expected = Series( + [0, 0, 1, 1, 1, 2, 2], + index=pd.DatetimeIndex( + ['2000-01-01 00:00:00', '2000-01-01 00:00:20', + '2000-01-01 00:00:40', '2000-01-01 00:01:00', + '2000-01-01 00:01:20', '2000-01-01 00:01:40', + '2000-01-01 00:02:00'], + dtype='datetime64[ns]', + freq='20S')) + assert_series_equal(result, expected) + + +def test_methods(): + g = test_frame.groupby('A') + r = g.resample('2s') + + for f in ['first', 'last', 'median', 'sem', 'sum', 'mean', + 'min', 'max']: + result = getattr(r, f)() + expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) + assert_frame_equal(result, expected) + + for f in ['size']: + result = getattr(r, f)() + expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) + assert_series_equal(result, expected) + + for f in ['count']: + result = getattr(r, f)() + expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) + assert_frame_equal(result, expected) + + # series only + for f in ['nunique']: + result = getattr(r.B, f)() + expected = g.B.apply(lambda x: getattr(x.resample('2s'), f)()) + assert_series_equal(result, expected) + + for f in ['nearest', 'backfill', 'ffill', 'asfreq']: + result = getattr(r, f)() + expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) + assert_frame_equal(result, expected) + + result = r.ohlc() + expected = g.apply(lambda x: x.resample('2s').ohlc()) + assert_frame_equal(result, expected) + + for f in ['std', 'var']: + result = getattr(r, f)(ddof=1) + expected = g.apply(lambda x: getattr(x.resample('2s'), f)(ddof=1)) + assert_frame_equal(result, expected) + + +def test_apply(): + + g = test_frame.groupby('A') + r = g.resample('2s') + + # reduction + expected = g.resample('2s').sum() + + def f(x): + return x.resample('2s').sum() + + result = r.apply(f) + assert_frame_equal(result, expected) + + def f(x): + return x.resample('2s').apply(lambda y: y.sum()) + + result = g.apply(f) + assert_frame_equal(result, expected) + + +def test_apply_with_mutated_index(): + # GH 15169 + index = pd.date_range('1-1-2015', '12-31-15', freq='D') + df = DataFrame(data={'col1': np.random.rand(len(index))}, index=index) + + def f(x): + s = Series([1, 2], index=['a', 'b']) + return s + + expected = df.groupby(pd.Grouper(freq='M')).apply(f) + + result = df.resample('M').apply(f) + assert_frame_equal(result, expected) + + # A case for series + expected = df['col1'].groupby(pd.Grouper(freq='M')).apply(f) + result = df['col1'].resample('M').apply(f) + assert_series_equal(result, expected) + + +def test_resample_groupby_with_label(): + # GH 13235 + index = date_range('2000-01-01', freq='2D', periods=5) + df = DataFrame(index=index, + data={'col0': [0, 0, 1, 1, 2], 'col1': [1, 1, 1, 1, 1]} + ) + result = df.groupby('col0').resample('1W', label='left').sum() + + mi = [np.array([0, 0, 1, 2]), + pd.to_datetime(np.array(['1999-12-26', '2000-01-02', + '2000-01-02', '2000-01-02']) + ) + ] + mindex = pd.MultiIndex.from_arrays(mi, names=['col0', None]) + expected = DataFrame(data={'col0': [0, 0, 2, 2], 'col1': [1, 1, 2, 1]}, + index=mindex + ) + + assert_frame_equal(result, expected) + + +def test_consistency_with_window(): + + # consistent return values with window + df = test_frame + expected = pd.Int64Index([1, 2, 3], name='A') + result = df.groupby('A').resample('2s').mean() + assert result.index.nlevels == 2 + tm.assert_index_equal(result.index.levels[0], expected) + + result = df.groupby('A').rolling(20).mean() + assert result.index.nlevels == 2 + tm.assert_index_equal(result.index.levels[0], expected) + + +def test_median_duplicate_columns(): + # GH 14233 + + df = DataFrame(np.random.randn(20, 3), + columns=list('aaa'), + index=pd.date_range('2012-01-01', periods=20, freq='s')) + df2 = df.copy() + df2.columns = ['a', 'b', 'c'] + expected = df2.resample('5s').median() + result = df.resample('5s').median() + expected.columns = result.columns + assert_frame_equal(result, expected) diff --git a/pandas/tests/resample/test_time_grouper.py b/pandas/tests/resample/test_time_grouper.py new file mode 100644 index 0000000000000..2f330d1f2484b --- /dev/null +++ b/pandas/tests/resample/test_time_grouper.py @@ -0,0 +1,266 @@ +from datetime import datetime +from operator import methodcaller + +import numpy as np +import pytest + +import pandas as pd +from pandas import DataFrame, Series +from pandas.core.indexes.datetimes import date_range +from pandas.core.resample import TimeGrouper +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal + +test_series = Series(np.random.randn(1000), + index=date_range('1/1/2000', periods=1000)) + + +def test_apply(): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + grouper = pd.TimeGrouper(freq='A', label='right', closed='right') + + grouped = test_series.groupby(grouper) + + def f(x): + return x.sort_values()[-3:] + + applied = grouped.apply(f) + expected = test_series.groupby(lambda x: x.year).apply(f) + + applied.index = applied.index.droplevel(0) + expected.index = expected.index.droplevel(0) + assert_series_equal(applied, expected) + + +def test_count(): + test_series[::3] = np.nan + + expected = test_series.groupby(lambda x: x.year).count() + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + grouper = pd.TimeGrouper(freq='A', label='right', closed='right') + result = test_series.groupby(grouper).count() + expected.index = result.index + assert_series_equal(result, expected) + + result = test_series.resample('A').count() + expected.index = result.index + assert_series_equal(result, expected) + + +def test_numpy_reduction(): + result = test_series.resample('A', closed='right').prod() + + expected = test_series.groupby(lambda x: x.year).agg(np.prod) + expected.index = result.index + + assert_series_equal(result, expected) + + +def test_apply_iteration(): + # #2300 + N = 1000 + ind = pd.date_range(start="2000-01-01", freq="D", periods=N) + df = DataFrame({'open': 1, 'close': 2}, index=ind) + tg = TimeGrouper('M') + + _, grouper, _ = tg._get_grouper(df) + + # Errors + grouped = df.groupby(grouper, group_keys=False) + + def f(df): + return df['close'] / df['open'] + + # it works! + result = grouped.apply(f) + tm.assert_index_equal(result.index, df.index) + + +@pytest.mark.parametrize('name, func', [ + ('Int64Index', tm.makeIntIndex), + ('Index', tm.makeUnicodeIndex), + ('Float64Index', tm.makeFloatIndex), + ('MultiIndex', lambda m: tm.makeCustomIndex(m, 2)) +]) +def test_fails_on_no_datetime_index(name, func): + n = 2 + index = func(n) + df = DataFrame({'a': np.random.randn(n)}, index=index) + + msg = ("Only valid with DatetimeIndex, TimedeltaIndex " + "or PeriodIndex, but got an instance of '{}'".format(name)) + with pytest.raises(TypeError, match=msg): + df.groupby(TimeGrouper('D')) + + +def test_aaa_group_order(): + # GH 12840 + # check TimeGrouper perform stable sorts + n = 20 + data = np.random.randn(n, 4) + df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), + datetime(2013, 1, 3), datetime(2013, 1, 4), + datetime(2013, 1, 5)] * 4 + grouped = df.groupby(TimeGrouper(key='key', freq='D')) + + tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 1)), + df[::5]) + tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 2)), + df[1::5]) + tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 3)), + df[2::5]) + tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 4)), + df[3::5]) + tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 5)), + df[4::5]) + + +def test_aggregate_normal(resample_method): + """Check TimeGrouper's aggregation is identical as normal groupby.""" + + if resample_method == 'ohlc': + pytest.xfail(reason='DataError: No numeric types to aggregate') + + data = np.random.randn(20, 4) + normal_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + normal_df['key'] = [1, 2, 3, 4, 5] * 4 + + dt_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + dt_df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), + datetime(2013, 1, 3), datetime(2013, 1, 4), + datetime(2013, 1, 5)] * 4 + + normal_grouped = normal_df.groupby('key') + dt_grouped = dt_df.groupby(TimeGrouper(key='key', freq='D')) + + expected = getattr(normal_grouped, resample_method)() + dt_result = getattr(dt_grouped, resample_method)() + expected.index = date_range(start='2013-01-01', freq='D', + periods=5, name='key') + tm.assert_equal(expected, dt_result) + + # if TimeGrouper is used included, 'nth' doesn't work yet + + """ + for func in ['nth']: + expected = getattr(normal_grouped, func)(3) + expected.index = date_range(start='2013-01-01', + freq='D', periods=5, name='key') + dt_result = getattr(dt_grouped, func)(3) + assert_frame_equal(expected, dt_result) + """ + + +@pytest.mark.parametrize('method, method_args, unit', [ + ('sum', dict(), 0), + ('sum', dict(min_count=0), 0), + ('sum', dict(min_count=1), np.nan), + ('prod', dict(), 1), + ('prod', dict(min_count=0), 1), + ('prod', dict(min_count=1), np.nan) +]) +def test_resample_entirly_nat_window(method, method_args, unit): + s = pd.Series([0] * 2 + [np.nan] * 2, + index=pd.date_range('2017', periods=4)) + result = methodcaller(method, **method_args)(s.resample("2d")) + expected = pd.Series([0.0, unit], + index=pd.to_datetime(['2017-01-01', + '2017-01-03'])) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize('func, fill_value', [ + ('min', np.nan), + ('max', np.nan), + ('sum', 0), + ('prod', 1), + ('count', 0), +]) +def test_aggregate_with_nat(func, fill_value): + # check TimeGrouper's aggregation is identical as normal groupby + # if NaT is included, 'var', 'std', 'mean', 'first','last' + # and 'nth' doesn't work yet + + n = 20 + data = np.random.randn(n, 4).astype('int64') + normal_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + normal_df['key'] = [1, 2, np.nan, 4, 5] * 4 + + dt_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + dt_df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), pd.NaT, + datetime(2013, 1, 4), datetime(2013, 1, 5)] * 4 + + normal_grouped = normal_df.groupby('key') + dt_grouped = dt_df.groupby(TimeGrouper(key='key', freq='D')) + + normal_result = getattr(normal_grouped, func)() + dt_result = getattr(dt_grouped, func)() + + pad = DataFrame([[fill_value] * 4], index=[3], + columns=['A', 'B', 'C', 'D']) + expected = normal_result.append(pad) + expected = expected.sort_index() + expected.index = date_range(start='2013-01-01', freq='D', + periods=5, name='key') + assert_frame_equal(expected, dt_result) + assert dt_result.index.name == 'key' + + +def test_aggregate_with_nat_size(): + # GH 9925 + n = 20 + data = np.random.randn(n, 4).astype('int64') + normal_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + normal_df['key'] = [1, 2, np.nan, 4, 5] * 4 + + dt_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) + dt_df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), pd.NaT, + datetime(2013, 1, 4), datetime(2013, 1, 5)] * 4 + + normal_grouped = normal_df.groupby('key') + dt_grouped = dt_df.groupby(TimeGrouper(key='key', freq='D')) + + normal_result = normal_grouped.size() + dt_result = dt_grouped.size() + + pad = Series([0], index=[3]) + expected = normal_result.append(pad) + expected = expected.sort_index() + expected.index = date_range(start='2013-01-01', freq='D', + periods=5, name='key') + assert_series_equal(expected, dt_result) + assert dt_result.index.name == 'key' + + +def test_repr(): + # GH18203 + result = repr(TimeGrouper(key='A', freq='H')) + expected = ("TimeGrouper(key='A', freq=, axis=0, sort=True, " + "closed='left', label='left', how='mean', " + "convention='e', base=0)") + assert result == expected + + +@pytest.mark.parametrize('method, method_args, expected_values', [ + ('sum', dict(), [1, 0, 1]), + ('sum', dict(min_count=0), [1, 0, 1]), + ('sum', dict(min_count=1), [1, np.nan, 1]), + ('sum', dict(min_count=2), [np.nan, np.nan, np.nan]), + ('prod', dict(), [1, 1, 1]), + ('prod', dict(min_count=0), [1, 1, 1]), + ('prod', dict(min_count=1), [1, np.nan, 1]), + ('prod', dict(min_count=2), [np.nan, np.nan, np.nan]), +]) +def test_upsample_sum(method, method_args, expected_values): + s = pd.Series(1, index=pd.date_range("2017", periods=2, freq="H")) + resampled = s.resample("30T") + index = pd.to_datetime(['2017-01-01T00:00:00', + '2017-01-01T00:30:00', + '2017-01-01T01:00:00']) + result = methodcaller(method, **method_args)(resampled) + expected = pd.Series(expected_values, index=index) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/resample/test_timedelta.py b/pandas/tests/resample/test_timedelta.py new file mode 100644 index 0000000000000..3498d30d11689 --- /dev/null +++ b/pandas/tests/resample/test_timedelta.py @@ -0,0 +1,128 @@ +from datetime import timedelta + +import numpy as np + +import pandas as pd +from pandas import DataFrame, Series +from pandas.core.indexes.timedeltas import timedelta_range +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal + + +def test_asfreq_bug(): + df = DataFrame(data=[1, 3], + index=[timedelta(), timedelta(minutes=3)]) + result = df.resample('1T').asfreq() + expected = DataFrame(data=[1, np.nan, np.nan, 3], + index=timedelta_range('0 day', + periods=4, + freq='1T')) + assert_frame_equal(result, expected) + + +def test_resample_with_nat(): + # GH 13223 + index = pd.to_timedelta(['0s', pd.NaT, '2s']) + result = DataFrame({'value': [2, 3, 5]}, index).resample('1s').mean() + expected = DataFrame({'value': [2.5, np.nan, 5.0]}, + index=timedelta_range('0 day', + periods=3, + freq='1S')) + assert_frame_equal(result, expected) + + +def test_resample_as_freq_with_subperiod(): + # GH 13022 + index = timedelta_range('00:00:00', '00:10:00', freq='5T') + df = DataFrame(data={'value': [1, 5, 10]}, index=index) + result = df.resample('2T').asfreq() + expected_data = {'value': [1, np.nan, np.nan, np.nan, np.nan, 10]} + expected = DataFrame(data=expected_data, + index=timedelta_range('00:00:00', + '00:10:00', freq='2T')) + tm.assert_frame_equal(result, expected) + + +def test_resample_with_timedeltas(): + + expected = DataFrame({'A': np.arange(1480)}) + expected = expected.groupby(expected.index // 30).sum() + expected.index = pd.timedelta_range('0 days', freq='30T', periods=50) + + df = DataFrame({'A': np.arange(1480)}, index=pd.to_timedelta( + np.arange(1480), unit='T')) + result = df.resample('30T').sum() + + assert_frame_equal(result, expected) + + s = df['A'] + result = s.resample('30T').sum() + assert_series_equal(result, expected['A']) + + +def test_resample_single_period_timedelta(): + + s = Series(list(range(5)), index=pd.timedelta_range( + '1 day', freq='s', periods=5)) + result = s.resample('2s').sum() + expected = Series([1, 5, 4], index=pd.timedelta_range( + '1 day', freq='2s', periods=3)) + assert_series_equal(result, expected) + + +def test_resample_timedelta_idempotency(): + + # GH 12072 + index = pd.timedelta_range('0', periods=9, freq='10L') + series = Series(range(9), index=index) + result = series.resample('10L').mean() + expected = series + assert_series_equal(result, expected) + + +def test_resample_base_with_timedeltaindex(): + + # GH 10530 + rng = timedelta_range(start='0s', periods=25, freq='s') + ts = Series(np.random.randn(len(rng)), index=rng) + + with_base = ts.resample('2s', base=5).mean() + without_base = ts.resample('2s').mean() + + exp_without_base = timedelta_range(start='0s', end='25s', freq='2s') + exp_with_base = timedelta_range(start='5s', end='29s', freq='2s') + + tm.assert_index_equal(without_base.index, exp_without_base) + tm.assert_index_equal(with_base.index, exp_with_base) + + +def test_resample_categorical_data_with_timedeltaindex(): + # GH #12169 + df = DataFrame({'Group_obj': 'A'}, + index=pd.to_timedelta(list(range(20)), unit='s')) + df['Group'] = df['Group_obj'].astype('category') + result = df.resample('10s').agg(lambda x: (x.value_counts().index[0])) + expected = DataFrame({'Group_obj': ['A', 'A'], + 'Group': ['A', 'A']}, + index=pd.to_timedelta([0, 10], unit='s')) + expected = expected.reindex(['Group_obj', 'Group'], axis=1) + expected['Group'] = expected['Group_obj'].astype('category') + tm.assert_frame_equal(result, expected) + + +def test_resample_timedelta_values(): + # GH 13119 + # check that timedelta dtype is preserved when NaT values are + # introduced by the resampling + + times = timedelta_range('1 day', '4 day', freq='4D') + df = DataFrame({'time': times}, index=times) + + times2 = timedelta_range('1 day', '4 day', freq='2D') + exp = Series(times2, index=times2, name='time') + exp.iloc[1] = pd.NaT + + res = df.resample('2D').first()['time'] + tm.assert_series_equal(res, exp) + res = df['time'].resample('2D').first() + tm.assert_series_equal(res, exp) diff --git a/pandas/tests/reshape/__init__.py b/pandas/tests/reshape/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/tools/data/cut_data.csv b/pandas/tests/reshape/data/cut_data.csv similarity index 99% rename from pandas/tests/tools/data/cut_data.csv rename to pandas/tests/reshape/data/cut_data.csv index 7d9d480599579..c198ec77e45da 100644 --- a/pandas/tests/tools/data/cut_data.csv +++ b/pandas/tests/reshape/data/cut_data.csv @@ -1 +1 @@ -1.001 0.994 0.9951 0.9956 0.9956 0.9951 0.9949 1.001 0.994 0.9938 0.9908 0.9947 0.992 0.9912 1.0002 0.9914 0.9928 0.9892 0.9917 0.9955 0.9892 0.9912 0.993 0.9937 0.9951 0.9955 0.993 0.9961 0.9914 0.9906 0.9974 0.9934 0.992 0.9939 0.9962 0.9905 0.9934 0.9906 0.9999 0.9999 0.9937 0.9937 0.9954 0.9934 0.9934 0.9931 0.994 0.9939 0.9954 0.995 0.9917 0.9914 0.991 0.9911 0.993 0.9908 0.9962 0.9972 0.9931 0.9926 0.9951 0.9972 0.991 0.9931 0.9927 0.9934 0.9903 0.992 0.9926 0.9962 0.9956 0.9958 0.9964 0.9941 0.9926 0.9962 0.9898 0.9912 0.9961 0.9949 0.9929 0.9985 0.9946 0.9966 0.9974 0.9975 0.9974 0.9972 0.9974 0.9975 0.9974 0.9957 0.99 0.9899 0.9916 0.9969 0.9979 0.9913 0.9956 0.9979 0.9975 0.9962 0.997 1 0.9975 0.9974 0.9962 0.999 0.999 0.9927 0.9959 1 0.9982 0.9968 0.9968 0.994 0.9914 0.9911 0.9982 0.9982 0.9934 0.9984 0.9952 0.9952 0.9928 0.9912 0.994 0.9958 0.9924 0.9924 0.994 0.9958 0.9979 0.9982 0.9961 0.9979 0.992 0.9975 0.9917 0.9923 0.9927 0.9975 0.992 0.9947 0.9921 0.9905 0.9918 0.9951 0.9917 0.994 0.9934 0.9968 0.994 0.9919 0.9966 0.9979 0.9979 0.9898 0.9894 0.9894 0.9898 0.998 0.9932 0.9979 0.997 0.9972 0.9974 0.9896 0.9968 0.9958 0.9906 0.9917 0.9902 0.9918 0.999 0.9927 0.991 0.9972 0.9931 0.995 0.9951 0.9936 1.001 0.9979 0.997 0.9972 0.9954 0.9924 0.9906 0.9962 0.9962 1.001 0.9928 0.9942 0.9942 0.9942 0.9942 0.9961 0.998 0.9961 0.9984 0.998 0.9973 0.9949 0.9924 0.9972 0.9958 0.9968 0.9938 0.993 0.994 0.9918 0.9958 0.9944 0.9912 0.9961 0.9939 0.9961 0.9989 0.9938 0.9939 0.9971 0.9912 0.9936 0.9929 0.9998 0.9938 0.9969 0.9938 0.9998 0.9972 0.9976 0.9976 0.9979 0.9979 0.9979 0.9979 0.9972 0.9918 0.9982 0.9985 0.9944 0.9903 0.9934 0.9975 0.9923 0.99 0.9905 0.9905 0.996 0.9964 0.998 0.9975 0.9913 0.9932 0.9935 0.9927 0.9927 0.9912 0.9904 0.9939 0.9996 0.9944 0.9977 0.9912 0.9996 0.9965 0.9944 0.9945 0.9944 0.9965 0.9944 0.9972 0.9949 0.9966 0.9954 0.9954 0.9915 0.9919 0.9916 0.99 0.9909 0.9938 0.9982 0.9988 0.9961 0.9978 0.9979 0.9979 0.9979 0.9979 0.9945 1 0.9957 0.9968 0.9934 0.9976 0.9932 0.997 0.9923 0.9914 0.992 0.9914 0.9914 0.9949 0.9949 0.995 0.995 0.9927 0.9928 0.9917 0.9918 0.9954 0.9941 0.9941 0.9934 0.9927 0.9938 0.9933 0.9934 0.9927 0.9938 0.9927 0.9946 0.993 0.9946 0.9976 0.9944 0.9978 0.992 0.9912 0.9927 0.9906 0.9954 0.9923 0.9906 0.991 0.9972 0.9945 0.9934 0.9964 0.9948 0.9962 0.9931 0.993 0.9942 0.9906 0.9995 0.998 0.997 0.9914 0.992 0.9924 0.992 0.9937 0.9978 0.9978 0.9927 0.994 0.9935 0.9968 0.9941 0.9942 0.9978 0.9923 0.9912 0.9923 0.9927 0.9931 0.9941 0.9927 0.9931 0.9934 0.9936 0.9893 0.9893 0.9919 0.9924 0.9927 0.9919 0.9924 0.9975 0.9969 0.9936 0.991 0.9893 0.9906 0.9941 0.995 0.9983 0.9983 0.9916 0.9957 0.99 0.9976 0.992 0.9917 0.9917 0.9993 0.9908 0.9917 0.9976 0.9934 1 0.9918 0.992 0.9896 0.9932 0.992 0.9917 0.9999 0.998 0.9918 0.9918 0.9999 0.998 0.9927 0.9959 0.9927 0.9929 0.9898 0.9954 0.9954 0.9954 0.9954 0.9954 0.9954 0.9974 0.9936 0.9978 0.9974 0.9927 0.9934 0.9938 0.9922 0.992 0.9935 0.9906 0.9934 0.9934 0.9913 0.9938 0.9898 0.9975 0.9975 0.9937 0.9914 0.9982 0.9982 0.9929 0.9971 0.9921 0.9931 0.9924 0.9929 0.9982 0.9892 0.9956 0.9924 0.9971 0.9956 0.9982 0.9973 0.9932 0.9976 0.9962 0.9956 0.9932 0.9976 0.9992 0.9983 0.9937 0.99 0.9944 0.9938 0.9965 0.9893 0.9927 0.994 0.9928 0.9964 0.9917 0.9972 0.9964 0.9954 0.993 0.9928 0.9916 0.9936 0.9962 0.9899 0.9898 0.996 0.9907 0.994 0.9913 0.9976 0.9904 0.992 0.9976 0.999 0.9975 0.9937 0.9937 0.998 0.998 0.9944 0.9938 0.9907 0.9938 0.9921 0.9908 0.9931 0.9915 0.9952 0.9926 0.9934 0.992 0.9918 0.9942 0.9942 0.9942 0.9901 0.9898 0.9902 0.9934 0.9906 0.9898 0.9896 0.9922 0.9947 0.9945 0.9976 0.9976 0.9976 0.9987 0.9987 0.9976 0.992 0.9955 0.9953 0.9976 0.992 0.9952 0.9983 0.9933 0.9958 0.9922 0.9928 0.9976 0.9976 0.9916 0.9901 0.9976 0.9901 0.9916 0.9982 0.993 0.9969 0.991 0.9953 0.9924 0.9969 0.9928 0.9945 0.9967 0.9944 0.9928 0.9929 0.9948 0.9976 0.9912 0.9987 0.99 0.991 0.9933 0.9933 0.9899 0.9912 0.9912 0.9976 0.994 0.9947 0.9954 0.993 0.9954 0.9963 0.992 0.9926 0.995 0.9983 0.992 0.9968 0.9905 0.9904 0.9926 0.9968 0.9928 0.9949 0.9909 0.9937 0.9914 0.9905 0.9904 0.9924 0.9924 0.9965 0.9965 0.9993 0.9965 0.9908 0.992 0.9978 0.9978 0.9978 0.9978 0.9912 0.9928 0.9928 0.993 0.9993 0.9965 0.9937 0.9913 0.9934 0.9952 0.9983 0.9957 0.9957 0.9916 0.9999 0.9999 0.9936 0.9972 0.9933 0.9934 0.9931 0.9976 0.9937 0.9937 0.991 0.9979 0.9971 0.9969 0.9968 0.9961 0.993 0.9973 0.9944 0.9986 0.9986 0.9986 0.9986 0.9972 0.9917 0.992 0.9932 0.9936 0.9915 0.9922 0.9934 0.9952 0.9972 0.9934 0.9958 0.9944 0.9908 0.9958 0.9925 0.9966 0.9972 0.9912 0.995 0.9928 0.9968 0.9955 0.9981 0.991 0.991 0.991 0.992 0.9931 0.997 0.9948 0.9923 0.9976 0.9938 0.9984 0.9972 0.9922 0.9935 0.9944 0.9942 0.9944 0.9997 0.9977 0.9912 0.9982 0.9982 0.9983 0.998 0.9894 0.9927 0.9917 0.9904 0.993 0.9941 0.9943 0.99855 0.99345 0.998 0.9916 0.9916 0.99475 0.99325 0.9933 0.9969 1.0002 0.9933 0.9937 0.99685 0.99455 0.9917 0.99035 0.9914 0.99225 0.99155 0.9954 0.99455 0.9924 0.99695 0.99655 0.9934 0.998 0.9971 0.9948 0.998 0.9971 0.99215 0.9948 0.9915 0.99115 0.9932 0.9977 0.99535 0.99165 0.9953 0.9928 0.9958 0.9928 0.9928 0.9964 0.9987 0.9953 0.9932 0.9907 0.99755 0.99935 0.9932 0.9932 0.9958 0.99585 1.00055 0.9985 0.99505 0.992 0.9988 0.99175 0.9962 0.9962 0.9942 0.9927 0.9927 0.99985 0.997 0.9918 0.99215 0.99865 0.9992 1.0006 0.99135 0.99715 0.9992 1.0006 0.99865 0.99815 0.99815 0.99815 0.9949 0.99815 0.99815 0.99225 0.99445 0.99225 0.99335 0.99625 0.9971 0.9983 0.99445 0.99085 0.9977 0.9953 0.99775 0.99795 0.99505 0.9977 0.9975 0.99745 0.9976 0.99775 0.9953 0.9932 0.99405 1 0.99785 0.9939 0.9939 0.99675 0.9939 0.99675 0.98965 0.9971 0.99445 0.9945 0.9939 0.9958 0.9956 0.99055 0.9959 0.9925 0.9963 0.9935 0.99105 0.99045 0.9963 0.99155 0.99085 0.99085 0.99085 0.9924 0.9924 0.99975 0.99975 0.99315 0.9917 0.9917 0.99845 0.9921 0.99975 0.9909 0.99315 0.99855 0.9934 0.9978 0.9934 0.9949 0.99855 0.9986 0.99725 0.9946 0.99255 0.9996 0.9939 0.99 0.9937 0.9886 0.9934 1 0.9994 0.9926 0.9956 0.9978 0.9915 0.9939 0.9932 0.993 0.9898 0.9921 0.9932 0.9919 0.993 0.9953 0.9928 0.9928 0.9976 0.9906 0.9918 0.99185 0.9918 0.99185 0.994 0.9908 0.9928 0.9896 0.9908 0.9918 0.9952 0.9923 0.9915 0.9952 0.9947 0.9983 0.9975 0.995 0.9944 0.994 0.9944 0.9908 0.99795 0.9985 0.99425 0.99425 0.9943 0.9924 0.9946 0.9924 0.995 0.9919 0.99 0.9923 0.9956 0.9978 0.9978 0.9967 0.9934 0.9936 0.9932 0.9934 0.998 0.9978 0.9929 0.9974 0.99685 0.99495 0.99745 0.99505 0.992 0.9978 0.9956 0.9982 0.99485 0.9971 0.99265 0.9904 0.9965 0.9946 0.99965 0.9935 0.996 0.9942 0.9936 0.9965 0.9928 0.9928 0.9965 0.9936 0.9938 0.9926 0.9926 0.9983 0.9983 0.992 0.9983 0.9923 0.9972 0.9928 0.9928 0.9994 0.991 0.9906 0.9894 0.9898 0.9994 0.991 0.9925 0.9956 0.9946 0.9966 0.9951 0.9927 0.9927 0.9951 0.9894 0.9907 0.9925 0.9928 0.9941 0.9941 0.9925 0.9935 0.9932 0.9944 0.9972 0.994 0.9956 0.9927 0.9924 0.9966 0.9997 0.9936 0.9936 0.9952 0.9952 0.9928 0.9911 0.993 0.9911 0.9932 0.993 0.993 0.9932 0.9932 0.9943 0.9968 0.9994 0.9926 0.9968 0.9932 0.9916 0.9946 0.9925 0.9925 0.9935 0.9962 0.9928 0.993 0.993 0.9956 0.9941 0.9972 0.9948 0.9955 0.9972 0.9972 0.9983 0.9942 0.9936 0.9956 0.9953 0.9918 0.995 0.992 0.9952 1.001 0.9924 0.9932 0.9937 0.9918 0.9934 0.991 0.9962 0.9932 0.9908 0.9962 0.9918 0.9941 0.9931 0.9981 0.9931 0.9944 0.992 0.9966 0.9956 0.9956 0.9949 1.0002 0.9942 0.9923 0.9917 0.9931 0.992 1.0002 0.9953 0.9951 0.9974 0.9904 0.9974 0.9944 1.0004 0.9952 0.9956 0.995 0.995 0.9995 0.9942 0.9977 0.992 0.992 0.9995 0.9934 1.0006 0.9982 0.9928 0.9945 0.9963 0.9906 0.9956 0.9942 0.9962 0.9894 0.995 0.9908 0.9914 0.9938 0.9977 0.9922 0.992 0.9903 0.9893 0.9952 0.9903 0.9912 0.9983 0.9937 0.9932 0.9928 0.9922 0.9976 0.9922 0.9974 0.998 0.9931 0.9911 0.9944 0.9937 0.9974 0.989 0.992 0.9928 0.9918 0.9936 0.9944 0.9988 0.994 0.9953 0.9986 0.9914 0.9934 0.996 0.9937 0.9921 0.998 0.996 0.9933 0.9933 0.9959 0.9936 0.9953 0.9938 0.9952 0.9959 0.9959 0.9937 0.992 0.9967 0.9944 0.9998 0.9998 0.9942 0.9998 0.9945 0.9998 0.9946 0.9942 0.9928 0.9946 0.9927 0.9938 0.9918 0.9945 0.9966 0.9954 0.9913 0.9931 0.9986 0.9965 0.9984 0.9952 0.9956 0.9949 0.9954 0.996 0.9931 0.992 0.9912 0.9978 0.9938 0.9914 0.9932 0.9944 0.9913 0.9948 0.998 0.9998 0.9964 0.9992 0.9948 0.9998 0.998 0.9939 0.992 0.9922 0.9955 0.9917 0.9917 0.9954 0.9986 0.9955 0.9917 0.9907 0.9922 0.9958 0.993 0.9917 0.9926 0.9959 0.9906 0.9993 0.993 0.9906 0.992 0.992 0.994 0.9959 0.9908 0.9902 0.9908 0.9943 0.9921 0.9911 0.9986 0.992 0.992 0.9943 0.9937 0.993 0.9902 0.9928 0.9896 0.998 0.9954 0.9938 0.9918 0.9896 0.9944 0.9999 0.9953 0.992 0.9925 0.9981 0.9952 0.9927 0.9927 0.9911 0.9936 0.9959 0.9946 0.9948 0.9955 0.9951 0.9952 0.9946 0.9946 0.9944 0.9938 0.9963 0.991 1.0003 0.9966 0.9993 1.0003 0.9938 0.9965 0.9938 0.9993 0.9938 1.0003 0.9966 0.9942 0.9928 0.991 0.9911 0.9977 0.9927 0.9911 0.991 0.9912 0.9907 0.9902 0.992 0.994 0.9966 0.993 0.993 0.993 0.9966 0.9942 0.9925 0.9925 0.9928 0.995 0.9939 0.9958 0.9952 1 0.9948 0.99 0.9958 0.9948 0.9949 0.997 0.9927 0.9938 0.9949 0.9953 0.997 0.9932 0.9927 0.9932 0.9955 0.9914 0.991 0.992 0.9924 0.9927 0.9911 0.9958 0.9928 0.9902 0.994 0.994 0.9972 1.0004 0.991 0.9918 0.995 0.9941 0.9956 0.9956 0.9959 0.9922 0.9931 0.9959 0.9984 0.9908 0.991 0.9928 0.9936 0.9941 0.9924 0.9917 0.9906 0.995 0.9956 0.9955 0.9907 1 0.9953 0.9911 0.9922 0.9951 0.9948 0.9906 0.994 0.9907 0.9927 0.9914 0.9958 1 0.9984 0.9941 0.9944 0.998 0.998 0.9902 0.9911 0.9929 0.993 0.9918 0.992 0.9932 0.992 0.994 0.9923 0.993 0.9956 0.9907 0.99 0.9918 0.9926 0.995 0.99 0.99 0.9946 0.9907 0.9898 0.9918 0.9986 0.9986 0.9928 0.9986 0.9979 0.994 0.9937 0.9938 0.9942 0.9944 0.993 0.9986 0.9932 0.9934 0.9928 0.9925 0.9944 0.9909 0.9932 0.9934 1.0001 0.992 0.9916 0.998 0.9919 0.9925 0.9977 0.9944 0.991 0.99 0.9917 0.9923 0.9928 0.9923 0.9928 0.9902 0.9893 0.9917 0.9982 1.0005 0.9923 0.9951 0.9956 0.998 0.9928 0.9938 0.9914 0.9955 0.9924 0.9911 0.9917 0.9917 0.9932 0.9955 0.9929 0.9955 0.9958 1.0012 0.9968 0.9911 0.9924 0.991 0.9946 0.9928 0.9946 0.9917 0.9918 0.9926 0.9931 0.9932 0.9903 0.9928 0.9929 0.9958 0.9955 0.9911 0.9938 0.9942 0.9945 0.9962 0.992 0.9927 0.9948 0.9945 0.9942 0.9952 0.9942 0.9958 0.9918 0.9932 1.0004 0.9972 0.9998 0.9918 0.9918 0.9964 0.9936 0.9931 0.9938 0.9934 0.99 0.9914 0.9904 0.994 0.9938 0.9933 0.9909 0.9942 0.9945 0.9954 0.996 0.9991 0.993 0.9942 0.9934 0.9939 0.9937 0.994 0.9926 0.9951 0.9952 0.9935 0.9938 0.9939 0.9933 0.9927 0.998 0.9997 0.9981 0.992 0.9954 0.992 0.9997 0.9981 0.9943 0.9941 0.9936 0.9996 0.9932 0.9926 0.9936 0.992 0.9936 0.9996 0.993 0.9924 0.9928 0.9926 0.9952 0.9945 0.9945 0.9903 0.9932 0.9953 0.9936 0.9912 0.9962 0.9965 0.9932 0.9967 0.9953 0.9963 0.992 0.991 0.9958 0.99 0.991 0.9958 0.9938 0.9996 0.9946 0.9974 0.9945 0.9946 0.9974 0.9957 0.9931 0.9947 0.9953 0.9931 0.9946 0.9978 0.9989 1.0004 0.9938 0.9934 0.9978 0.9956 0.9982 0.9948 0.9956 0.9982 0.9926 0.991 0.9945 0.9916 0.9953 0.9938 0.9956 0.9906 0.9956 0.9932 0.9914 0.9938 0.996 0.9906 0.98815 0.9942 0.9903 0.9906 0.9935 1.0024 0.9968 0.9906 0.9941 0.9919 0.9928 0.9958 0.9932 0.9957 0.9937 0.9982 0.9928 0.9919 0.9956 0.9957 0.9954 0.993 0.9954 0.9987 0.9956 0.9928 0.9951 0.993 0.9928 0.9926 0.9938 1.0001 0.9933 0.9952 0.9934 0.9988 0.993 0.9952 0.9948 0.9998 0.9971 0.9998 0.9962 0.9948 0.99 0.9942 0.9965 0.9912 0.9978 0.9928 1.0103 0.9956 0.9936 0.9929 0.9966 0.9964 0.996 0.9959 0.9954 0.9914 1.0103 1.0004 0.9911 0.9938 0.9927 0.9922 0.9924 0.9963 0.9936 0.9951 0.9951 0.9955 0.9961 0.9936 0.992 0.9944 0.9944 1.0008 0.9962 0.9986 0.9986 1 0.9986 0.9982 1 0.9949 0.9915 0.9951 0.9986 0.9927 0.9955 0.9952 0.9928 0.9982 0.9914 0.9927 0.9918 0.9944 0.9969 0.9955 0.9954 0.9955 0.9921 0.9934 0.9998 0.9946 0.9984 0.9924 0.9939 0.995 0.9957 0.9953 0.9912 0.9939 0.9921 0.9954 0.9933 0.9941 0.995 0.9977 0.9912 0.9945 0.9952 0.9924 0.9986 0.9953 0.9939 0.9929 0.9988 0.9906 0.9914 0.9978 0.9928 0.9948 0.9978 0.9946 0.9908 0.9954 0.9906 0.99705 0.9982 0.9932 0.9977 0.994 0.9982 0.9929 0.9924 0.9966 0.9921 0.9967 0.9934 0.9914 0.99705 0.9961 0.9967 0.9926 0.99605 0.99435 0.9948 0.9916 0.997 0.9961 0.9967 0.9961 0.9955 0.9922 0.9918 0.9955 0.9941 0.9955 0.9955 0.9924 0.9973 0.999 0.9941 0.9922 0.9922 0.9953 0.9945 0.9945 0.9957 0.9932 0.9945 0.9913 0.9909 0.9939 0.991 0.9954 0.9943 0.993 1.0002 0.9946 0.9953 0.9918 0.9936 0.9984 0.9956 0.9966 0.9942 0.9984 0.9956 0.9966 0.9974 0.9944 1.0008 0.9974 1.0008 0.9928 0.9944 0.9908 0.9917 0.9911 0.9912 0.9953 0.9932 0.9896 0.9889 0.9912 0.9926 0.9911 0.9964 0.9974 0.9944 0.9974 0.9964 0.9963 0.9948 0.9948 0.9953 0.9948 0.9953 0.9949 0.9988 0.9954 0.992 0.9984 0.9954 0.9926 0.992 0.9976 0.9972 0.991 0.998 0.9966 0.998 1.0007 0.992 0.9925 0.991 0.9934 0.9955 0.9944 0.9981 0.9968 0.9946 0.9946 0.9981 0.9946 0.997 0.9924 0.9958 0.994 0.9958 0.9984 0.9948 0.9932 0.9952 0.9924 0.9945 0.9976 0.9976 0.9938 0.9997 0.994 0.9921 0.9986 0.9987 0.9991 0.9987 0.9991 0.9991 0.9948 0.9987 0.993 0.9988 1 0.9932 0.9991 0.9989 1 1 0.9952 0.9969 0.9966 0.9966 0.9976 0.99 0.9988 0.9942 0.9984 0.9932 0.9969 0.9966 0.9933 0.9916 0.9914 0.9966 0.9958 0.9926 0.9939 0.9953 0.9906 0.9914 0.9958 0.9926 0.9991 0.9994 0.9976 0.9966 0.9953 0.9923 0.993 0.9931 0.9932 0.9926 0.9938 0.9966 0.9974 0.9924 0.9948 0.9964 0.9924 0.9966 0.9974 0.9938 0.9928 0.9959 1.0001 0.9959 1.0001 0.9968 0.9932 0.9954 0.9992 0.9932 0.9939 0.9952 0.9996 0.9966 0.9925 0.996 0.9996 0.9973 0.9937 0.9966 1.0017 0.993 0.993 0.9959 0.9958 1.0017 0.9958 0.9979 0.9941 0.997 0.9934 0.9927 0.9944 0.9927 0.9963 1.0011 1.0011 0.9959 0.9973 0.9966 0.9932 0.9984 0.999 0.999 0.999 0.999 0.999 1.0006 0.9937 0.9954 0.997 0.9912 0.9939 0.999 0.9957 0.9926 0.9994 1.0004 0.9994 1.0004 1.0004 1.0002 0.9922 0.9922 0.9934 0.9926 0.9941 0.9994 1.0004 0.9924 0.9948 0.9935 0.9918 0.9948 0.9924 0.9979 0.993 0.994 0.991 0.993 0.9922 0.9979 0.9937 0.9928 0.9965 0.9928 0.9991 0.9948 0.9925 0.9958 0.9962 0.9965 0.9951 0.9944 0.9916 0.9987 0.9928 0.9926 0.9934 0.9944 0.9949 0.9926 0.997 0.9949 0.9948 0.992 0.9964 0.9926 0.9982 0.9955 0.9955 0.9958 0.9997 1.0001 1.0001 0.9918 0.9918 0.9931 1.0001 0.9926 0.9966 0.9932 0.9969 0.9925 0.9914 0.996 0.9952 0.9934 0.9939 0.9939 0.9906 0.9901 0.9948 0.995 0.9953 0.9953 0.9952 0.996 0.9948 0.9951 0.9931 0.9962 0.9948 0.9959 0.9962 0.9958 0.9948 0.9948 0.994 0.9942 0.9942 0.9948 0.9964 0.9958 0.9932 0.9986 0.9986 0.9988 0.9953 0.9983 1 0.9951 0.9983 0.9906 0.9981 0.9936 0.9951 0.9953 1.0005 0.9972 1 0.9969 1.0001 1.0001 1.0001 0.9934 0.9969 1.0001 0.9902 0.993 0.9914 0.9941 0.9967 0.9918 0.998 0.9967 0.9918 0.9957 0.9986 0.9958 0.9948 0.9918 0.9923 0.9998 0.9998 0.9914 0.9939 0.9966 0.995 0.9966 0.994 0.9972 0.9998 0.9998 0.9982 0.9924 0.9972 0.997 0.9954 0.9962 0.9972 0.9921 0.9905 0.9998 0.993 0.9941 0.9994 0.9962 0.992 0.9922 0.994 0.9897 0.9954 0.99 0.9948 0.9922 0.998 0.9944 0.9944 0.9986 0.9986 0.9986 0.9986 0.9986 0.996 0.9999 0.9986 0.9986 0.996 0.9951 0.9999 0.993 0.9982 0.992 0.9963 0.995 0.9956 0.997 0.9936 0.9935 0.9963 0.9967 0.9912 0.9981 0.9966 0.9967 0.9963 0.9935 0.9902 0.99 0.996 0.9966 0.9962 0.994 0.996 0.994 0.9944 0.9974 0.996 0.9922 0.9917 0.9918 0.9936 0.9938 0.9918 0.9939 0.9917 0.9981 0.9941 0.9928 0.9952 0.9898 0.9914 0.9981 0.9957 0.998 0.9957 0.9986 0.9983 0.9982 0.997 0.9947 0.997 0.9947 0.99416 0.99516 0.99496 0.9974 0.99579 0.9983 0.99471 0.9974 0.99644 0.99579 0.99699 0.99758 0.9977 0.99397 0.9983 0.99471 0.99243 0.9962 1.00182 0.99384 0.99582 0.9962 0.9924 0.99466 0.99212 0.99449 0.99748 0.99449 0.99748 0.99475 0.99189 0.99827 0.99752 0.99827 0.99479 0.99752 0.99642 1.00047 0.99382 0.99784 0.99486 0.99537 0.99382 0.99838 0.99566 0.99268 0.99566 0.99468 0.9933 0.99307 0.99907 0.99907 0.99907 0.99907 0.99471 0.99471 0.99907 0.99148 0.99383 0.99365 0.99272 0.99148 0.99235 0.99508 0.9946 0.99674 0.99018 0.99235 0.99084 0.99856 0.99591 0.9975 0.9944 0.99173 0.99378 0.99805 0.99534 0.99232 0.99805 0.99078 0.99534 0.99061 0.99182 0.9966 0.9912 0.99779 0.99814 0.99096 0.99379 0.99426 0.99228 0.99335 0.99595 0.99297 0.99687 0.99297 0.99687 0.99445 0.9986 0.99154 0.9981 0.98993 1.00241 0.99716 0.99437 0.9972 0.99756 0.99509 0.99572 0.99756 0.99175 0.99254 0.99509 0.99676 0.9979 0.99194 0.99077 0.99782 0.99942 0.99708 0.99353 0.99256 0.99199 0.9918 0.99354 0.99244 0.99831 0.99396 0.99724 0.99524 0.9927 0.99802 0.99512 0.99438 0.99679 0.99652 0.99698 0.99474 0.99511 0.99582 0.99125 0.99256 0.9911 0.99168 0.9911 0.99556 1.00098 0.99516 0.99516 0.99518 0.99347 0.9929 0.99347 0.99841 0.99362 0.99361 0.9914 0.99114 0.9925 0.99453 0.9938 0.9938 0.99806 0.9961 1.00016 0.9916 0.99116 0.99319 0.99517 0.99514 0.99566 0.99166 0.99587 0.99558 0.99117 0.99399 0.99741 0.99405 0.99622 1.00051 0.99803 0.99405 0.99773 0.99397 0.99622 0.99713 0.99274 1.00118 0.99176 0.9969 0.99771 0.99411 0.99771 0.99411 0.99194 0.99558 0.99194 0.99558 0.99577 0.99564 0.99578 0.99888 1.00014 0.99441 0.99594 0.99437 0.99594 0.9979 0.99434 0.99203 0.998 0.99316 0.998 0.99314 0.99316 0.99612 0.99295 0.99394 0.99642 0.99642 0.99248 0.99268 0.99954 0.99692 0.99592 0.99592 0.99692 0.99822 0.99822 0.99402 0.99404 0.99787 0.99347 0.99838 0.99839 0.99375 0.99155 0.9936 0.99434 0.9922 0.99571 0.99658 0.99076 0.99496 0.9937 0.99076 0.99542 0.99825 0.99289 0.99432 0.99523 0.99542 0.9959 0.99543 0.99662 0.99088 0.99088 0.99922 0.9966 0.99466 0.99922 0.99836 0.99836 0.99238 0.99645 1 1 0.99376 1 0.99513 0.99556 0.99556 0.99543 0.99886 0.99526 0.99166 0.99691 0.99732 0.99573 0.99656 0.99112 0.99214 0.99165 0.99004 0.99463 0.99683 0.99004 0.99596 0.99898 0.99114 0.99508 0.99306 0.99898 0.99508 0.99114 0.99342 0.99345 0.99772 0.99239 0.99502 0.99502 0.99479 0.99207 0.99497 0.99828 0.99542 0.99542 0.99228 0.99706 0.99497 0.99669 0.99828 0.99269 0.99196 0.99662 0.99475 0.99544 0.99944 0.99475 0.99544 0.9966 0.99066 0.9907 0.99066 0.998 0.9907 0.99066 0.99307 0.99106 0.99696 0.99106 0.99307 0.99167 0.99902 0.98992 0.99182 0.99556 0.99582 0.99182 0.98972 0.99352 0.9946 0.99273 0.99628 0.99582 0.99553 0.98914 0.99354 0.99976 0.99808 0.99808 0.99808 0.99808 0.99808 0.99808 0.9919 0.99808 0.99499 0.99655 0.99615 0.99296 0.99482 0.99079 0.99366 0.99434 0.98958 0.99434 0.99938 0.99059 0.99835 0.98958 0.99159 0.99159 0.98931 0.9938 0.99558 0.99563 0.98931 0.99691 0.9959 0.99159 0.99628 0.99076 0.99678 0.99678 0.99678 0.99089 0.99537 1.0002 0.99628 0.99089 0.99678 0.99076 0.99332 0.99316 0.99272 0.99636 0.99202 0.99148 0.99064 0.99884 0.99773 1.00013 0.98974 0.99773 1.00013 0.99112 0.99136 0.99132 0.99642 0.99488 0.99527 0.99578 0.99352 0.99199 0.99198 0.99756 0.99578 0.99561 0.99347 0.98936 0.99786 0.99705 0.9942 0.9948 0.99116 0.99688 0.98974 0.99542 0.99154 0.99118 0.99044 0.9914 0.9979 0.98892 0.99114 0.99188 0.99583 0.98892 0.98892 0.99704 0.9911 0.99334 0.99334 0.99094 0.99014 0.99304 0.99652 0.98944 0.99772 0.99367 0.99304 0.99183 0.99126 0.98944 0.99577 0.99772 0.99652 0.99428 0.99388 0.99208 0.99256 0.99388 0.9925 0.99904 0.99216 0.99208 0.99428 0.99165 0.99924 0.99924 0.99924 0.9956 0.99562 0.9972 0.99924 0.9958 0.99976 0.99976 0.99296 0.9957 0.9958 0.99579 0.99541 0.99976 0.99518 0.99168 0.99276 0.99085 0.99873 0.99172 0.99312 0.99276 0.9972 0.99278 0.99092 0.9962 0.99053 0.99858 0.9984 0.99335 0.99053 0.9949 0.9962 0.99092 0.99532 0.99727 0.99026 0.99668 0.99727 0.9952 0.99144 0.99144 0.99015 0.9914 0.99693 0.99035 0.99693 0.99035 0.99006 0.99126 0.98994 0.98985 0.9971 0.99882 0.99477 0.99478 0.99576 0.99578 0.99354 0.99244 0.99084 0.99612 0.99356 0.98952 0.99612 0.99084 0.99244 0.99955 0.99374 0.9892 0.99144 0.99352 0.99352 0.9935 0.99237 0.99144 0.99022 0.99032 1.03898 0.99587 0.99587 0.99587 0.99976 0.99354 0.99976 0.99552 0.99552 0.99587 0.99604 0.99584 0.98894 0.9963 0.993 0.98894 0.9963 0.99068 0.98964 0.99604 0.99584 0.9923 0.99437 0.993 0.99238 0.99801 0.99802 0.99566 0.99067 0.99066 0.9929 0.9934 0.99067 0.98912 0.99066 0.99228 0.98912 0.9958 0.99052 0.99312 0.9968 0.99502 0.99084 0.99573 0.99256 0.9959 0.99084 0.99084 0.99644 0.99526 0.9954 0.99095 0.99188 0.9909 0.99256 0.9959 0.99581 0.99132 0.98936 0.99136 0.99142 0.99232 0.99232 0.993 0.99311 0.99132 0.98993 0.99208 0.99776 0.99839 0.99574 0.99093 0.99156 0.99278 0.9924 0.98984 0.99035 0.9924 0.99165 0.9923 0.99278 0.99008 0.98964 0.99156 0.9909 0.98984 0.9889 0.99178 0.99076 0.9889 0.99046 0.98999 0.98946 0.98976 0.99046 0.99672 0.99482 0.98945 0.98883 0.99362 0.99075 0.99436 0.98988 0.99158 0.99265 0.99195 0.99168 0.9918 0.99313 0.9895 0.9932 0.99848 0.9909 0.99014 0.9952 0.99652 0.99848 0.99104 0.99772 0.9922 0.99076 0.99622 0.9902 0.99114 0.9938 0.99594 0.9902 0.99035 0.99032 0.99558 0.99622 0.99076 0.99413 0.99043 0.99043 0.98982 0.98934 0.9902 0.99449 0.99629 0.9948 0.98984 0.99326 0.99834 0.99555 0.98975 0.99216 0.99216 0.99834 0.9901 0.98975 0.99573 0.99326 0.99215 0.98993 0.99218 0.99555 0.99564 0.99564 0.99397 0.99576 0.99601 0.99564 0.99397 0.98713 0.99308 0.99308 0.99582 0.99494 0.9929 0.99471 0.9929 0.9929 0.99037 0.99304 0.99026 0.98986 0.99471 0.98951 0.99634 0.99368 0.99792 0.99026 0.99362 0.98919 0.99835 0.99835 0.99038 0.99104 0.99038 0.99286 0.99296 0.99835 0.9954 0.9914 0.99286 0.99604 0.99604 0.99119 0.99007 0.99507 0.99596 0.99011 0.99184 0.99469 0.99469 0.99406 0.99305 0.99096 0.98956 0.9921 0.99496 0.99406 0.99406 0.9888 0.98942 0.99082 0.98802 17.3 1.4 1.3 1.6 5.25 2.4 14.6 11.8 1.5 1.8 7.7 2 1.8 1.4 16.7 8.1 8 4.7 8.1 2.1 16.7 6.4 1.5 7.6 1.5 12.4 1.3 1.7 8.1 7.1 7.6 2.3 6.5 1.4 12.7 1.6 1.1 1.2 6.5 4.6 0.6 10.6 4.6 4.8 2.7 12.6 0.6 9.2 6.6 7 8.45 11.1 18.15 18.15 4.1 4.1 4.6 18.15 4.9 8.3 1.4 11.5 1.8 1.6 2.4 4.9 1.8 4.3 4.4 1.4 1.6 1.3 5.2 5.6 5.3 4.9 2.4 1.6 2.1 1.4 7.1 1.6 10.7 11.1 10.7 1.6 1.6 1.5 1.5 1.6 1.6 8 7.7 2.7 15.1 15.1 8.9 6 12.3 13.1 6.7 12.3 2.3 11.1 1.5 6.7 6 15.2 10.2 13.1 10.7 17.1 17.1 17.1 1.9 10.7 17.1 1.2 1.2 3.1 1.5 10.7 4.9 12.6 10.7 4.9 12.15 12 1.7 2.6 1.4 1.9 16.9 16.9 2.1 7 7.1 5.9 7.1 8.7 13.2 15.3 15.3 13.2 2.7 10.65 10 6.8 15.6 13.2 5.1 3 15.3 2.1 1.9 8.6 8.75 3.6 4.7 1.3 1.8 9.7 4 2.4 4.7 18.8 1.8 1.8 12.8 12.8 12.8 12.8 12.8 7.8 16.75 12.8 12.8 7.8 5.4 16.75 1.3 10.1 3.8 10.9 6.6 9.8 11.7 1.2 1.4 9.6 12.2 2.6 10.7 4.9 12.2 9.6 1.4 1.1 1 8.2 11.3 7.3 2.3 8.2 2.1 2 10 15.75 3.9 2 1.5 1.6 1.4 1.5 1.4 2 13.8 1.3 3.8 6.9 2.2 1.6 13.8 10.8 12.8 10.8 15.3 12.1 12 11.6 9.2 11.6 9.2 2.8 1.6 6.1 8.5 7.8 14.9 6.2 8.5 8.2 7.8 10.6 11.2 11.6 7.1 14.9 6.2 1.7 7.7 17.3 1.4 7.7 7.7 3.4 1.6 1.4 1.4 10.4 1.4 10.4 4.1 2.8 15.7 10.9 15.7 6.5 10.9 5.9 17.3 1.4 13.5 8.5 6.2 1.4 14.95 7.7 1.3 7.7 1.3 1.3 1.3 15.6 15.6 15.6 15.6 4.9 5 15.6 6.5 1.4 2.7 1.2 6.5 6.4 6.9 7.2 10.6 3.5 6.4 2.3 12.05 7 11.8 1.4 5 2.2 14.6 1.6 1.3 14.6 2.8 1.6 3.3 6.3 8.1 1.6 10.6 11.8 1.7 8.1 1.4 1.3 1.8 7.2 1.1 11.95 1.1 11.95 2.2 12.7 1.4 10.6 1.9 17.8 10.2 4.8 9.8 8.4 7.2 4.8 8.4 4.5 1.4 7.2 11 11.1 2.6 2 10.1 13.3 11.4 1.3 1.4 1.4 7 2 1.2 12.9 5 10.1 3.75 1.7 12.6 1.3 1.6 7.6 8.1 14.9 6 6 7.2 3 1.2 2 4.9 2 8.9 16.45 2 1.9 5.1 4.4 5.8 4.4 12.9 1.3 1.3 1.2 2.7 1.7 8.2 1.5 1.5 12.9 3.9 17.75 4.9 1.6 1.4 2 2 8.2 2.1 1.8 8.5 4.45 5.8 13 2.7 7.3 19.1 8.8 2.7 7.4 2.3 6.85 11.4 0.9 19.35 7.9 11.75 7.7 3 7.7 3 1.5 7.5 1.5 7.5 8.3 7.05 8.4 13.9 17.5 5.6 9.4 4.8 9.4 9.7 6.3 1.6 14.6 2.5 14.6 2.6 2.5 8.2 1.5 2.3 10 10 1.6 1.6 16 10.4 7.4 7.4 10.4 16.05 16.05 2.6 2.5 10.8 1.2 12.1 11.95 1.7 0.8 1.4 1.3 6.3 10.3 15.55 1.5 1.5 1.4 1.5 7.9 13 1 4.85 7.1 7.9 7.5 7.6 10.3 1.7 1.7 19.95 7.7 5.3 19.95 12.7 12.7 1.5 11.3 18.1 18.1 7 18.1 6.4 1.4 1.4 3.1 14.1 7.7 5.2 11.6 10.4 7.5 11.2 0.8 1.4 4.7 3.1 4 11.3 3.1 8.1 14.8 1.4 8.1 3.5 14.8 8.1 1.4 1.5 1.5 12.8 1.6 7.1 7.1 11.2 1.7 6.7 17.3 8.6 8.6 1.5 12.1 6.7 10.7 17.3 1.8 1.4 7.5 4.8 7.1 16.9 4.8 7.1 11.3 1.1 1.2 1.1 12.9 1.2 1.1 1.2 2.3 10 2.3 1.2 1.4 14.9 1.8 1.8 7 8.6 1.8 1.1 1.3 4.9 1.9 10.4 10 8.6 1.7 1.7 18.95 12.8 12.8 12.8 12.8 12.8 12.8 0.7 12.8 1.4 13.3 8.5 1.5 11.7 5 1.2 2.1 1.4 2.1 16 1.1 15.3 1.4 2.8 2.8 0.9 2.5 8.1 8.2 0.9 11.1 7.8 2.8 10.1 3.2 14.2 14.2 14.2 2.9 6 20.4 10.1 2.9 14.2 3.2 0.95 1.7 1.7 9 1.3 1.4 2.4 16 11.4 14.35 2.1 11.4 14.35 1.1 1.1 1.2 15.8 5.2 5.2 9.6 5.2 1.2 0.8 14.45 9.6 6.9 3.4 2.3 11 5.95 5.1 5.4 1.2 12.6 1 6.6 1.5 1 1.1 6.6 8.2 2 1.4 2 7.5 2 2 13.3 2.85 5.6 5.6 1 3.2 1 7.1 2.4 11.2 9.5 1 1.8 2.6 2.4 8 11.2 7.1 3.3 10.3 1.2 1.6 10.3 9.65 16.4 1.5 1.2 3.3 5 16.3 16.3 16.3 6.5 6.4 10.2 16.3 7.4 13.7 13.7 1.3 7.4 7.4 7.45 7.2 13.7 10.4 1.1 6.5 4.6 13.9 5.2 1.7 6.5 16.4 3.6 1.5 12.4 1.7 6.2 6.2 2.6 1.7 9.3 12.4 1.5 9.1 12 4.8 12.3 12 2.7 3.6 3.6 4.3 1.8 11.8 1.8 11.8 1.8 1.4 6.6 1.55 0.7 6.4 11.8 4.3 5.1 5.8 5.9 1.3 1.4 1.2 7.4 10.8 1.8 7.4 1.2 1.4 14.4 1.7 3.6 3.6 10.05 10.05 10.5 1.9 3.6 1.65 1.9 65.8 6.85 7.4 7.4 20.2 11 20.2 6.2 6.2 6.85 8 8.2 2.2 10.1 7.2 2.2 10.1 1.6 1.3 8 8.2 5.3 14 7.2 1.6 11.8 9.6 6.1 2.7 3.6 1.7 1.6 2.7 1 0.9 1.6 1 10.6 2 1.2 6.2 9.2 5 6.3 3.3 8 1.2 1.2 16.2 11.6 7.2 1.1 3.4 1.4 3.3 8 9.3 2.3 0.9 3.5 1.7 1.3 1.3 5.6 7.4 2.3 1 1.5 10 14.9 9.3 1 1 5.9 5 1.25 3.9 5 0.8 1 5.9 1.6 1.3 1 1.1 1.25 1.4 1.2 5 1.4 1.7 1.8 1.6 1.5 1.7 13.9 5.9 2.1 1.1 6.7 2.7 6.7 3.95 7.75 10.6 1.6 2.5 0.7 11.1 5.15 4.7 9.7 1.7 1.4 2 7.5 9.7 0.8 13.1 1.1 2.2 8.9 1.1 0.9 1.7 6.9 1.1 1 1 7.6 8.9 2.2 1.2 1 1 3.1 1.95 2.2 8.75 11.9 2.7 5.45 6.3 14.4 7.8 1.6 9.1 9.1 14.4 1.3 1.6 11.3 6.3 0.7 1.25 0.7 7.8 10.3 10.3 7.8 8.7 8.3 10.3 7.8 1.2 8.3 8.3 6.2 5 1.8 1.6 1.8 1.8 2.9 6 0.9 1.1 1.6 5.45 14.05 8 13.1 4.9 1.3 2.2 14.9 14.9 0.95 1.4 0.95 1.7 5.6 14.9 7.1 1.2 9.6 11.4 11.4 7.9 5 11.1 8 3.8 10.55 10.2 10.2 9.8 6.3 1.1 4.5 6.3 10.9 9.8 9.8 0.8 0.8 1.2 1.3 9.8 10.2 10.9 6.3 6.3 1.2 0.9 1.1 4.5 3.7 18.1 1.35 5.5 3.1 12.85 19.8 8.25 12.85 3.8 6.9 8.25 11.7 4.6 4 19.8 12.85 1.2 8.9 11.7 6.2 14.8 14.8 10.8 1.6 8.3 8.4 2.5 3.5 17.2 2.1 12.2 11.8 16.8 17.2 1.1 14.7 5.5 6.1 1.2 1.3 8.7 1.7 8.7 10.2 4.5 5.9 1.7 1.4 5.4 7.9 1.1 7 7 7.6 7 12.3 15.3 12.3 1.2 2.3 6.1 7.6 10.2 4.1 2.9 8.5 1.5 3.1 7.9 3.5 4.9 1.1 7 1.2 4.5 2.6 9.9 4.5 9.5 1.5 3.2 2.6 11.2 3.2 2.3 4.9 4.9 1.4 1.5 6.7 2.1 4.3 10.9 7 2.3 2.5 2.6 3.2 2.5 14.7 4.5 2.2 1.9 1.6 17.3 4.2 4.2 2.5 1.9 1.4 0.8 8 1.6 1.7 5.5 17.3 8.6 6.9 2.1 2.2 1.5 2.5 17.6 4.2 2.9 4.8 11.9 0.9 1.3 6.4 4.3 11.9 8.1 1.3 0.9 17.2 17.2 17.2 8.7 17.2 8.7 7.5 17.2 4.6 3.7 2.2 7.4 15.1 7.4 4.8 7.9 1 15.1 7.4 4.8 4.6 1.4 6.2 6.1 5.1 6.3 0.9 2.3 6.6 7.5 8.6 11.9 2.3 7.1 4.3 1.1 1 7.9 1 1 1 7.3 1.7 1.3 6.4 1.8 1.5 3.8 7.9 1 1.2 5.3 9.1 6.5 9.1 6.3 5.1 6.5 2.4 9.1 7.5 5 6.75 1.2 1.6 16.05 5 12.4 0.95 4.6 1.7 1 1.3 5 2.5 2.6 2.1 12.75 1.1 12.4 3.7 2.65 2.5 8.2 7.3 1.1 6.6 7 14.5 11.8 3 3.7 6 4.6 2.5 3.3 1 1.1 1.4 3.3 8.55 2.5 6.7 3.8 4.5 4.6 4.2 11.3 5.5 4.2 2.2 14.5 14.5 14.5 14.5 14.5 14.5 1.5 18.75 3.6 1.4 5.1 10.5 2 2.6 9.2 1.8 5.7 2.4 1.9 1.4 0.9 4.6 1.4 9.2 1.4 1.8 2.3 2.3 4.4 6.4 2.9 2.8 2.9 4.4 8.2 1 2.9 7 1.8 1.5 7 8.2 7.6 2.3 8.7 1 2.9 6.7 5 1.9 2 1.9 8.5 12.6 5.2 2.1 1.1 1.3 1.1 9.2 1.2 1.1 8.3 1.8 1.4 15.7 4.35 1.8 1.6 2 5 1.8 1.3 1 1.4 8.1 8.6 3.7 5.7 2.35 13.65 13.65 13.65 15.2 4.6 1.2 4.6 6.65 13.55 13.65 9.8 10.3 6.7 15.2 9.9 7.2 1.1 8.3 11.25 12.8 9.65 12.6 12.2 8.3 11.25 1.3 9.9 7.2 1.1 1.1 4.8 1.1 1.4 1.7 10.6 1.4 1.1 5.55 2.1 1.7 9 1.7 1.8 4.7 11.3 3.6 6.9 3.6 4.9 6.95 1.9 4.7 11.3 1.8 11.3 8.2 8.3 9.55 8.4 7.8 7.8 10.2 5.5 7.8 7.4 3.3 5 3.3 5 1.3 1.2 7.4 7.8 9.9 0.7 4.6 5.6 9.5 14.8 4.6 2.1 11.6 1.2 11.6 2.1 20.15 4.7 4.3 14.5 4.9 14.55 14.55 10.05 4.9 14.5 14.55 15.25 3.15 1.3 5.2 1.1 7.1 8.8 18.5 8.8 1.4 1.2 5 1.6 18.75 6 9.4 9.7 4.75 6 5.35 5.35 6.8 6.9 1.4 0.9 1.2 1.3 2.6 12 9.85 3.85 2 1.6 7.8 1.9 2 10.3 1.1 12 3.85 9.85 2 4 1.1 10.4 6.1 1.8 10.4 4.7 4 1.1 6.4 8.15 6.1 4.8 1.2 1.1 1.4 7.4 1.8 1 15.5 15.5 8.4 2.4 3.95 19.95 2 3 15.5 8.4 14.3 4.2 1.4 3 4.9 2.4 14.3 10.7 11 1.4 1.2 12.9 10.8 1.3 2 1.8 1.2 7.5 9.7 3.8 7.2 9.7 6.3 6.3 0.8 8.6 6.3 3.1 7.2 7.1 6.4 14.7 7.2 7.1 1.9 1.2 4.8 1.2 3.4 4.3 8.5 1.8 1.8 19.5 8.5 19.9 8.3 1.8 1.1 16.65 16.65 16.65 0.9 6.1 10.2 0.9 16.65 3.85 4.4 4.5 3.2 4.5 4.4 9.7 4.2 4.2 1.1 9.7 4.2 5.6 4.2 1.6 1.6 1.1 14.6 2.6 1.2 7.25 6.55 7 1.5 1.4 7.25 1 4.2 17.5 17.5 17.5 1.5 1.3 3.9 4.2 7.6 1 1.1 11.8 1.4 9.7 12.9 1.6 7.2 7.1 1.9 8.8 7.2 1.4 14.3 14.3 8.8 1.4 1.8 14.3 7.2 1.2 11.8 0.9 12.6 26.05 4.7 12.6 1.2 26.05 6.1 11.8 0.9 5.6 5.3 5.7 8 8 17.6 8 8.8 1.5 1.4 4.8 2.4 3.7 4.9 5.7 5.7 4.9 2 5.1 4.5 3.2 6.65 1.6 4 17.75 1.4 17.75 7.2 5.7 8.5 11.4 5.4 2.7 4.3 1.2 1.8 1.3 5.7 2.7 11.7 4.3 11 1.6 11.6 6.2 1.8 1.2 1 2.4 1.2 8.2 18.8 9.6 12.9 9.2 1.2 12.9 8 12.9 1.6 12 2.5 9.2 4.4 8.8 9.6 8 18.8 1.3 1.2 12.9 1.2 1.6 1.5 18.15 13.1 13.1 13.1 13.1 1 1.6 11.8 1.4 1 13.1 10.6 10.4 1.1 7.4 1.2 3.4 18.15 8 2.5 2 2 6.9 1.2 9.4 2.9 6.9 5.4 1.3 20.8 10.3 1.3 1.6 13.1 1.8 8 1.6 1.4 14.7 14.7 14.7 14.7 14.7 14.7 14.7 1.8 10.6 12.5 6.8 14.7 2.9 1.4 1.4 2.1 7.4 2.9 1.4 1.4 7.4 5 2.5 6.1 2.7 2.1 12.9 12.9 12.9 13.7 12.9 2.4 9.8 13.7 1.3 12.1 6.1 7.7 6.1 1.4 7.7 12.1 6.8 9.2 8.3 17.4 2.7 12.8 8.2 8.1 8.2 8.3 8 11.8 12 1.7 17.4 13.9 10.7 2 2.2 1.3 1.1 2 6.4 1.3 1.1 10.7 6.4 6.3 6.4 15.1 2 2 2.2 12.1 8.8 8.8 5.1 6.8 6.8 3.7 12.2 5.7 8.1 2.5 4 6.8 1 5.1 5.8 10.6 3.5 3.5 16.4 4.8 3.3 1.2 1.2 4.8 3.3 2.5 8.7 1.6 4 2.5 16.2 9 16.2 1.4 7 9 3.1 1.5 4.6 4.8 4.6 1.5 2.7 6.3 7.2 7.2 12.4 6.6 6.6 4 4.8 1.3 7.2 11.1 12.4 9.8 6.6 13.3 11.7 8 1.6 16.55 1.5 10.2 6.6 17.8 17.8 1.5 7.4 17.8 2 7.4 2 17.8 12.1 8.2 1.5 8.7 3.5 6.4 2.1 7.7 12.3 1.3 8.7 3.5 1.1 2.8 3.5 1.9 3.8 3.8 2.4 4.8 4.8 6.2 1.3 3.8 1.5 4.8 1.9 6.2 7.9 1.6 1.4 2.6 14.8 2.4 0.9 0.9 1.2 9.9 3.9 15.6 15.6 1.5 1.6 7.8 5.6 1.3 16.7 7.95 6.7 1.1 6.3 8.9 1 1.5 6.6 6.2 6.3 2.1 2.2 5.4 8.9 1 17.9 2.6 1.3 17.9 2.6 2.3 4.3 7.1 7.1 11.9 11.7 5.8 3.8 12.4 6.5 7.1 7.6 7.9 2.8 10.6 2.8 1.5 7.6 7.9 1.7 7.6 7.5 1.7 1.7 12.1 4.5 1.7 8 7.6 8.6 8.6 14.6 1.6 8.6 14.6 1.1 3.7 8.9 8.9 4.7 8.9 3.1 5.8 5.8 5.8 1 15.8 1.5 5.2 1.5 2.5 1 15.8 5.9 3.1 3.1 5.8 11.5 18 4.8 8.5 1.6 18 4.8 5.9 1.1 8.5 13.1 4.1 2.9 13.1 1.1 1.5 7.75 1.15 1 17.8 5.7 17.8 7.4 1.4 1.4 1 4.4 1.6 7.9 15.5 15.5 15.5 15.5 17.55 13.5 13.5 1.3 15.5 11.6 7.9 15.5 17.55 11.6 13.15 1.9 13.5 1.3 6.1 6.1 1.9 1.9 1.6 11.3 8.4 8.3 8.4 12.2 8 1.3 12.7 1.3 10.5 12.5 9.6 1.5 1.5 7.8 10.8 12.5 8.6 1.2 14.5 3.7 1.1 1.1 3.8 4.6 10.2 7.9 2.4 10.7 4.9 10.7 1.1 7.9 5.6 2.4 14.2 9.5 9.5 4.1 4.7 1.4 0.9 20.3 3.5 2.7 1.2 1.2 2 1.1 1.5 1.2 18.1 18.1 3.6 3.5 12.1 17.45 12.1 3 1.6 5.7 5.6 6.8 15.6 6 1.8 8.6 8.6 11.5 7.8 2.4 5 8.6 1.5 5.4 11.9 11.9 9 10 11.9 11.9 15.5 5.4 15 1.4 9.4 3.7 15 1.4 6.5 1.4 6.3 13.7 13.7 13.7 13.7 13.7 13.7 1.5 1.6 1.4 3.5 1 1.4 1.5 13.7 1.6 5.2 1.4 11.9 2.4 3.2 1.7 4.2 15.4 13 5.6 9.7 2.5 4 15.4 1.2 2 1.2 5.1 1.4 1.2 6.5 1.3 6.5 2.7 1.3 7.4 12.9 1.3 1.2 2.6 2.3 1.3 10.5 2.6 14.4 1.2 3.1 1.7 6 11.8 6.2 1.4 12.1 12.1 12.1 3.9 4.6 12.1 1.2 8.1 3.9 1.1 6.5 10.1 10.7 3.2 12.4 5.2 5 2.5 9.2 6.9 2 15 15 1.2 15 1.8 10.8 3.9 4.2 2 13.5 13.3 2.2 1.4 1.6 2.2 14.8 1.8 14.8 1.3 9.9 5.1 5.1 1.5 1.5 11.1 5.25 2.3 7.9 8 1.4 5.25 2.3 2.3 3.5 13.7 9.9 15.4 16 16 16 16 2.4 5.5 2.3 16.8 16 17.8 17.8 6.8 6.8 6.8 6.8 1.6 4.7 11.8 17.8 15.7 5.8 15.7 9 15.7 5.8 8.8 10.2 6.6 6.5 8.9 11.1 4.2 1.6 7.4 11.5 1.6 2 4.8 9.8 1.9 4.2 1.6 7.3 5.4 10.4 1.9 7.3 5.4 7.7 11.5 1.2 2.2 1 8.2 8.3 8.2 9.3 8.1 8.2 8.3 13.9 13.9 13.9 13.9 13.9 13.9 13.9 2 13.9 15.7 1.2 1.5 1.2 3.2 1.2 2.6 13.2 10.4 5.7 2.5 1.6 1.4 7.4 2.5 5.6 3.6 7.5 5.8 1.6 1.5 2.9 11.2 9.65 10.1 3.2 11.2 11.45 9.65 4.5 2.7 3.5 1.7 2.1 4.8 5 2.6 6.6 5 7.3 5 1.7 2.6 8.2 8.2 5 1.2 7.1 9.5 15.8 15.5 15.8 17.05 12.7 12.3 11.8 11.8 11.8 12.3 11.8 13.6 5.2 6.2 7.9 7.9 3.3 2.8 7.9 3.3 6.3 4.9 10.4 4.9 10.4 16 6.3 2.2 17.3 17.3 17.3 17.3 2.2 2.2 17.3 6.6 6.5 12.3 5 2.8 13.6 2.8 5.4 10.9 1.7 9.15 4.5 9.15 1.4 5.9 16.4 1.2 16.4 5.9 7.8 7.8 2.8 2.9 2.5 12.8 12.2 7.7 2.8 2.9 17.3 19.3 19.3 19.3 2.7 6.4 17.3 2.4 2.8 1.7 15.4 15.4 4.1 6.6 1.2 2.1 1 1.1 1.4 1.6 9.8 1.9 1.3 7.9 7.9 4.5 22.6 7.9 3.5 1.2 4.5 2 7.8 0.9 2.9 2.9 3.5 4.2 9.7 10.5 1.1 16.1 1.1 8.1 6.2 7.7 2.4 16.3 2.3 8.4 8.5 6 1.1 1.75 2.6 1.3 2.1 1.1 1.1 2.8 9 2.8 2.2 5.1 3.5 12.7 7.5 2 3.5 14.3 9.8 12.7 12.7 5.1 3.5 12.7 12.9 12.9 1.3 10.5 1.5 12.7 12.9 1.2 6.2 8.8 3.9 1.3 9.1 9.1 3.9 1.8 2.1 1.4 14.7 9.1 1.9 1.8 9.6 3.9 1.3 11.8 1.9 12 7.9 9.3 4.6 2.2 10.2 10.6 1.4 9.1 11.1 9.1 4.4 2.8 1.1 1.3 1.2 3.3 9.7 2.3 1.1 11.4 1.2 14.7 13.8 1.3 6.3 7.9 2 11.8 1.2 10 5.2 1.2 7.2 9.9 5.3 13.55 2.2 9.9 4.3 13 13.55 1 1.1 6.9 13.4 4.6 9.9 3 5.8 12.9 3.2 0.8 2.5 2.4 7.2 7.3 6.3 4.25 1.2 2 4.25 4.7 4.5 1.4 4.1 5.3 4.2 6.65 8.2 2.6 2.6 2 12.2 2.3 8.2 5 10.7 10.8 1.7 1.3 1.7 12.7 1.3 1.2 1.3 5.7 3.4 1.1 1 1 1.65 6.8 6.8 4.9 1.4 2.5 10.8 10.8 10.8 10.8 2.8 1.3 2 1.1 8.2 6 6.1 8.2 8.8 6.1 6 1.2 11.4 1.3 1.3 6.2 3.2 4.5 9.9 6.2 11.4 1.3 1.3 0.9 0.7 1 1 10.4 1.3 12.5 12.5 12.5 12.5 19.25 1.1 12.5 19.25 9 1.2 9 1.3 12.8 12.8 7.6 7.6 1.4 8.3 9 1.85 12.55 1.4 1.8 4 12.55 9 3 1.85 7.9 2.6 1.2 7.1 7.9 1.3 10.7 7.7 8.4 10.7 12.7 1.8 7.7 10.5 1.6 1.85 10.5 10.5 1 1.2 1.7 1.6 9 1.9 1.2 1.5 3.9 3.6 1.2 5 2.9 10.4 11.4 18.35 18.4 1.2 7.1 1.3 1.5 10.2 2.2 3.5 3.5 3.9 7.4 7.4 11 1.5 3.9 5.4 1.5 5 1.2 13 13 13 13 8.6 1.7 1.2 1.2 1.2 2 19.4 0.8 6.3 6.4 12.1 12.1 12.9 2.4 4.3 4.2 12.9 1.7 2.2 12.1 3.4 7.4 7.3 1.1 1.1 1.4 14.5 8 1.1 1.1 2.2 5.8 0.9 6.4 10.9 7.3 8.3 1.3 3.3 1 1.1 1 5.1 3.2 12.6 3.7 1.7 5.1 1 1.3 1.5 4.6 10.3 6.1 6.1 1.2 10.3 9.9 1.6 1.1 1.5 1.2 1.5 1.1 11.5 7.8 7.4 1.45 8.9 1.1 1 2.5 1.1 2.4 2.3 5.1 2.5 8.9 2.5 8.9 1.6 1.4 3.9 13.7 13.7 9.2 7.8 7.6 7.7 3 1.3 4 1.1 2 1.9 1.4 4.5 10.1 6.6 1.9 12.4 1.6 2.5 1.2 2.5 0.8 0.9 8.1 8.1 11.75 1.3 1.9 8.3 8.1 5.7 1.9 1.2 11.75 2.2 0.9 1.3 1.6 8 1.2 1.1 0.8 \ No newline at end of file +1.001 0.994 0.9951 0.9956 0.9956 0.9951 0.9949 1.001 0.994 0.9938 0.9908 0.9947 0.992 0.9912 1.0002 0.9914 0.9928 0.9892 0.9917 0.9955 0.9892 0.9912 0.993 0.9937 0.9951 0.9955 0.993 0.9961 0.9914 0.9906 0.9974 0.9934 0.992 0.9939 0.9962 0.9905 0.9934 0.9906 0.9999 0.9999 0.9937 0.9937 0.9954 0.9934 0.9934 0.9931 0.994 0.9939 0.9954 0.995 0.9917 0.9914 0.991 0.9911 0.993 0.9908 0.9962 0.9972 0.9931 0.9926 0.9951 0.9972 0.991 0.9931 0.9927 0.9934 0.9903 0.992 0.9926 0.9962 0.9956 0.9958 0.9964 0.9941 0.9926 0.9962 0.9898 0.9912 0.9961 0.9949 0.9929 0.9985 0.9946 0.9966 0.9974 0.9975 0.9974 0.9972 0.9974 0.9975 0.9974 0.9957 0.99 0.9899 0.9916 0.9969 0.9979 0.9913 0.9956 0.9979 0.9975 0.9962 0.997 1 0.9975 0.9974 0.9962 0.999 0.999 0.9927 0.9959 1 0.9982 0.9968 0.9968 0.994 0.9914 0.9911 0.9982 0.9982 0.9934 0.9984 0.9952 0.9952 0.9928 0.9912 0.994 0.9958 0.9924 0.9924 0.994 0.9958 0.9979 0.9982 0.9961 0.9979 0.992 0.9975 0.9917 0.9923 0.9927 0.9975 0.992 0.9947 0.9921 0.9905 0.9918 0.9951 0.9917 0.994 0.9934 0.9968 0.994 0.9919 0.9966 0.9979 0.9979 0.9898 0.9894 0.9894 0.9898 0.998 0.9932 0.9979 0.997 0.9972 0.9974 0.9896 0.9968 0.9958 0.9906 0.9917 0.9902 0.9918 0.999 0.9927 0.991 0.9972 0.9931 0.995 0.9951 0.9936 1.001 0.9979 0.997 0.9972 0.9954 0.9924 0.9906 0.9962 0.9962 1.001 0.9928 0.9942 0.9942 0.9942 0.9942 0.9961 0.998 0.9961 0.9984 0.998 0.9973 0.9949 0.9924 0.9972 0.9958 0.9968 0.9938 0.993 0.994 0.9918 0.9958 0.9944 0.9912 0.9961 0.9939 0.9961 0.9989 0.9938 0.9939 0.9971 0.9912 0.9936 0.9929 0.9998 0.9938 0.9969 0.9938 0.9998 0.9972 0.9976 0.9976 0.9979 0.9979 0.9979 0.9979 0.9972 0.9918 0.9982 0.9985 0.9944 0.9903 0.9934 0.9975 0.9923 0.99 0.9905 0.9905 0.996 0.9964 0.998 0.9975 0.9913 0.9932 0.9935 0.9927 0.9927 0.9912 0.9904 0.9939 0.9996 0.9944 0.9977 0.9912 0.9996 0.9965 0.9944 0.9945 0.9944 0.9965 0.9944 0.9972 0.9949 0.9966 0.9954 0.9954 0.9915 0.9919 0.9916 0.99 0.9909 0.9938 0.9982 0.9988 0.9961 0.9978 0.9979 0.9979 0.9979 0.9979 0.9945 1 0.9957 0.9968 0.9934 0.9976 0.9932 0.997 0.9923 0.9914 0.992 0.9914 0.9914 0.9949 0.9949 0.995 0.995 0.9927 0.9928 0.9917 0.9918 0.9954 0.9941 0.9941 0.9934 0.9927 0.9938 0.9933 0.9934 0.9927 0.9938 0.9927 0.9946 0.993 0.9946 0.9976 0.9944 0.9978 0.992 0.9912 0.9927 0.9906 0.9954 0.9923 0.9906 0.991 0.9972 0.9945 0.9934 0.9964 0.9948 0.9962 0.9931 0.993 0.9942 0.9906 0.9995 0.998 0.997 0.9914 0.992 0.9924 0.992 0.9937 0.9978 0.9978 0.9927 0.994 0.9935 0.9968 0.9941 0.9942 0.9978 0.9923 0.9912 0.9923 0.9927 0.9931 0.9941 0.9927 0.9931 0.9934 0.9936 0.9893 0.9893 0.9919 0.9924 0.9927 0.9919 0.9924 0.9975 0.9969 0.9936 0.991 0.9893 0.9906 0.9941 0.995 0.9983 0.9983 0.9916 0.9957 0.99 0.9976 0.992 0.9917 0.9917 0.9993 0.9908 0.9917 0.9976 0.9934 1 0.9918 0.992 0.9896 0.9932 0.992 0.9917 0.9999 0.998 0.9918 0.9918 0.9999 0.998 0.9927 0.9959 0.9927 0.9929 0.9898 0.9954 0.9954 0.9954 0.9954 0.9954 0.9954 0.9974 0.9936 0.9978 0.9974 0.9927 0.9934 0.9938 0.9922 0.992 0.9935 0.9906 0.9934 0.9934 0.9913 0.9938 0.9898 0.9975 0.9975 0.9937 0.9914 0.9982 0.9982 0.9929 0.9971 0.9921 0.9931 0.9924 0.9929 0.9982 0.9892 0.9956 0.9924 0.9971 0.9956 0.9982 0.9973 0.9932 0.9976 0.9962 0.9956 0.9932 0.9976 0.9992 0.9983 0.9937 0.99 0.9944 0.9938 0.9965 0.9893 0.9927 0.994 0.9928 0.9964 0.9917 0.9972 0.9964 0.9954 0.993 0.9928 0.9916 0.9936 0.9962 0.9899 0.9898 0.996 0.9907 0.994 0.9913 0.9976 0.9904 0.992 0.9976 0.999 0.9975 0.9937 0.9937 0.998 0.998 0.9944 0.9938 0.9907 0.9938 0.9921 0.9908 0.9931 0.9915 0.9952 0.9926 0.9934 0.992 0.9918 0.9942 0.9942 0.9942 0.9901 0.9898 0.9902 0.9934 0.9906 0.9898 0.9896 0.9922 0.9947 0.9945 0.9976 0.9976 0.9976 0.9987 0.9987 0.9976 0.992 0.9955 0.9953 0.9976 0.992 0.9952 0.9983 0.9933 0.9958 0.9922 0.9928 0.9976 0.9976 0.9916 0.9901 0.9976 0.9901 0.9916 0.9982 0.993 0.9969 0.991 0.9953 0.9924 0.9969 0.9928 0.9945 0.9967 0.9944 0.9928 0.9929 0.9948 0.9976 0.9912 0.9987 0.99 0.991 0.9933 0.9933 0.9899 0.9912 0.9912 0.9976 0.994 0.9947 0.9954 0.993 0.9954 0.9963 0.992 0.9926 0.995 0.9983 0.992 0.9968 0.9905 0.9904 0.9926 0.9968 0.9928 0.9949 0.9909 0.9937 0.9914 0.9905 0.9904 0.9924 0.9924 0.9965 0.9965 0.9993 0.9965 0.9908 0.992 0.9978 0.9978 0.9978 0.9978 0.9912 0.9928 0.9928 0.993 0.9993 0.9965 0.9937 0.9913 0.9934 0.9952 0.9983 0.9957 0.9957 0.9916 0.9999 0.9999 0.9936 0.9972 0.9933 0.9934 0.9931 0.9976 0.9937 0.9937 0.991 0.9979 0.9971 0.9969 0.9968 0.9961 0.993 0.9973 0.9944 0.9986 0.9986 0.9986 0.9986 0.9972 0.9917 0.992 0.9932 0.9936 0.9915 0.9922 0.9934 0.9952 0.9972 0.9934 0.9958 0.9944 0.9908 0.9958 0.9925 0.9966 0.9972 0.9912 0.995 0.9928 0.9968 0.9955 0.9981 0.991 0.991 0.991 0.992 0.9931 0.997 0.9948 0.9923 0.9976 0.9938 0.9984 0.9972 0.9922 0.9935 0.9944 0.9942 0.9944 0.9997 0.9977 0.9912 0.9982 0.9982 0.9983 0.998 0.9894 0.9927 0.9917 0.9904 0.993 0.9941 0.9943 0.99855 0.99345 0.998 0.9916 0.9916 0.99475 0.99325 0.9933 0.9969 1.0002 0.9933 0.9937 0.99685 0.99455 0.9917 0.99035 0.9914 0.99225 0.99155 0.9954 0.99455 0.9924 0.99695 0.99655 0.9934 0.998 0.9971 0.9948 0.998 0.9971 0.99215 0.9948 0.9915 0.99115 0.9932 0.9977 0.99535 0.99165 0.9953 0.9928 0.9958 0.9928 0.9928 0.9964 0.9987 0.9953 0.9932 0.9907 0.99755 0.99935 0.9932 0.9932 0.9958 0.99585 1.00055 0.9985 0.99505 0.992 0.9988 0.99175 0.9962 0.9962 0.9942 0.9927 0.9927 0.99985 0.997 0.9918 0.99215 0.99865 0.9992 1.0006 0.99135 0.99715 0.9992 1.0006 0.99865 0.99815 0.99815 0.99815 0.9949 0.99815 0.99815 0.99225 0.99445 0.99225 0.99335 0.99625 0.9971 0.9983 0.99445 0.99085 0.9977 0.9953 0.99775 0.99795 0.99505 0.9977 0.9975 0.99745 0.9976 0.99775 0.9953 0.9932 0.99405 1 0.99785 0.9939 0.9939 0.99675 0.9939 0.99675 0.98965 0.9971 0.99445 0.9945 0.9939 0.9958 0.9956 0.99055 0.9959 0.9925 0.9963 0.9935 0.99105 0.99045 0.9963 0.99155 0.99085 0.99085 0.99085 0.9924 0.9924 0.99975 0.99975 0.99315 0.9917 0.9917 0.99845 0.9921 0.99975 0.9909 0.99315 0.99855 0.9934 0.9978 0.9934 0.9949 0.99855 0.9986 0.99725 0.9946 0.99255 0.9996 0.9939 0.99 0.9937 0.9886 0.9934 1 0.9994 0.9926 0.9956 0.9978 0.9915 0.9939 0.9932 0.993 0.9898 0.9921 0.9932 0.9919 0.993 0.9953 0.9928 0.9928 0.9976 0.9906 0.9918 0.99185 0.9918 0.99185 0.994 0.9908 0.9928 0.9896 0.9908 0.9918 0.9952 0.9923 0.9915 0.9952 0.9947 0.9983 0.9975 0.995 0.9944 0.994 0.9944 0.9908 0.99795 0.9985 0.99425 0.99425 0.9943 0.9924 0.9946 0.9924 0.995 0.9919 0.99 0.9923 0.9956 0.9978 0.9978 0.9967 0.9934 0.9936 0.9932 0.9934 0.998 0.9978 0.9929 0.9974 0.99685 0.99495 0.99745 0.99505 0.992 0.9978 0.9956 0.9982 0.99485 0.9971 0.99265 0.9904 0.9965 0.9946 0.99965 0.9935 0.996 0.9942 0.9936 0.9965 0.9928 0.9928 0.9965 0.9936 0.9938 0.9926 0.9926 0.9983 0.9983 0.992 0.9983 0.9923 0.9972 0.9928 0.9928 0.9994 0.991 0.9906 0.9894 0.9898 0.9994 0.991 0.9925 0.9956 0.9946 0.9966 0.9951 0.9927 0.9927 0.9951 0.9894 0.9907 0.9925 0.9928 0.9941 0.9941 0.9925 0.9935 0.9932 0.9944 0.9972 0.994 0.9956 0.9927 0.9924 0.9966 0.9997 0.9936 0.9936 0.9952 0.9952 0.9928 0.9911 0.993 0.9911 0.9932 0.993 0.993 0.9932 0.9932 0.9943 0.9968 0.9994 0.9926 0.9968 0.9932 0.9916 0.9946 0.9925 0.9925 0.9935 0.9962 0.9928 0.993 0.993 0.9956 0.9941 0.9972 0.9948 0.9955 0.9972 0.9972 0.9983 0.9942 0.9936 0.9956 0.9953 0.9918 0.995 0.992 0.9952 1.001 0.9924 0.9932 0.9937 0.9918 0.9934 0.991 0.9962 0.9932 0.9908 0.9962 0.9918 0.9941 0.9931 0.9981 0.9931 0.9944 0.992 0.9966 0.9956 0.9956 0.9949 1.0002 0.9942 0.9923 0.9917 0.9931 0.992 1.0002 0.9953 0.9951 0.9974 0.9904 0.9974 0.9944 1.0004 0.9952 0.9956 0.995 0.995 0.9995 0.9942 0.9977 0.992 0.992 0.9995 0.9934 1.0006 0.9982 0.9928 0.9945 0.9963 0.9906 0.9956 0.9942 0.9962 0.9894 0.995 0.9908 0.9914 0.9938 0.9977 0.9922 0.992 0.9903 0.9893 0.9952 0.9903 0.9912 0.9983 0.9937 0.9932 0.9928 0.9922 0.9976 0.9922 0.9974 0.998 0.9931 0.9911 0.9944 0.9937 0.9974 0.989 0.992 0.9928 0.9918 0.9936 0.9944 0.9988 0.994 0.9953 0.9986 0.9914 0.9934 0.996 0.9937 0.9921 0.998 0.996 0.9933 0.9933 0.9959 0.9936 0.9953 0.9938 0.9952 0.9959 0.9959 0.9937 0.992 0.9967 0.9944 0.9998 0.9998 0.9942 0.9998 0.9945 0.9998 0.9946 0.9942 0.9928 0.9946 0.9927 0.9938 0.9918 0.9945 0.9966 0.9954 0.9913 0.9931 0.9986 0.9965 0.9984 0.9952 0.9956 0.9949 0.9954 0.996 0.9931 0.992 0.9912 0.9978 0.9938 0.9914 0.9932 0.9944 0.9913 0.9948 0.998 0.9998 0.9964 0.9992 0.9948 0.9998 0.998 0.9939 0.992 0.9922 0.9955 0.9917 0.9917 0.9954 0.9986 0.9955 0.9917 0.9907 0.9922 0.9958 0.993 0.9917 0.9926 0.9959 0.9906 0.9993 0.993 0.9906 0.992 0.992 0.994 0.9959 0.9908 0.9902 0.9908 0.9943 0.9921 0.9911 0.9986 0.992 0.992 0.9943 0.9937 0.993 0.9902 0.9928 0.9896 0.998 0.9954 0.9938 0.9918 0.9896 0.9944 0.9999 0.9953 0.992 0.9925 0.9981 0.9952 0.9927 0.9927 0.9911 0.9936 0.9959 0.9946 0.9948 0.9955 0.9951 0.9952 0.9946 0.9946 0.9944 0.9938 0.9963 0.991 1.0003 0.9966 0.9993 1.0003 0.9938 0.9965 0.9938 0.9993 0.9938 1.0003 0.9966 0.9942 0.9928 0.991 0.9911 0.9977 0.9927 0.9911 0.991 0.9912 0.9907 0.9902 0.992 0.994 0.9966 0.993 0.993 0.993 0.9966 0.9942 0.9925 0.9925 0.9928 0.995 0.9939 0.9958 0.9952 1 0.9948 0.99 0.9958 0.9948 0.9949 0.997 0.9927 0.9938 0.9949 0.9953 0.997 0.9932 0.9927 0.9932 0.9955 0.9914 0.991 0.992 0.9924 0.9927 0.9911 0.9958 0.9928 0.9902 0.994 0.994 0.9972 1.0004 0.991 0.9918 0.995 0.9941 0.9956 0.9956 0.9959 0.9922 0.9931 0.9959 0.9984 0.9908 0.991 0.9928 0.9936 0.9941 0.9924 0.9917 0.9906 0.995 0.9956 0.9955 0.9907 1 0.9953 0.9911 0.9922 0.9951 0.9948 0.9906 0.994 0.9907 0.9927 0.9914 0.9958 1 0.9984 0.9941 0.9944 0.998 0.998 0.9902 0.9911 0.9929 0.993 0.9918 0.992 0.9932 0.992 0.994 0.9923 0.993 0.9956 0.9907 0.99 0.9918 0.9926 0.995 0.99 0.99 0.9946 0.9907 0.9898 0.9918 0.9986 0.9986 0.9928 0.9986 0.9979 0.994 0.9937 0.9938 0.9942 0.9944 0.993 0.9986 0.9932 0.9934 0.9928 0.9925 0.9944 0.9909 0.9932 0.9934 1.0001 0.992 0.9916 0.998 0.9919 0.9925 0.9977 0.9944 0.991 0.99 0.9917 0.9923 0.9928 0.9923 0.9928 0.9902 0.9893 0.9917 0.9982 1.0005 0.9923 0.9951 0.9956 0.998 0.9928 0.9938 0.9914 0.9955 0.9924 0.9911 0.9917 0.9917 0.9932 0.9955 0.9929 0.9955 0.9958 1.0012 0.9968 0.9911 0.9924 0.991 0.9946 0.9928 0.9946 0.9917 0.9918 0.9926 0.9931 0.9932 0.9903 0.9928 0.9929 0.9958 0.9955 0.9911 0.9938 0.9942 0.9945 0.9962 0.992 0.9927 0.9948 0.9945 0.9942 0.9952 0.9942 0.9958 0.9918 0.9932 1.0004 0.9972 0.9998 0.9918 0.9918 0.9964 0.9936 0.9931 0.9938 0.9934 0.99 0.9914 0.9904 0.994 0.9938 0.9933 0.9909 0.9942 0.9945 0.9954 0.996 0.9991 0.993 0.9942 0.9934 0.9939 0.9937 0.994 0.9926 0.9951 0.9952 0.9935 0.9938 0.9939 0.9933 0.9927 0.998 0.9997 0.9981 0.992 0.9954 0.992 0.9997 0.9981 0.9943 0.9941 0.9936 0.9996 0.9932 0.9926 0.9936 0.992 0.9936 0.9996 0.993 0.9924 0.9928 0.9926 0.9952 0.9945 0.9945 0.9903 0.9932 0.9953 0.9936 0.9912 0.9962 0.9965 0.9932 0.9967 0.9953 0.9963 0.992 0.991 0.9958 0.99 0.991 0.9958 0.9938 0.9996 0.9946 0.9974 0.9945 0.9946 0.9974 0.9957 0.9931 0.9947 0.9953 0.9931 0.9946 0.9978 0.9989 1.0004 0.9938 0.9934 0.9978 0.9956 0.9982 0.9948 0.9956 0.9982 0.9926 0.991 0.9945 0.9916 0.9953 0.9938 0.9956 0.9906 0.9956 0.9932 0.9914 0.9938 0.996 0.9906 0.98815 0.9942 0.9903 0.9906 0.9935 1.0024 0.9968 0.9906 0.9941 0.9919 0.9928 0.9958 0.9932 0.9957 0.9937 0.9982 0.9928 0.9919 0.9956 0.9957 0.9954 0.993 0.9954 0.9987 0.9956 0.9928 0.9951 0.993 0.9928 0.9926 0.9938 1.0001 0.9933 0.9952 0.9934 0.9988 0.993 0.9952 0.9948 0.9998 0.9971 0.9998 0.9962 0.9948 0.99 0.9942 0.9965 0.9912 0.9978 0.9928 1.0103 0.9956 0.9936 0.9929 0.9966 0.9964 0.996 0.9959 0.9954 0.9914 1.0103 1.0004 0.9911 0.9938 0.9927 0.9922 0.9924 0.9963 0.9936 0.9951 0.9951 0.9955 0.9961 0.9936 0.992 0.9944 0.9944 1.0008 0.9962 0.9986 0.9986 1 0.9986 0.9982 1 0.9949 0.9915 0.9951 0.9986 0.9927 0.9955 0.9952 0.9928 0.9982 0.9914 0.9927 0.9918 0.9944 0.9969 0.9955 0.9954 0.9955 0.9921 0.9934 0.9998 0.9946 0.9984 0.9924 0.9939 0.995 0.9957 0.9953 0.9912 0.9939 0.9921 0.9954 0.9933 0.9941 0.995 0.9977 0.9912 0.9945 0.9952 0.9924 0.9986 0.9953 0.9939 0.9929 0.9988 0.9906 0.9914 0.9978 0.9928 0.9948 0.9978 0.9946 0.9908 0.9954 0.9906 0.99705 0.9982 0.9932 0.9977 0.994 0.9982 0.9929 0.9924 0.9966 0.9921 0.9967 0.9934 0.9914 0.99705 0.9961 0.9967 0.9926 0.99605 0.99435 0.9948 0.9916 0.997 0.9961 0.9967 0.9961 0.9955 0.9922 0.9918 0.9955 0.9941 0.9955 0.9955 0.9924 0.9973 0.999 0.9941 0.9922 0.9922 0.9953 0.9945 0.9945 0.9957 0.9932 0.9945 0.9913 0.9909 0.9939 0.991 0.9954 0.9943 0.993 1.0002 0.9946 0.9953 0.9918 0.9936 0.9984 0.9956 0.9966 0.9942 0.9984 0.9956 0.9966 0.9974 0.9944 1.0008 0.9974 1.0008 0.9928 0.9944 0.9908 0.9917 0.9911 0.9912 0.9953 0.9932 0.9896 0.9889 0.9912 0.9926 0.9911 0.9964 0.9974 0.9944 0.9974 0.9964 0.9963 0.9948 0.9948 0.9953 0.9948 0.9953 0.9949 0.9988 0.9954 0.992 0.9984 0.9954 0.9926 0.992 0.9976 0.9972 0.991 0.998 0.9966 0.998 1.0007 0.992 0.9925 0.991 0.9934 0.9955 0.9944 0.9981 0.9968 0.9946 0.9946 0.9981 0.9946 0.997 0.9924 0.9958 0.994 0.9958 0.9984 0.9948 0.9932 0.9952 0.9924 0.9945 0.9976 0.9976 0.9938 0.9997 0.994 0.9921 0.9986 0.9987 0.9991 0.9987 0.9991 0.9991 0.9948 0.9987 0.993 0.9988 1 0.9932 0.9991 0.9989 1 1 0.9952 0.9969 0.9966 0.9966 0.9976 0.99 0.9988 0.9942 0.9984 0.9932 0.9969 0.9966 0.9933 0.9916 0.9914 0.9966 0.9958 0.9926 0.9939 0.9953 0.9906 0.9914 0.9958 0.9926 0.9991 0.9994 0.9976 0.9966 0.9953 0.9923 0.993 0.9931 0.9932 0.9926 0.9938 0.9966 0.9974 0.9924 0.9948 0.9964 0.9924 0.9966 0.9974 0.9938 0.9928 0.9959 1.0001 0.9959 1.0001 0.9968 0.9932 0.9954 0.9992 0.9932 0.9939 0.9952 0.9996 0.9966 0.9925 0.996 0.9996 0.9973 0.9937 0.9966 1.0017 0.993 0.993 0.9959 0.9958 1.0017 0.9958 0.9979 0.9941 0.997 0.9934 0.9927 0.9944 0.9927 0.9963 1.0011 1.0011 0.9959 0.9973 0.9966 0.9932 0.9984 0.999 0.999 0.999 0.999 0.999 1.0006 0.9937 0.9954 0.997 0.9912 0.9939 0.999 0.9957 0.9926 0.9994 1.0004 0.9994 1.0004 1.0004 1.0002 0.9922 0.9922 0.9934 0.9926 0.9941 0.9994 1.0004 0.9924 0.9948 0.9935 0.9918 0.9948 0.9924 0.9979 0.993 0.994 0.991 0.993 0.9922 0.9979 0.9937 0.9928 0.9965 0.9928 0.9991 0.9948 0.9925 0.9958 0.9962 0.9965 0.9951 0.9944 0.9916 0.9987 0.9928 0.9926 0.9934 0.9944 0.9949 0.9926 0.997 0.9949 0.9948 0.992 0.9964 0.9926 0.9982 0.9955 0.9955 0.9958 0.9997 1.0001 1.0001 0.9918 0.9918 0.9931 1.0001 0.9926 0.9966 0.9932 0.9969 0.9925 0.9914 0.996 0.9952 0.9934 0.9939 0.9939 0.9906 0.9901 0.9948 0.995 0.9953 0.9953 0.9952 0.996 0.9948 0.9951 0.9931 0.9962 0.9948 0.9959 0.9962 0.9958 0.9948 0.9948 0.994 0.9942 0.9942 0.9948 0.9964 0.9958 0.9932 0.9986 0.9986 0.9988 0.9953 0.9983 1 0.9951 0.9983 0.9906 0.9981 0.9936 0.9951 0.9953 1.0005 0.9972 1 0.9969 1.0001 1.0001 1.0001 0.9934 0.9969 1.0001 0.9902 0.993 0.9914 0.9941 0.9967 0.9918 0.998 0.9967 0.9918 0.9957 0.9986 0.9958 0.9948 0.9918 0.9923 0.9998 0.9998 0.9914 0.9939 0.9966 0.995 0.9966 0.994 0.9972 0.9998 0.9998 0.9982 0.9924 0.9972 0.997 0.9954 0.9962 0.9972 0.9921 0.9905 0.9998 0.993 0.9941 0.9994 0.9962 0.992 0.9922 0.994 0.9897 0.9954 0.99 0.9948 0.9922 0.998 0.9944 0.9944 0.9986 0.9986 0.9986 0.9986 0.9986 0.996 0.9999 0.9986 0.9986 0.996 0.9951 0.9999 0.993 0.9982 0.992 0.9963 0.995 0.9956 0.997 0.9936 0.9935 0.9963 0.9967 0.9912 0.9981 0.9966 0.9967 0.9963 0.9935 0.9902 0.99 0.996 0.9966 0.9962 0.994 0.996 0.994 0.9944 0.9974 0.996 0.9922 0.9917 0.9918 0.9936 0.9938 0.9918 0.9939 0.9917 0.9981 0.9941 0.9928 0.9952 0.9898 0.9914 0.9981 0.9957 0.998 0.9957 0.9986 0.9983 0.9982 0.997 0.9947 0.997 0.9947 0.99416 0.99516 0.99496 0.9974 0.99579 0.9983 0.99471 0.9974 0.99644 0.99579 0.99699 0.99758 0.9977 0.99397 0.9983 0.99471 0.99243 0.9962 1.00182 0.99384 0.99582 0.9962 0.9924 0.99466 0.99212 0.99449 0.99748 0.99449 0.99748 0.99475 0.99189 0.99827 0.99752 0.99827 0.99479 0.99752 0.99642 1.00047 0.99382 0.99784 0.99486 0.99537 0.99382 0.99838 0.99566 0.99268 0.99566 0.99468 0.9933 0.99307 0.99907 0.99907 0.99907 0.99907 0.99471 0.99471 0.99907 0.99148 0.99383 0.99365 0.99272 0.99148 0.99235 0.99508 0.9946 0.99674 0.99018 0.99235 0.99084 0.99856 0.99591 0.9975 0.9944 0.99173 0.99378 0.99805 0.99534 0.99232 0.99805 0.99078 0.99534 0.99061 0.99182 0.9966 0.9912 0.99779 0.99814 0.99096 0.99379 0.99426 0.99228 0.99335 0.99595 0.99297 0.99687 0.99297 0.99687 0.99445 0.9986 0.99154 0.9981 0.98993 1.00241 0.99716 0.99437 0.9972 0.99756 0.99509 0.99572 0.99756 0.99175 0.99254 0.99509 0.99676 0.9979 0.99194 0.99077 0.99782 0.99942 0.99708 0.99353 0.99256 0.99199 0.9918 0.99354 0.99244 0.99831 0.99396 0.99724 0.99524 0.9927 0.99802 0.99512 0.99438 0.99679 0.99652 0.99698 0.99474 0.99511 0.99582 0.99125 0.99256 0.9911 0.99168 0.9911 0.99556 1.00098 0.99516 0.99516 0.99518 0.99347 0.9929 0.99347 0.99841 0.99362 0.99361 0.9914 0.99114 0.9925 0.99453 0.9938 0.9938 0.99806 0.9961 1.00016 0.9916 0.99116 0.99319 0.99517 0.99514 0.99566 0.99166 0.99587 0.99558 0.99117 0.99399 0.99741 0.99405 0.99622 1.00051 0.99803 0.99405 0.99773 0.99397 0.99622 0.99713 0.99274 1.00118 0.99176 0.9969 0.99771 0.99411 0.99771 0.99411 0.99194 0.99558 0.99194 0.99558 0.99577 0.99564 0.99578 0.99888 1.00014 0.99441 0.99594 0.99437 0.99594 0.9979 0.99434 0.99203 0.998 0.99316 0.998 0.99314 0.99316 0.99612 0.99295 0.99394 0.99642 0.99642 0.99248 0.99268 0.99954 0.99692 0.99592 0.99592 0.99692 0.99822 0.99822 0.99402 0.99404 0.99787 0.99347 0.99838 0.99839 0.99375 0.99155 0.9936 0.99434 0.9922 0.99571 0.99658 0.99076 0.99496 0.9937 0.99076 0.99542 0.99825 0.99289 0.99432 0.99523 0.99542 0.9959 0.99543 0.99662 0.99088 0.99088 0.99922 0.9966 0.99466 0.99922 0.99836 0.99836 0.99238 0.99645 1 1 0.99376 1 0.99513 0.99556 0.99556 0.99543 0.99886 0.99526 0.99166 0.99691 0.99732 0.99573 0.99656 0.99112 0.99214 0.99165 0.99004 0.99463 0.99683 0.99004 0.99596 0.99898 0.99114 0.99508 0.99306 0.99898 0.99508 0.99114 0.99342 0.99345 0.99772 0.99239 0.99502 0.99502 0.99479 0.99207 0.99497 0.99828 0.99542 0.99542 0.99228 0.99706 0.99497 0.99669 0.99828 0.99269 0.99196 0.99662 0.99475 0.99544 0.99944 0.99475 0.99544 0.9966 0.99066 0.9907 0.99066 0.998 0.9907 0.99066 0.99307 0.99106 0.99696 0.99106 0.99307 0.99167 0.99902 0.98992 0.99182 0.99556 0.99582 0.99182 0.98972 0.99352 0.9946 0.99273 0.99628 0.99582 0.99553 0.98914 0.99354 0.99976 0.99808 0.99808 0.99808 0.99808 0.99808 0.99808 0.9919 0.99808 0.99499 0.99655 0.99615 0.99296 0.99482 0.99079 0.99366 0.99434 0.98958 0.99434 0.99938 0.99059 0.99835 0.98958 0.99159 0.99159 0.98931 0.9938 0.99558 0.99563 0.98931 0.99691 0.9959 0.99159 0.99628 0.99076 0.99678 0.99678 0.99678 0.99089 0.99537 1.0002 0.99628 0.99089 0.99678 0.99076 0.99332 0.99316 0.99272 0.99636 0.99202 0.99148 0.99064 0.99884 0.99773 1.00013 0.98974 0.99773 1.00013 0.99112 0.99136 0.99132 0.99642 0.99488 0.99527 0.99578 0.99352 0.99199 0.99198 0.99756 0.99578 0.99561 0.99347 0.98936 0.99786 0.99705 0.9942 0.9948 0.99116 0.99688 0.98974 0.99542 0.99154 0.99118 0.99044 0.9914 0.9979 0.98892 0.99114 0.99188 0.99583 0.98892 0.98892 0.99704 0.9911 0.99334 0.99334 0.99094 0.99014 0.99304 0.99652 0.98944 0.99772 0.99367 0.99304 0.99183 0.99126 0.98944 0.99577 0.99772 0.99652 0.99428 0.99388 0.99208 0.99256 0.99388 0.9925 0.99904 0.99216 0.99208 0.99428 0.99165 0.99924 0.99924 0.99924 0.9956 0.99562 0.9972 0.99924 0.9958 0.99976 0.99976 0.99296 0.9957 0.9958 0.99579 0.99541 0.99976 0.99518 0.99168 0.99276 0.99085 0.99873 0.99172 0.99312 0.99276 0.9972 0.99278 0.99092 0.9962 0.99053 0.99858 0.9984 0.99335 0.99053 0.9949 0.9962 0.99092 0.99532 0.99727 0.99026 0.99668 0.99727 0.9952 0.99144 0.99144 0.99015 0.9914 0.99693 0.99035 0.99693 0.99035 0.99006 0.99126 0.98994 0.98985 0.9971 0.99882 0.99477 0.99478 0.99576 0.99578 0.99354 0.99244 0.99084 0.99612 0.99356 0.98952 0.99612 0.99084 0.99244 0.99955 0.99374 0.9892 0.99144 0.99352 0.99352 0.9935 0.99237 0.99144 0.99022 0.99032 1.03898 0.99587 0.99587 0.99587 0.99976 0.99354 0.99976 0.99552 0.99552 0.99587 0.99604 0.99584 0.98894 0.9963 0.993 0.98894 0.9963 0.99068 0.98964 0.99604 0.99584 0.9923 0.99437 0.993 0.99238 0.99801 0.99802 0.99566 0.99067 0.99066 0.9929 0.9934 0.99067 0.98912 0.99066 0.99228 0.98912 0.9958 0.99052 0.99312 0.9968 0.99502 0.99084 0.99573 0.99256 0.9959 0.99084 0.99084 0.99644 0.99526 0.9954 0.99095 0.99188 0.9909 0.99256 0.9959 0.99581 0.99132 0.98936 0.99136 0.99142 0.99232 0.99232 0.993 0.99311 0.99132 0.98993 0.99208 0.99776 0.99839 0.99574 0.99093 0.99156 0.99278 0.9924 0.98984 0.99035 0.9924 0.99165 0.9923 0.99278 0.99008 0.98964 0.99156 0.9909 0.98984 0.9889 0.99178 0.99076 0.9889 0.99046 0.98999 0.98946 0.98976 0.99046 0.99672 0.99482 0.98945 0.98883 0.99362 0.99075 0.99436 0.98988 0.99158 0.99265 0.99195 0.99168 0.9918 0.99313 0.9895 0.9932 0.99848 0.9909 0.99014 0.9952 0.99652 0.99848 0.99104 0.99772 0.9922 0.99076 0.99622 0.9902 0.99114 0.9938 0.99594 0.9902 0.99035 0.99032 0.99558 0.99622 0.99076 0.99413 0.99043 0.99043 0.98982 0.98934 0.9902 0.99449 0.99629 0.9948 0.98984 0.99326 0.99834 0.99555 0.98975 0.99216 0.99216 0.99834 0.9901 0.98975 0.99573 0.99326 0.99215 0.98993 0.99218 0.99555 0.99564 0.99564 0.99397 0.99576 0.99601 0.99564 0.99397 0.98713 0.99308 0.99308 0.99582 0.99494 0.9929 0.99471 0.9929 0.9929 0.99037 0.99304 0.99026 0.98986 0.99471 0.98951 0.99634 0.99368 0.99792 0.99026 0.99362 0.98919 0.99835 0.99835 0.99038 0.99104 0.99038 0.99286 0.99296 0.99835 0.9954 0.9914 0.99286 0.99604 0.99604 0.99119 0.99007 0.99507 0.99596 0.99011 0.99184 0.99469 0.99469 0.99406 0.99305 0.99096 0.98956 0.9921 0.99496 0.99406 0.99406 0.9888 0.98942 0.99082 0.98802 17.3 1.4 1.3 1.6 5.25 2.4 14.6 11.8 1.5 1.8 7.7 2 1.8 1.4 16.7 8.1 8 4.7 8.1 2.1 16.7 6.4 1.5 7.6 1.5 12.4 1.3 1.7 8.1 7.1 7.6 2.3 6.5 1.4 12.7 1.6 1.1 1.2 6.5 4.6 0.6 10.6 4.6 4.8 2.7 12.6 0.6 9.2 6.6 7 8.45 11.1 18.15 18.15 4.1 4.1 4.6 18.15 4.9 8.3 1.4 11.5 1.8 1.6 2.4 4.9 1.8 4.3 4.4 1.4 1.6 1.3 5.2 5.6 5.3 4.9 2.4 1.6 2.1 1.4 7.1 1.6 10.7 11.1 10.7 1.6 1.6 1.5 1.5 1.6 1.6 8 7.7 2.7 15.1 15.1 8.9 6 12.3 13.1 6.7 12.3 2.3 11.1 1.5 6.7 6 15.2 10.2 13.1 10.7 17.1 17.1 17.1 1.9 10.7 17.1 1.2 1.2 3.1 1.5 10.7 4.9 12.6 10.7 4.9 12.15 12 1.7 2.6 1.4 1.9 16.9 16.9 2.1 7 7.1 5.9 7.1 8.7 13.2 15.3 15.3 13.2 2.7 10.65 10 6.8 15.6 13.2 5.1 3 15.3 2.1 1.9 8.6 8.75 3.6 4.7 1.3 1.8 9.7 4 2.4 4.7 18.8 1.8 1.8 12.8 12.8 12.8 12.8 12.8 7.8 16.75 12.8 12.8 7.8 5.4 16.75 1.3 10.1 3.8 10.9 6.6 9.8 11.7 1.2 1.4 9.6 12.2 2.6 10.7 4.9 12.2 9.6 1.4 1.1 1 8.2 11.3 7.3 2.3 8.2 2.1 2 10 15.75 3.9 2 1.5 1.6 1.4 1.5 1.4 2 13.8 1.3 3.8 6.9 2.2 1.6 13.8 10.8 12.8 10.8 15.3 12.1 12 11.6 9.2 11.6 9.2 2.8 1.6 6.1 8.5 7.8 14.9 6.2 8.5 8.2 7.8 10.6 11.2 11.6 7.1 14.9 6.2 1.7 7.7 17.3 1.4 7.7 7.7 3.4 1.6 1.4 1.4 10.4 1.4 10.4 4.1 2.8 15.7 10.9 15.7 6.5 10.9 5.9 17.3 1.4 13.5 8.5 6.2 1.4 14.95 7.7 1.3 7.7 1.3 1.3 1.3 15.6 15.6 15.6 15.6 4.9 5 15.6 6.5 1.4 2.7 1.2 6.5 6.4 6.9 7.2 10.6 3.5 6.4 2.3 12.05 7 11.8 1.4 5 2.2 14.6 1.6 1.3 14.6 2.8 1.6 3.3 6.3 8.1 1.6 10.6 11.8 1.7 8.1 1.4 1.3 1.8 7.2 1.1 11.95 1.1 11.95 2.2 12.7 1.4 10.6 1.9 17.8 10.2 4.8 9.8 8.4 7.2 4.8 8.4 4.5 1.4 7.2 11 11.1 2.6 2 10.1 13.3 11.4 1.3 1.4 1.4 7 2 1.2 12.9 5 10.1 3.75 1.7 12.6 1.3 1.6 7.6 8.1 14.9 6 6 7.2 3 1.2 2 4.9 2 8.9 16.45 2 1.9 5.1 4.4 5.8 4.4 12.9 1.3 1.3 1.2 2.7 1.7 8.2 1.5 1.5 12.9 3.9 17.75 4.9 1.6 1.4 2 2 8.2 2.1 1.8 8.5 4.45 5.8 13 2.7 7.3 19.1 8.8 2.7 7.4 2.3 6.85 11.4 0.9 19.35 7.9 11.75 7.7 3 7.7 3 1.5 7.5 1.5 7.5 8.3 7.05 8.4 13.9 17.5 5.6 9.4 4.8 9.4 9.7 6.3 1.6 14.6 2.5 14.6 2.6 2.5 8.2 1.5 2.3 10 10 1.6 1.6 16 10.4 7.4 7.4 10.4 16.05 16.05 2.6 2.5 10.8 1.2 12.1 11.95 1.7 0.8 1.4 1.3 6.3 10.3 15.55 1.5 1.5 1.4 1.5 7.9 13 1 4.85 7.1 7.9 7.5 7.6 10.3 1.7 1.7 19.95 7.7 5.3 19.95 12.7 12.7 1.5 11.3 18.1 18.1 7 18.1 6.4 1.4 1.4 3.1 14.1 7.7 5.2 11.6 10.4 7.5 11.2 0.8 1.4 4.7 3.1 4 11.3 3.1 8.1 14.8 1.4 8.1 3.5 14.8 8.1 1.4 1.5 1.5 12.8 1.6 7.1 7.1 11.2 1.7 6.7 17.3 8.6 8.6 1.5 12.1 6.7 10.7 17.3 1.8 1.4 7.5 4.8 7.1 16.9 4.8 7.1 11.3 1.1 1.2 1.1 12.9 1.2 1.1 1.2 2.3 10 2.3 1.2 1.4 14.9 1.8 1.8 7 8.6 1.8 1.1 1.3 4.9 1.9 10.4 10 8.6 1.7 1.7 18.95 12.8 12.8 12.8 12.8 12.8 12.8 0.7 12.8 1.4 13.3 8.5 1.5 11.7 5 1.2 2.1 1.4 2.1 16 1.1 15.3 1.4 2.8 2.8 0.9 2.5 8.1 8.2 0.9 11.1 7.8 2.8 10.1 3.2 14.2 14.2 14.2 2.9 6 20.4 10.1 2.9 14.2 3.2 0.95 1.7 1.7 9 1.3 1.4 2.4 16 11.4 14.35 2.1 11.4 14.35 1.1 1.1 1.2 15.8 5.2 5.2 9.6 5.2 1.2 0.8 14.45 9.6 6.9 3.4 2.3 11 5.95 5.1 5.4 1.2 12.6 1 6.6 1.5 1 1.1 6.6 8.2 2 1.4 2 7.5 2 2 13.3 2.85 5.6 5.6 1 3.2 1 7.1 2.4 11.2 9.5 1 1.8 2.6 2.4 8 11.2 7.1 3.3 10.3 1.2 1.6 10.3 9.65 16.4 1.5 1.2 3.3 5 16.3 16.3 16.3 6.5 6.4 10.2 16.3 7.4 13.7 13.7 1.3 7.4 7.4 7.45 7.2 13.7 10.4 1.1 6.5 4.6 13.9 5.2 1.7 6.5 16.4 3.6 1.5 12.4 1.7 6.2 6.2 2.6 1.7 9.3 12.4 1.5 9.1 12 4.8 12.3 12 2.7 3.6 3.6 4.3 1.8 11.8 1.8 11.8 1.8 1.4 6.6 1.55 0.7 6.4 11.8 4.3 5.1 5.8 5.9 1.3 1.4 1.2 7.4 10.8 1.8 7.4 1.2 1.4 14.4 1.7 3.6 3.6 10.05 10.05 10.5 1.9 3.6 1.65 1.9 65.8 6.85 7.4 7.4 20.2 11 20.2 6.2 6.2 6.85 8 8.2 2.2 10.1 7.2 2.2 10.1 1.6 1.3 8 8.2 5.3 14 7.2 1.6 11.8 9.6 6.1 2.7 3.6 1.7 1.6 2.7 1 0.9 1.6 1 10.6 2 1.2 6.2 9.2 5 6.3 3.3 8 1.2 1.2 16.2 11.6 7.2 1.1 3.4 1.4 3.3 8 9.3 2.3 0.9 3.5 1.7 1.3 1.3 5.6 7.4 2.3 1 1.5 10 14.9 9.3 1 1 5.9 5 1.25 3.9 5 0.8 1 5.9 1.6 1.3 1 1.1 1.25 1.4 1.2 5 1.4 1.7 1.8 1.6 1.5 1.7 13.9 5.9 2.1 1.1 6.7 2.7 6.7 3.95 7.75 10.6 1.6 2.5 0.7 11.1 5.15 4.7 9.7 1.7 1.4 2 7.5 9.7 0.8 13.1 1.1 2.2 8.9 1.1 0.9 1.7 6.9 1.1 1 1 7.6 8.9 2.2 1.2 1 1 3.1 1.95 2.2 8.75 11.9 2.7 5.45 6.3 14.4 7.8 1.6 9.1 9.1 14.4 1.3 1.6 11.3 6.3 0.7 1.25 0.7 7.8 10.3 10.3 7.8 8.7 8.3 10.3 7.8 1.2 8.3 8.3 6.2 5 1.8 1.6 1.8 1.8 2.9 6 0.9 1.1 1.6 5.45 14.05 8 13.1 4.9 1.3 2.2 14.9 14.9 0.95 1.4 0.95 1.7 5.6 14.9 7.1 1.2 9.6 11.4 11.4 7.9 5 11.1 8 3.8 10.55 10.2 10.2 9.8 6.3 1.1 4.5 6.3 10.9 9.8 9.8 0.8 0.8 1.2 1.3 9.8 10.2 10.9 6.3 6.3 1.2 0.9 1.1 4.5 3.7 18.1 1.35 5.5 3.1 12.85 19.8 8.25 12.85 3.8 6.9 8.25 11.7 4.6 4 19.8 12.85 1.2 8.9 11.7 6.2 14.8 14.8 10.8 1.6 8.3 8.4 2.5 3.5 17.2 2.1 12.2 11.8 16.8 17.2 1.1 14.7 5.5 6.1 1.2 1.3 8.7 1.7 8.7 10.2 4.5 5.9 1.7 1.4 5.4 7.9 1.1 7 7 7.6 7 12.3 15.3 12.3 1.2 2.3 6.1 7.6 10.2 4.1 2.9 8.5 1.5 3.1 7.9 3.5 4.9 1.1 7 1.2 4.5 2.6 9.9 4.5 9.5 1.5 3.2 2.6 11.2 3.2 2.3 4.9 4.9 1.4 1.5 6.7 2.1 4.3 10.9 7 2.3 2.5 2.6 3.2 2.5 14.7 4.5 2.2 1.9 1.6 17.3 4.2 4.2 2.5 1.9 1.4 0.8 8 1.6 1.7 5.5 17.3 8.6 6.9 2.1 2.2 1.5 2.5 17.6 4.2 2.9 4.8 11.9 0.9 1.3 6.4 4.3 11.9 8.1 1.3 0.9 17.2 17.2 17.2 8.7 17.2 8.7 7.5 17.2 4.6 3.7 2.2 7.4 15.1 7.4 4.8 7.9 1 15.1 7.4 4.8 4.6 1.4 6.2 6.1 5.1 6.3 0.9 2.3 6.6 7.5 8.6 11.9 2.3 7.1 4.3 1.1 1 7.9 1 1 1 7.3 1.7 1.3 6.4 1.8 1.5 3.8 7.9 1 1.2 5.3 9.1 6.5 9.1 6.3 5.1 6.5 2.4 9.1 7.5 5 6.75 1.2 1.6 16.05 5 12.4 0.95 4.6 1.7 1 1.3 5 2.5 2.6 2.1 12.75 1.1 12.4 3.7 2.65 2.5 8.2 7.3 1.1 6.6 7 14.5 11.8 3 3.7 6 4.6 2.5 3.3 1 1.1 1.4 3.3 8.55 2.5 6.7 3.8 4.5 4.6 4.2 11.3 5.5 4.2 2.2 14.5 14.5 14.5 14.5 14.5 14.5 1.5 18.75 3.6 1.4 5.1 10.5 2 2.6 9.2 1.8 5.7 2.4 1.9 1.4 0.9 4.6 1.4 9.2 1.4 1.8 2.3 2.3 4.4 6.4 2.9 2.8 2.9 4.4 8.2 1 2.9 7 1.8 1.5 7 8.2 7.6 2.3 8.7 1 2.9 6.7 5 1.9 2 1.9 8.5 12.6 5.2 2.1 1.1 1.3 1.1 9.2 1.2 1.1 8.3 1.8 1.4 15.7 4.35 1.8 1.6 2 5 1.8 1.3 1 1.4 8.1 8.6 3.7 5.7 2.35 13.65 13.65 13.65 15.2 4.6 1.2 4.6 6.65 13.55 13.65 9.8 10.3 6.7 15.2 9.9 7.2 1.1 8.3 11.25 12.8 9.65 12.6 12.2 8.3 11.25 1.3 9.9 7.2 1.1 1.1 4.8 1.1 1.4 1.7 10.6 1.4 1.1 5.55 2.1 1.7 9 1.7 1.8 4.7 11.3 3.6 6.9 3.6 4.9 6.95 1.9 4.7 11.3 1.8 11.3 8.2 8.3 9.55 8.4 7.8 7.8 10.2 5.5 7.8 7.4 3.3 5 3.3 5 1.3 1.2 7.4 7.8 9.9 0.7 4.6 5.6 9.5 14.8 4.6 2.1 11.6 1.2 11.6 2.1 20.15 4.7 4.3 14.5 4.9 14.55 14.55 10.05 4.9 14.5 14.55 15.25 3.15 1.3 5.2 1.1 7.1 8.8 18.5 8.8 1.4 1.2 5 1.6 18.75 6 9.4 9.7 4.75 6 5.35 5.35 6.8 6.9 1.4 0.9 1.2 1.3 2.6 12 9.85 3.85 2 1.6 7.8 1.9 2 10.3 1.1 12 3.85 9.85 2 4 1.1 10.4 6.1 1.8 10.4 4.7 4 1.1 6.4 8.15 6.1 4.8 1.2 1.1 1.4 7.4 1.8 1 15.5 15.5 8.4 2.4 3.95 19.95 2 3 15.5 8.4 14.3 4.2 1.4 3 4.9 2.4 14.3 10.7 11 1.4 1.2 12.9 10.8 1.3 2 1.8 1.2 7.5 9.7 3.8 7.2 9.7 6.3 6.3 0.8 8.6 6.3 3.1 7.2 7.1 6.4 14.7 7.2 7.1 1.9 1.2 4.8 1.2 3.4 4.3 8.5 1.8 1.8 19.5 8.5 19.9 8.3 1.8 1.1 16.65 16.65 16.65 0.9 6.1 10.2 0.9 16.65 3.85 4.4 4.5 3.2 4.5 4.4 9.7 4.2 4.2 1.1 9.7 4.2 5.6 4.2 1.6 1.6 1.1 14.6 2.6 1.2 7.25 6.55 7 1.5 1.4 7.25 1 4.2 17.5 17.5 17.5 1.5 1.3 3.9 4.2 7.6 1 1.1 11.8 1.4 9.7 12.9 1.6 7.2 7.1 1.9 8.8 7.2 1.4 14.3 14.3 8.8 1.4 1.8 14.3 7.2 1.2 11.8 0.9 12.6 26.05 4.7 12.6 1.2 26.05 6.1 11.8 0.9 5.6 5.3 5.7 8 8 17.6 8 8.8 1.5 1.4 4.8 2.4 3.7 4.9 5.7 5.7 4.9 2 5.1 4.5 3.2 6.65 1.6 4 17.75 1.4 17.75 7.2 5.7 8.5 11.4 5.4 2.7 4.3 1.2 1.8 1.3 5.7 2.7 11.7 4.3 11 1.6 11.6 6.2 1.8 1.2 1 2.4 1.2 8.2 18.8 9.6 12.9 9.2 1.2 12.9 8 12.9 1.6 12 2.5 9.2 4.4 8.8 9.6 8 18.8 1.3 1.2 12.9 1.2 1.6 1.5 18.15 13.1 13.1 13.1 13.1 1 1.6 11.8 1.4 1 13.1 10.6 10.4 1.1 7.4 1.2 3.4 18.15 8 2.5 2 2 6.9 1.2 9.4 2.9 6.9 5.4 1.3 20.8 10.3 1.3 1.6 13.1 1.8 8 1.6 1.4 14.7 14.7 14.7 14.7 14.7 14.7 14.7 1.8 10.6 12.5 6.8 14.7 2.9 1.4 1.4 2.1 7.4 2.9 1.4 1.4 7.4 5 2.5 6.1 2.7 2.1 12.9 12.9 12.9 13.7 12.9 2.4 9.8 13.7 1.3 12.1 6.1 7.7 6.1 1.4 7.7 12.1 6.8 9.2 8.3 17.4 2.7 12.8 8.2 8.1 8.2 8.3 8 11.8 12 1.7 17.4 13.9 10.7 2 2.2 1.3 1.1 2 6.4 1.3 1.1 10.7 6.4 6.3 6.4 15.1 2 2 2.2 12.1 8.8 8.8 5.1 6.8 6.8 3.7 12.2 5.7 8.1 2.5 4 6.8 1 5.1 5.8 10.6 3.5 3.5 16.4 4.8 3.3 1.2 1.2 4.8 3.3 2.5 8.7 1.6 4 2.5 16.2 9 16.2 1.4 7 9 3.1 1.5 4.6 4.8 4.6 1.5 2.7 6.3 7.2 7.2 12.4 6.6 6.6 4 4.8 1.3 7.2 11.1 12.4 9.8 6.6 13.3 11.7 8 1.6 16.55 1.5 10.2 6.6 17.8 17.8 1.5 7.4 17.8 2 7.4 2 17.8 12.1 8.2 1.5 8.7 3.5 6.4 2.1 7.7 12.3 1.3 8.7 3.5 1.1 2.8 3.5 1.9 3.8 3.8 2.4 4.8 4.8 6.2 1.3 3.8 1.5 4.8 1.9 6.2 7.9 1.6 1.4 2.6 14.8 2.4 0.9 0.9 1.2 9.9 3.9 15.6 15.6 1.5 1.6 7.8 5.6 1.3 16.7 7.95 6.7 1.1 6.3 8.9 1 1.5 6.6 6.2 6.3 2.1 2.2 5.4 8.9 1 17.9 2.6 1.3 17.9 2.6 2.3 4.3 7.1 7.1 11.9 11.7 5.8 3.8 12.4 6.5 7.1 7.6 7.9 2.8 10.6 2.8 1.5 7.6 7.9 1.7 7.6 7.5 1.7 1.7 12.1 4.5 1.7 8 7.6 8.6 8.6 14.6 1.6 8.6 14.6 1.1 3.7 8.9 8.9 4.7 8.9 3.1 5.8 5.8 5.8 1 15.8 1.5 5.2 1.5 2.5 1 15.8 5.9 3.1 3.1 5.8 11.5 18 4.8 8.5 1.6 18 4.8 5.9 1.1 8.5 13.1 4.1 2.9 13.1 1.1 1.5 7.75 1.15 1 17.8 5.7 17.8 7.4 1.4 1.4 1 4.4 1.6 7.9 15.5 15.5 15.5 15.5 17.55 13.5 13.5 1.3 15.5 11.6 7.9 15.5 17.55 11.6 13.15 1.9 13.5 1.3 6.1 6.1 1.9 1.9 1.6 11.3 8.4 8.3 8.4 12.2 8 1.3 12.7 1.3 10.5 12.5 9.6 1.5 1.5 7.8 10.8 12.5 8.6 1.2 14.5 3.7 1.1 1.1 3.8 4.6 10.2 7.9 2.4 10.7 4.9 10.7 1.1 7.9 5.6 2.4 14.2 9.5 9.5 4.1 4.7 1.4 0.9 20.3 3.5 2.7 1.2 1.2 2 1.1 1.5 1.2 18.1 18.1 3.6 3.5 12.1 17.45 12.1 3 1.6 5.7 5.6 6.8 15.6 6 1.8 8.6 8.6 11.5 7.8 2.4 5 8.6 1.5 5.4 11.9 11.9 9 10 11.9 11.9 15.5 5.4 15 1.4 9.4 3.7 15 1.4 6.5 1.4 6.3 13.7 13.7 13.7 13.7 13.7 13.7 1.5 1.6 1.4 3.5 1 1.4 1.5 13.7 1.6 5.2 1.4 11.9 2.4 3.2 1.7 4.2 15.4 13 5.6 9.7 2.5 4 15.4 1.2 2 1.2 5.1 1.4 1.2 6.5 1.3 6.5 2.7 1.3 7.4 12.9 1.3 1.2 2.6 2.3 1.3 10.5 2.6 14.4 1.2 3.1 1.7 6 11.8 6.2 1.4 12.1 12.1 12.1 3.9 4.6 12.1 1.2 8.1 3.9 1.1 6.5 10.1 10.7 3.2 12.4 5.2 5 2.5 9.2 6.9 2 15 15 1.2 15 1.8 10.8 3.9 4.2 2 13.5 13.3 2.2 1.4 1.6 2.2 14.8 1.8 14.8 1.3 9.9 5.1 5.1 1.5 1.5 11.1 5.25 2.3 7.9 8 1.4 5.25 2.3 2.3 3.5 13.7 9.9 15.4 16 16 16 16 2.4 5.5 2.3 16.8 16 17.8 17.8 6.8 6.8 6.8 6.8 1.6 4.7 11.8 17.8 15.7 5.8 15.7 9 15.7 5.8 8.8 10.2 6.6 6.5 8.9 11.1 4.2 1.6 7.4 11.5 1.6 2 4.8 9.8 1.9 4.2 1.6 7.3 5.4 10.4 1.9 7.3 5.4 7.7 11.5 1.2 2.2 1 8.2 8.3 8.2 9.3 8.1 8.2 8.3 13.9 13.9 13.9 13.9 13.9 13.9 13.9 2 13.9 15.7 1.2 1.5 1.2 3.2 1.2 2.6 13.2 10.4 5.7 2.5 1.6 1.4 7.4 2.5 5.6 3.6 7.5 5.8 1.6 1.5 2.9 11.2 9.65 10.1 3.2 11.2 11.45 9.65 4.5 2.7 3.5 1.7 2.1 4.8 5 2.6 6.6 5 7.3 5 1.7 2.6 8.2 8.2 5 1.2 7.1 9.5 15.8 15.5 15.8 17.05 12.7 12.3 11.8 11.8 11.8 12.3 11.8 13.6 5.2 6.2 7.9 7.9 3.3 2.8 7.9 3.3 6.3 4.9 10.4 4.9 10.4 16 6.3 2.2 17.3 17.3 17.3 17.3 2.2 2.2 17.3 6.6 6.5 12.3 5 2.8 13.6 2.8 5.4 10.9 1.7 9.15 4.5 9.15 1.4 5.9 16.4 1.2 16.4 5.9 7.8 7.8 2.8 2.9 2.5 12.8 12.2 7.7 2.8 2.9 17.3 19.3 19.3 19.3 2.7 6.4 17.3 2.4 2.8 1.7 15.4 15.4 4.1 6.6 1.2 2.1 1 1.1 1.4 1.6 9.8 1.9 1.3 7.9 7.9 4.5 22.6 7.9 3.5 1.2 4.5 2 7.8 0.9 2.9 2.9 3.5 4.2 9.7 10.5 1.1 16.1 1.1 8.1 6.2 7.7 2.4 16.3 2.3 8.4 8.5 6 1.1 1.75 2.6 1.3 2.1 1.1 1.1 2.8 9 2.8 2.2 5.1 3.5 12.7 7.5 2 3.5 14.3 9.8 12.7 12.7 5.1 3.5 12.7 12.9 12.9 1.3 10.5 1.5 12.7 12.9 1.2 6.2 8.8 3.9 1.3 9.1 9.1 3.9 1.8 2.1 1.4 14.7 9.1 1.9 1.8 9.6 3.9 1.3 11.8 1.9 12 7.9 9.3 4.6 2.2 10.2 10.6 1.4 9.1 11.1 9.1 4.4 2.8 1.1 1.3 1.2 3.3 9.7 2.3 1.1 11.4 1.2 14.7 13.8 1.3 6.3 7.9 2 11.8 1.2 10 5.2 1.2 7.2 9.9 5.3 13.55 2.2 9.9 4.3 13 13.55 1 1.1 6.9 13.4 4.6 9.9 3 5.8 12.9 3.2 0.8 2.5 2.4 7.2 7.3 6.3 4.25 1.2 2 4.25 4.7 4.5 1.4 4.1 5.3 4.2 6.65 8.2 2.6 2.6 2 12.2 2.3 8.2 5 10.7 10.8 1.7 1.3 1.7 12.7 1.3 1.2 1.3 5.7 3.4 1.1 1 1 1.65 6.8 6.8 4.9 1.4 2.5 10.8 10.8 10.8 10.8 2.8 1.3 2 1.1 8.2 6 6.1 8.2 8.8 6.1 6 1.2 11.4 1.3 1.3 6.2 3.2 4.5 9.9 6.2 11.4 1.3 1.3 0.9 0.7 1 1 10.4 1.3 12.5 12.5 12.5 12.5 19.25 1.1 12.5 19.25 9 1.2 9 1.3 12.8 12.8 7.6 7.6 1.4 8.3 9 1.85 12.55 1.4 1.8 4 12.55 9 3 1.85 7.9 2.6 1.2 7.1 7.9 1.3 10.7 7.7 8.4 10.7 12.7 1.8 7.7 10.5 1.6 1.85 10.5 10.5 1 1.2 1.7 1.6 9 1.9 1.2 1.5 3.9 3.6 1.2 5 2.9 10.4 11.4 18.35 18.4 1.2 7.1 1.3 1.5 10.2 2.2 3.5 3.5 3.9 7.4 7.4 11 1.5 3.9 5.4 1.5 5 1.2 13 13 13 13 8.6 1.7 1.2 1.2 1.2 2 19.4 0.8 6.3 6.4 12.1 12.1 12.9 2.4 4.3 4.2 12.9 1.7 2.2 12.1 3.4 7.4 7.3 1.1 1.1 1.4 14.5 8 1.1 1.1 2.2 5.8 0.9 6.4 10.9 7.3 8.3 1.3 3.3 1 1.1 1 5.1 3.2 12.6 3.7 1.7 5.1 1 1.3 1.5 4.6 10.3 6.1 6.1 1.2 10.3 9.9 1.6 1.1 1.5 1.2 1.5 1.1 11.5 7.8 7.4 1.45 8.9 1.1 1 2.5 1.1 2.4 2.3 5.1 2.5 8.9 2.5 8.9 1.6 1.4 3.9 13.7 13.7 9.2 7.8 7.6 7.7 3 1.3 4 1.1 2 1.9 1.4 4.5 10.1 6.6 1.9 12.4 1.6 2.5 1.2 2.5 0.8 0.9 8.1 8.1 11.75 1.3 1.9 8.3 8.1 5.7 1.9 1.2 11.75 2.2 0.9 1.3 1.6 8 1.2 1.1 0.8 \ No newline at end of file diff --git a/pandas/tests/reshape/merge/__init__.py b/pandas/tests/reshape/merge/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/tools/data/allow_exact_matches.csv b/pandas/tests/reshape/merge/data/allow_exact_matches.csv similarity index 100% rename from pandas/tests/tools/data/allow_exact_matches.csv rename to pandas/tests/reshape/merge/data/allow_exact_matches.csv diff --git a/pandas/tests/tools/data/allow_exact_matches_and_tolerance.csv b/pandas/tests/reshape/merge/data/allow_exact_matches_and_tolerance.csv similarity index 100% rename from pandas/tests/tools/data/allow_exact_matches_and_tolerance.csv rename to pandas/tests/reshape/merge/data/allow_exact_matches_and_tolerance.csv diff --git a/pandas/tests/tools/data/asof.csv b/pandas/tests/reshape/merge/data/asof.csv similarity index 100% rename from pandas/tests/tools/data/asof.csv rename to pandas/tests/reshape/merge/data/asof.csv diff --git a/pandas/tests/tools/data/asof2.csv b/pandas/tests/reshape/merge/data/asof2.csv similarity index 100% rename from pandas/tests/tools/data/asof2.csv rename to pandas/tests/reshape/merge/data/asof2.csv diff --git a/pandas/tests/tools/data/quotes.csv b/pandas/tests/reshape/merge/data/quotes.csv similarity index 100% rename from pandas/tests/tools/data/quotes.csv rename to pandas/tests/reshape/merge/data/quotes.csv diff --git a/pandas/tests/tools/data/quotes2.csv b/pandas/tests/reshape/merge/data/quotes2.csv similarity index 100% rename from pandas/tests/tools/data/quotes2.csv rename to pandas/tests/reshape/merge/data/quotes2.csv diff --git a/pandas/tests/tools/data/tolerance.csv b/pandas/tests/reshape/merge/data/tolerance.csv similarity index 100% rename from pandas/tests/tools/data/tolerance.csv rename to pandas/tests/reshape/merge/data/tolerance.csv diff --git a/pandas/tests/tools/data/trades.csv b/pandas/tests/reshape/merge/data/trades.csv similarity index 100% rename from pandas/tests/tools/data/trades.csv rename to pandas/tests/reshape/merge/data/trades.csv diff --git a/pandas/tests/tools/data/trades2.csv b/pandas/tests/reshape/merge/data/trades2.csv similarity index 100% rename from pandas/tests/tools/data/trades2.csv rename to pandas/tests/reshape/merge/data/trades2.csv diff --git a/pandas/tests/tools/test_join.py b/pandas/tests/reshape/merge/test_join.py similarity index 74% rename from pandas/tests/tools/test_join.py rename to pandas/tests/reshape/merge/test_join.py index b65f800802bca..62c9047b17f3d 100644 --- a/pandas/tests/tools/test_join.py +++ b/pandas/tests/reshape/merge/test_join.py @@ -1,25 +1,26 @@ # pylint: disable=E1103 -from numpy.random import randn import numpy as np +from numpy.random import randn +import pytest -import pandas as pd -from pandas.compat import lrange +from pandas._libs import join as libjoin import pandas.compat as compat -from pandas.util.testing import assert_frame_equal -from pandas import DataFrame, MultiIndex, Series, Index, merge, concat +from pandas.compat import lrange -from pandas._libs import join as libjoin +import pandas as pd +from pandas import DataFrame, Index, MultiIndex, Series, concat, merge +from pandas.tests.reshape.merge.test_merge import NGROUPS, N, get_test_data import pandas.util.testing as tm -from pandas.tests.tools.test_merge import get_test_data, N, NGROUPS - +from pandas.util.testing import assert_frame_equal a_ = np.array -class TestJoin(tm.TestCase): +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") +class TestJoin(object): - def setUp(self): + def setup_method(self, method): # aggregate multiple columns self.df = DataFrame({'key1': get_test_data(), 'key2': get_test_data(), @@ -62,8 +63,8 @@ def test_cython_left_outer_join(self): exp_rs = exp_rs.take(exp_ri) exp_rs[exp_ri == -1] = -1 - self.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) - self.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) + tm.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) + tm.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) def test_cython_right_outer_join(self): left = a_([0, 1, 2, 1, 2, 0, 0, 1, 2, 3, 3], dtype=np.int64) @@ -88,8 +89,8 @@ def test_cython_right_outer_join(self): exp_rs = exp_rs.take(exp_ri) exp_rs[exp_ri == -1] = -1 - self.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) - self.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) + tm.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) + tm.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) def test_cython_inner_join(self): left = a_([0, 1, 2, 1, 2, 0, 0, 1, 2, 3, 3], dtype=np.int64) @@ -112,8 +113,8 @@ def test_cython_inner_join(self): exp_rs = exp_rs.take(exp_ri) exp_rs[exp_ri == -1] = -1 - self.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) - self.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) + tm.assert_numpy_array_equal(ls, exp_ls, check_dtype=False) + tm.assert_numpy_array_equal(rs, exp_rs, check_dtype=False) def test_left_outer_join(self): joined_key2 = merge(self.df, self.df2, on='key2') @@ -151,25 +152,25 @@ def test_handle_overlap(self): joined = merge(self.df, self.df2, on='key2', suffixes=['.foo', '.bar']) - self.assertIn('key1.foo', joined) - self.assertIn('key1.bar', joined) + assert 'key1.foo' in joined + assert 'key1.bar' in joined def test_handle_overlap_arbitrary_key(self): joined = merge(self.df, self.df2, left_on='key2', right_on='key1', suffixes=['.foo', '.bar']) - self.assertIn('key1.foo', joined) - self.assertIn('key2.bar', joined) + assert 'key1.foo' in joined + assert 'key2.bar' in joined def test_join_on(self): target = self.target source = self.source merged = target.join(source, on='C') - self.assert_series_equal(merged['MergedA'], target['A'], - check_names=False) - self.assert_series_equal(merged['MergedD'], target['D'], - check_names=False) + tm.assert_series_equal(merged['MergedA'], target['A'], + check_names=False) + tm.assert_series_equal(merged['MergedD'], target['D'], + check_names=False) # join with duplicates (fix regression from DataFrame/Matrix merge) df = DataFrame({'key': ['a', 'a', 'b', 'b', 'c']}) @@ -188,54 +189,67 @@ def test_join_on(self): columns=['three']) joined = df_a.join(df_b, on='one') joined = joined.join(df_c, on='one') - self.assertTrue(np.isnan(joined['two']['c'])) - self.assertTrue(np.isnan(joined['three']['c'])) + assert np.isnan(joined['two']['c']) + assert np.isnan(joined['three']['c']) # merge column not p resent - self.assertRaises(KeyError, target.join, source, on='E') + with pytest.raises(KeyError, match="^'E'$"): + target.join(source, on='E') # overlap source_copy = source.copy() source_copy['A'] = 0 - self.assertRaises(ValueError, target.join, source_copy, on='A') + msg = ("You are trying to merge on float64 and object columns. If" + " you wish to proceed you should use pd.concat") + with pytest.raises(ValueError, match=msg): + target.join(source_copy, on='A') def test_join_on_fails_with_different_right_index(self): - with tm.assertRaises(ValueError): - df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), - 'b': np.random.randn(3)}) - df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), - 'b': np.random.randn(10)}, - index=tm.makeCustomIndex(10, 2)) + df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), + 'b': np.random.randn(3)}) + df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), + 'b': np.random.randn(10)}, + index=tm.makeCustomIndex(10, 2)) + msg = (r'len\(left_on\) must equal the number of levels in the index' + ' of "right"') + with pytest.raises(ValueError, match=msg): merge(df, df2, left_on='a', right_index=True) def test_join_on_fails_with_different_left_index(self): - with tm.assertRaises(ValueError): - df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), - 'b': np.random.randn(3)}, - index=tm.makeCustomIndex(10, 2)) - df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), - 'b': np.random.randn(10)}) + df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), + 'b': np.random.randn(3)}, + index=tm.makeCustomIndex(3, 2)) + df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), + 'b': np.random.randn(10)}) + msg = (r'len\(right_on\) must equal the number of levels in the index' + ' of "left"') + with pytest.raises(ValueError, match=msg): merge(df, df2, right_on='b', left_index=True) def test_join_on_fails_with_different_column_counts(self): - with tm.assertRaises(ValueError): - df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), - 'b': np.random.randn(3)}) - df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), - 'b': np.random.randn(10)}, - index=tm.makeCustomIndex(10, 2)) + df = DataFrame({'a': np.random.choice(['m', 'f'], size=3), + 'b': np.random.randn(3)}) + df2 = DataFrame({'a': np.random.choice(['m', 'f'], size=10), + 'b': np.random.randn(10)}, + index=tm.makeCustomIndex(10, 2)) + msg = r"len\(right_on\) must equal len\(left_on\)" + with pytest.raises(ValueError, match=msg): merge(df, df2, right_on='a', left_on=['a', 'b']) - def test_join_on_fails_with_wrong_object_type(self): - # GH12081 - wrongly_typed = [Series([0, 1]), 2, 'str', None, np.array([0, 1])] - df = DataFrame({'a': [1, 1]}) + @pytest.mark.parametrize("wrong_type", [2, 'str', None, np.array([0, 1])]) + def test_join_on_fails_with_wrong_object_type(self, wrong_type): + # GH12081 - original issue + + # GH21220 - merging of Series and DataFrame is now allowed + # Edited test to remove the Series object from test parameters - for obj in wrongly_typed: - with tm.assertRaisesRegexp(ValueError, str(type(obj))): - merge(obj, df, left_on='a', right_on='a') - with tm.assertRaisesRegexp(ValueError, str(type(obj))): - merge(df, obj, left_on='a', right_on='a') + df = DataFrame({'a': [1, 1]}) + msg = ("Can only merge Series or DataFrame objects, a {} was passed" + .format(str(type(wrong_type)))) + with pytest.raises(TypeError, match=msg): + merge(wrong_type, df, left_on='a', right_on='a') + with pytest.raises(TypeError, match=msg): + merge(df, wrong_type, left_on='a', right_on='a') def test_join_on_pass_vector(self): expected = self.target.join(self.source, on='C') @@ -249,13 +263,13 @@ def test_join_with_len0(self): # nothing to merge merged = self.target.join(self.source.reindex([]), on='C') for col in self.source: - self.assertIn(col, merged) - self.assertTrue(merged[col].isnull().all()) + assert col in merged + assert merged[col].isna().all() merged2 = self.target.join(self.source.reindex([]), on='C', how='inner') - self.assert_index_equal(merged2.columns, merged.columns) - self.assertEqual(len(merged2), 0) + tm.assert_index_equal(merged2.columns, merged.columns) + assert len(merged2) == 0 def test_join_on_inner(self): df = DataFrame({'key': ['a', 'a', 'd', 'b', 'b', 'c']}) @@ -264,12 +278,12 @@ def test_join_on_inner(self): joined = df.join(df2, on='key', how='inner') expected = df.join(df2, on='key') - expected = expected[expected['value'].notnull()] - self.assert_series_equal(joined['key'], expected['key'], - check_dtype=False) - self.assert_series_equal(joined['value'], expected['value'], - check_dtype=False) - self.assert_index_equal(joined.index, expected.index) + expected = expected[expected['value'].notna()] + tm.assert_series_equal(joined['key'], expected['key'], + check_dtype=False) + tm.assert_series_equal(joined['value'], expected['value'], + check_dtype=False) + tm.assert_index_equal(joined.index, expected.index) def test_join_on_singlekey_list(self): df = DataFrame({'key': ['a', 'a', 'b', 'b', 'c']}) @@ -295,12 +309,30 @@ def test_join_on_series_buglet(self): 'b': [2, 2]}, index=df.index) tm.assert_frame_equal(result, expected) - def test_join_index_mixed(self): + def test_join_index_mixed(self, join_type): + # no overlapping blocks + df1 = DataFrame(index=np.arange(10)) + df1['bool'] = True + df1['string'] = 'foo' + + df2 = DataFrame(index=np.arange(5, 15)) + df2['int'] = 1 + df2['float'] = 1. + + joined = df1.join(df2, how=join_type) + expected = _join_by_hand(df1, df2, how=join_type) + assert_frame_equal(joined, expected) + + joined = df2.join(df1, how=join_type) + expected = _join_by_hand(df2, df1, how=join_type) + assert_frame_equal(joined, expected) + + def test_join_index_mixed_overlap(self): df1 = DataFrame({'A': 1., 'B': 2, 'C': 'foo', 'D': True}, index=np.arange(10), columns=['A', 'B', 'C', 'D']) - self.assertEqual(df1['B'].dtype, np.int64) - self.assertEqual(df1['D'].dtype, np.bool_) + assert df1['B'].dtype == np.int64 + assert df1['D'].dtype == np.bool_ df2 = DataFrame({'A': 1., 'B': 2, 'C': 'foo', 'D': True}, index=np.arange(0, 10, 2), @@ -315,25 +347,6 @@ def test_join_index_mixed(self): expected = _join_by_hand(df1, df2) assert_frame_equal(joined, expected) - # no overlapping blocks - df1 = DataFrame(index=np.arange(10)) - df1['bool'] = True - df1['string'] = 'foo' - - df2 = DataFrame(index=np.arange(5, 15)) - df2['int'] = 1 - df2['float'] = 1. - - for kind in ['inner', 'outer', 'left', 'right']: - - joined = df1.join(df2, how=kind) - expected = _join_by_hand(df1, df2, how=kind) - assert_frame_equal(joined, expected) - - joined = df2.join(df1, how=kind) - expected = _join_by_hand(df2, df1, how=kind) - assert_frame_equal(joined, expected) - def test_join_empty_bug(self): # generated an exception in 0.4.3 x = DataFrame() @@ -372,7 +385,7 @@ def test_join_multiindex(self): expected = df1.reindex(ex_index).join(df2.reindex(ex_index)) expected.index.names = index1.names assert_frame_equal(joined, expected) - self.assertEqual(joined.index.names, index1.names) + assert joined.index.names == index1.names df1 = df1.sort_index(level=1) df2 = df2.sort_index(level=1) @@ -383,7 +396,7 @@ def test_join_multiindex(self): expected.index.names = index1.names assert_frame_equal(joined, expected) - self.assertEqual(joined.index.names, index1.names) + assert joined.index.names == index1.names def test_join_inner_multiindex(self): key1 = ['bar', 'bar', 'bar', 'foo', 'foo', 'baz', 'baz', 'qux', @@ -397,8 +410,8 @@ def test_join_inner_multiindex(self): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['first', 'second']) to_join = DataFrame(np.random.randn(10, 3), index=index, columns=['j_one', 'j_two', 'j_three']) @@ -420,7 +433,7 @@ def test_join_inner_multiindex(self): expected = expected.drop(['first', 'second'], axis=1) expected.index = joined.index - self.assertTrue(joined.index.is_monotonic) + assert joined.index.is_monotonic assert_frame_equal(joined, expected) # _assert_same_contents(expected, expected2.loc[:, expected.columns]) @@ -435,17 +448,17 @@ def test_join_hierarchical_mixed(self): # GH 9455, 12219 with tm.assert_produces_warning(UserWarning): result = merge(new_df, other_df, left_index=True, right_index=True) - self.assertTrue(('b', 'mean') in result) - self.assertTrue('b' in result) + assert ('b', 'mean') in result + assert 'b' in result def test_join_float64_float32(self): a = DataFrame(randn(10, 2), columns=['a', 'b'], dtype=np.float64) b = DataFrame(randn(10, 1), columns=['c'], dtype=np.float32) joined = a.join(b) - self.assertEqual(joined.dtypes['a'], 'float64') - self.assertEqual(joined.dtypes['b'], 'float64') - self.assertEqual(joined.dtypes['c'], 'float32') + assert joined.dtypes['a'] == 'float64' + assert joined.dtypes['b'] == 'float64' + assert joined.dtypes['c'] == 'float32' a = np.random.randint(0, 5, 100).astype('int64') b = np.random.random(100).astype('float64') @@ -454,10 +467,10 @@ def test_join_float64_float32(self): xpdf = DataFrame({'a': a, 'b': b, 'c': c}) s = DataFrame(np.random.random(5).astype('float32'), columns=['md']) rs = df.merge(s, left_on='a', right_index=True) - self.assertEqual(rs.dtypes['a'], 'int64') - self.assertEqual(rs.dtypes['b'], 'float64') - self.assertEqual(rs.dtypes['c'], 'float32') - self.assertEqual(rs.dtypes['md'], 'float32') + assert rs.dtypes['a'] == 'int64' + assert rs.dtypes['b'] == 'float64' + assert rs.dtypes['c'] == 'float32' + assert rs.dtypes['md'] == 'float32' xp = xpdf.merge(s, left_on='a', right_index=True) assert_frame_equal(rs, xp) @@ -529,7 +542,7 @@ def test_join_sort(self): # smoke test joined = left.join(right, on='key', sort=False) - self.assert_index_equal(joined.index, pd.Index(lrange(4))) + tm.assert_index_equal(joined.index, pd.Index(lrange(4))) def test_join_mixed_non_unique_index(self): # GH 12814, unorderable types in py3 with a non-unique index @@ -548,6 +561,18 @@ def test_join_mixed_non_unique_index(self): index=[1, 2, 2, 'a']) tm.assert_frame_equal(result, expected) + def test_join_non_unique_period_index(self): + # GH #16871 + index = pd.period_range('2016-01-01', periods=16, freq='M') + df = DataFrame([i for i in range(len(index))], + index=index, columns=['pnum']) + df2 = concat([df, df]) + result = df.join(df2, how='inner', rsuffix='_df2') + expected = DataFrame( + np.tile(np.arange(16, dtype=np.int64).repeat(2).reshape(-1, 1), 2), + columns=['pnum', 'pnum_df2'], index=df2.sort_index().index) + tm.assert_frame_equal(result, expected) + def test_mixed_type_join_with_suffix(self): # GH #916 df = DataFrame(np.random.randn(20, 6), @@ -587,7 +612,9 @@ def _check_diff_index(df_list, result, exp_index): joined = df_list[0].join(df_list[1:], how='inner') _check_diff_index(df_list, joined, df.index[2:8]) - self.assertRaises(ValueError, df_list[0].join, df_list[1:], on='a') + msg = "Joining multiple DataFrames only supported for joining on index" + with pytest.raises(ValueError, match=msg): + df_list[0].join(df_list[1:], on='a') def test_join_many_mixed(self): df = DataFrame(np.random.randn(8, 4), columns=['A', 'B', 'C', 'D']) @@ -628,88 +655,54 @@ def test_join_dups(self): 'y_y', 'x_x', 'y_x', 'x_y', 'y_y'] assert_frame_equal(dta, expected) - def test_panel_join(self): - panel = tm.makePanel() - tm.add_nans(panel) - - p1 = panel.iloc[:2, :10, :3] - p2 = panel.iloc[2:, 5:, 2:] - - # left join - result = p1.join(p2) - expected = p1.copy() - expected['ItemC'] = p2['ItemC'] - tm.assert_panel_equal(result, expected) - - # right join - result = p1.join(p2, how='right') - expected = p2.copy() - expected['ItemA'] = p1['ItemA'] - expected['ItemB'] = p1['ItemB'] - expected = expected.reindex(items=['ItemA', 'ItemB', 'ItemC']) - tm.assert_panel_equal(result, expected) - - # inner join - result = p1.join(p2, how='inner') - expected = panel.iloc[:, 5:10, 2:3] - tm.assert_panel_equal(result, expected) - - # outer join - result = p1.join(p2, how='outer') - expected = p1.reindex(major=panel.major_axis, - minor=panel.minor_axis) - expected = expected.join(p2.reindex(major=panel.major_axis, - minor=panel.minor_axis)) - tm.assert_panel_equal(result, expected) - - def test_panel_join_overlap(self): - panel = tm.makePanel() - tm.add_nans(panel) - - p1 = panel.loc[['ItemA', 'ItemB', 'ItemC']] - p2 = panel.loc[['ItemB', 'ItemC']] - - # Expected index is - # - # ItemA, ItemB_p1, ItemC_p1, ItemB_p2, ItemC_p2 - joined = p1.join(p2, lsuffix='_p1', rsuffix='_p2') - p1_suf = p1.loc[['ItemB', 'ItemC']].add_suffix('_p1') - p2_suf = p2.loc[['ItemB', 'ItemC']].add_suffix('_p2') - no_overlap = panel.loc[['ItemA']] - expected = no_overlap.join(p1_suf.join(p2_suf)) - tm.assert_panel_equal(joined, expected) - - def test_panel_join_many(self): - tm.K = 10 - panel = tm.makePanel() - tm.K = 4 - - panels = [panel.iloc[:2], panel.iloc[2:6], panel.iloc[6:]] - - joined = panels[0].join(panels[1:]) - tm.assert_panel_equal(joined, panel) - - panels = [panel.iloc[:2, :-5], - panel.iloc[2:6, 2:], - panel.iloc[6:, 5:-7]] - - data_dict = {} - for p in panels: - data_dict.update(p.iteritems()) - - joined = panels[0].join(panels[1:], how='inner') - expected = pd.Panel.from_dict(data_dict, intersect=True) - tm.assert_panel_equal(joined, expected) - - joined = panels[0].join(panels[1:], how='outer') - expected = pd.Panel.from_dict(data_dict, intersect=False) - tm.assert_panel_equal(joined, expected) - - # edge cases - self.assertRaises(ValueError, panels[0].join, panels[1:], - how='outer', lsuffix='foo', rsuffix='bar') - self.assertRaises(ValueError, panels[0].join, panels[1:], - how='right') + def test_join_multi_to_multi(self, join_type): + # GH 20475 + leftindex = MultiIndex.from_product([list('abc'), list('xy'), [1, 2]], + names=['abc', 'xy', 'num']) + left = DataFrame({'v1': range(12)}, index=leftindex) + + rightindex = MultiIndex.from_product([list('abc'), list('xy')], + names=['abc', 'xy']) + right = DataFrame({'v2': [100 * i for i in range(1, 7)]}, + index=rightindex) + + result = left.join(right, on=['abc', 'xy'], how=join_type) + expected = (left.reset_index() + .merge(right.reset_index(), + on=['abc', 'xy'], how=join_type) + .set_index(['abc', 'xy', 'num']) + ) + assert_frame_equal(expected, result) + + msg = (r'len\(left_on\) must equal the number of levels in the index' + ' of "right"') + with pytest.raises(ValueError, match=msg): + left.join(right, on='xy', how=join_type) + + with pytest.raises(ValueError, match=msg): + right.join(left, on=['abc', 'xy'], how=join_type) + + def test_join_on_tz_aware_datetimeindex(self): + # GH 23931 + df1 = pd.DataFrame( + { + 'date': pd.date_range(start='2018-01-01', periods=5, + tz='America/Chicago'), + 'vals': list('abcde') + } + ) + + df2 = pd.DataFrame( + { + 'date': pd.date_range(start='2018-01-03', periods=5, + tz='America/Chicago'), + 'vals_2': list('tuvwx') + } + ) + result = df1.join(df2.set_index('date'), on='date') + expected = df1.copy() + expected['vals_2'] = pd.Series([np.nan] * len(expected), dtype=object) + assert_frame_equal(result, expected) def _check_join(left, right, result, join_col, how='left', @@ -717,7 +710,7 @@ def _check_join(left, right, result, join_col, how='left', # some smoke tests for c in join_col: - assert(result[c].notnull().all()) + assert(result[c].notna().all()) left_grouped = left.groupby(join_col) right_grouped = right.groupby(join_col) @@ -771,7 +764,7 @@ def _assert_same_contents(join_chunk, source): jvalues = join_chunk.fillna(NA_SENTINEL).drop_duplicates().values svalues = source.fillna(NA_SENTINEL).drop_duplicates().values - rows = set(tuple(row) for row in jvalues) + rows = {tuple(row) for row in jvalues} assert(len(rows) == len(source)) assert(all(tuple(row) in rows for row in svalues)) @@ -780,7 +773,7 @@ def _assert_all_na(join_chunk, source_columns, join_col): for c in source_columns: if c in join_col: continue - assert(join_chunk[c].isnull().all()) + assert(join_chunk[c].isna().all()) def _join_by_hand(a, b, how='left'): diff --git a/pandas/tests/reshape/merge/test_merge.py b/pandas/tests/reshape/merge/test_merge.py new file mode 100644 index 0000000000000..7a97368504fd6 --- /dev/null +++ b/pandas/tests/reshape/merge/test_merge.py @@ -0,0 +1,1668 @@ +# pylint: disable=E1103 + +from collections import OrderedDict +from datetime import date, datetime +import random +import re + +import numpy as np +from numpy import nan +import pytest + +from pandas.compat import lrange + +from pandas.core.dtypes.common import is_categorical_dtype, is_object_dtype +from pandas.core.dtypes.dtypes import CategoricalDtype + +import pandas as pd +from pandas import ( + Categorical, CategoricalIndex, DataFrame, DatetimeIndex, Float64Index, + Int64Index, MultiIndex, RangeIndex, Series, UInt64Index) +from pandas.api.types import CategoricalDtype as CDT +from pandas.core.reshape.concat import concat +from pandas.core.reshape.merge import MergeError, merge +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal + +N = 50 +NGROUPS = 8 + + +def get_test_data(ngroups=NGROUPS, n=N): + unique_groups = lrange(ngroups) + arr = np.asarray(np.tile(unique_groups, n // ngroups)) + + if len(arr) < n: + arr = np.asarray(list(arr) + unique_groups[:n - len(arr)]) + + random.shuffle(arr) + return arr + + +def get_series(): + return [ + pd.Series([1], dtype='int64'), + pd.Series([1], dtype='Int64'), + pd.Series([1.23]), + pd.Series(['foo']), + pd.Series([True]), + pd.Series([pd.Timestamp('2018-01-01')]), + pd.Series([pd.Timestamp('2018-01-01', tz='US/Eastern')]), + ] + + +def get_series_na(): + return [ + pd.Series([np.nan], dtype='Int64'), + pd.Series([np.nan], dtype='float'), + pd.Series([np.nan], dtype='object'), + pd.Series([pd.NaT]), + ] + + +@pytest.fixture(params=get_series(), ids=lambda x: x.dtype.name) +def series_of_dtype(request): + """ + A parametrized fixture returning a variety of Series of different + dtypes + """ + return request.param + + +@pytest.fixture(params=get_series(), ids=lambda x: x.dtype.name) +def series_of_dtype2(request): + """ + A duplicate of the series_of_dtype fixture, so that it can be used + twice by a single function + """ + return request.param + + +@pytest.fixture(params=get_series_na(), ids=lambda x: x.dtype.name) +def series_of_dtype_all_na(request): + """ + A parametrized fixture returning a variety of Series with all NA + values + """ + return request.param + + +class TestMerge(object): + + def setup_method(self, method): + # aggregate multiple columns + self.df = DataFrame({'key1': get_test_data(), + 'key2': get_test_data(), + 'data1': np.random.randn(N), + 'data2': np.random.randn(N)}) + + # exclude a couple keys for fun + self.df = self.df[self.df['key2'] > 1] + + self.df2 = DataFrame({'key1': get_test_data(n=N // 5), + 'key2': get_test_data(ngroups=NGROUPS // 2, + n=N // 5), + 'value': np.random.randn(N // 5)}) + + self.left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], + 'v1': np.random.randn(7)}) + self.right = DataFrame({'v2': np.random.randn(4)}, + index=['d', 'b', 'c', 'a']) + + def test_merge_inner_join_empty(self): + # GH 15328 + df_empty = pd.DataFrame() + df_a = pd.DataFrame({'a': [1, 2]}, index=[0, 1], dtype='int64') + result = pd.merge(df_empty, df_a, left_index=True, right_index=True) + expected = pd.DataFrame({'a': []}, index=[], dtype='int64') + assert_frame_equal(result, expected) + + def test_merge_common(self): + joined = merge(self.df, self.df2) + exp = merge(self.df, self.df2, on=['key1', 'key2']) + tm.assert_frame_equal(joined, exp) + + def test_merge_index_as_on_arg(self): + # GH14355 + + left = self.df.set_index('key1') + right = self.df2.set_index('key1') + result = merge(left, right, on='key1') + expected = merge(self.df, self.df2, on='key1').set_index('key1') + assert_frame_equal(result, expected) + + def test_merge_index_singlekey_right_vs_left(self): + left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], + 'v1': np.random.randn(7)}) + right = DataFrame({'v2': np.random.randn(4)}, + index=['d', 'b', 'c', 'a']) + + merged1 = merge(left, right, left_on='key', + right_index=True, how='left', sort=False) + merged2 = merge(right, left, right_on='key', + left_index=True, how='right', sort=False) + assert_frame_equal(merged1, merged2.loc[:, merged1.columns]) + + merged1 = merge(left, right, left_on='key', + right_index=True, how='left', sort=True) + merged2 = merge(right, left, right_on='key', + left_index=True, how='right', sort=True) + assert_frame_equal(merged1, merged2.loc[:, merged1.columns]) + + def test_merge_index_singlekey_inner(self): + left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], + 'v1': np.random.randn(7)}) + right = DataFrame({'v2': np.random.randn(4)}, + index=['d', 'b', 'c', 'a']) + + # inner join + result = merge(left, right, left_on='key', right_index=True, + how='inner') + expected = left.join(right, on='key').loc[result.index] + assert_frame_equal(result, expected) + + result = merge(right, left, right_on='key', left_index=True, + how='inner') + expected = left.join(right, on='key').loc[result.index] + assert_frame_equal(result, expected.loc[:, result.columns]) + + def test_merge_misspecified(self): + msg = "Must pass right_on or right_index=True" + with pytest.raises(pd.errors.MergeError, match=msg): + merge(self.left, self.right, left_index=True) + msg = "Must pass left_on or left_index=True" + with pytest.raises(pd.errors.MergeError, match=msg): + merge(self.left, self.right, right_index=True) + + msg = ('Can only pass argument "on" OR "left_on" and "right_on", not' + ' a combination of both') + with pytest.raises(pd.errors.MergeError, match=msg): + merge(self.left, self.left, left_on='key', on='key') + + msg = r"len\(right_on\) must equal len\(left_on\)" + with pytest.raises(ValueError, match=msg): + merge(self.df, self.df2, left_on=['key1'], + right_on=['key1', 'key2']) + + def test_index_and_on_parameters_confusion(self): + msg = ("right_index parameter must be of type bool, not" + r" <(class|type) 'list'>") + with pytest.raises(ValueError, match=msg): + merge(self.df, self.df2, how='left', + left_index=False, right_index=['key1', 'key2']) + msg = ("left_index parameter must be of type bool, not " + r"<(class|type) 'list'>") + with pytest.raises(ValueError, match=msg): + merge(self.df, self.df2, how='left', + left_index=['key1', 'key2'], right_index=False) + with pytest.raises(ValueError, match=msg): + merge(self.df, self.df2, how='left', + left_index=['key1', 'key2'], right_index=['key1', 'key2']) + + def test_merge_overlap(self): + merged = merge(self.left, self.left, on='key') + exp_len = (self.left['key'].value_counts() ** 2).sum() + assert len(merged) == exp_len + assert 'v1_x' in merged + assert 'v1_y' in merged + + def test_merge_different_column_key_names(self): + left = DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'], + 'value': [1, 2, 3, 4]}) + right = DataFrame({'rkey': ['foo', 'bar', 'qux', 'foo'], + 'value': [5, 6, 7, 8]}) + + merged = left.merge(right, left_on='lkey', right_on='rkey', + how='outer', sort=True) + + exp = pd.Series(['bar', 'baz', 'foo', 'foo', 'foo', 'foo', np.nan], + name='lkey') + tm.assert_series_equal(merged['lkey'], exp) + + exp = pd.Series(['bar', np.nan, 'foo', 'foo', 'foo', 'foo', 'qux'], + name='rkey') + tm.assert_series_equal(merged['rkey'], exp) + + exp = pd.Series([2, 3, 1, 1, 4, 4, np.nan], name='value_x') + tm.assert_series_equal(merged['value_x'], exp) + + exp = pd.Series([6, np.nan, 5, 8, 5, 8, 7], name='value_y') + tm.assert_series_equal(merged['value_y'], exp) + + def test_merge_copy(self): + left = DataFrame({'a': 0, 'b': 1}, index=lrange(10)) + right = DataFrame({'c': 'foo', 'd': 'bar'}, index=lrange(10)) + + merged = merge(left, right, left_index=True, + right_index=True, copy=True) + + merged['a'] = 6 + assert (left['a'] == 0).all() + + merged['d'] = 'peekaboo' + assert (right['d'] == 'bar').all() + + def test_merge_nocopy(self): + left = DataFrame({'a': 0, 'b': 1}, index=lrange(10)) + right = DataFrame({'c': 'foo', 'd': 'bar'}, index=lrange(10)) + + merged = merge(left, right, left_index=True, + right_index=True, copy=False) + + merged['a'] = 6 + assert (left['a'] == 6).all() + + merged['d'] = 'peekaboo' + assert (right['d'] == 'peekaboo').all() + + def test_intelligently_handle_join_key(self): + # #733, be a bit more 1337 about not returning unconsolidated DataFrame + + left = DataFrame({'key': [1, 1, 2, 2, 3], + 'value': lrange(5)}, columns=['value', 'key']) + right = DataFrame({'key': [1, 1, 2, 3, 4, 5], + 'rvalue': lrange(6)}) + + joined = merge(left, right, on='key', how='outer') + expected = DataFrame({'key': [1, 1, 1, 1, 2, 2, 3, 4, 5], + 'value': np.array([0, 0, 1, 1, 2, 3, 4, + np.nan, np.nan]), + 'rvalue': [0, 1, 0, 1, 2, 2, 3, 4, 5]}, + columns=['value', 'key', 'rvalue']) + assert_frame_equal(joined, expected) + + def test_merge_join_key_dtype_cast(self): + # #8596 + + df1 = DataFrame({'key': [1], 'v1': [10]}) + df2 = DataFrame({'key': [2], 'v1': [20]}) + df = merge(df1, df2, how='outer') + assert df['key'].dtype == 'int64' + + df1 = DataFrame({'key': [True], 'v1': [1]}) + df2 = DataFrame({'key': [False], 'v1': [0]}) + df = merge(df1, df2, how='outer') + + # GH13169 + # this really should be bool + assert df['key'].dtype == 'object' + + df1 = DataFrame({'val': [1]}) + df2 = DataFrame({'val': [2]}) + lkey = np.array([1]) + rkey = np.array([2]) + df = merge(df1, df2, left_on=lkey, right_on=rkey, how='outer') + assert df['key_0'].dtype == 'int64' + + def test_handle_join_key_pass_array(self): + left = DataFrame({'key': [1, 1, 2, 2, 3], + 'value': lrange(5)}, columns=['value', 'key']) + right = DataFrame({'rvalue': lrange(6)}) + key = np.array([1, 1, 2, 3, 4, 5]) + + merged = merge(left, right, left_on='key', right_on=key, how='outer') + merged2 = merge(right, left, left_on=key, right_on='key', how='outer') + + assert_series_equal(merged['key'], merged2['key']) + assert merged['key'].notna().all() + assert merged2['key'].notna().all() + + left = DataFrame({'value': lrange(5)}, columns=['value']) + right = DataFrame({'rvalue': lrange(6)}) + lkey = np.array([1, 1, 2, 2, 3]) + rkey = np.array([1, 1, 2, 3, 4, 5]) + + merged = merge(left, right, left_on=lkey, right_on=rkey, how='outer') + tm.assert_series_equal(merged['key_0'], Series([1, 1, 1, 1, 2, + 2, 3, 4, 5], + name='key_0')) + + left = DataFrame({'value': lrange(3)}) + right = DataFrame({'rvalue': lrange(6)}) + + key = np.array([0, 1, 1, 2, 2, 3], dtype=np.int64) + merged = merge(left, right, left_index=True, right_on=key, how='outer') + tm.assert_series_equal(merged['key_0'], Series(key, name='key_0')) + + def test_no_overlap_more_informative_error(self): + dt = datetime.now() + df1 = DataFrame({'x': ['a']}, index=[dt]) + + df2 = DataFrame({'y': ['b', 'c']}, index=[dt, dt]) + + msg = ('No common columns to perform merge on. ' + 'Merge options: left_on={lon}, right_on={ron}, ' + 'left_index={lidx}, right_index={ridx}' + .format(lon=None, ron=None, lidx=False, ridx=False)) + + with pytest.raises(MergeError, match=msg): + merge(df1, df2) + + def test_merge_non_unique_indexes(self): + + dt = datetime(2012, 5, 1) + dt2 = datetime(2012, 5, 2) + dt3 = datetime(2012, 5, 3) + dt4 = datetime(2012, 5, 4) + + df1 = DataFrame({'x': ['a']}, index=[dt]) + df2 = DataFrame({'y': ['b', 'c']}, index=[dt, dt]) + _check_merge(df1, df2) + + # Not monotonic + df1 = DataFrame({'x': ['a', 'b', 'q']}, index=[dt2, dt, dt4]) + df2 = DataFrame({'y': ['c', 'd', 'e', 'f', 'g', 'h']}, + index=[dt3, dt3, dt2, dt2, dt, dt]) + _check_merge(df1, df2) + + df1 = DataFrame({'x': ['a', 'b']}, index=[dt, dt]) + df2 = DataFrame({'y': ['c', 'd']}, index=[dt, dt]) + _check_merge(df1, df2) + + def test_merge_non_unique_index_many_to_many(self): + dt = datetime(2012, 5, 1) + dt2 = datetime(2012, 5, 2) + dt3 = datetime(2012, 5, 3) + df1 = DataFrame({'x': ['a', 'b', 'c', 'd']}, + index=[dt2, dt2, dt, dt]) + df2 = DataFrame({'y': ['e', 'f', 'g', ' h', 'i']}, + index=[dt2, dt2, dt3, dt, dt]) + _check_merge(df1, df2) + + def test_left_merge_empty_dataframe(self): + left = DataFrame({'key': [1], 'value': [2]}) + right = DataFrame({'key': []}) + + result = merge(left, right, on='key', how='left') + assert_frame_equal(result, left) + + result = merge(right, left, on='key', how='right') + assert_frame_equal(result, left) + + @pytest.mark.parametrize('kwarg', + [dict(left_index=True, right_index=True), + dict(left_index=True, right_on='x'), + dict(left_on='a', right_index=True), + dict(left_on='a', right_on='x')]) + def test_merge_left_empty_right_empty(self, join_type, kwarg): + # GH 10824 + left = pd.DataFrame([], columns=['a', 'b', 'c']) + right = pd.DataFrame([], columns=['x', 'y', 'z']) + + exp_in = pd.DataFrame([], columns=['a', 'b', 'c', 'x', 'y', 'z'], + index=pd.Index([], dtype=object), + dtype=object) + + result = pd.merge(left, right, how=join_type, **kwarg) + tm.assert_frame_equal(result, exp_in) + + def test_merge_left_empty_right_notempty(self): + # GH 10824 + left = pd.DataFrame([], columns=['a', 'b', 'c']) + right = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=['x', 'y', 'z']) + + exp_out = pd.DataFrame({'a': np.array([np.nan] * 3, dtype=object), + 'b': np.array([np.nan] * 3, dtype=object), + 'c': np.array([np.nan] * 3, dtype=object), + 'x': [1, 4, 7], + 'y': [2, 5, 8], + 'z': [3, 6, 9]}, + columns=['a', 'b', 'c', 'x', 'y', 'z']) + exp_in = exp_out[0:0] # make empty DataFrame keeping dtype + # result will have object dtype + exp_in.index = exp_in.index.astype(object) + + def check1(exp, kwarg): + result = pd.merge(left, right, how='inner', **kwarg) + tm.assert_frame_equal(result, exp) + result = pd.merge(left, right, how='left', **kwarg) + tm.assert_frame_equal(result, exp) + + def check2(exp, kwarg): + result = pd.merge(left, right, how='right', **kwarg) + tm.assert_frame_equal(result, exp) + result = pd.merge(left, right, how='outer', **kwarg) + tm.assert_frame_equal(result, exp) + + for kwarg in [dict(left_index=True, right_index=True), + dict(left_index=True, right_on='x')]: + check1(exp_in, kwarg) + check2(exp_out, kwarg) + + kwarg = dict(left_on='a', right_index=True) + check1(exp_in, kwarg) + exp_out['a'] = [0, 1, 2] + check2(exp_out, kwarg) + + kwarg = dict(left_on='a', right_on='x') + check1(exp_in, kwarg) + exp_out['a'] = np.array([np.nan] * 3, dtype=object) + check2(exp_out, kwarg) + + def test_merge_left_notempty_right_empty(self): + # GH 10824 + left = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + columns=['a', 'b', 'c']) + right = pd.DataFrame([], columns=['x', 'y', 'z']) + + exp_out = pd.DataFrame({'a': [1, 4, 7], + 'b': [2, 5, 8], + 'c': [3, 6, 9], + 'x': np.array([np.nan] * 3, dtype=object), + 'y': np.array([np.nan] * 3, dtype=object), + 'z': np.array([np.nan] * 3, dtype=object)}, + columns=['a', 'b', 'c', 'x', 'y', 'z']) + exp_in = exp_out[0:0] # make empty DataFrame keeping dtype + # result will have object dtype + exp_in.index = exp_in.index.astype(object) + + def check1(exp, kwarg): + result = pd.merge(left, right, how='inner', **kwarg) + tm.assert_frame_equal(result, exp) + result = pd.merge(left, right, how='right', **kwarg) + tm.assert_frame_equal(result, exp) + + def check2(exp, kwarg): + result = pd.merge(left, right, how='left', **kwarg) + tm.assert_frame_equal(result, exp) + result = pd.merge(left, right, how='outer', **kwarg) + tm.assert_frame_equal(result, exp) + + for kwarg in [dict(left_index=True, right_index=True), + dict(left_index=True, right_on='x'), + dict(left_on='a', right_index=True), + dict(left_on='a', right_on='x')]: + check1(exp_in, kwarg) + check2(exp_out, kwarg) + + def test_merge_empty_frame(self, series_of_dtype, series_of_dtype2): + # GH 25183 + df = pd.DataFrame({'key': series_of_dtype, 'value': series_of_dtype2}, + columns=['key', 'value']) + df_empty = df[:0] + expected = pd.DataFrame({ + 'value_x': pd.Series(dtype=df.dtypes['value']), + 'key': pd.Series(dtype=df.dtypes['key']), + 'value_y': pd.Series(dtype=df.dtypes['value']), + }, columns=['value_x', 'key', 'value_y']) + actual = df_empty.merge(df, on='key') + assert_frame_equal(actual, expected) + + def test_merge_all_na_column(self, series_of_dtype, + series_of_dtype_all_na): + # GH 25183 + df_left = pd.DataFrame( + {'key': series_of_dtype, 'value': series_of_dtype_all_na}, + columns=['key', 'value']) + df_right = pd.DataFrame( + {'key': series_of_dtype, 'value': series_of_dtype_all_na}, + columns=['key', 'value']) + expected = pd.DataFrame({ + 'key': series_of_dtype, + 'value_x': series_of_dtype_all_na, + 'value_y': series_of_dtype_all_na, + }, columns=['key', 'value_x', 'value_y']) + actual = df_left.merge(df_right, on='key') + assert_frame_equal(actual, expected) + + def test_merge_nosort(self): + # #2098, anything to do? + + from datetime import datetime + + d = {"var1": np.random.randint(0, 10, size=10), + "var2": np.random.randint(0, 10, size=10), + "var3": [datetime(2012, 1, 12), + datetime(2011, 2, 4), + datetime(2010, 2, 3), + datetime(2012, 1, 12), + datetime(2011, 2, 4), + datetime(2012, 4, 3), + datetime(2012, 3, 4), + datetime(2008, 5, 1), + datetime(2010, 2, 3), + datetime(2012, 2, 3)]} + df = DataFrame.from_dict(d) + var3 = df.var3.unique() + var3.sort() + new = DataFrame.from_dict({"var3": var3, + "var8": np.random.random(7)}) + + result = df.merge(new, on="var3", sort=False) + exp = merge(df, new, on='var3', sort=False) + assert_frame_equal(result, exp) + + assert (df.var3.unique() == result.var3.unique()).all() + + def test_merge_nan_right(self): + df1 = DataFrame({"i1": [0, 1], "i2": [0, 1]}) + df2 = DataFrame({"i1": [0], "i3": [0]}) + result = df1.join(df2, on="i1", rsuffix="_") + expected = (DataFrame({'i1': {0: 0.0, 1: 1}, 'i2': {0: 0, 1: 1}, + 'i1_': {0: 0, 1: np.nan}, + 'i3': {0: 0.0, 1: np.nan}, + None: {0: 0, 1: 0}}) + .set_index(None) + .reset_index()[['i1', 'i2', 'i1_', 'i3']]) + assert_frame_equal(result, expected, check_dtype=False) + + df1 = DataFrame({"i1": [0, 1], "i2": [0.5, 1.5]}) + df2 = DataFrame({"i1": [0], "i3": [0.7]}) + result = df1.join(df2, rsuffix="_", on='i1') + expected = (DataFrame({'i1': {0: 0, 1: 1}, 'i1_': {0: 0.0, 1: nan}, + 'i2': {0: 0.5, 1: 1.5}, + 'i3': {0: 0.69999999999999996, + 1: nan}}) + [['i1', 'i2', 'i1_', 'i3']]) + assert_frame_equal(result, expected) + + def test_merge_type(self): + class NotADataFrame(DataFrame): + + @property + def _constructor(self): + return NotADataFrame + + nad = NotADataFrame(self.df) + result = nad.merge(self.df2, on='key1') + + assert isinstance(result, NotADataFrame) + + def test_join_append_timedeltas(self): + + import datetime as dt + from pandas import NaT + + # timedelta64 issues with join/merge + # GH 5695 + + d = {'d': dt.datetime(2013, 11, 5, 5, 56), 't': dt.timedelta(0, 22500)} + df = DataFrame(columns=list('dt')) + df = df.append(d, ignore_index=True) + result = df.append(d, ignore_index=True) + expected = DataFrame({'d': [dt.datetime(2013, 11, 5, 5, 56), + dt.datetime(2013, 11, 5, 5, 56)], + 't': [dt.timedelta(0, 22500), + dt.timedelta(0, 22500)]}) + assert_frame_equal(result, expected) + + td = np.timedelta64(300000000) + lhs = DataFrame(Series([td, td], index=["A", "B"])) + rhs = DataFrame(Series([td], index=["A"])) + + result = lhs.join(rhs, rsuffix='r', how="left") + expected = DataFrame({'0': Series([td, td], index=list('AB')), + '0r': Series([td, NaT], index=list('AB'))}) + assert_frame_equal(result, expected) + + def test_other_datetime_unit(self): + # GH 13389 + df1 = pd.DataFrame({'entity_id': [101, 102]}) + s = pd.Series([None, None], index=[101, 102], name='days') + + for dtype in ['datetime64[D]', 'datetime64[h]', 'datetime64[m]', + 'datetime64[s]', 'datetime64[ms]', 'datetime64[us]', + 'datetime64[ns]']: + + df2 = s.astype(dtype).to_frame('days') + # coerces to datetime64[ns], thus sholuld not be affected + assert df2['days'].dtype == 'datetime64[ns]' + + result = df1.merge(df2, left_on='entity_id', right_index=True) + + exp = pd.DataFrame({'entity_id': [101, 102], + 'days': np.array(['nat', 'nat'], + dtype='datetime64[ns]')}, + columns=['entity_id', 'days']) + tm.assert_frame_equal(result, exp) + + @pytest.mark.parametrize("unit", ['D', 'h', 'm', 's', 'ms', 'us', 'ns']) + def test_other_timedelta_unit(self, unit): + # GH 13389 + df1 = pd.DataFrame({'entity_id': [101, 102]}) + s = pd.Series([None, None], index=[101, 102], name='days') + + dtype = "m8[{}]".format(unit) + df2 = s.astype(dtype).to_frame('days') + assert df2['days'].dtype == 'm8[ns]' + + result = df1.merge(df2, left_on='entity_id', right_index=True) + + exp = pd.DataFrame({'entity_id': [101, 102], + 'days': np.array(['nat', 'nat'], + dtype=dtype)}, + columns=['entity_id', 'days']) + tm.assert_frame_equal(result, exp) + + def test_overlapping_columns_error_message(self): + df = DataFrame({'key': [1, 2, 3], + 'v1': [4, 5, 6], + 'v2': [7, 8, 9]}) + df2 = DataFrame({'key': [1, 2, 3], + 'v1': [4, 5, 6], + 'v2': [7, 8, 9]}) + + df.columns = ['key', 'foo', 'foo'] + df2.columns = ['key', 'bar', 'bar'] + expected = DataFrame({'key': [1, 2, 3], + 'v1': [4, 5, 6], + 'v2': [7, 8, 9], + 'v3': [4, 5, 6], + 'v4': [7, 8, 9]}) + expected.columns = ['key', 'foo', 'foo', 'bar', 'bar'] + assert_frame_equal(merge(df, df2), expected) + + # #2649, #10639 + df2.columns = ['key1', 'foo', 'foo'] + msg = (r"Data columns not unique: Index\(\[u?'foo', u?'foo'\]," + r" dtype='object'\)") + with pytest.raises(MergeError, match=msg): + merge(df, df2) + + def test_merge_on_datetime64tz(self): + + # GH11405 + left = pd.DataFrame({'key': pd.date_range('20151010', periods=2, + tz='US/Eastern'), + 'value': [1, 2]}) + right = pd.DataFrame({'key': pd.date_range('20151011', periods=3, + tz='US/Eastern'), + 'value': [1, 2, 3]}) + + expected = DataFrame({'key': pd.date_range('20151010', periods=4, + tz='US/Eastern'), + 'value_x': [1, 2, np.nan, np.nan], + 'value_y': [np.nan, 1, 2, 3]}) + result = pd.merge(left, right, on='key', how='outer') + assert_frame_equal(result, expected) + + left = pd.DataFrame({'key': [1, 2], + 'value': pd.date_range('20151010', periods=2, + tz='US/Eastern')}) + right = pd.DataFrame({'key': [2, 3], + 'value': pd.date_range('20151011', periods=2, + tz='US/Eastern')}) + expected = DataFrame({ + 'key': [1, 2, 3], + 'value_x': list(pd.date_range('20151010', periods=2, + tz='US/Eastern')) + [pd.NaT], + 'value_y': [pd.NaT] + list(pd.date_range('20151011', periods=2, + tz='US/Eastern'))}) + result = pd.merge(left, right, on='key', how='outer') + assert_frame_equal(result, expected) + assert result['value_x'].dtype == 'datetime64[ns, US/Eastern]' + assert result['value_y'].dtype == 'datetime64[ns, US/Eastern]' + + def test_merge_on_datetime64tz_empty(self): + # https://github.com/pandas-dev/pandas/issues/25014 + dtz = pd.DatetimeTZDtype(tz='UTC') + right = pd.DataFrame({'date': [pd.Timestamp('2018', tz=dtz.tz)], + 'value': [4.0], + 'date2': [pd.Timestamp('2019', tz=dtz.tz)]}, + columns=['date', 'value', 'date2']) + left = right[:0] + result = left.merge(right, on='date') + expected = pd.DataFrame({ + 'value_x': pd.Series(dtype=float), + 'date2_x': pd.Series(dtype=dtz), + 'date': pd.Series(dtype=dtz), + 'value_y': pd.Series(dtype=float), + 'date2_y': pd.Series(dtype=dtz), + }, columns=['value_x', 'date2_x', 'date', 'value_y', 'date2_y']) + tm.assert_frame_equal(result, expected) + + def test_merge_datetime64tz_with_dst_transition(self): + # GH 18885 + df1 = pd.DataFrame(pd.date_range( + '2017-10-29 01:00', periods=4, freq='H', tz='Europe/Madrid'), + columns=['date']) + df1['value'] = 1 + df2 = pd.DataFrame({ + 'date': pd.to_datetime([ + '2017-10-29 03:00:00', '2017-10-29 04:00:00', + '2017-10-29 05:00:00' + ]), + 'value': 2 + }) + df2['date'] = df2['date'].dt.tz_localize('UTC').dt.tz_convert( + 'Europe/Madrid') + result = pd.merge(df1, df2, how='outer', on='date') + expected = pd.DataFrame({ + 'date': pd.date_range( + '2017-10-29 01:00', periods=7, freq='H', tz='Europe/Madrid'), + 'value_x': [1] * 4 + [np.nan] * 3, + 'value_y': [np.nan] * 4 + [2] * 3 + }) + assert_frame_equal(result, expected) + + def test_merge_non_unique_period_index(self): + # GH #16871 + index = pd.period_range('2016-01-01', periods=16, freq='M') + df = DataFrame([i for i in range(len(index))], + index=index, columns=['pnum']) + df2 = concat([df, df]) + result = df.merge(df2, left_index=True, right_index=True, how='inner') + expected = DataFrame( + np.tile(np.arange(16, dtype=np.int64).repeat(2).reshape(-1, 1), 2), + columns=['pnum_x', 'pnum_y'], index=df2.sort_index().index) + tm.assert_frame_equal(result, expected) + + def test_merge_on_periods(self): + left = pd.DataFrame({'key': pd.period_range('20151010', periods=2, + freq='D'), + 'value': [1, 2]}) + right = pd.DataFrame({'key': pd.period_range('20151011', periods=3, + freq='D'), + 'value': [1, 2, 3]}) + + expected = DataFrame({'key': pd.period_range('20151010', periods=4, + freq='D'), + 'value_x': [1, 2, np.nan, np.nan], + 'value_y': [np.nan, 1, 2, 3]}) + result = pd.merge(left, right, on='key', how='outer') + assert_frame_equal(result, expected) + + left = pd.DataFrame({'key': [1, 2], + 'value': pd.period_range('20151010', periods=2, + freq='D')}) + right = pd.DataFrame({'key': [2, 3], + 'value': pd.period_range('20151011', periods=2, + freq='D')}) + + exp_x = pd.period_range('20151010', periods=2, freq='D') + exp_y = pd.period_range('20151011', periods=2, freq='D') + expected = DataFrame({'key': [1, 2, 3], + 'value_x': list(exp_x) + [pd.NaT], + 'value_y': [pd.NaT] + list(exp_y)}) + result = pd.merge(left, right, on='key', how='outer') + assert_frame_equal(result, expected) + assert result['value_x'].dtype == 'Period[D]' + assert result['value_y'].dtype == 'Period[D]' + + def test_indicator(self): + # PR #10054. xref #7412 and closes #8790. + df1 = DataFrame({'col1': [0, 1], 'col_conflict': [1, 2], + 'col_left': ['a', 'b']}) + df1_copy = df1.copy() + + df2 = DataFrame({'col1': [1, 2, 3, 4, 5], + 'col_conflict': [1, 2, 3, 4, 5], + 'col_right': [2, 2, 2, 2, 2]}) + df2_copy = df2.copy() + + df_result = DataFrame({ + 'col1': [0, 1, 2, 3, 4, 5], + 'col_conflict_x': [1, 2, np.nan, np.nan, np.nan, np.nan], + 'col_left': ['a', 'b', np.nan, np.nan, np.nan, np.nan], + 'col_conflict_y': [np.nan, 1, 2, 3, 4, 5], + 'col_right': [np.nan, 2, 2, 2, 2, 2]}) + df_result['_merge'] = Categorical( + ['left_only', 'both', 'right_only', + 'right_only', 'right_only', 'right_only'], + categories=['left_only', 'right_only', 'both']) + + df_result = df_result[['col1', 'col_conflict_x', 'col_left', + 'col_conflict_y', 'col_right', '_merge']] + + test = merge(df1, df2, on='col1', how='outer', indicator=True) + assert_frame_equal(test, df_result) + test = df1.merge(df2, on='col1', how='outer', indicator=True) + assert_frame_equal(test, df_result) + + # No side effects + assert_frame_equal(df1, df1_copy) + assert_frame_equal(df2, df2_copy) + + # Check with custom name + df_result_custom_name = df_result + df_result_custom_name = df_result_custom_name.rename( + columns={'_merge': 'custom_name'}) + + test_custom_name = merge( + df1, df2, on='col1', how='outer', indicator='custom_name') + assert_frame_equal(test_custom_name, df_result_custom_name) + test_custom_name = df1.merge( + df2, on='col1', how='outer', indicator='custom_name') + assert_frame_equal(test_custom_name, df_result_custom_name) + + # Check only accepts strings and booleans + msg = "indicator option can only accept boolean or string arguments" + with pytest.raises(ValueError, match=msg): + merge(df1, df2, on='col1', how='outer', indicator=5) + with pytest.raises(ValueError, match=msg): + df1.merge(df2, on='col1', how='outer', indicator=5) + + # Check result integrity + + test2 = merge(df1, df2, on='col1', how='left', indicator=True) + assert (test2._merge != 'right_only').all() + test2 = df1.merge(df2, on='col1', how='left', indicator=True) + assert (test2._merge != 'right_only').all() + + test3 = merge(df1, df2, on='col1', how='right', indicator=True) + assert (test3._merge != 'left_only').all() + test3 = df1.merge(df2, on='col1', how='right', indicator=True) + assert (test3._merge != 'left_only').all() + + test4 = merge(df1, df2, on='col1', how='inner', indicator=True) + assert (test4._merge == 'both').all() + test4 = df1.merge(df2, on='col1', how='inner', indicator=True) + assert (test4._merge == 'both').all() + + # Check if working name in df + for i in ['_right_indicator', '_left_indicator', '_merge']: + df_badcolumn = DataFrame({'col1': [1, 2], i: [2, 2]}) + + msg = ("Cannot use `indicator=True` option when data contains a" + " column named {}|" + "Cannot use name of an existing column for indicator" + " column").format(i) + with pytest.raises(ValueError, match=msg): + merge(df1, df_badcolumn, on='col1', + how='outer', indicator=True) + with pytest.raises(ValueError, match=msg): + df1.merge(df_badcolumn, on='col1', how='outer', indicator=True) + + # Check for name conflict with custom name + df_badcolumn = DataFrame( + {'col1': [1, 2], 'custom_column_name': [2, 2]}) + + msg = "Cannot use name of an existing column for indicator column" + with pytest.raises(ValueError, match=msg): + merge(df1, df_badcolumn, on='col1', how='outer', + indicator='custom_column_name') + with pytest.raises(ValueError, match=msg): + df1.merge(df_badcolumn, on='col1', how='outer', + indicator='custom_column_name') + + # Merge on multiple columns + df3 = DataFrame({'col1': [0, 1], 'col2': ['a', 'b']}) + + df4 = DataFrame({'col1': [1, 1, 3], 'col2': ['b', 'x', 'y']}) + + hand_coded_result = DataFrame({'col1': [0, 1, 1, 3], + 'col2': ['a', 'b', 'x', 'y']}) + hand_coded_result['_merge'] = Categorical( + ['left_only', 'both', 'right_only', 'right_only'], + categories=['left_only', 'right_only', 'both']) + + test5 = merge(df3, df4, on=['col1', 'col2'], + how='outer', indicator=True) + assert_frame_equal(test5, hand_coded_result) + test5 = df3.merge(df4, on=['col1', 'col2'], + how='outer', indicator=True) + assert_frame_equal(test5, hand_coded_result) + + def test_validation(self): + left = DataFrame({'a': ['a', 'b', 'c', 'd'], + 'b': ['cat', 'dog', 'weasel', 'horse']}, + index=range(4)) + + right = DataFrame({'a': ['a', 'b', 'c', 'd', 'e'], + 'c': ['meow', 'bark', 'um... weasel noise?', + 'nay', 'chirp']}, + index=range(5)) + + # Make sure no side effects. + left_copy = left.copy() + right_copy = right.copy() + + result = merge(left, right, left_index=True, right_index=True, + validate='1:1') + assert_frame_equal(left, left_copy) + assert_frame_equal(right, right_copy) + + # make sure merge still correct + expected = DataFrame({'a_x': ['a', 'b', 'c', 'd'], + 'b': ['cat', 'dog', 'weasel', 'horse'], + 'a_y': ['a', 'b', 'c', 'd'], + 'c': ['meow', 'bark', 'um... weasel noise?', + 'nay']}, + index=range(4), + columns=['a_x', 'b', 'a_y', 'c']) + + result = merge(left, right, left_index=True, right_index=True, + validate='one_to_one') + assert_frame_equal(result, expected) + + expected_2 = DataFrame({'a': ['a', 'b', 'c', 'd'], + 'b': ['cat', 'dog', 'weasel', 'horse'], + 'c': ['meow', 'bark', 'um... weasel noise?', + 'nay']}, + index=range(4)) + + result = merge(left, right, on='a', validate='1:1') + assert_frame_equal(left, left_copy) + assert_frame_equal(right, right_copy) + assert_frame_equal(result, expected_2) + + result = merge(left, right, on='a', validate='one_to_one') + assert_frame_equal(result, expected_2) + + # One index, one column + expected_3 = DataFrame({'b': ['cat', 'dog', 'weasel', 'horse'], + 'a': ['a', 'b', 'c', 'd'], + 'c': ['meow', 'bark', 'um... weasel noise?', + 'nay']}, + columns=['b', 'a', 'c'], + index=range(4)) + + left_index_reset = left.set_index('a') + result = merge(left_index_reset, right, left_index=True, + right_on='a', validate='one_to_one') + assert_frame_equal(result, expected_3) + + # Dups on right + right_w_dups = right.append(pd.DataFrame({'a': ['e'], 'c': ['moo']}, + index=[4])) + merge(left, right_w_dups, left_index=True, right_index=True, + validate='one_to_many') + + msg = ("Merge keys are not unique in right dataset; not a one-to-one" + " merge") + with pytest.raises(MergeError, match=msg): + merge(left, right_w_dups, left_index=True, right_index=True, + validate='one_to_one') + + with pytest.raises(MergeError, match=msg): + merge(left, right_w_dups, on='a', validate='one_to_one') + + # Dups on left + left_w_dups = left.append(pd.DataFrame({'a': ['a'], 'c': ['cow']}, + index=[3]), sort=True) + merge(left_w_dups, right, left_index=True, right_index=True, + validate='many_to_one') + + msg = ("Merge keys are not unique in left dataset; not a one-to-one" + " merge") + with pytest.raises(MergeError, match=msg): + merge(left_w_dups, right, left_index=True, right_index=True, + validate='one_to_one') + + with pytest.raises(MergeError, match=msg): + merge(left_w_dups, right, on='a', validate='one_to_one') + + # Dups on both + merge(left_w_dups, right_w_dups, on='a', validate='many_to_many') + + msg = ("Merge keys are not unique in right dataset; not a many-to-one" + " merge") + with pytest.raises(MergeError, match=msg): + merge(left_w_dups, right_w_dups, left_index=True, + right_index=True, validate='many_to_one') + + msg = ("Merge keys are not unique in left dataset; not a one-to-many" + " merge") + with pytest.raises(MergeError, match=msg): + merge(left_w_dups, right_w_dups, on='a', + validate='one_to_many') + + # Check invalid arguments + msg = "Not a valid argument for validate" + with pytest.raises(ValueError, match=msg): + merge(left, right, on='a', validate='jibberish') + + # Two column merge, dups in both, but jointly no dups. + left = DataFrame({'a': ['a', 'a', 'b', 'b'], + 'b': [0, 1, 0, 1], + 'c': ['cat', 'dog', 'weasel', 'horse']}, + index=range(4)) + + right = DataFrame({'a': ['a', 'a', 'b'], + 'b': [0, 1, 0], + 'd': ['meow', 'bark', 'um... weasel noise?']}, + index=range(3)) + + expected_multi = DataFrame({'a': ['a', 'a', 'b'], + 'b': [0, 1, 0], + 'c': ['cat', 'dog', 'weasel'], + 'd': ['meow', 'bark', + 'um... weasel noise?']}, + index=range(3)) + + msg = ("Merge keys are not unique in either left or right dataset;" + " not a one-to-one merge") + with pytest.raises(MergeError, match=msg): + merge(left, right, on='a', validate='1:1') + + result = merge(left, right, on=['a', 'b'], validate='1:1') + assert_frame_equal(result, expected_multi) + + def test_merge_two_empty_df_no_division_error(self): + # GH17776, PR #17846 + a = pd.DataFrame({'a': [], 'b': [], 'c': []}) + with np.errstate(divide='raise'): + merge(a, a, on=('a', 'b')) + + @pytest.mark.parametrize('how', ['right', 'outer']) + def test_merge_on_index_with_more_values(self, how): + # GH 24212 + # pd.merge gets [0, 1, 2, -1, -1, -1] as left_indexer, ensure that + # -1 is interpreted as a missing value instead of the last element + df1 = pd.DataFrame({'a': [1, 2, 3], 'key': [0, 2, 2]}) + df2 = pd.DataFrame({'b': [1, 2, 3, 4, 5]}) + result = df1.merge(df2, left_on='key', right_index=True, how=how) + expected = pd.DataFrame([[1.0, 0, 1], + [2.0, 2, 3], + [3.0, 2, 3], + [np.nan, 1, 2], + [np.nan, 3, 4], + [np.nan, 4, 5]], + columns=['a', 'key', 'b']) + expected.set_index(Int64Index([0, 1, 2, 1, 3, 4]), inplace=True) + assert_frame_equal(result, expected) + + def test_merge_right_index_right(self): + # Note: the expected output here is probably incorrect. + # See https://github.com/pandas-dev/pandas/issues/17257 for more. + # We include this as a regression test for GH-24897. + left = pd.DataFrame({'a': [1, 2, 3], 'key': [0, 1, 1]}) + right = pd.DataFrame({'b': [1, 2, 3]}) + + expected = pd.DataFrame({'a': [1, 2, 3, None], + 'key': [0, 1, 1, 2], + 'b': [1, 2, 2, 3]}, + columns=['a', 'key', 'b'], + index=[0, 1, 2, 2]) + result = left.merge(right, left_on='key', right_index=True, + how='right') + tm.assert_frame_equal(result, expected) + + +def _check_merge(x, y): + for how in ['inner', 'left', 'outer']: + result = x.join(y, how=how) + + expected = merge(x.reset_index(), y.reset_index(), how=how, + sort=True) + expected = expected.set_index('index') + + # TODO check_names on merge? + assert_frame_equal(result, expected, check_names=False) + + +class TestMergeDtypes(object): + + @pytest.mark.parametrize('right_vals', [ + ['foo', 'bar'], + Series(['foo', 'bar']).astype('category'), + ]) + def test_different(self, right_vals): + + left = DataFrame({'A': ['foo', 'bar'], + 'B': Series(['foo', 'bar']).astype('category'), + 'C': [1, 2], + 'D': [1.0, 2.0], + 'E': Series([1, 2], dtype='uint64'), + 'F': Series([1, 2], dtype='int32')}) + right = DataFrame({'A': right_vals}) + + # GH 9780 + # We allow merging on object and categorical cols and cast + # categorical cols to object + result = pd.merge(left, right, on='A') + assert is_object_dtype(result.A.dtype) + + @pytest.mark.parametrize('d1', [np.int64, np.int32, + np.int16, np.int8, np.uint8]) + @pytest.mark.parametrize('d2', [np.int64, np.float64, + np.float32, np.float16]) + def test_join_multi_dtypes(self, d1, d2): + + dtype1 = np.dtype(d1) + dtype2 = np.dtype(d2) + + left = DataFrame({'k1': np.array([0, 1, 2] * 8, dtype=dtype1), + 'k2': ['foo', 'bar'] * 12, + 'v': np.array(np.arange(24), dtype=np.int64)}) + + index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) + right = DataFrame({'v2': np.array([5, 7], dtype=dtype2)}, index=index) + + result = left.join(right, on=['k1', 'k2']) + + expected = left.copy() + + if dtype2.kind == 'i': + dtype2 = np.dtype('float64') + expected['v2'] = np.array(np.nan, dtype=dtype2) + expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 + expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 + + tm.assert_frame_equal(result, expected) + + result = left.join(right, on=['k1', 'k2'], sort=True) + expected.sort_values(['k1', 'k2'], kind='mergesort', inplace=True) + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('int_vals, float_vals, exp_vals', [ + ([1, 2, 3], [1.0, 2.0, 3.0], {'X': [1, 2, 3], 'Y': [1.0, 2.0, 3.0]}), + ([1, 2, 3], [1.0, 3.0], {'X': [1, 3], 'Y': [1.0, 3.0]}), + ([1, 2], [1.0, 2.0, 3.0], {'X': [1, 2], 'Y': [1.0, 2.0]}), + ]) + def test_merge_on_ints_floats(self, int_vals, float_vals, exp_vals): + # GH 16572 + # Check that float column is not cast to object if + # merging on float and int columns + A = DataFrame({'X': int_vals}) + B = DataFrame({'Y': float_vals}) + expected = DataFrame(exp_vals) + + result = A.merge(B, left_on='X', right_on='Y') + assert_frame_equal(result, expected) + + result = B.merge(A, left_on='Y', right_on='X') + assert_frame_equal(result, expected[['Y', 'X']]) + + def test_merge_on_ints_floats_warning(self): + # GH 16572 + # merge will produce a warning when merging on int and + # float columns where the float values are not exactly + # equal to their int representation + A = DataFrame({'X': [1, 2, 3]}) + B = DataFrame({'Y': [1.1, 2.5, 3.0]}) + expected = DataFrame({'X': [3], 'Y': [3.0]}) + + with tm.assert_produces_warning(UserWarning): + result = A.merge(B, left_on='X', right_on='Y') + assert_frame_equal(result, expected) + + with tm.assert_produces_warning(UserWarning): + result = B.merge(A, left_on='Y', right_on='X') + assert_frame_equal(result, expected[['Y', 'X']]) + + # test no warning if float has NaNs + B = DataFrame({'Y': [np.nan, np.nan, 3.0]}) + + with tm.assert_produces_warning(None): + result = B.merge(A, left_on='Y', right_on='X') + assert_frame_equal(result, expected[['Y', 'X']]) + + def test_merge_incompat_infer_boolean_object(self): + # GH21119: bool + object bool merge OK + df1 = DataFrame({'key': Series([True, False], dtype=object)}) + df2 = DataFrame({'key': [True, False]}) + + expected = DataFrame({'key': [True, False]}, dtype=object) + result = pd.merge(df1, df2, on='key') + assert_frame_equal(result, expected) + result = pd.merge(df2, df1, on='key') + assert_frame_equal(result, expected) + + # with missing value + df1 = DataFrame({'key': Series([True, False, np.nan], dtype=object)}) + df2 = DataFrame({'key': [True, False]}) + + expected = DataFrame({'key': [True, False]}, dtype=object) + result = pd.merge(df1, df2, on='key') + assert_frame_equal(result, expected) + result = pd.merge(df2, df1, on='key') + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('df1_vals, df2_vals', [ + + # merge on category coerces to object + ([0, 1, 2], Series(['a', 'b', 'a']).astype('category')), + ([0.0, 1.0, 2.0], Series(['a', 'b', 'a']).astype('category')), + + # no not infer + ([0, 1], pd.Series([False, True], dtype=object)), + ([0, 1], pd.Series([False, True], dtype=bool)), + ]) + def test_merge_incompat_dtypes_are_ok(self, df1_vals, df2_vals): + # these are explicity allowed incompat merges, that pass thru + # the result type is dependent on if the values on the rhs are + # inferred, otherwise these will be coereced to object + + df1 = DataFrame({'A': df1_vals}) + df2 = DataFrame({'A': df2_vals}) + + result = pd.merge(df1, df2, on=['A']) + assert is_object_dtype(result.A.dtype) + result = pd.merge(df2, df1, on=['A']) + assert is_object_dtype(result.A.dtype) + + @pytest.mark.parametrize('df1_vals, df2_vals', [ + # do not infer to numeric + + (Series([1, 2], dtype='uint64'), ["a", "b", "c"]), + (Series([1, 2], dtype='int32'), ["a", "b", "c"]), + ([0, 1, 2], ["0", "1", "2"]), + ([0.0, 1.0, 2.0], ["0", "1", "2"]), + ([0, 1, 2], [u"0", u"1", u"2"]), + (pd.date_range('1/1/2011', periods=2, freq='D'), ['2011-01-01', + '2011-01-02']), + (pd.date_range('1/1/2011', periods=2, freq='D'), [0, 1]), + (pd.date_range('1/1/2011', periods=2, freq='D'), [0.0, 1.0]), + (pd.date_range('20130101', periods=3), + pd.date_range('20130101', periods=3, tz='US/Eastern')), + ]) + def test_merge_incompat_dtypes_error(self, df1_vals, df2_vals): + # GH 9780, GH 15800 + # Raise a ValueError when a user tries to merge on + # dtypes that are incompatible (e.g., obj and int/float) + + df1 = DataFrame({'A': df1_vals}) + df2 = DataFrame({'A': df2_vals}) + + msg = ("You are trying to merge on {lk_dtype} and " + "{rk_dtype} columns. If you wish to proceed " + "you should use pd.concat".format(lk_dtype=df1['A'].dtype, + rk_dtype=df2['A'].dtype)) + msg = re.escape(msg) + with pytest.raises(ValueError, match=msg): + pd.merge(df1, df2, on=['A']) + + # Check that error still raised when swapping order of dataframes + msg = ("You are trying to merge on {lk_dtype} and " + "{rk_dtype} columns. If you wish to proceed " + "you should use pd.concat".format(lk_dtype=df2['A'].dtype, + rk_dtype=df1['A'].dtype)) + msg = re.escape(msg) + with pytest.raises(ValueError, match=msg): + pd.merge(df2, df1, on=['A']) + + +@pytest.fixture +def left(): + np.random.seed(1234) + return DataFrame( + {'X': Series(np.random.choice( + ['foo', 'bar'], + size=(10,))).astype(CDT(['foo', 'bar'])), + 'Y': np.random.choice(['one', 'two', 'three'], size=(10,))}) + + +@pytest.fixture +def right(): + np.random.seed(1234) + return DataFrame( + {'X': Series(['foo', 'bar']).astype(CDT(['foo', 'bar'])), + 'Z': [1, 2]}) + + +class TestMergeCategorical(object): + + def test_identical(self, left): + # merging on the same, should preserve dtypes + merged = pd.merge(left, left, on='X') + result = merged.dtypes.sort_index() + expected = Series([CategoricalDtype(), + np.dtype('O'), + np.dtype('O')], + index=['X', 'Y_x', 'Y_y']) + assert_series_equal(result, expected) + + def test_basic(self, left, right): + # we have matching Categorical dtypes in X + # so should preserve the merged column + merged = pd.merge(left, right, on='X') + result = merged.dtypes.sort_index() + expected = Series([CategoricalDtype(), + np.dtype('O'), + np.dtype('int64')], + index=['X', 'Y', 'Z']) + assert_series_equal(result, expected) + + def test_merge_categorical(self): + # GH 9426 + + right = DataFrame({'c': {0: 'a', + 1: 'b', + 2: 'c', + 3: 'd', + 4: 'e'}, + 'd': {0: 'null', + 1: 'null', + 2: 'null', + 3: 'null', + 4: 'null'}}) + left = DataFrame({'a': {0: 'f', + 1: 'f', + 2: 'f', + 3: 'f', + 4: 'f'}, + 'b': {0: 'g', + 1: 'g', + 2: 'g', + 3: 'g', + 4: 'g'}}) + df = pd.merge(left, right, how='left', left_on='b', right_on='c') + + # object-object + expected = df.copy() + + # object-cat + # note that we propagate the category + # because we don't have any matching rows + cright = right.copy() + cright['d'] = cright['d'].astype('category') + result = pd.merge(left, cright, how='left', left_on='b', right_on='c') + expected['d'] = expected['d'].astype(CategoricalDtype(['null'])) + tm.assert_frame_equal(result, expected) + + # cat-object + cleft = left.copy() + cleft['b'] = cleft['b'].astype('category') + result = pd.merge(cleft, cright, how='left', left_on='b', right_on='c') + tm.assert_frame_equal(result, expected) + + # cat-cat + cright = right.copy() + cright['d'] = cright['d'].astype('category') + cleft = left.copy() + cleft['b'] = cleft['b'].astype('category') + result = pd.merge(cleft, cright, how='left', left_on='b', right_on='c') + tm.assert_frame_equal(result, expected) + + def tests_merge_categorical_unordered_equal(self): + # GH-19551 + df1 = DataFrame({ + 'Foo': Categorical(['A', 'B', 'C'], categories=['A', 'B', 'C']), + 'Left': ['A0', 'B0', 'C0'], + }) + + df2 = DataFrame({ + 'Foo': Categorical(['C', 'B', 'A'], categories=['C', 'B', 'A']), + 'Right': ['C1', 'B1', 'A1'], + }) + result = pd.merge(df1, df2, on=['Foo']) + expected = DataFrame({ + 'Foo': pd.Categorical(['A', 'B', 'C']), + 'Left': ['A0', 'B0', 'C0'], + 'Right': ['A1', 'B1', 'C1'], + }) + assert_frame_equal(result, expected) + + def test_other_columns(self, left, right): + # non-merge columns should preserve if possible + right = right.assign(Z=right.Z.astype('category')) + + merged = pd.merge(left, right, on='X') + result = merged.dtypes.sort_index() + expected = Series([CategoricalDtype(), + np.dtype('O'), + CategoricalDtype()], + index=['X', 'Y', 'Z']) + assert_series_equal(result, expected) + + # categories are preserved + assert left.X.values.is_dtype_equal(merged.X.values) + assert right.Z.values.is_dtype_equal(merged.Z.values) + + @pytest.mark.parametrize( + 'change', [lambda x: x, + lambda x: x.astype(CDT(['foo', 'bar', 'bah'])), + lambda x: x.astype(CDT(ordered=True))]) + def test_dtype_on_merged_different(self, change, join_type, left, right): + # our merging columns, X now has 2 different dtypes + # so we must be object as a result + + X = change(right.X.astype('object')) + right = right.assign(X=X) + assert is_categorical_dtype(left.X.values) + # assert not left.X.values.is_dtype_equal(right.X.values) + + merged = pd.merge(left, right, on='X', how=join_type) + + result = merged.dtypes.sort_index() + expected = Series([np.dtype('O'), + np.dtype('O'), + np.dtype('int64')], + index=['X', 'Y', 'Z']) + assert_series_equal(result, expected) + + def test_self_join_multiple_categories(self): + # GH 16767 + # non-duplicates should work with multiple categories + m = 5 + df = pd.DataFrame({ + 'a': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] * m, + 'b': ['t', 'w', 'x', 'y', 'z'] * 2 * m, + 'c': [letter + for each in ['m', 'n', 'u', 'p', 'o'] + for letter in [each] * 2 * m], + 'd': [letter + for each in ['aa', 'bb', 'cc', 'dd', 'ee', + 'ff', 'gg', 'hh', 'ii', 'jj'] + for letter in [each] * m]}) + + # change them all to categorical variables + df = df.apply(lambda x: x.astype('category')) + + # self-join should equal ourselves + result = pd.merge(df, df, on=list(df.columns)) + + assert_frame_equal(result, df) + + def test_dtype_on_categorical_dates(self): + # GH 16900 + # dates should not be coerced to ints + + df = pd.DataFrame( + [[date(2001, 1, 1), 1.1], + [date(2001, 1, 2), 1.3]], + columns=['date', 'num2'] + ) + df['date'] = df['date'].astype('category') + + df2 = pd.DataFrame( + [[date(2001, 1, 1), 1.3], + [date(2001, 1, 3), 1.4]], + columns=['date', 'num4'] + ) + df2['date'] = df2['date'].astype('category') + + expected_outer = pd.DataFrame([ + [pd.Timestamp('2001-01-01'), 1.1, 1.3], + [pd.Timestamp('2001-01-02'), 1.3, np.nan], + [pd.Timestamp('2001-01-03'), np.nan, 1.4]], + columns=['date', 'num2', 'num4'] + ) + result_outer = pd.merge(df, df2, how='outer', on=['date']) + assert_frame_equal(result_outer, expected_outer) + + expected_inner = pd.DataFrame( + [[pd.Timestamp('2001-01-01'), 1.1, 1.3]], + columns=['date', 'num2', 'num4'] + ) + result_inner = pd.merge(df, df2, how='inner', on=['date']) + assert_frame_equal(result_inner, expected_inner) + + @pytest.mark.parametrize('ordered', [True, False]) + @pytest.mark.parametrize('category_column,categories,expected_categories', + [([False, True, True, False], [True, False], + [True, False]), + ([2, 1, 1, 2], [1, 2], [1, 2]), + (['False', 'True', 'True', 'False'], + ['True', 'False'], ['True', 'False'])]) + def test_merging_with_bool_or_int_cateorical_column(self, category_column, + categories, + expected_categories, + ordered): + # GH 17187 + # merging with a boolean/int categorical column + df1 = pd.DataFrame({'id': [1, 2, 3, 4], + 'cat': category_column}) + df1['cat'] = df1['cat'].astype(CDT(categories, ordered=ordered)) + df2 = pd.DataFrame({'id': [2, 4], 'num': [1, 9]}) + result = df1.merge(df2) + expected = pd.DataFrame({'id': [2, 4], 'cat': expected_categories, + 'num': [1, 9]}) + expected['cat'] = expected['cat'].astype( + CDT(categories, ordered=ordered)) + assert_frame_equal(expected, result) + + def test_merge_on_int_array(self): + # GH 23020 + df = pd.DataFrame({'A': pd.Series([1, 2, np.nan], dtype='Int64'), + 'B': 1}) + result = pd.merge(df, df, on='A') + expected = pd.DataFrame({'A': pd.Series([1, 2, np.nan], dtype='Int64'), + 'B_x': 1, + 'B_y': 1}) + assert_frame_equal(result, expected) + + +@pytest.fixture +def left_df(): + return DataFrame({'a': [20, 10, 0]}, index=[2, 1, 0]) + + +@pytest.fixture +def right_df(): + return DataFrame({'b': [300, 100, 200]}, index=[3, 1, 2]) + + +class TestMergeOnIndexes(object): + + @pytest.mark.parametrize( + "how, sort, expected", + [('inner', False, DataFrame({'a': [20, 10], + 'b': [200, 100]}, + index=[2, 1])), + ('inner', True, DataFrame({'a': [10, 20], + 'b': [100, 200]}, + index=[1, 2])), + ('left', False, DataFrame({'a': [20, 10, 0], + 'b': [200, 100, np.nan]}, + index=[2, 1, 0])), + ('left', True, DataFrame({'a': [0, 10, 20], + 'b': [np.nan, 100, 200]}, + index=[0, 1, 2])), + ('right', False, DataFrame({'a': [np.nan, 10, 20], + 'b': [300, 100, 200]}, + index=[3, 1, 2])), + ('right', True, DataFrame({'a': [10, 20, np.nan], + 'b': [100, 200, 300]}, + index=[1, 2, 3])), + ('outer', False, DataFrame({'a': [0, 10, 20, np.nan], + 'b': [np.nan, 100, 200, 300]}, + index=[0, 1, 2, 3])), + ('outer', True, DataFrame({'a': [0, 10, 20, np.nan], + 'b': [np.nan, 100, 200, 300]}, + index=[0, 1, 2, 3]))]) + def test_merge_on_indexes(self, left_df, right_df, how, sort, expected): + result = pd.merge(left_df, right_df, + left_index=True, + right_index=True, + how=how, + sort=sort) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize( + 'index', [ + CategoricalIndex(['A', 'B'], categories=['A', 'B'], name='index_col'), + Float64Index([1.0, 2.0], name='index_col'), + Int64Index([1, 2], name='index_col'), + UInt64Index([1, 2], name='index_col'), + RangeIndex(start=0, stop=2, name='index_col'), + DatetimeIndex(["2018-01-01", "2018-01-02"], name='index_col'), + ], ids=lambda x: type(x).__name__) +def test_merge_index_types(index): + # gh-20777 + # assert key access is consistent across index types + left = DataFrame({"left_data": [1, 2]}, index=index) + right = DataFrame({"right_data": [1.0, 2.0]}, index=index) + + result = left.merge(right, on=['index_col']) + + expected = DataFrame( + OrderedDict([('left_data', [1, 2]), ('right_data', [1.0, 2.0])]), + index=index) + assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("on,left_on,right_on,left_index,right_index,nm", [ + (['outer', 'inner'], None, None, False, False, 'B'), + (None, None, None, True, True, 'B'), + (None, ['outer', 'inner'], None, False, True, 'B'), + (None, None, ['outer', 'inner'], True, False, 'B'), + (['outer', 'inner'], None, None, False, False, None), + (None, None, None, True, True, None), + (None, ['outer', 'inner'], None, False, True, None), + (None, None, ['outer', 'inner'], True, False, None)]) +def test_merge_series(on, left_on, right_on, left_index, right_index, nm): + # GH 21220 + a = pd.DataFrame({"A": [1, 2, 3, 4]}, + index=pd.MultiIndex.from_product([['a', 'b'], [0, 1]], + names=['outer', 'inner'])) + b = pd.Series([1, 2, 3, 4], + index=pd.MultiIndex.from_product([['a', 'b'], [1, 2]], + names=['outer', 'inner']), name=nm) + expected = pd.DataFrame({"A": [2, 4], "B": [1, 3]}, + index=pd.MultiIndex.from_product([['a', 'b'], [1]], + names=['outer', 'inner'])) + if nm is not None: + result = pd.merge(a, b, on=on, left_on=left_on, right_on=right_on, + left_index=left_index, right_index=right_index) + tm.assert_frame_equal(result, expected) + else: + msg = "Cannot merge a Series without a name" + with pytest.raises(ValueError, match=msg): + result = pd.merge(a, b, on=on, left_on=left_on, right_on=right_on, + left_index=left_index, right_index=right_index) + + +@pytest.mark.parametrize("col1, col2, kwargs, expected_cols", [ + (0, 0, dict(suffixes=("", "_dup")), ["0", "0_dup"]), + (0, 0, dict(suffixes=(None, "_dup")), [0, "0_dup"]), + (0, 0, dict(suffixes=("_x", "_y")), ["0_x", "0_y"]), + ("a", 0, dict(suffixes=(None, "_y")), ["a", 0]), + (0.0, 0.0, dict(suffixes=("_x", None)), ["0.0_x", 0.0]), + ("b", "b", dict(suffixes=(None, "_y")), ["b", "b_y"]), + ("a", "a", dict(suffixes=("_x", None)), ["a_x", "a"]), + ("a", "b", dict(suffixes=("_x", None)), ["a", "b"]), + ("a", "a", dict(suffixes=[None, "_x"]), ["a", "a_x"]), + (0, 0, dict(suffixes=["_a", None]), ["0_a", 0]), + ("a", "a", dict(), ["a_x", "a_y"]), + (0, 0, dict(), ["0_x", "0_y"]) +]) +def test_merge_suffix(col1, col2, kwargs, expected_cols): + # issue: 24782 + a = pd.DataFrame({col1: [1, 2, 3]}) + b = pd.DataFrame({col2: [4, 5, 6]}) + + expected = pd.DataFrame([[1, 4], [2, 5], [3, 6]], + columns=expected_cols) + + result = a.merge(b, left_index=True, right_index=True, **kwargs) + tm.assert_frame_equal(result, expected) + + result = pd.merge(a, b, left_index=True, right_index=True, **kwargs) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("col1, col2, suffixes", [ + ("a", "a", [None, None]), + ("a", "a", (None, None)), + ("a", "a", ("", None)), + (0, 0, [None, None]), + (0, 0, (None, "")) +]) +def test_merge_suffix_error(col1, col2, suffixes): + # issue: 24782 + a = pd.DataFrame({col1: [1, 2, 3]}) + b = pd.DataFrame({col2: [3, 4, 5]}) + + # TODO: might reconsider current raise behaviour, see issue 24782 + msg = "columns overlap but no suffix specified" + with pytest.raises(ValueError, match=msg): + pd.merge(a, b, left_index=True, right_index=True, suffixes=suffixes) + + +@pytest.mark.parametrize("col1, col2, suffixes", [ + ("a", "a", None), + (0, 0, None) +]) +def test_merge_suffix_none_error(col1, col2, suffixes): + # issue: 24782 + a = pd.DataFrame({col1: [1, 2, 3]}) + b = pd.DataFrame({col2: [3, 4, 5]}) + + # TODO: might reconsider current raise behaviour, see GH24782 + msg = "iterable" + with pytest.raises(TypeError, match=msg): + pd.merge(a, b, left_index=True, right_index=True, suffixes=suffixes) diff --git a/pandas/tests/tools/test_merge_asof.py b/pandas/tests/reshape/merge/test_merge_asof.py similarity index 79% rename from pandas/tests/tools/test_merge_asof.py rename to pandas/tests/reshape/merge/test_merge_asof.py index c9460cc74c94a..1d1d7d48adaab 100644 --- a/pandas/tests/tools/test_merge_asof.py +++ b/pandas/tests/reshape/merge/test_merge_asof.py @@ -1,19 +1,17 @@ -import os - -import pytz import numpy as np +import pytest +import pytz + import pandas as pd -from pandas import (merge_asof, read_csv, - to_datetime, Timedelta) -from pandas.tools.merge import MergeError -from pandas.util import testing as tm +from pandas import Timedelta, merge_asof, read_csv, to_datetime +from pandas.core.reshape.merge import MergeError from pandas.util.testing import assert_frame_equal -class TestAsOfMerge(tm.TestCase): +class TestAsOfMerge(object): - def read_data(self, name, dedupe=False): - path = os.path.join(tm.get_data_path(), name) + def read_data(self, datapath, name, dedupe=False): + path = datapath('reshape', 'merge', 'data', name) x = read_csv(path) if dedupe: x = (x.drop_duplicates(['time', 'ticker'], keep='last') @@ -22,15 +20,17 @@ def read_data(self, name, dedupe=False): x.time = to_datetime(x.time) return x - def setUp(self): + @pytest.fixture(autouse=True) + def setup_method(self, datapath): - self.trades = self.read_data('trades.csv') - self.quotes = self.read_data('quotes.csv', dedupe=True) - self.asof = self.read_data('asof.csv') - self.tolerance = self.read_data('tolerance.csv') - self.allow_exact_matches = self.read_data('allow_exact_matches.csv') + self.trades = self.read_data(datapath, 'trades.csv') + self.quotes = self.read_data(datapath, 'quotes.csv', dedupe=True) + self.asof = self.read_data(datapath, 'asof.csv') + self.tolerance = self.read_data(datapath, 'tolerance.csv') + self.allow_exact_matches = self.read_data(datapath, + 'allow_exact_matches.csv') self.allow_exact_matches_and_tolerance = self.read_data( - 'allow_exact_matches_and_tolerance.csv') + datapath, 'allow_exact_matches_and_tolerance.csv') def test_examples1(self): """ doc-string examples """ @@ -91,11 +91,30 @@ def test_examples2(self): by='ticker', tolerance=pd.Timedelta('2ms')) - pd.merge_asof(trades, quotes, - on='time', - by='ticker', - tolerance=pd.Timedelta('10ms'), - allow_exact_matches=False) + expected = pd.DataFrame({ + 'time': pd.to_datetime(['20160525 13:30:00.023', + '20160525 13:30:00.038', + '20160525 13:30:00.048', + '20160525 13:30:00.048', + '20160525 13:30:00.048']), + 'ticker': ['MSFT', 'MSFT', 'GOOG', 'GOOG', 'AAPL'], + 'price': [51.95, 51.95, + 720.77, 720.92, 98.00], + 'quantity': [75, 155, + 100, 100, 100], + 'bid': [np.nan, 51.97, np.nan, + np.nan, np.nan], + 'ask': [np.nan, 51.98, np.nan, + np.nan, np.nan]}, + columns=['time', 'ticker', 'price', 'quantity', + 'bid', 'ask']) + + result = pd.merge_asof(trades, quotes, + on='time', + by='ticker', + tolerance=pd.Timedelta('10ms'), + allow_exact_matches=False) + assert_frame_equal(result, expected) def test_examples3(self): """ doc-string examples """ @@ -200,14 +219,14 @@ def test_multi_index(self): # MultiIndex is prohibited trades = self.trades.set_index(['time', 'price']) quotes = self.quotes.set_index('time') - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, left_index=True, right_index=True) trades = self.trades.set_index('time') quotes = self.quotes.set_index(['time', 'bid']) - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, left_index=True, right_index=True) @@ -217,7 +236,7 @@ def test_on_and_index(self): # 'on' parameter and index together is prohibited trades = self.trades.set_index('time') quotes = self.quotes.set_index('time') - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, left_on='price', left_index=True, @@ -225,7 +244,7 @@ def test_on_and_index(self): trades = self.trades.set_index('time') quotes = self.quotes.set_index('time') - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, right_on='bid', left_index=True, @@ -399,15 +418,15 @@ def test_multiby_indexed(self): assert_frame_equal(expected, result) - with self.assertRaises(MergeError): + with pytest.raises(MergeError): pd.merge_asof(left, right, left_index=True, right_index=True, left_by=['k1', 'k2'], right_by=['k1']) - def test_basic2(self): + def test_basic2(self, datapath): - expected = self.read_data('asof2.csv') - trades = self.read_data('trades2.csv') - quotes = self.read_data('quotes2.csv', dedupe=True) + expected = self.read_data(datapath, 'asof2.csv') + trades = self.read_data(datapath, 'trades2.csv') + quotes = self.read_data(datapath, 'quotes2.csv', dedupe=True) result = merge_asof(trades, quotes, on='time', @@ -432,29 +451,29 @@ def test_valid_join_keys(self): trades = self.trades quotes = self.quotes - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, left_on='time', right_on='bid', by='ticker') - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, on=['time', 'ticker'], by='ticker') - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, by='ticker') - def test_with_duplicates(self): + def test_with_duplicates(self, datapath): q = pd.concat([self.quotes, self.quotes]).sort_values( ['time', 'ticker']).reset_index(drop=True) result = merge_asof(self.trades, q, on='time', by='ticker') - expected = self.read_data('asof.csv') + expected = self.read_data(datapath, 'asof.csv') assert_frame_equal(result, expected) def test_with_duplicates_no_on(self): @@ -474,7 +493,7 @@ def test_valid_allow_exact_matches(self): trades = self.trades quotes = self.quotes - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, on='time', by='ticker', @@ -498,27 +517,27 @@ def test_valid_tolerance(self): tolerance=1) # incompat - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, on='time', by='ticker', tolerance=1) # invalid - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades.reset_index(), quotes.reset_index(), on='index', by='ticker', tolerance=1.0) # invalid negative - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades, quotes, on='time', by='ticker', tolerance=-Timedelta('1s')) - with self.assertRaises(MergeError): + with pytest.raises(MergeError): merge_asof(trades.reset_index(), quotes.reset_index(), on='index', by='ticker', @@ -530,24 +549,24 @@ def test_non_sorted(self): quotes = self.quotes.sort_values('time', ascending=False) # we require that we are already sorted on time & quotes - self.assertFalse(trades.time.is_monotonic) - self.assertFalse(quotes.time.is_monotonic) - with self.assertRaises(ValueError): + assert not trades.time.is_monotonic + assert not quotes.time.is_monotonic + with pytest.raises(ValueError): merge_asof(trades, quotes, on='time', by='ticker') trades = self.trades.sort_values('time') - self.assertTrue(trades.time.is_monotonic) - self.assertFalse(quotes.time.is_monotonic) - with self.assertRaises(ValueError): + assert trades.time.is_monotonic + assert not quotes.time.is_monotonic + with pytest.raises(ValueError): merge_asof(trades, quotes, on='time', by='ticker') quotes = self.quotes.sort_values('time') - self.assertTrue(trades.time.is_monotonic) - self.assertTrue(quotes.time.is_monotonic) + assert trades.time.is_monotonic + assert quotes.time.is_monotonic # ok, though has dupes merge_asof(trades, self.quotes, @@ -601,26 +620,41 @@ def test_tolerance_nearest(self): def test_tolerance_tz(self): # GH 14844 left = pd.DataFrame( - {'date': pd.DatetimeIndex(start=pd.to_datetime('2016-01-02'), - freq='D', periods=5, - tz=pytz.timezone('UTC')), + {'date': pd.date_range(start=pd.to_datetime('2016-01-02'), + freq='D', periods=5, + tz=pytz.timezone('UTC')), 'value1': np.arange(5)}) right = pd.DataFrame( - {'date': pd.DatetimeIndex(start=pd.to_datetime('2016-01-01'), - freq='D', periods=5, - tz=pytz.timezone('UTC')), + {'date': pd.date_range(start=pd.to_datetime('2016-01-01'), + freq='D', periods=5, + tz=pytz.timezone('UTC')), 'value2': list("ABCDE")}) result = pd.merge_asof(left, right, on='date', tolerance=pd.Timedelta('1 day')) expected = pd.DataFrame( - {'date': pd.DatetimeIndex(start=pd.to_datetime('2016-01-02'), - freq='D', periods=5, - tz=pytz.timezone('UTC')), + {'date': pd.date_range(start=pd.to_datetime('2016-01-02'), + freq='D', periods=5, + tz=pytz.timezone('UTC')), 'value1': np.arange(5), 'value2': list("BCDEE")}) assert_frame_equal(result, expected) + def test_tolerance_float(self): + # GH22981 + left = pd.DataFrame({'a': [1.1, 3.5, 10.9], + 'left_val': ['a', 'b', 'c']}) + right = pd.DataFrame({'a': [1.0, 2.5, 3.3, 7.5, 11.5], + 'right_val': [1.0, 2.5, 3.3, 7.5, 11.5]}) + + expected = pd.DataFrame({'a': [1.1, 3.5, 10.9], + 'left_val': ['a', 'b', 'c'], + 'right_val': [1, 3.3, np.nan]}) + + result = pd.merge_asof(left, right, on='a', direction='nearest', + tolerance=0.5) + assert_frame_equal(result, expected) + def test_index_tolerance(self): # GH 15135 expected = self.tolerance.set_index('time') @@ -871,77 +905,64 @@ def test_on_float(self): assert_frame_equal(result, expected) - def test_on_specialized_type(self): - # GH13936 - for dtype in [np.uint8, np.uint16, np.uint32, np.uint64, - np.int8, np.int16, np.int32, np.int64, - np.float16, np.float32, np.float64]: - df1 = pd.DataFrame({ - 'value': [5, 2, 25, 100, 78, 120, 79], - 'symbol': list("ABCDEFG")}, - columns=['symbol', 'value']) - df1.value = dtype(df1.value) - - df2 = pd.DataFrame({ - 'value': [0, 80, 120, 125], - 'result': list('xyzw')}, - columns=['value', 'result']) - df2.value = dtype(df2.value) - - df1 = df1.sort_values('value').reset_index(drop=True) - - if dtype == np.float16: - with self.assertRaises(MergeError): - pd.merge_asof(df1, df2, on='value') - continue - - result = pd.merge_asof(df1, df2, on='value') - - expected = pd.DataFrame( - {'symbol': list("BACEGDF"), - 'value': [2, 5, 25, 78, 79, 100, 120], - 'result': list('xxxxxyz') - }, columns=['symbol', 'value', 'result']) - expected.value = dtype(expected.value) - - assert_frame_equal(result, expected) - - def test_on_specialized_type_by_int(self): - # GH13936 - for dtype in [np.uint8, np.uint16, np.uint32, np.uint64, - np.int8, np.int16, np.int32, np.int64, - np.float16, np.float32, np.float64]: - df1 = pd.DataFrame({ - 'value': [5, 2, 25, 100, 78, 120, 79], - 'key': [1, 2, 3, 2, 3, 1, 2], - 'symbol': list("ABCDEFG")}, - columns=['symbol', 'key', 'value']) - df1.value = dtype(df1.value) - - df2 = pd.DataFrame({ - 'value': [0, 80, 120, 125], - 'key': [1, 2, 2, 3], - 'result': list('xyzw')}, - columns=['value', 'key', 'result']) - df2.value = dtype(df2.value) - - df1 = df1.sort_values('value').reset_index(drop=True) - - if dtype == np.float16: - with self.assertRaises(MergeError): - pd.merge_asof(df1, df2, on='value', by='key') - else: - result = pd.merge_asof(df1, df2, on='value', by='key') + def test_on_specialized_type(self, any_real_dtype): + # see gh-13936 + dtype = np.dtype(any_real_dtype).type + + df1 = pd.DataFrame({ + "value": [5, 2, 25, 100, 78, 120, 79], + "symbol": list("ABCDEFG")}, + columns=["symbol", "value"]) + df1.value = dtype(df1.value) + + df2 = pd.DataFrame({ + "value": [0, 80, 120, 125], + "result": list("xyzw")}, + columns=["value", "result"]) + df2.value = dtype(df2.value) + + df1 = df1.sort_values("value").reset_index(drop=True) + result = pd.merge_asof(df1, df2, on="value") + + expected = pd.DataFrame( + {"symbol": list("BACEGDF"), + "value": [2, 5, 25, 78, 79, 100, 120], + "result": list("xxxxxyz") + }, columns=["symbol", "value", "result"]) + expected.value = dtype(expected.value) + + assert_frame_equal(result, expected) + + def test_on_specialized_type_by_int(self, any_real_dtype): + # see gh-13936 + dtype = np.dtype(any_real_dtype).type - expected = pd.DataFrame({ - 'symbol': list("BACEGDF"), - 'key': [2, 1, 3, 3, 2, 2, 1], - 'value': [2, 5, 25, 78, 79, 100, 120], - 'result': [np.nan, 'x', np.nan, np.nan, np.nan, 'y', 'x']}, - columns=['symbol', 'key', 'value', 'result']) - expected.value = dtype(expected.value) + df1 = pd.DataFrame({ + "value": [5, 2, 25, 100, 78, 120, 79], + "key": [1, 2, 3, 2, 3, 1, 2], + "symbol": list("ABCDEFG")}, + columns=["symbol", "key", "value"]) + df1.value = dtype(df1.value) + + df2 = pd.DataFrame({ + "value": [0, 80, 120, 125], + "key": [1, 2, 2, 3], + "result": list("xyzw")}, + columns=["value", "key", "result"]) + df2.value = dtype(df2.value) + + df1 = df1.sort_values("value").reset_index(drop=True) + result = pd.merge_asof(df1, df2, on="value", by="key") - assert_frame_equal(result, expected) + expected = pd.DataFrame({ + "symbol": list("BACEGDF"), + "key": [2, 1, 3, 3, 2, 2, 1], + "value": [2, 5, 25, 78, 79, 100, 120], + "result": [np.nan, "x", np.nan, np.nan, np.nan, "y", "x"]}, + columns=["symbol", "key", "value", "result"]) + expected.value = dtype(expected.value) + + assert_frame_equal(result, expected) def test_on_float_by_int(self): # type specialize both "by" and "on" parameters @@ -972,3 +993,46 @@ def test_on_float_by_int(self): columns=['symbol', 'exch', 'price', 'mpv']) assert_frame_equal(result, expected) + + def test_merge_datatype_error(self): + """ Tests merge datatype mismatch error """ + msg = r'merge keys \[0\] object and int64, must be the same type' + + left = pd.DataFrame({'left_val': [1, 5, 10], + 'a': ['a', 'b', 'c']}) + right = pd.DataFrame({'right_val': [1, 2, 3, 6, 7], + 'a': [1, 2, 3, 6, 7]}) + + with pytest.raises(MergeError, match=msg): + merge_asof(left, right, on='a') + + @pytest.mark.parametrize('func', [lambda x: x, lambda x: to_datetime(x)], + ids=['numeric', 'datetime']) + @pytest.mark.parametrize('side', ['left', 'right']) + def test_merge_on_nans(self, func, side): + # GH 23189 + msg = "Merge keys contain null values on {} side".format(side) + nulls = func([1.0, 5.0, np.nan]) + non_nulls = func([1.0, 5.0, 10.]) + df_null = pd.DataFrame({'a': nulls, 'left_val': ['a', 'b', 'c']}) + df = pd.DataFrame({'a': non_nulls, 'right_val': [1, 6, 11]}) + + with pytest.raises(ValueError, match=msg): + if side == 'left': + merge_asof(df_null, df, on='a') + else: + merge_asof(df, df_null, on='a') + + def test_merge_by_col_tz_aware(self): + # GH 21184 + left = pd.DataFrame( + {'by_col': pd.DatetimeIndex(['2018-01-01']).tz_localize('UTC'), + 'on_col': [2], 'values': ['a']}) + right = pd.DataFrame( + {'by_col': pd.DatetimeIndex(['2018-01-01']).tz_localize('UTC'), + 'on_col': [1], 'values': ['b']}) + result = pd.merge_asof(left, right, by='by_col', on='on_col') + expected = pd.DataFrame([ + [pd.Timestamp('2018-01-01', tz='UTC'), 2, 'a', 'b'] + ], columns=['by_col', 'on_col', 'values_x', 'values_y']) + assert_frame_equal(result, expected) diff --git a/pandas/tests/reshape/merge/test_merge_index_as_string.py b/pandas/tests/reshape/merge/test_merge_index_as_string.py new file mode 100644 index 0000000000000..12d9483af8761 --- /dev/null +++ b/pandas/tests/reshape/merge/test_merge_index_as_string.py @@ -0,0 +1,177 @@ +import numpy as np +import pytest + +from pandas import DataFrame +from pandas.util.testing import assert_frame_equal + + +@pytest.fixture +def df1(): + return DataFrame(dict( + outer=[1, 1, 1, 2, 2, 2, 2, 3, 3, 4, 4], + inner=[1, 2, 3, 1, 2, 3, 4, 1, 2, 1, 2], + v1=np.linspace(0, 1, 11))) + + +@pytest.fixture +def df2(): + return DataFrame(dict( + outer=[1, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 3], + inner=[1, 2, 2, 3, 3, 4, 2, 3, 1, 1, 2, 3], + v2=np.linspace(10, 11, 12))) + + +@pytest.fixture(params=[[], ['outer'], ['outer', 'inner']]) +def left_df(request, df1): + """ Construct left test DataFrame with specified levels + (any of 'outer', 'inner', and 'v1')""" + levels = request.param + if levels: + df1 = df1.set_index(levels) + + return df1 + + +@pytest.fixture(params=[[], ['outer'], ['outer', 'inner']]) +def right_df(request, df2): + """ Construct right test DataFrame with specified levels + (any of 'outer', 'inner', and 'v2')""" + levels = request.param + + if levels: + df2 = df2.set_index(levels) + + return df2 + + +def compute_expected(df_left, df_right, + on=None, left_on=None, right_on=None, how=None): + """ + Compute the expected merge result for the test case. + + This method computes the expected result of merging two DataFrames on + a combination of their columns and index levels. It does so by + explicitly dropping/resetting their named index levels, performing a + merge on their columns, and then finally restoring the appropriate + index in the result. + + Parameters + ---------- + df_left : DataFrame + The left DataFrame (may have zero or more named index levels) + df_right : DataFrame + The right DataFrame (may have zero or more named index levels) + on : list of str + The on parameter to the merge operation + left_on : list of str + The left_on parameter to the merge operation + right_on : list of str + The right_on parameter to the merge operation + how : str + The how parameter to the merge operation + + Returns + ------- + DataFrame + The expected merge result + """ + + # Handle on param if specified + if on is not None: + left_on, right_on = on, on + + # Compute input named index levels + left_levels = [n for n in df_left.index.names if n is not None] + right_levels = [n for n in df_right.index.names if n is not None] + + # Compute output named index levels + output_levels = [i for i in left_on + if i in right_levels and i in left_levels] + + # Drop index levels that aren't involved in the merge + drop_left = [n for n in left_levels if n not in left_on] + if drop_left: + df_left = df_left.reset_index(drop_left, drop=True) + + drop_right = [n for n in right_levels if n not in right_on] + if drop_right: + df_right = df_right.reset_index(drop_right, drop=True) + + # Convert remaining index levels to columns + reset_left = [n for n in left_levels if n in left_on] + if reset_left: + df_left = df_left.reset_index(level=reset_left) + + reset_right = [n for n in right_levels if n in right_on] + if reset_right: + df_right = df_right.reset_index(level=reset_right) + + # Perform merge + expected = df_left.merge(df_right, + left_on=left_on, + right_on=right_on, + how=how) + + # Restore index levels + if output_levels: + expected = expected.set_index(output_levels) + + return expected + + +@pytest.mark.parametrize('on,how', + [(['outer'], 'inner'), + (['inner'], 'left'), + (['outer', 'inner'], 'right'), + (['inner', 'outer'], 'outer')]) +def test_merge_indexes_and_columns_on(left_df, right_df, on, how): + + # Construct expected result + expected = compute_expected(left_df, right_df, on=on, how=how) + + # Perform merge + result = left_df.merge(right_df, on=on, how=how) + assert_frame_equal(result, expected, check_like=True) + + +@pytest.mark.parametrize('left_on,right_on,how', + [(['outer'], ['outer'], 'inner'), + (['inner'], ['inner'], 'right'), + (['outer', 'inner'], ['outer', 'inner'], 'left'), + (['inner', 'outer'], ['inner', 'outer'], 'outer')]) +def test_merge_indexes_and_columns_lefton_righton( + left_df, right_df, left_on, right_on, how): + + # Construct expected result + expected = compute_expected(left_df, right_df, + left_on=left_on, + right_on=right_on, + how=how) + + # Perform merge + result = left_df.merge(right_df, + left_on=left_on, right_on=right_on, how=how) + assert_frame_equal(result, expected, check_like=True) + + +@pytest.mark.parametrize('left_index', + ['inner', ['inner', 'outer']]) +def test_join_indexes_and_columns_on(df1, df2, left_index, join_type): + + # Construct left_df + left_df = df1.set_index(left_index) + + # Construct right_df + right_df = df2.set_index(['outer', 'inner']) + + # Result + expected = (left_df.reset_index() + .join(right_df, on=['outer', 'inner'], how=join_type, + lsuffix='_x', rsuffix='_y') + .set_index(left_index)) + + # Perform join + result = left_df.join(right_df, on=['outer', 'inner'], how=join_type, + lsuffix='_x', rsuffix='_y') + + assert_frame_equal(result, expected, check_like=True) diff --git a/pandas/tests/tools/test_merge_ordered.py b/pandas/tests/reshape/merge/test_merge_ordered.py similarity index 72% rename from pandas/tests/tools/test_merge_ordered.py rename to pandas/tests/reshape/merge/test_merge_ordered.py index e4a41ea9a28eb..414f46cdb296c 100644 --- a/pandas/tests/tools/test_merge_ordered.py +++ b/pandas/tests/reshape/merge/test_merge_ordered.py @@ -1,27 +1,20 @@ +from numpy import nan +import pytest + import pandas as pd from pandas import DataFrame, merge_ordered -from pandas.util import testing as tm from pandas.util.testing import assert_frame_equal -from numpy import nan - -class TestOrderedMerge(tm.TestCase): +class TestMergeOrdered(object): - def setUp(self): + def setup_method(self, method): self.left = DataFrame({'key': ['a', 'c', 'e'], 'lvalue': [1, 2., 3]}) self.right = DataFrame({'key': ['b', 'c', 'd', 'f'], 'rvalue': [1, 2, 3., 4]}) - def test_deprecation(self): - - with tm.assert_produces_warning(FutureWarning): - pd.ordered_merge(self.left, self.right, on='key') - - # GH #813 - def test_basic(self): result = merge_ordered(self.left, self.right, on='key') expected = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'f'], @@ -57,7 +50,7 @@ def test_multigroup(self): assert_frame_equal(result, result2.loc[:, result.columns]) result = merge_ordered(left, self.right, on='key', left_by='group') - self.assertTrue(result['group'].notnull().all()) + assert result['group'].notna().all() def test_merge_type(self): class NotADataFrame(DataFrame): @@ -69,7 +62,7 @@ def _constructor(self): nad = NotADataFrame(self.left) result = nad.merge(self.right, on='key') - tm.assertIsInstance(result, NotADataFrame) + assert isinstance(result, NotADataFrame) def test_empty_sequence_concat(self): # GH 9157 @@ -83,8 +76,28 @@ def test_empty_sequence_concat(self): ([None, None], none_pat) ] for df_seq, pattern in test_cases: - tm.assertRaisesRegexp(ValueError, pattern, pd.concat, df_seq) + with pytest.raises(ValueError, match=pattern): + pd.concat(df_seq) pd.concat([pd.DataFrame()]) pd.concat([None, pd.DataFrame()]) pd.concat([pd.DataFrame(), None]) + + def test_doc_example(self): + left = DataFrame({'group': list('aaabbb'), + 'key': ['a', 'c', 'e', 'a', 'c', 'e'], + 'lvalue': [1, 2, 3] * 2, + }) + + right = DataFrame({'key': ['b', 'c', 'd'], + 'rvalue': [1, 2, 3]}) + + result = merge_ordered(left, right, fill_method='ffill', + left_by='group') + + expected = DataFrame({'group': list('aaaaabbbbb'), + 'key': ['a', 'b', 'c', 'd', 'e'] * 2, + 'lvalue': [1, 1, 2, 2, 3] * 2, + 'rvalue': [nan, 1, 2, 3, 3] * 2}) + + assert_frame_equal(result, expected) diff --git a/pandas/tests/reshape/merge/test_multi.py b/pandas/tests/reshape/merge/test_multi.py new file mode 100644 index 0000000000000..7e8b5b1120bc6 --- /dev/null +++ b/pandas/tests/reshape/merge/test_multi.py @@ -0,0 +1,668 @@ +# pylint: disable=E1103 + +from collections import OrderedDict + +import numpy as np +from numpy import nan +from numpy.random import randn +import pytest + +import pandas as pd +from pandas import DataFrame, Index, MultiIndex, Series +from pandas.core.reshape.concat import concat +from pandas.core.reshape.merge import merge +import pandas.util.testing as tm + + +@pytest.fixture +def left(): + """left dataframe (not multi-indexed) for multi-index join tests""" + # a little relevant example with NAs + key1 = ['bar', 'bar', 'bar', 'foo', 'foo', 'baz', 'baz', 'qux', + 'qux', 'snap'] + key2 = ['two', 'one', 'three', 'one', 'two', 'one', 'two', 'two', + 'three', 'one'] + + data = np.random.randn(len(key1)) + return DataFrame({'key1': key1, 'key2': key2, 'data': data}) + + +@pytest.fixture +def right(): + """right dataframe (multi-indexed) for multi-index join tests""" + index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], + ['one', 'two', 'three']], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + names=['key1', 'key2']) + + return DataFrame(np.random.randn(10, 3), index=index, + columns=['j_one', 'j_two', 'j_three']) + + +@pytest.fixture +def left_multi(): + return ( + DataFrame( + dict(Origin=['A', 'A', 'B', 'B', 'C'], + Destination=['A', 'B', 'A', 'C', 'A'], + Period=['AM', 'AM', 'IP', 'AM', 'OP'], + TripPurp=['hbw', 'nhb', 'hbo', 'nhb', 'hbw'], + Trips=[1987, 3647, 2470, 4296, 4444]), + columns=['Origin', 'Destination', 'Period', + 'TripPurp', 'Trips']) + .set_index(['Origin', 'Destination', 'Period', 'TripPurp'])) + + +@pytest.fixture +def right_multi(): + return ( + DataFrame( + dict(Origin=['A', 'A', 'B', 'B', 'C', 'C', 'E'], + Destination=['A', 'B', 'A', 'B', 'A', 'B', 'F'], + Period=['AM', 'AM', 'IP', 'AM', 'OP', 'IP', 'AM'], + LinkType=['a', 'b', 'c', 'b', 'a', 'b', 'a'], + Distance=[100, 80, 90, 80, 75, 35, 55]), + columns=['Origin', 'Destination', 'Period', + 'LinkType', 'Distance']) + .set_index(['Origin', 'Destination', 'Period', 'LinkType'])) + + +@pytest.fixture +def on_cols_multi(): + return ['Origin', 'Destination', 'Period'] + + +@pytest.fixture +def idx_cols_multi(): + return ['Origin', 'Destination', 'Period', 'TripPurp', 'LinkType'] + + +class TestMergeMulti(object): + + def setup_method(self): + self.index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], + ['one', 'two', 'three']], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + names=['first', 'second']) + self.to_join = DataFrame(np.random.randn(10, 3), index=self.index, + columns=['j_one', 'j_two', 'j_three']) + + # a little relevant example with NAs + key1 = ['bar', 'bar', 'bar', 'foo', 'foo', 'baz', 'baz', 'qux', + 'qux', 'snap'] + key2 = ['two', 'one', 'three', 'one', 'two', 'one', 'two', 'two', + 'three', 'one'] + + data = np.random.randn(len(key1)) + self.data = DataFrame({'key1': key1, 'key2': key2, + 'data': data}) + + def test_merge_on_multikey(self, left, right, join_type): + on_cols = ['key1', 'key2'] + result = (left.join(right, on=on_cols, how=join_type) + .reset_index(drop=True)) + + expected = pd.merge(left, right.reset_index(), + on=on_cols, how=join_type) + + tm.assert_frame_equal(result, expected) + + result = (left.join(right, on=on_cols, how=join_type, sort=True) + .reset_index(drop=True)) + + expected = pd.merge(left, right.reset_index(), + on=on_cols, how=join_type, sort=True) + + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize("sort", [False, True]) + def test_left_join_multi_index(self, left, right, sort): + icols = ['1st', '2nd', '3rd'] + + def bind_cols(df): + iord = lambda a: 0 if a != a else ord(a) + f = lambda ts: ts.map(iord) - ord('a') + return (f(df['1st']) + f(df['3rd']) * 1e2 + + df['2nd'].fillna(0) * 1e4) + + def run_asserts(left, right, sort): + res = left.join(right, on=icols, how='left', sort=sort) + + assert len(left) < len(res) + 1 + assert not res['4th'].isna().any() + assert not res['5th'].isna().any() + + tm.assert_series_equal( + res['4th'], - res['5th'], check_names=False) + result = bind_cols(res.iloc[:, :-2]) + tm.assert_series_equal(res['4th'], result, check_names=False) + assert result.name is None + + if sort: + tm.assert_frame_equal( + res, res.sort_values(icols, kind='mergesort')) + + out = merge(left, right.reset_index(), on=icols, + sort=sort, how='left') + + res.index = np.arange(len(res)) + tm.assert_frame_equal(out, res) + + lc = list(map(chr, np.arange(ord('a'), ord('z') + 1))) + left = DataFrame(np.random.choice(lc, (5000, 2)), + columns=['1st', '3rd']) + left.insert(1, '2nd', np.random.randint(0, 1000, len(left))) + + i = np.random.permutation(len(left)) + right = left.iloc[i].copy() + + left['4th'] = bind_cols(left) + right['5th'] = - bind_cols(right) + right.set_index(icols, inplace=True) + + run_asserts(left, right, sort) + + # inject some nulls + left.loc[1::23, '1st'] = np.nan + left.loc[2::37, '2nd'] = np.nan + left.loc[3::43, '3rd'] = np.nan + left['4th'] = bind_cols(left) + + i = np.random.permutation(len(left)) + right = left.iloc[i, :-1] + right['5th'] = - bind_cols(right) + right.set_index(icols, inplace=True) + + run_asserts(left, right, sort) + + @pytest.mark.parametrize("sort", [False, True]) + def test_merge_right_vs_left(self, left, right, sort): + # compare left vs right merge with multikey + on_cols = ['key1', 'key2'] + merged_left_right = left.merge(right, + left_on=on_cols, right_index=True, + how='left', sort=sort) + + merge_right_left = right.merge(left, + right_on=on_cols, left_index=True, + how='right', sort=sort) + + # Reorder columns + merge_right_left = merge_right_left[merged_left_right.columns] + + tm.assert_frame_equal(merged_left_right, merge_right_left) + + def test_compress_group_combinations(self): + + # ~ 40000000 possible unique groups + key1 = tm.rands_array(10, 10000) + key1 = np.tile(key1, 2) + key2 = key1[::-1] + + df = DataFrame({'key1': key1, 'key2': key2, + 'value1': np.random.randn(20000)}) + + df2 = DataFrame({'key1': key1[::2], 'key2': key2[::2], + 'value2': np.random.randn(10000)}) + + # just to hit the label compression code path + merge(df, df2, how='outer') + + def test_left_join_index_preserve_order(self): + + on_cols = ['k1', 'k2'] + left = DataFrame({'k1': [0, 1, 2] * 8, + 'k2': ['foo', 'bar'] * 12, + 'v': np.array(np.arange(24), dtype=np.int64)}) + + index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) + right = DataFrame({'v2': [5, 7]}, index=index) + + result = left.join(right, on=on_cols) + + expected = left.copy() + expected['v2'] = np.nan + expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 + expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 + + tm.assert_frame_equal(result, expected) + + result.sort_values(on_cols, kind='mergesort', inplace=True) + expected = left.join(right, on=on_cols, sort=True) + + tm.assert_frame_equal(result, expected) + + # test join with multi dtypes blocks + left = DataFrame({'k1': [0, 1, 2] * 8, + 'k2': ['foo', 'bar'] * 12, + 'k3': np.array([0, 1, 2] * 8, dtype=np.float32), + 'v': np.array(np.arange(24), dtype=np.int32)}) + + index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) + right = DataFrame({'v2': [5, 7]}, index=index) + + result = left.join(right, on=on_cols) + + expected = left.copy() + expected['v2'] = np.nan + expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 + expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 + + tm.assert_frame_equal(result, expected) + + result = result.sort_values(on_cols, kind='mergesort') + expected = left.join(right, on=on_cols, sort=True) + + tm.assert_frame_equal(result, expected) + + def test_left_join_index_multi_match_multiindex(self): + left = DataFrame([ + ['X', 'Y', 'C', 'a'], + ['W', 'Y', 'C', 'e'], + ['V', 'Q', 'A', 'h'], + ['V', 'R', 'D', 'i'], + ['X', 'Y', 'D', 'b'], + ['X', 'Y', 'A', 'c'], + ['W', 'Q', 'B', 'f'], + ['W', 'R', 'C', 'g'], + ['V', 'Y', 'C', 'j'], + ['X', 'Y', 'B', 'd']], + columns=['cola', 'colb', 'colc', 'tag'], + index=[3, 2, 0, 1, 7, 6, 4, 5, 9, 8]) + + right = (DataFrame([ + ['W', 'R', 'C', 0], + ['W', 'Q', 'B', 3], + ['W', 'Q', 'B', 8], + ['X', 'Y', 'A', 1], + ['X', 'Y', 'A', 4], + ['X', 'Y', 'B', 5], + ['X', 'Y', 'C', 6], + ['X', 'Y', 'C', 9], + ['X', 'Q', 'C', -6], + ['X', 'R', 'C', -9], + ['V', 'Y', 'C', 7], + ['V', 'R', 'D', 2], + ['V', 'R', 'D', -1], + ['V', 'Q', 'A', -3]], + columns=['col1', 'col2', 'col3', 'val']) + .set_index(['col1', 'col2', 'col3'])) + + result = left.join(right, on=['cola', 'colb', 'colc'], how='left') + + expected = DataFrame([ + ['X', 'Y', 'C', 'a', 6], + ['X', 'Y', 'C', 'a', 9], + ['W', 'Y', 'C', 'e', nan], + ['V', 'Q', 'A', 'h', -3], + ['V', 'R', 'D', 'i', 2], + ['V', 'R', 'D', 'i', -1], + ['X', 'Y', 'D', 'b', nan], + ['X', 'Y', 'A', 'c', 1], + ['X', 'Y', 'A', 'c', 4], + ['W', 'Q', 'B', 'f', 3], + ['W', 'Q', 'B', 'f', 8], + ['W', 'R', 'C', 'g', 0], + ['V', 'Y', 'C', 'j', 7], + ['X', 'Y', 'B', 'd', 5]], + columns=['cola', 'colb', 'colc', 'tag', 'val'], + index=[3, 3, 2, 0, 1, 1, 7, 6, 6, 4, 4, 5, 9, 8]) + + tm.assert_frame_equal(result, expected) + + result = left.join(right, on=['cola', 'colb', 'colc'], + how='left', sort=True) + + expected = expected.sort_values(['cola', 'colb', 'colc'], + kind='mergesort') + + tm.assert_frame_equal(result, expected) + + def test_left_join_index_multi_match(self): + left = DataFrame([ + ['c', 0], + ['b', 1], + ['a', 2], + ['b', 3]], + columns=['tag', 'val'], + index=[2, 0, 1, 3]) + + right = (DataFrame([ + ['a', 'v'], + ['c', 'w'], + ['c', 'x'], + ['d', 'y'], + ['a', 'z'], + ['c', 'r'], + ['e', 'q'], + ['c', 's']], + columns=['tag', 'char']) + .set_index('tag')) + + result = left.join(right, on='tag', how='left') + + expected = DataFrame([ + ['c', 0, 'w'], + ['c', 0, 'x'], + ['c', 0, 'r'], + ['c', 0, 's'], + ['b', 1, nan], + ['a', 2, 'v'], + ['a', 2, 'z'], + ['b', 3, nan]], + columns=['tag', 'val', 'char'], + index=[2, 2, 2, 2, 0, 1, 1, 3]) + + tm.assert_frame_equal(result, expected) + + result = left.join(right, on='tag', how='left', sort=True) + expected2 = expected.sort_values('tag', kind='mergesort') + + tm.assert_frame_equal(result, expected2) + + # GH7331 - maintain left frame order in left merge + result = merge(left, right.reset_index(), how='left', on='tag') + expected.index = np.arange(len(expected)) + tm.assert_frame_equal(result, expected) + + def test_left_merge_na_buglet(self): + left = DataFrame({'id': list('abcde'), 'v1': randn(5), + 'v2': randn(5), 'dummy': list('abcde'), + 'v3': randn(5)}, + columns=['id', 'v1', 'v2', 'dummy', 'v3']) + right = DataFrame({'id': ['a', 'b', np.nan, np.nan, np.nan], + 'sv3': [1.234, 5.678, np.nan, np.nan, np.nan]}) + + result = merge(left, right, on='id', how='left') + + rdf = right.drop(['id'], axis=1) + expected = left.join(rdf) + tm.assert_frame_equal(result, expected) + + def test_merge_na_keys(self): + data = [[1950, "A", 1.5], + [1950, "B", 1.5], + [1955, "B", 1.5], + [1960, "B", np.nan], + [1970, "B", 4.], + [1950, "C", 4.], + [1960, "C", np.nan], + [1965, "C", 3.], + [1970, "C", 4.]] + + frame = DataFrame(data, columns=["year", "panel", "data"]) + + other_data = [[1960, 'A', np.nan], + [1970, 'A', np.nan], + [1955, 'A', np.nan], + [1965, 'A', np.nan], + [1965, 'B', np.nan], + [1955, 'C', np.nan]] + other = DataFrame(other_data, columns=['year', 'panel', 'data']) + + result = frame.merge(other, how='outer') + + expected = frame.fillna(-999).merge(other.fillna(-999), how='outer') + expected = expected.replace(-999, np.nan) + + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize("klass", [None, np.asarray, Series, Index]) + def test_merge_datetime_index(self, klass): + # see gh-19038 + df = DataFrame([1, 2, 3], + ["2016-01-01", "2017-01-01", "2018-01-01"], + columns=["a"]) + df.index = pd.to_datetime(df.index) + on_vector = df.index.year + + if klass is not None: + on_vector = klass(on_vector) + + expected = DataFrame( + OrderedDict([ + ("a", [1, 2, 3]), + ("key_1", [2016, 2017, 2018]), + ]) + ) + + result = df.merge(df, on=["a", on_vector], how="inner") + tm.assert_frame_equal(result, expected) + + expected = DataFrame( + OrderedDict([ + ("key_0", [2016, 2017, 2018]), + ("a_x", [1, 2, 3]), + ("a_y", [1, 2, 3]), + ]) + ) + + result = df.merge(df, on=[df.index.year], how="inner") + tm.assert_frame_equal(result, expected) + + def test_join_multi_levels(self): + + # GH 3662 + # merge multi-levels + household = ( + DataFrame( + dict(household_id=[1, 2, 3], + male=[0, 1, 0], + wealth=[196087.3, 316478.7, 294750]), + columns=['household_id', 'male', 'wealth']) + .set_index('household_id')) + portfolio = ( + DataFrame( + dict(household_id=[1, 2, 2, 3, 3, 3, 4], + asset_id=["nl0000301109", "nl0000289783", "gb00b03mlx29", + "gb00b03mlx29", "lu0197800237", "nl0000289965", + np.nan], + name=["ABN Amro", "Robeco", "Royal Dutch Shell", + "Royal Dutch Shell", + "AAB Eastern Europe Equity Fund", + "Postbank BioTech Fonds", np.nan], + share=[1.0, 0.4, 0.6, 0.15, 0.6, 0.25, 1.0]), + columns=['household_id', 'asset_id', 'name', 'share']) + .set_index(['household_id', 'asset_id'])) + result = household.join(portfolio, how='inner') + expected = ( + DataFrame( + dict(male=[0, 1, 1, 0, 0, 0], + wealth=[196087.3, 316478.7, 316478.7, + 294750.0, 294750.0, 294750.0], + name=['ABN Amro', 'Robeco', 'Royal Dutch Shell', + 'Royal Dutch Shell', + 'AAB Eastern Europe Equity Fund', + 'Postbank BioTech Fonds'], + share=[1.00, 0.40, 0.60, 0.15, 0.60, 0.25], + household_id=[1, 2, 2, 3, 3, 3], + asset_id=['nl0000301109', 'nl0000289783', 'gb00b03mlx29', + 'gb00b03mlx29', 'lu0197800237', + 'nl0000289965'])) + .set_index(['household_id', 'asset_id']) + .reindex(columns=['male', 'wealth', 'name', 'share'])) + tm.assert_frame_equal(result, expected) + + # equivalency + result = (merge(household.reset_index(), portfolio.reset_index(), + on=['household_id'], how='inner') + .set_index(['household_id', 'asset_id'])) + tm.assert_frame_equal(result, expected) + + result = household.join(portfolio, how='outer') + expected = (concat([ + expected, + (DataFrame( + dict(share=[1.00]), + index=MultiIndex.from_tuples( + [(4, np.nan)], + names=['household_id', 'asset_id']))) + ], axis=0, sort=True).reindex(columns=expected.columns)) + tm.assert_frame_equal(result, expected) + + # invalid cases + household.index.name = 'foo' + + with pytest.raises(ValueError): + household.join(portfolio, how='inner') + + portfolio2 = portfolio.copy() + portfolio2.index.set_names(['household_id', 'foo']) + + with pytest.raises(ValueError): + portfolio2.join(portfolio, how='inner') + + def test_join_multi_levels2(self): + + # some more advanced merges + # GH6360 + household = ( + DataFrame( + dict(household_id=[1, 2, 2, 3, 3, 3, 4], + asset_id=["nl0000301109", "nl0000301109", "gb00b03mlx29", + "gb00b03mlx29", "lu0197800237", "nl0000289965", + np.nan], + share=[1.0, 0.4, 0.6, 0.15, 0.6, 0.25, 1.0]), + columns=['household_id', 'asset_id', 'share']) + .set_index(['household_id', 'asset_id'])) + + log_return = DataFrame(dict( + asset_id=["gb00b03mlx29", "gb00b03mlx29", + "gb00b03mlx29", "lu0197800237", "lu0197800237"], + t=[233, 234, 235, 180, 181], + log_return=[.09604978, -.06524096, .03532373, .03025441, .036997] + )).set_index(["asset_id", "t"]) + + expected = ( + DataFrame(dict( + household_id=[2, 2, 2, 3, 3, 3, 3, 3], + asset_id=["gb00b03mlx29", "gb00b03mlx29", + "gb00b03mlx29", "gb00b03mlx29", + "gb00b03mlx29", "gb00b03mlx29", + "lu0197800237", "lu0197800237"], + t=[233, 234, 235, 233, 234, 235, 180, 181], + share=[0.6, 0.6, 0.6, 0.15, 0.15, 0.15, 0.6, 0.6], + log_return=[.09604978, -.06524096, .03532373, + .09604978, -.06524096, .03532373, + .03025441, .036997] + )) + .set_index(["household_id", "asset_id", "t"]) + .reindex(columns=['share', 'log_return'])) + + # this is the equivalency + result = (merge(household.reset_index(), log_return.reset_index(), + on=['asset_id'], how='inner') + .set_index(['household_id', 'asset_id', 't'])) + tm.assert_frame_equal(result, expected) + + expected = ( + DataFrame(dict( + household_id=[1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4], + asset_id=["nl0000301109", "nl0000301109", "gb00b03mlx29", + "gb00b03mlx29", "gb00b03mlx29", + "gb00b03mlx29", "gb00b03mlx29", "gb00b03mlx29", + "lu0197800237", "lu0197800237", + "nl0000289965", None], + t=[None, None, 233, 234, 235, 233, 234, + 235, 180, 181, None, None], + share=[1.0, 0.4, 0.6, 0.6, 0.6, 0.15, + 0.15, 0.15, 0.6, 0.6, 0.25, 1.0], + log_return=[None, None, .09604978, -.06524096, .03532373, + .09604978, -.06524096, .03532373, + .03025441, .036997, None, None] + )) + .set_index(["household_id", "asset_id", "t"]) + .reindex(columns=['share', 'log_return'])) + + result = (merge(household.reset_index(), log_return.reset_index(), + on=['asset_id'], how='outer') + .set_index(['household_id', 'asset_id', 't'])) + + tm.assert_frame_equal(result, expected) + + +class TestJoinMultiMulti(object): + + def test_join_multi_multi(self, left_multi, right_multi, join_type, + on_cols_multi, idx_cols_multi): + # Multi-index join tests + expected = (pd.merge(left_multi.reset_index(), + right_multi.reset_index(), + how=join_type, on=on_cols_multi). + set_index(idx_cols_multi).sort_index()) + + result = left_multi.join(right_multi, how=join_type).sort_index() + tm.assert_frame_equal(result, expected) + + def test_join_multi_empty_frames(self, left_multi, right_multi, join_type, + on_cols_multi, idx_cols_multi): + + left_multi = left_multi.drop(columns=left_multi.columns) + right_multi = right_multi.drop(columns=right_multi.columns) + + expected = (pd.merge(left_multi.reset_index(), + right_multi.reset_index(), + how=join_type, on=on_cols_multi) + .set_index(idx_cols_multi).sort_index()) + + result = left_multi.join(right_multi, how=join_type).sort_index() + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize("box", [None, np.asarray, Series, Index]) + def test_merge_datetime_index(self, box): + # see gh-19038 + df = DataFrame([1, 2, 3], + ["2016-01-01", "2017-01-01", "2018-01-01"], + columns=["a"]) + df.index = pd.to_datetime(df.index) + on_vector = df.index.year + + if box is not None: + on_vector = box(on_vector) + + expected = DataFrame( + OrderedDict([ + ("a", [1, 2, 3]), + ("key_1", [2016, 2017, 2018]), + ]) + ) + + result = df.merge(df, on=["a", on_vector], how="inner") + tm.assert_frame_equal(result, expected) + + expected = DataFrame( + OrderedDict([ + ("key_0", [2016, 2017, 2018]), + ("a_x", [1, 2, 3]), + ("a_y", [1, 2, 3]), + ]) + ) + + result = df.merge(df, on=[df.index.year], how="inner") + tm.assert_frame_equal(result, expected) + + def test_single_common_level(self): + index_left = pd.MultiIndex.from_tuples([('K0', 'X0'), ('K0', 'X1'), + ('K1', 'X2')], + names=['key', 'X']) + + left = pd.DataFrame({'A': ['A0', 'A1', 'A2'], + 'B': ['B0', 'B1', 'B2']}, + index=index_left) + + index_right = pd.MultiIndex.from_tuples([('K0', 'Y0'), ('K1', 'Y1'), + ('K2', 'Y2'), ('K2', 'Y3')], + names=['key', 'Y']) + + right = pd.DataFrame({'C': ['C0', 'C1', 'C2', 'C3'], + 'D': ['D0', 'D1', 'D2', 'D3']}, + index=index_right) + + result = left.join(right) + expected = (pd.merge(left.reset_index(), right.reset_index(), + on=['key'], how='inner') + .set_index(['key', 'X', 'Y'])) + + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/tools/test_concat.py b/pandas/tests/reshape/test_concat.py similarity index 59% rename from pandas/tests/tools/test_concat.py rename to pandas/tests/reshape/test_concat.py index c41924a7987bd..a186d32ed8800 100644 --- a/pandas/tests/tools/test_concat.py +++ b/pandas/tests/reshape/test_concat.py @@ -1,25 +1,47 @@ +from collections import deque +import datetime as dt +from datetime import datetime +from decimal import Decimal +from itertools import combinations from warnings import catch_warnings + +import dateutil import numpy as np from numpy.random import randn +import pytest + +from pandas.compat import PY2, Iterable, StringIO, iteritems + +from pandas.core.dtypes.dtypes import CategoricalDtype -from datetime import datetime -from pandas.compat import StringIO, iteritems import pandas as pd -from pandas import (DataFrame, concat, - read_csv, isnull, Series, date_range, - Index, Panel, MultiIndex, Timestamp, - DatetimeIndex) +from pandas import ( + Categorical, DataFrame, DatetimeIndex, Index, MultiIndex, Panel, Series, + Timestamp, concat, date_range, isna, read_csv) +from pandas.tests.extension.decimal import to_decimal from pandas.util import testing as tm -from pandas.util.testing import (assert_frame_equal, - makeCustomDataframe as mkdf, - assert_almost_equal) +from pandas.util.testing import assert_frame_equal, makeCustomDataframe as mkdf -import pytest + +@pytest.fixture(params=[True, False]) +def sort(request): + """Boolean sort keyword for concat and DataFrame.append.""" + return request.param + + +@pytest.fixture(params=[True, False, None]) +def sort_with_none(request): + """Boolean sort keyword for concat and DataFrame.append. + + Includes the default of None + """ + # TODO: Replace with sort once keyword changes. + return request.param -class ConcatenateBase(tm.TestCase): +class ConcatenateBase(object): - def setUp(self): + def setup_method(self, method): self.frame = DataFrame(tm.getSeriesData()) self.mixed_frame = self.frame.copy() self.mixed_frame['foo'] = 'bar' @@ -31,7 +53,7 @@ class TestConcatAppendCommon(ConcatenateBase): Test common dtype coercion rules between concat and append. """ - def setUp(self): + def setup_method(self, method): dt_data = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), @@ -65,14 +87,14 @@ def _check_expected_dtype(self, obj, label): """ if isinstance(obj, pd.Index): if label == 'bool': - self.assertEqual(obj.dtype, 'object') + assert obj.dtype == 'object' else: - self.assertEqual(obj.dtype, label) + assert obj.dtype == label elif isinstance(obj, pd.Series): if label.startswith('period'): - self.assertEqual(obj.dtype, 'object') + assert obj.dtype == 'Period[M]' else: - self.assertEqual(obj.dtype, label) + assert obj.dtype == label else: raise ValueError @@ -124,10 +146,10 @@ def test_concatlike_same_dtypes(self): tm.assert_index_equal(res, exp) # cannot append non-index - with tm.assertRaisesRegexp(TypeError, 'all inputs must be Index'): + with pytest.raises(TypeError, match='all inputs must be Index'): pd.Index(vals1).append(vals2) - with tm.assertRaisesRegexp(TypeError, 'all inputs must be Index'): + with pytest.raises(TypeError, match='all inputs must be Index'): pd.Index(vals1).append([pd.Index(vals2), vals3]) # ----- Series ----- # @@ -174,17 +196,19 @@ def test_concatlike_same_dtypes(self): tm.assert_series_equal(res, exp, check_index_type=True) # cannot append non-index - msg = "cannot concatenate a non-NDFrame object" - with tm.assertRaisesRegexp(TypeError, msg): + msg = (r'cannot concatenate object of type \"(.+?)\";' + ' only pd.Series, pd.DataFrame, and pd.Panel' + r' \(deprecated\) objs are valid') + with pytest.raises(TypeError, match=msg): pd.Series(vals1).append(vals2) - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): pd.Series(vals1).append([pd.Series(vals2), vals3]) - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): pd.concat([pd.Series(vals1), vals2]) - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): pd.concat([pd.Series(vals1), pd.Series(vals2), vals3]) def test_concatlike_dtypes_coercion(self): @@ -272,103 +296,103 @@ def test_concatlike_common_coerce_to_pandas_object(self): res = dti.append(tdi) tm.assert_index_equal(res, exp) - tm.assertIsInstance(res[0], pd.Timestamp) - tm.assertIsInstance(res[-1], pd.Timedelta) + assert isinstance(res[0], pd.Timestamp) + assert isinstance(res[-1], pd.Timedelta) dts = pd.Series(dti) tds = pd.Series(tdi) res = dts.append(tds) tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - tm.assertIsInstance(res.iloc[0], pd.Timestamp) - tm.assertIsInstance(res.iloc[-1], pd.Timedelta) + assert isinstance(res.iloc[0], pd.Timestamp) + assert isinstance(res.iloc[-1], pd.Timedelta) res = pd.concat([dts, tds]) tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - tm.assertIsInstance(res.iloc[0], pd.Timestamp) - tm.assertIsInstance(res.iloc[-1], pd.Timedelta) + assert isinstance(res.iloc[0], pd.Timestamp) + assert isinstance(res.iloc[-1], pd.Timedelta) - def test_concatlike_datetimetz(self): + def test_concatlike_datetimetz(self, tz_aware_fixture): + tz = tz_aware_fixture # GH 7795 - for tz in ['UTC', 'US/Eastern', 'Asia/Tokyo']: - dti1 = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], tz=tz) - dti2 = pd.DatetimeIndex(['2012-01-01', '2012-01-02'], tz=tz) - - exp = pd.DatetimeIndex(['2011-01-01', '2011-01-02', - '2012-01-01', '2012-01-02'], tz=tz) - - res = dti1.append(dti2) - tm.assert_index_equal(res, exp) + dti1 = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], tz=tz) + dti2 = pd.DatetimeIndex(['2012-01-01', '2012-01-02'], tz=tz) - dts1 = pd.Series(dti1) - dts2 = pd.Series(dti2) - res = dts1.append(dts2) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) + exp = pd.DatetimeIndex(['2011-01-01', '2011-01-02', + '2012-01-01', '2012-01-02'], tz=tz) - res = pd.concat([dts1, dts2]) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - - def test_concatlike_datetimetz_short(self): - # GH 7795 - for tz in ['UTC', 'US/Eastern', 'Asia/Tokyo', 'EST5EDT']: - - ix1 = pd.DatetimeIndex(start='2014-07-15', end='2014-07-17', - freq='D', tz=tz) - ix2 = pd.DatetimeIndex(['2014-07-11', '2014-07-21'], tz=tz) - df1 = pd.DataFrame(0, index=ix1, columns=['A', 'B']) - df2 = pd.DataFrame(0, index=ix2, columns=['A', 'B']) + res = dti1.append(dti2) + tm.assert_index_equal(res, exp) - exp_idx = pd.DatetimeIndex(['2014-07-15', '2014-07-16', - '2014-07-17', '2014-07-11', - '2014-07-21'], tz=tz) - exp = pd.DataFrame(0, index=exp_idx, columns=['A', 'B']) + dts1 = pd.Series(dti1) + dts2 = pd.Series(dti2) + res = dts1.append(dts2) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - tm.assert_frame_equal(df1.append(df2), exp) - tm.assert_frame_equal(pd.concat([df1, df2]), exp) + res = pd.concat([dts1, dts2]) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - def test_concatlike_datetimetz_to_object(self): + @pytest.mark.parametrize('tz', + ['UTC', 'US/Eastern', 'Asia/Tokyo', 'EST5EDT']) + def test_concatlike_datetimetz_short(self, tz): + # GH#7795 + ix1 = pd.date_range(start='2014-07-15', end='2014-07-17', + freq='D', tz=tz) + ix2 = pd.DatetimeIndex(['2014-07-11', '2014-07-21'], tz=tz) + df1 = pd.DataFrame(0, index=ix1, columns=['A', 'B']) + df2 = pd.DataFrame(0, index=ix2, columns=['A', 'B']) + + exp_idx = pd.DatetimeIndex(['2014-07-15', '2014-07-16', + '2014-07-17', '2014-07-11', + '2014-07-21'], tz=tz) + exp = pd.DataFrame(0, index=exp_idx, columns=['A', 'B']) + + tm.assert_frame_equal(df1.append(df2), exp) + tm.assert_frame_equal(pd.concat([df1, df2]), exp) + + def test_concatlike_datetimetz_to_object(self, tz_aware_fixture): + tz = tz_aware_fixture # GH 13660 # different tz coerces to object - for tz in ['UTC', 'US/Eastern', 'Asia/Tokyo']: - dti1 = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], tz=tz) - dti2 = pd.DatetimeIndex(['2012-01-01', '2012-01-02']) + dti1 = pd.DatetimeIndex(['2011-01-01', '2011-01-02'], tz=tz) + dti2 = pd.DatetimeIndex(['2012-01-01', '2012-01-02']) - exp = pd.Index([pd.Timestamp('2011-01-01', tz=tz), - pd.Timestamp('2011-01-02', tz=tz), - pd.Timestamp('2012-01-01'), - pd.Timestamp('2012-01-02')], dtype=object) + exp = pd.Index([pd.Timestamp('2011-01-01', tz=tz), + pd.Timestamp('2011-01-02', tz=tz), + pd.Timestamp('2012-01-01'), + pd.Timestamp('2012-01-02')], dtype=object) - res = dti1.append(dti2) - tm.assert_index_equal(res, exp) + res = dti1.append(dti2) + tm.assert_index_equal(res, exp) - dts1 = pd.Series(dti1) - dts2 = pd.Series(dti2) - res = dts1.append(dts2) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) + dts1 = pd.Series(dti1) + dts2 = pd.Series(dti2) + res = dts1.append(dts2) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - res = pd.concat([dts1, dts2]) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) + res = pd.concat([dts1, dts2]) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - # different tz - dti3 = pd.DatetimeIndex(['2012-01-01', '2012-01-02'], - tz='US/Pacific') + # different tz + dti3 = pd.DatetimeIndex(['2012-01-01', '2012-01-02'], + tz='US/Pacific') - exp = pd.Index([pd.Timestamp('2011-01-01', tz=tz), - pd.Timestamp('2011-01-02', tz=tz), - pd.Timestamp('2012-01-01', tz='US/Pacific'), - pd.Timestamp('2012-01-02', tz='US/Pacific')], - dtype=object) + exp = pd.Index([pd.Timestamp('2011-01-01', tz=tz), + pd.Timestamp('2011-01-02', tz=tz), + pd.Timestamp('2012-01-01', tz='US/Pacific'), + pd.Timestamp('2012-01-02', tz='US/Pacific')], + dtype=object) - res = dti1.append(dti3) - # tm.assert_index_equal(res, exp) + res = dti1.append(dti3) + # tm.assert_index_equal(res, exp) - dts1 = pd.Series(dti1) - dts3 = pd.Series(dti3) - res = dts1.append(dts3) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) + dts1 = pd.Series(dti1) + dts3 = pd.Series(dti3) + res = dts1.append(dts3) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) - res = pd.concat([dts1, dts3]) - tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) + res = pd.concat([dts1, dts3]) + tm.assert_series_equal(res, pd.Series(exp, index=[0, 1, 0, 1])) def test_concatlike_common_period(self): # GH 13660 @@ -467,14 +491,23 @@ def test_concat_categorical(self): tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) - # completelly different categories (same dtype) => not-category + # completely different categories (same dtype) => not-category s1 = pd.Series([10, 11, np.nan], dtype='category') s2 = pd.Series([np.nan, 1, 3, 2], dtype='category') - exp = pd.Series([10, 11, np.nan, np.nan, 1, 3, 2]) + exp = pd.Series([10, 11, np.nan, np.nan, 1, 3, 2], dtype='object') tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) + def test_union_categorical_same_categories_different_order(self): + # https://github.com/pandas-dev/pandas/issues/19096 + a = pd.Series(Categorical(['a', 'b', 'c'], categories=['a', 'b', 'c'])) + b = pd.Series(Categorical(['a', 'b', 'c'], categories=['b', 'a', 'c'])) + result = pd.concat([a, b], ignore_index=True) + expected = pd.Series(Categorical(['a', 'b', 'c', 'a', 'b', 'c'], + categories=['a', 'b', 'c'])) + tm.assert_series_equal(result, expected) + def test_concat_categorical_coercion(self): # GH 13524 @@ -482,12 +515,12 @@ def test_concat_categorical_coercion(self): s1 = pd.Series([1, 2, np.nan], dtype='category') s2 = pd.Series([2, 1, 2]) - exp = pd.Series([1, 2, np.nan, 2, 1, 2]) + exp = pd.Series([1, 2, np.nan, 2, 1, 2], dtype='object') tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) # result shouldn't be affected by 1st elem dtype - exp = pd.Series([2, 1, 2, 1, 2, np.nan]) + exp = pd.Series([2, 1, 2, 1, 2, np.nan], dtype='object') tm.assert_series_equal(pd.concat([s2, s1], ignore_index=True), exp) tm.assert_series_equal(s2.append(s1, ignore_index=True), exp) @@ -503,15 +536,15 @@ def test_concat_categorical_coercion(self): tm.assert_series_equal(pd.concat([s2, s1], ignore_index=True), exp) tm.assert_series_equal(s2.append(s1, ignore_index=True), exp) - # completelly different categories => not-category + # completely different categories => not-category s1 = pd.Series([10, 11, np.nan], dtype='category') s2 = pd.Series([1, 3, 2]) - exp = pd.Series([10, 11, np.nan, 1, 3, 2]) + exp = pd.Series([10, 11, np.nan, 1, 3, 2], dtype='object') tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) - exp = pd.Series([1, 3, 2, 10, 11, np.nan]) + exp = pd.Series([1, 3, 2, 10, 11, np.nan], dtype='object') tm.assert_series_equal(pd.concat([s2, s1], ignore_index=True), exp) tm.assert_series_equal(s2.append(s1, ignore_index=True), exp) @@ -547,11 +580,13 @@ def test_concat_categorical_3elem_coercion(self): s2 = pd.Series([2, 1, 2], dtype='category') s3 = pd.Series([1, 2, 1, 2, np.nan]) - exp = pd.Series([1, 2, np.nan, 2, 1, 2, 1, 2, 1, 2, np.nan]) + exp = pd.Series([1, 2, np.nan, 2, 1, 2, 1, 2, 1, 2, np.nan], + dtype='object') tm.assert_series_equal(pd.concat([s1, s2, s3], ignore_index=True), exp) tm.assert_series_equal(s1.append([s2, s3], ignore_index=True), exp) - exp = pd.Series([1, 2, 1, 2, np.nan, 1, 2, np.nan, 2, 1, 2]) + exp = pd.Series([1, 2, 1, 2, np.nan, 1, 2, np.nan, 2, 1, 2], + dtype='object') tm.assert_series_equal(pd.concat([s3, s1, s2], ignore_index=True), exp) tm.assert_series_equal(s3.append([s1, s2], ignore_index=True), exp) @@ -635,7 +670,7 @@ def test_concat_categorical_coercion_nan(self): s1 = pd.Series([1, np.nan], dtype='category') s2 = pd.Series([np.nan, np.nan]) - exp = pd.Series([1, np.nan, np.nan, np.nan]) + exp = pd.Series([1, np.nan, np.nan, np.nan], dtype='object') tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) @@ -643,7 +678,7 @@ def test_concat_categorical_coercion_nan(self): s1 = pd.Series([np.nan, np.nan], dtype='category') s2 = pd.Series([np.nan, np.nan]) - exp = pd.Series([np.nan, np.nan, np.nan, np.nan], dtype=object) + exp = pd.Series([np.nan, np.nan, np.nan, np.nan]) tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), exp) tm.assert_series_equal(s1.append(s2, ignore_index=True), exp) tm.assert_series_equal(pd.concat([s2, s1], ignore_index=True), exp) @@ -677,7 +712,7 @@ def test_concat_categorical_empty(self): tm.assert_series_equal(s1.append(s2, ignore_index=True), s2) s1 = pd.Series([], dtype='category') - s2 = pd.Series([]) + s2 = pd.Series([], dtype='object') # different dtype => not-category tm.assert_series_equal(pd.concat([s1, s2], ignore_index=True), s2) @@ -699,7 +734,7 @@ def test_concat_categorical_empty(self): class TestAppend(ConcatenateBase): - def test_append(self): + def test_append(self, sort): begin_index = self.frame.index[:5] end_index = self.frame.index[5:] @@ -707,25 +742,26 @@ def test_append(self): end_frame = self.frame.reindex(end_index) appended = begin_frame.append(end_frame) - assert_almost_equal(appended['A'], self.frame['A']) + tm.assert_almost_equal(appended['A'], self.frame['A']) del end_frame['A'] - partial_appended = begin_frame.append(end_frame) - self.assertIn('A', partial_appended) + partial_appended = begin_frame.append(end_frame, sort=sort) + assert 'A' in partial_appended - partial_appended = end_frame.append(begin_frame) - self.assertIn('A', partial_appended) + partial_appended = end_frame.append(begin_frame, sort=sort) + assert 'A' in partial_appended # mixed type handling appended = self.mixed_frame[:5].append(self.mixed_frame[5:]) - assert_frame_equal(appended, self.mixed_frame) + tm.assert_frame_equal(appended, self.mixed_frame) # what to test here - mixed_appended = self.mixed_frame[:5].append(self.frame[5:]) - mixed_appended2 = self.frame[:5].append(self.mixed_frame[5:]) + mixed_appended = self.mixed_frame[:5].append(self.frame[5:], sort=sort) + mixed_appended2 = self.frame[:5].append(self.mixed_frame[5:], + sort=sort) # all equal except 'foo' column - assert_frame_equal( + tm.assert_frame_equal( mixed_appended.reindex(columns=['A', 'B', 'C', 'D']), mixed_appended2.reindex(columns=['A', 'B', 'C', 'D'])) @@ -733,30 +769,30 @@ def test_append(self): empty = DataFrame({}) appended = self.frame.append(empty) - assert_frame_equal(self.frame, appended) - self.assertIsNot(appended, self.frame) + tm.assert_frame_equal(self.frame, appended) + assert appended is not self.frame appended = empty.append(self.frame) - assert_frame_equal(self.frame, appended) - self.assertIsNot(appended, self.frame) + tm.assert_frame_equal(self.frame, appended) + assert appended is not self.frame - # overlap - self.assertRaises(ValueError, self.frame.append, self.frame, - verify_integrity=True) + # Overlap + msg = "Indexes have overlapping values" + with pytest.raises(ValueError, match=msg): + self.frame.append(self.frame, verify_integrity=True) - # new columns - # GH 6129 + # see gh-6129: new columns df = DataFrame({'a': {'x': 1, 'y': 2}, 'b': {'x': 3, 'y': 4}}) row = Series([5, 6, 7], index=['a', 'b', 'c'], name='z') expected = DataFrame({'a': {'x': 1, 'y': 2, 'z': 5}, 'b': { 'x': 3, 'y': 4, 'z': 6}, 'c': {'z': 7}}) result = df.append(row) - assert_frame_equal(result, expected) + tm.assert_frame_equal(result, expected) - def test_append_length0_frame(self): + def test_append_length0_frame(self, sort): df = DataFrame(columns=['A', 'B', 'C']) df3 = DataFrame(index=[0, 1], columns=['A', 'B']) - df5 = df.append(df3) + df5 = df.append(df3, sort=sort) expected = DataFrame(index=[0, 1], columns=['A', 'B', 'C']) assert_frame_equal(df5, expected) @@ -777,7 +813,33 @@ def test_append_records(self): expected = DataFrame(np.concatenate((arr1, arr2))) assert_frame_equal(result, expected) - def test_append_different_columns(self): + # rewrite sort fixture, since we also want to test default of None + def test_append_sorts(self, sort_with_none): + df1 = pd.DataFrame({"a": [1, 2], "b": [1, 2]}, columns=['b', 'a']) + df2 = pd.DataFrame({"a": [1, 2], 'c': [3, 4]}, index=[2, 3]) + + if sort_with_none is None: + # only warn if not explicitly specified + # don't check stacklevel since its set for concat, and append + # has an extra stack. + ctx = tm.assert_produces_warning(FutureWarning, + check_stacklevel=False) + else: + ctx = tm.assert_produces_warning(None) + + with ctx: + result = df1.append(df2, sort=sort_with_none) + + # for None / True + expected = pd.DataFrame({"b": [1, 2, None, None], + "a": [1, 2, 1, 2], + "c": [None, None, 3, 4]}, + columns=['a', 'b', 'c']) + if sort_with_none is False: + expected = expected[['b', 'a', 'c']] + tm.assert_frame_equal(result, expected) + + def test_append_different_columns(self, sort): df = DataFrame({'bools': np.random.randn(10) > 0, 'ints': np.random.randint(0, 10, 10), 'floats': np.random.randn(10), @@ -786,11 +848,11 @@ def test_append_different_columns(self): a = df[:5].loc[:, ['bools', 'ints', 'floats']] b = df[5:].loc[:, ['strings', 'ints', 'floats']] - appended = a.append(b) - self.assertTrue(isnull(appended['strings'][0:4]).all()) - self.assertTrue(isnull(appended['bools'][5:]).all()) + appended = a.append(b, sort=sort) + assert isna(appended['strings'][0:4]).all() + assert isna(appended['bools'][5:]).all() - def test_append_many(self): + def test_append_many(self, sort): chunks = [self.frame[:5], self.frame[5:10], self.frame[10:15], self.frame[15:]] @@ -799,10 +861,10 @@ def test_append_many(self): chunks[-1] = chunks[-1].copy() chunks[-1]['foo'] = 'bar' - result = chunks[0].append(chunks[1:]) + result = chunks[0].append(chunks[1:], sort=sort) tm.assert_frame_equal(result.loc[:, self.frame.columns], self.frame) - self.assertTrue((result['foo'][15:] == 'bar').all()) - self.assertTrue(result['foo'][:15].isnull().all()) + assert (result['foo'][15:] == 'bar').all() + assert result['foo'][:15].isna().all() def test_append_preserve_index_name(self): # #980 @@ -813,14 +875,115 @@ def test_append_preserve_index_name(self): df2 = df2.set_index(['A']) result = df1.append(df2) - self.assertEqual(result.index.name, 'A') + assert result.index.name == 'A' + + indexes_can_append = [ + pd.RangeIndex(3), + pd.Index([4, 5, 6]), + pd.Index([4.5, 5.5, 6.5]), + pd.Index(list('abc')), + pd.CategoricalIndex('A B C'.split()), + pd.CategoricalIndex('D E F'.split(), ordered=True), + pd.DatetimeIndex([dt.datetime(2013, 1, 3, 0, 0), + dt.datetime(2013, 1, 3, 6, 10), + dt.datetime(2013, 1, 3, 7, 12)]), + ] + + indexes_cannot_append_with_other = [ + pd.IntervalIndex.from_breaks([0, 1, 2, 3]), + pd.MultiIndex.from_arrays(['A B C'.split(), 'D E F'.split()]), + ] + + all_indexes = indexes_can_append + indexes_cannot_append_with_other + + @pytest.mark.parametrize("index", + all_indexes, + ids=lambda x: x.__class__.__name__) + def test_append_same_columns_type(self, index): + # GH18359 + + # df wider than ser + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=index) + ser_index = index[:2] + ser = pd.Series([7, 8], index=ser_index, name=2) + result = df.append(ser) + expected = pd.DataFrame([[1., 2., 3.], [4, 5, 6], [7, 8, np.nan]], + index=[0, 1, 2], + columns=index) + assert_frame_equal(result, expected) + + # ser wider than df + ser_index = index + index = index[:2] + df = pd.DataFrame([[1, 2], [4, 5]], columns=index) + ser = pd.Series([7, 8, 9], index=ser_index, name=2) + result = df.append(ser) + expected = pd.DataFrame([[1, 2, np.nan], [4, 5, np.nan], [7, 8, 9]], + index=[0, 1, 2], + columns=ser_index) + assert_frame_equal(result, expected) + + @pytest.mark.parametrize("df_columns, series_index", + combinations(indexes_can_append, r=2), + ids=lambda x: x.__class__.__name__) + def test_append_different_columns_types(self, df_columns, series_index): + # GH18359 + # See also test 'test_append_different_columns_types_raises' below + # for errors raised when appending + + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=df_columns) + ser = pd.Series([7, 8, 9], index=series_index, name=2) + + result = df.append(ser) + idx_diff = ser.index.difference(df_columns) + combined_columns = Index(df_columns.tolist()).append(idx_diff) + expected = pd.DataFrame([[1., 2., 3., np.nan, np.nan, np.nan], + [4, 5, 6, np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan, 7, 8, 9]], + index=[0, 1, 2], + columns=combined_columns) + assert_frame_equal(result, expected) - def test_append_dtype_coerce(self): + @pytest.mark.parametrize('index_can_append', indexes_can_append, + ids=lambda x: x.__class__.__name__) + @pytest.mark.parametrize('index_cannot_append_with_other', + indexes_cannot_append_with_other, + ids=lambda x: x.__class__.__name__) + def test_append_different_columns_types_raises( + self, index_can_append, index_cannot_append_with_other): + # GH18359 + # Dataframe.append will raise if IntervalIndex/MultiIndex appends + # or is appended to a different index type + # + # See also test 'test_append_different_columns_types' above for + # appending without raising. + + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=index_can_append) + ser = pd.Series([7, 8, 9], index=index_cannot_append_with_other, + name=2) + msg = ("the other index needs to be an IntervalIndex too, but was" + r" type {}|" + r"object of type '(int|long|float|Timestamp)' has no len\(\)|" + "Expected tuple, got str") + with pytest.raises(TypeError, match=msg.format( + index_can_append.__class__.__name__)): + df.append(ser) + + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], + columns=index_cannot_append_with_other) + ser = pd.Series([7, 8, 9], index=index_can_append, name=2) + msg = (r"unorderable types: (Interval|int)\(\) > " + r"(int|long|float|str)\(\)|" + r"Expected tuple, got (int|long|float|str)|" + r"Cannot compare type 'Timestamp' with type '(int|long)'|" + r"'>' not supported between instances of 'int' and 'str'") + with pytest.raises(TypeError, match=msg): + df.append(ser) + + def test_append_dtype_coerce(self, sort): # GH 4993 # appending with datetime will incorrectly convert datetime64 - import datetime as dt - from pandas import NaT df1 = DataFrame(index=[1, 2], data=[dt.datetime(2013, 1, 1, 0, 0), dt.datetime(2013, 1, 2, 0, 0)], @@ -831,63 +994,88 @@ def test_append_dtype_coerce(self): dt.datetime(2013, 1, 4, 7, 10)]], columns=['start_time', 'end_time']) - expected = concat([Series([NaT, NaT, dt.datetime(2013, 1, 3, 6, 10), + expected = concat([Series([pd.NaT, + pd.NaT, + dt.datetime(2013, 1, 3, 6, 10), dt.datetime(2013, 1, 4, 7, 10)], name='end_time'), Series([dt.datetime(2013, 1, 1, 0, 0), dt.datetime(2013, 1, 2, 0, 0), dt.datetime(2013, 1, 3, 0, 0), dt.datetime(2013, 1, 4, 0, 0)], - name='start_time')], axis=1) - result = df1.append(df2, ignore_index=True) + name='start_time')], + axis=1, sort=sort) + result = df1.append(df2, ignore_index=True, sort=sort) + if sort: + expected = expected[['end_time', 'start_time']] + else: + expected = expected[['start_time', 'end_time']] + assert_frame_equal(result, expected) - def test_append_missing_column_proper_upcast(self): + def test_append_missing_column_proper_upcast(self, sort): df1 = DataFrame({'A': np.array([1, 2, 3, 4], dtype='i8')}) df2 = DataFrame({'B': np.array([True, False, True, False], dtype=bool)}) - appended = df1.append(df2, ignore_index=True) - self.assertEqual(appended['A'].dtype, 'f8') - self.assertEqual(appended['B'].dtype, 'O') + appended = df1.append(df2, ignore_index=True, sort=sort) + assert appended['A'].dtype == 'f8' + assert appended['B'].dtype == 'O' + + def test_append_empty_frame_to_series_with_dateutil_tz(self): + # GH 23682 + date = Timestamp('2018-10-24 07:30:00', tz=dateutil.tz.tzutc()) + s = Series({'date': date, 'a': 1.0, 'b': 2.0}) + df = DataFrame(columns=['c', 'd']) + result = df.append(s, ignore_index=True) + # n.b. it's not clear to me that expected is correct here. + # It's possible that the `date` column should have + # datetime64[ns, tz] dtype for both result and expected. + # that would be more consistent with new columns having + # their own dtype (float for a and b, datetime64ns, tz for date). + expected = DataFrame([[np.nan, np.nan, 1., 2., date]], + columns=['c', 'd', 'a', 'b', 'date'], + dtype=object) + # These columns get cast to object after append + expected['a'] = expected['a'].astype(float) + expected['b'] = expected['b'].astype(float) + assert_frame_equal(result, expected) class TestConcatenate(ConcatenateBase): def test_concat_copy(self): - df = DataFrame(np.random.randn(4, 3)) df2 = DataFrame(np.random.randint(0, 10, size=4).reshape(4, 1)) df3 = DataFrame({5: 'foo'}, index=range(4)) - # these are actual copies + # These are actual copies. result = concat([df, df2, df3], axis=1, copy=True) + for b in result._data.blocks: - self.assertIsNone(b.values.base) + assert b.values.base is None - # these are the same + # These are the same. result = concat([df, df2, df3], axis=1, copy=False) + for b in result._data.blocks: if b.is_float: - self.assertTrue( - b.values.base is df._data.blocks[0].values.base) + assert b.values.base is df._data.blocks[0].values.base elif b.is_integer: - self.assertTrue( - b.values.base is df2._data.blocks[0].values.base) + assert b.values.base is df2._data.blocks[0].values.base elif b.is_object: - self.assertIsNotNone(b.values.base) + assert b.values.base is not None - # float block was consolidated + # Float block was consolidated. df4 = DataFrame(np.random.randn(4, 1)) result = concat([df, df2, df3, df4], axis=1, copy=False) for b in result._data.blocks: if b.is_float: - self.assertIsNone(b.values.base) + assert b.values.base is None elif b.is_integer: - self.assertTrue( - b.values.base is df2._data.blocks[0].values.base) + assert b.values.base is df2._data.blocks[0].values.base elif b.is_object: - self.assertIsNotNone(b.values.base) + assert b.values.base is not None def test_concat_with_group_keys(self): df = DataFrame(np.random.randn(4, 3)) @@ -933,11 +1121,11 @@ def test_concat_keys_specific_levels(self): levels=[level], names=['group_key']) - self.assert_index_equal(result.columns.levels[0], - Index(level, name='group_key')) - self.assertEqual(result.columns.names[0], 'group_key') + tm.assert_index_equal(result.columns.levels[0], + Index(level, name='group_key')) + assert result.columns.names[0] == 'group_key' - def test_concat_dataframe_keys_bug(self): + def test_concat_dataframe_keys_bug(self, sort): t1 = DataFrame({ 'value': Series([1, 2, 3], index=Index(['a', 'b', 'c'], name='id'))}) @@ -945,9 +1133,8 @@ def test_concat_dataframe_keys_bug(self): 'value': Series([7, 8], index=Index(['a', 'b'], name='id'))}) # it works - result = concat([t1, t2], axis=1, keys=['t1', 't2']) - self.assertEqual(list(result.columns), [('t1', 'value'), - ('t2', 'value')]) + result = concat([t1, t2], axis=1, keys=['t1', 't2'], sort=sort) + assert list(result.columns) == [('t1', 'value'), ('t2', 'value')] def test_concat_series_partial_columns_names(self): # GH10698 @@ -992,7 +1179,7 @@ def test_concat_dict(self): expected = concat([frames[k] for k in keys], keys=keys) tm.assert_frame_equal(result, expected) - def test_concat_ignore_index(self): + def test_concat_ignore_index(self, sort): frame1 = DataFrame({"test1": ["a", "b", "c"], "test2": [1, 2, 3], "test3": [4.5, 3.2, 1.2]}) @@ -1000,7 +1187,8 @@ def test_concat_ignore_index(self): frame1.index = Index(["x", "y", "z"]) frame2.index = Index(["x", "y", "q"]) - v1 = concat([frame1, frame2], axis=1, ignore_index=True) + v1 = concat([frame1, frame2], axis=1, + ignore_index=True, sort=sort) nan = np.nan expected = DataFrame([[nan, nan, nan, 4.3], @@ -1008,23 +1196,25 @@ def test_concat_ignore_index(self): ['b', 2, 3.2, 2.2], ['c', 3, 1.2, nan]], index=Index(["q", "x", "y", "z"])) + if not sort: + expected = expected.loc[['x', 'y', 'z', 'q']] tm.assert_frame_equal(v1, expected) def test_concat_multiindex_with_keys(self): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['first', 'second']) frame = DataFrame(np.random.randn(10, 3), index=index, columns=Index(['A', 'B', 'C'], name='exp')) result = concat([frame, frame], keys=[0, 1], names=['iteration']) - self.assertEqual(result.index.names, ('iteration',) + index.names) + assert result.index.names == ('iteration',) + index.names tm.assert_frame_equal(result.loc[0], frame) tm.assert_frame_equal(result.loc[1], frame) - self.assertEqual(result.index.nlevels, 3) + assert result.index.nlevels == 3 def test_concat_multiindex_with_tz(self): # GH 6606 @@ -1047,6 +1237,30 @@ def test_concat_multiindex_with_tz(self): result = concat([df, df]) tm.assert_frame_equal(result, expected) + def test_concat_multiindex_with_none_in_index_names(self): + # GH 15787 + index = pd.MultiIndex.from_product([[1], range(5)], + names=['level1', None]) + df = pd.DataFrame({'col': range(5)}, index=index, dtype=np.int32) + + result = concat([df, df], keys=[1, 2], names=['level2']) + index = pd.MultiIndex.from_product([[1, 2], [1], range(5)], + names=['level2', 'level1', None]) + expected = pd.DataFrame({'col': list(range(5)) * 2}, + index=index, dtype=np.int32) + assert_frame_equal(result, expected) + + result = concat([df, df[:2]], keys=[1, 2], names=['level2']) + level2 = [1] * 5 + [2] * 2 + level1 = [1] * 7 + no_name = list(range(5)) + list(range(2)) + tuples = list(zip(level2, level1, no_name)) + index = pd.MultiIndex.from_tuples(tuples, + names=['level2', 'level1', None]) + expected = pd.DataFrame({'col': no_name}, index=index, + dtype=np.int32) + assert_frame_equal(result, expected) + def test_concat_keys_and_levels(self): df = DataFrame(np.random.randn(1, 3)) df2 = DataFrame(np.random.randn(1, 4)) @@ -1060,40 +1274,43 @@ def test_concat_keys_and_levels(self): names=names) expected = concat([df, df2, df, df2]) exp_index = MultiIndex(levels=levels + [[0]], - labels=[[0, 0, 1, 1], [0, 1, 0, 1], - [0, 0, 0, 0]], + codes=[[0, 0, 1, 1], [0, 1, 0, 1], + [0, 0, 0, 0]], names=names + [None]) expected.index = exp_index - assert_frame_equal(result, expected) + tm.assert_frame_equal(result, expected) # no names - result = concat([df, df2, df, df2], keys=[('foo', 'one'), ('foo', 'two'), ('baz', 'one'), ('baz', 'two')], levels=levels) - self.assertEqual(result.index.names, (None,) * 3) + assert result.index.names == (None,) * 3 # no levels result = concat([df, df2, df, df2], keys=[('foo', 'one'), ('foo', 'two'), ('baz', 'one'), ('baz', 'two')], names=['first', 'second']) - self.assertEqual(result.index.names, ('first', 'second') + (None,)) - self.assert_index_equal(result.index.levels[0], - Index(['baz', 'foo'], name='first')) + assert result.index.names == ('first', 'second') + (None,) + tm.assert_index_equal(result.index.levels[0], + Index(['baz', 'foo'], name='first')) def test_concat_keys_levels_no_overlap(self): # GH #1406 df = DataFrame(np.random.randn(1, 3), index=['a']) df2 = DataFrame(np.random.randn(1, 4), index=['b']) - self.assertRaises(ValueError, concat, [df, df], - keys=['one', 'two'], levels=[['foo', 'bar', 'baz']]) + msg = "Values not found in passed level" + with pytest.raises(ValueError, match=msg): + concat([df, df], + keys=['one', 'two'], levels=[['foo', 'bar', 'baz']]) - self.assertRaises(ValueError, concat, [df, df2], - keys=['one', 'two'], levels=[['foo', 'bar', 'baz']]) + msg = "Key one not in level" + with pytest.raises(ValueError, match=msg): + concat([df, df2], + keys=['one', 'two'], levels=[['foo', 'bar', 'baz']]) def test_concat_rename_index(self): a = DataFrame(np.random.rand(3, 3), @@ -1112,7 +1329,7 @@ def test_concat_rename_index(self): exp.index.set_names(names, inplace=True) tm.assert_frame_equal(result, exp) - self.assertEqual(result.index.names, exp.index.names) + assert result.index.names == exp.index.names def test_crossed_dtypes_weird_corner(self): columns = ['A', 'B', 'C', 'D'] @@ -1137,7 +1354,7 @@ def test_crossed_dtypes_weird_corner(self): df2 = DataFrame(np.random.randn(1, 4), index=['b']) result = concat( [df, df2], keys=['one', 'two'], names=['first', 'second']) - self.assertEqual(result.index.names, ('first', 'second')) + assert result.index.names == ('first', 'second') def test_dups_index(self): # GH 4771 @@ -1181,16 +1398,16 @@ def test_dups_index(self): result = df.append(df) assert_frame_equal(result, expected) - def test_with_mixed_tuples(self): + def test_with_mixed_tuples(self, sort): # 10697 # columns have mixed tuples, so handle properly df1 = DataFrame({u'A': 'foo', (u'B', 1): 'bar'}, index=range(2)) df2 = DataFrame({u'B': 'foo', (u'B', 1): 'bar'}, index=range(2)) # it works - concat([df1, df2]) + concat([df1, df2], sort=sort) - def test_handle_empty_objects(self): + def test_handle_empty_objects(self, sort): df = DataFrame(np.random.randn(10, 4), columns=list('abcd')) baz = df[:5].copy() @@ -1198,9 +1415,9 @@ def test_handle_empty_objects(self): empty = df[5:5] frames = [baz, empty, empty, df[5:]] - concatted = concat(frames, axis=0) + concatted = concat(frames, axis=0, sort=sort) - expected = df.loc[:, ['a', 'b', 'c', 'd', 'foo']] + expected = df.reindex(columns=['a', 'b', 'c', 'd', 'foo']) expected['foo'] = expected['foo'].astype('O') expected.loc[0:4, 'foo'] = 'bar' @@ -1282,10 +1499,6 @@ def test_concat_mixed_objs(self): result = concat([s1, df, s2], ignore_index=True) assert_frame_equal(result, expected) - # invalid concatente of mixed dims - panel = tm.makePanel() - self.assertRaises(ValueError, lambda: concat([panel, s1], axis=1)) - def test_empty_dtype_coerce(self): # xref to #12411 @@ -1321,34 +1534,10 @@ def test_dtype_coerceion(self): result = concat([df.iloc[[0]], df.iloc[[1]]]) tm.assert_series_equal(result.dtypes, df.dtypes) - def test_panel_concat_other_axes(self): - panel = tm.makePanel() - - p1 = panel.iloc[:, :5, :] - p2 = panel.iloc[:, 5:, :] - - result = concat([p1, p2], axis=1) - tm.assert_panel_equal(result, panel) - - p1 = panel.iloc[:, :, :2] - p2 = panel.iloc[:, :, 2:] - - result = concat([p1, p2], axis=2) - tm.assert_panel_equal(result, panel) - - # if things are a bit misbehaved - p1 = panel.iloc[:2, :, :2] - p2 = panel.iloc[:, :, 2:] - p1['ItemC'] = 'baz' - - result = concat([p1, p2], axis=2) - - expected = panel.copy() - expected['ItemC'] = expected['ItemC'].astype('O') - expected.loc['ItemC', :, :2] = 'baz' - tm.assert_panel_equal(result, expected) - - def test_panel_concat_buglet(self): + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") + # Panel.rename warning we don't care about + @pytest.mark.filterwarnings("ignore:Using:FutureWarning") + def test_panel_concat_buglet(self, sort): # #2257 def make_panel(): index = 5 @@ -1358,53 +1547,19 @@ def df(): return DataFrame(np.random.randn(index, cols), index=["I%s" % i for i in range(index)], columns=["C%s" % i for i in range(cols)]) - return Panel(dict([("Item%s" % x, df()) for x in ['A', 'B', 'C']])) + return Panel({"Item%s" % x: df() for x in ['A', 'B', 'C']}) panel1 = make_panel() panel2 = make_panel() - panel2 = panel2.rename_axis(dict([(x, "%s_1" % x) - for x in panel2.major_axis]), - axis=1) + panel2 = panel2.rename(major_axis={x: "%s_1" % x + for x in panel2.major_axis}) - panel3 = panel2.rename_axis(lambda x: '%s_1' % x, axis=1) - panel3 = panel3.rename_axis(lambda x: '%s_1' % x, axis=2) + panel3 = panel2.rename(major_axis=lambda x: '%s_1' % x) + panel3 = panel3.rename(minor_axis=lambda x: '%s_1' % x) # it works! - concat([panel1, panel3], axis=1, verify_integrity=True) - - def test_panel4d_concat(self): - with catch_warnings(record=True): - p4d = tm.makePanel4D() - - p1 = p4d.iloc[:, :, :5, :] - p2 = p4d.iloc[:, :, 5:, :] - - result = concat([p1, p2], axis=2) - tm.assert_panel4d_equal(result, p4d) - - p1 = p4d.iloc[:, :, :, :2] - p2 = p4d.iloc[:, :, :, 2:] - - result = concat([p1, p2], axis=3) - tm.assert_panel4d_equal(result, p4d) - - def test_panel4d_concat_mixed_type(self): - with catch_warnings(record=True): - p4d = tm.makePanel4D() - - # if things are a bit misbehaved - p1 = p4d.iloc[:, :2, :, :2] - p2 = p4d.iloc[:, :, :, 2:] - p1['L5'] = 'baz' - - result = concat([p1, p2], axis=3) - - p2['L5'] = np.nan - expected = concat([p1, p2], axis=3) - expected = expected.loc[result.labels] - - tm.assert_panel4d_equal(result, expected) + concat([panel1, panel3], axis=1, verify_integrity=True, sort=sort) def test_concat_series(self): @@ -1415,21 +1570,21 @@ def test_concat_series(self): result = concat(pieces) tm.assert_series_equal(result, ts) - self.assertEqual(result.name, ts.name) + assert result.name == ts.name result = concat(pieces, keys=[0, 1, 2]) expected = ts.copy() ts.index = DatetimeIndex(np.array(ts.index.values, dtype='M8[ns]')) - exp_labels = [np.repeat([0, 1, 2], [len(x) for x in pieces]), - np.arange(len(ts))] + exp_codes = [np.repeat([0, 1, 2], [len(x) for x in pieces]), + np.arange(len(ts))] exp_index = MultiIndex(levels=[[0, 1, 2], ts.index], - labels=exp_labels) + codes=exp_codes) expected.index = exp_index tm.assert_series_equal(result, expected) - def test_concat_series_axis1(self): + def test_concat_series_axis1(self, sort=sort): ts = tm.makeTimeSeries() pieces = [ts[:-2], ts[2:], ts[2:-2]] @@ -1452,16 +1607,33 @@ def test_concat_series_axis1(self): s2.name = None result = concat([s, s2], axis=1) - self.assertTrue(np.array_equal( - result.columns, Index(['A', 0], dtype='object'))) + tm.assert_index_equal(result.columns, + Index(['A', 0], dtype='object')) # must reindex, #2603 s = Series(randn(3), index=['c', 'a', 'b'], name='A') s2 = Series(randn(4), index=['d', 'a', 'b', 'c'], name='B') - result = concat([s, s2], axis=1) + result = concat([s, s2], axis=1, sort=sort) expected = DataFrame({'A': s, 'B': s2}) assert_frame_equal(result, expected) + def test_concat_series_axis1_names_applied(self): + # ensure names argument is not ignored on axis=1, #23490 + s = Series([1, 2, 3]) + s2 = Series([4, 5, 6]) + result = concat([s, s2], axis=1, keys=['a', 'b'], names=['A']) + expected = DataFrame([[1, 4], [2, 5], [3, 6]], + columns=pd.Index(['a', 'b'], name='A')) + assert_frame_equal(result, expected) + + result = concat([s, s2], axis=1, keys=[('a', 1), ('b', 2)], + names=['A', 'B']) + expected = DataFrame([[1, 4], [2, 5], [3, 6]], + columns=MultiIndex.from_tuples([('a', 1), + ('b', 2)], + names=['A', 'B'])) + assert_frame_equal(result, expected) + def test_concat_single_with_key(self): df = DataFrame(np.random.randn(10, 4)) @@ -1475,18 +1647,19 @@ def test_concat_exclude_none(self): pieces = [df[:5], None, None, df[5:]] result = concat(pieces) tm.assert_frame_equal(result, df) - self.assertRaises(ValueError, concat, [None, None]) + with pytest.raises(ValueError, match="All objects passed were None"): + concat([None, None]) def test_concat_datetime64_block(self): - from pandas.tseries.index import date_range + from pandas.core.indexes.datetimes import date_range rng = date_range('1/1/2000', periods=10) df = DataFrame({'time': rng}) result = concat([df, df]) - self.assertTrue((result.iloc[:10]['time'] == rng).all()) - self.assertTrue((result.iloc[10:]['time'] == rng).all()) + assert (result.iloc[:10]['time'] == rng).all() + assert (result.iloc[10:]['time'] == rng).all() def test_concat_timedelta64_block(self): from pandas import to_timedelta @@ -1496,8 +1669,8 @@ def test_concat_timedelta64_block(self): df = DataFrame({'time': rng}) result = concat([df, df]) - self.assertTrue((result.iloc[:10]['time'] == rng).all()) - self.assertTrue((result.iloc[10:]['time'] == rng).all()) + assert (result.iloc[:10]['time'] == rng).all() + assert (result.iloc[10:]['time'] == rng).all() def test_concat_keys_with_none(self): # #1649 @@ -1522,7 +1695,7 @@ def test_concat_bug_1719(self): left = concat([ts1, ts2], join='outer', axis=1) right = concat([ts2, ts1], join='outer', axis=1) - self.assertEqual(len(left), len(right)) + assert len(left) == len(right) def test_concat_bug_2972(self): ts0 = Series(np.zeros(5)) @@ -1537,10 +1710,10 @@ def test_concat_bug_2972(self): def test_concat_bug_3602(self): # GH 3602, duplicate columns - df1 = DataFrame({'firmNo': [0, 0, 0, 0], 'stringvar': [ - 'rrr', 'rrr', 'rrr', 'rrr'], 'prc': [6, 6, 6, 6]}) - df2 = DataFrame({'misc': [1, 2, 3, 4], 'prc': [ - 6, 6, 6, 6], 'C': [9, 10, 11, 12]}) + df1 = DataFrame({'firmNo': [0, 0, 0, 0], 'prc': [6, 6, 6, 6], + 'stringvar': ['rrr', 'rrr', 'rrr', 'rrr']}) + df2 = DataFrame({'C': [9, 10, 11, 12], 'misc': [1, 2, 3, 4], + 'prc': [6, 6, 6, 6]}) expected = DataFrame([[0, 6, 'rrr', 9, 1, 6], [0, 6, 'rrr', 10, 2, 6], [0, 6, 'rrr', 11, 3, 6], @@ -1566,11 +1739,11 @@ def test_concat_series_axis1_same_names_ignore_index(self): s2 = Series(randn(len(dates)), index=dates, name='value') result = concat([s1, s2], axis=1, ignore_index=True) - self.assertTrue(np.array_equal(result.columns, [0, 1])) + expected = Index([0, 1]) - def test_concat_iterables(self): - from collections import deque, Iterable + tm.assert_index_equal(result.columns, expected) + def test_concat_iterables(self): # GH8645 check concat works with tuples, list, generators, and weird # stuff like deque and custom iterables df1 = DataFrame([1, 2, 3]) @@ -1608,13 +1781,20 @@ def test_concat_invalid(self): # trying to concat a ndframe with a non-ndframe df1 = mkdf(10, 2) + msg = ('cannot concatenate object of type "{}";' + ' only pd.Series, pd.DataFrame, and pd.Panel' + r' \(deprecated\) objs are valid') for obj in [1, dict(), [1, 2], (1, 2)]: - self.assertRaises(TypeError, lambda x: concat([df1, obj])) + with pytest.raises(TypeError, match=msg.format(type(obj))): + concat([df1, obj]) def test_concat_invalid_first_argument(self): df1 = mkdf(10, 2) df2 = mkdf(10, 2) - self.assertRaises(TypeError, concat, df1, df2) + msg = ('first argument must be an iterable of pandas ' + 'objects, you passed an object of type "DataFrame"') + with pytest.raises(TypeError, match=msg): + concat(df1, df2) # generator ok though concat(DataFrame(np.random.rand(5, 5)) for _ in range(3)) @@ -1679,8 +1859,7 @@ def test_concat_tz_frame(self): assert_frame_equal(df2, df3) def test_concat_tz_series(self): - # GH 11755 - # tz and no tz + # gh-11755: tz and no tz x = Series(date_range('20151124 08:00', '20151124 09:00', freq='1h', tz='UTC')) @@ -1690,8 +1869,7 @@ def test_concat_tz_series(self): result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - # GH 11887 - # concat tz and object + # gh-11887: concat tz and object x = Series(date_range('20151124 08:00', '20151124 09:00', freq='1h', tz='UTC')) @@ -1701,10 +1879,8 @@ def test_concat_tz_series(self): result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - # 12217 - # 12306 fixed I think - - # Concat'ing two UTC times + # see gh-12217 and gh-12306 + # Concatenating two UTC times first = pd.DataFrame([[datetime(2016, 1, 1)]]) first[0] = first[0].dt.tz_localize('UTC') @@ -1712,9 +1888,9 @@ def test_concat_tz_series(self): second[0] = second[0].dt.tz_localize('UTC') result = pd.concat([first, second]) - self.assertEqual(result[0].dtype, 'datetime64[ns, UTC]') + assert result[0].dtype == 'datetime64[ns, UTC]' - # Concat'ing two London times + # Concatenating two London times first = pd.DataFrame([[datetime(2016, 1, 1)]]) first[0] = first[0].dt.tz_localize('Europe/London') @@ -1722,9 +1898,9 @@ def test_concat_tz_series(self): second[0] = second[0].dt.tz_localize('Europe/London') result = pd.concat([first, second]) - self.assertEqual(result[0].dtype, 'datetime64[ns, Europe/London]') + assert result[0].dtype == 'datetime64[ns, Europe/London]' - # Concat'ing 2+1 London times + # Concatenating 2+1 London times first = pd.DataFrame([[datetime(2016, 1, 1)], [datetime(2016, 1, 2)]]) first[0] = first[0].dt.tz_localize('Europe/London') @@ -1732,7 +1908,7 @@ def test_concat_tz_series(self): second[0] = second[0].dt.tz_localize('Europe/London') result = pd.concat([first, second]) - self.assertEqual(result[0].dtype, 'datetime64[ns, Europe/London]') + assert result[0].dtype == 'datetime64[ns, Europe/London]' # Concat'ing 1+2 London times first = pd.DataFrame([[datetime(2016, 1, 1)]]) @@ -1742,11 +1918,10 @@ def test_concat_tz_series(self): second[0] = second[0].dt.tz_localize('Europe/London') result = pd.concat([first, second]) - self.assertEqual(result[0].dtype, 'datetime64[ns, Europe/London]') + assert result[0].dtype == 'datetime64[ns, Europe/London]' def test_concat_tz_series_with_datetimelike(self): - # GH 12620 - # tz and timedelta + # see gh-12620: tz and timedelta x = [pd.Timestamp('2011-01-01', tz='US/Eastern'), pd.Timestamp('2011-02-01', tz='US/Eastern')] y = [pd.Timedelta('1 day'), pd.Timedelta('2 day')] @@ -1759,39 +1934,109 @@ def test_concat_tz_series_with_datetimelike(self): tm.assert_series_equal(result, pd.Series(x + y, dtype='object')) def test_concat_tz_series_tzlocal(self): - # GH 13583 - tm._skip_if_no_dateutil() - import dateutil + # see gh-13583 x = [pd.Timestamp('2011-01-01', tz=dateutil.tz.tzlocal()), pd.Timestamp('2011-02-01', tz=dateutil.tz.tzlocal())] y = [pd.Timestamp('2012-01-01', tz=dateutil.tz.tzlocal()), pd.Timestamp('2012-02-01', tz=dateutil.tz.tzlocal())] + result = concat([pd.Series(x), pd.Series(y)], ignore_index=True) tm.assert_series_equal(result, pd.Series(x + y)) - self.assertEqual(result.dtype, 'datetime64[ns, tzlocal()]') + assert result.dtype == 'datetime64[ns, tzlocal()]' + + @pytest.mark.parametrize('tz1', [None, 'UTC']) + @pytest.mark.parametrize('tz2', [None, 'UTC']) + @pytest.mark.parametrize('s', [pd.NaT, pd.Timestamp('20150101')]) + def test_concat_NaT_dataframes_all_NaT_axis_0(self, tz1, tz2, s): + # GH 12396 + + # tz-naive + first = pd.DataFrame([[pd.NaT], [pd.NaT]]).apply( + lambda x: x.dt.tz_localize(tz1)) + second = pd.DataFrame([s]).apply(lambda x: x.dt.tz_localize(tz2)) + + result = pd.concat([first, second], axis=0) + expected = pd.DataFrame(pd.Series( + [pd.NaT, pd.NaT, s], index=[0, 1, 0])) + expected = expected.apply(lambda x: x.dt.tz_localize(tz2)) + if tz1 != tz2: + expected = expected.astype(object) + + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('tz1', [None, 'UTC']) + @pytest.mark.parametrize('tz2', [None, 'UTC']) + def test_concat_NaT_dataframes_all_NaT_axis_1(self, tz1, tz2): + # GH 12396 + + first = pd.DataFrame(pd.Series([pd.NaT, pd.NaT]).dt.tz_localize(tz1)) + second = pd.DataFrame(pd.Series( + [pd.NaT]).dt.tz_localize(tz2), columns=[1]) + expected = pd.DataFrame( + {0: pd.Series([pd.NaT, pd.NaT]).dt.tz_localize(tz1), + 1: pd.Series([pd.NaT, pd.NaT]).dt.tz_localize(tz2)} + ) + result = pd.concat([first, second], axis=1) + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('tz1', [None, 'UTC']) + @pytest.mark.parametrize('tz2', [None, 'UTC']) + def test_concat_NaT_series_dataframe_all_NaT(self, tz1, tz2): + # GH 12396 + + # tz-naive + first = pd.Series([pd.NaT, pd.NaT]).dt.tz_localize(tz1) + second = pd.DataFrame([[pd.Timestamp('2015/01/01', tz=tz2)], + [pd.Timestamp('2016/01/01', tz=tz2)]], + index=[2, 3]) + + expected = pd.DataFrame([pd.NaT, pd.NaT, + pd.Timestamp('2015/01/01', tz=tz2), + pd.Timestamp('2016/01/01', tz=tz2)]) + if tz1 != tz2: + expected = expected.astype(object) + + result = pd.concat([first, second]) + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('tz', [None, 'UTC']) + def test_concat_NaT_dataframes(self, tz): + # GH 12396 + + first = pd.DataFrame([[pd.NaT], [pd.NaT]]) + first = first.apply(lambda x: x.dt.tz_localize(tz)) + second = pd.DataFrame([[pd.Timestamp('2015/01/01', tz=tz)], + [pd.Timestamp('2016/01/01', tz=tz)]], + index=[2, 3]) + expected = pd.DataFrame([pd.NaT, pd.NaT, + pd.Timestamp('2015/01/01', tz=tz), + pd.Timestamp('2016/01/01', tz=tz)]) + + result = pd.concat([first, second], axis=0) + assert_frame_equal(result, expected) def test_concat_period_series(self): x = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='D')) y = Series(pd.PeriodIndex(['2015-10-01', '2016-01-01'], freq='D')) - expected = Series([x[0], x[1], y[0], y[1]], dtype='object') + expected = Series([x[0], x[1], y[0], y[1]], dtype='Period[D]') result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'object') - # different freq + def test_concat_period_multiple_freq_series(self): x = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='D')) y = Series(pd.PeriodIndex(['2015-10-01', '2016-01-01'], freq='M')) expected = Series([x[0], x[1], y[0], y[1]], dtype='object') result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'object') + assert result.dtype == 'object' + def test_concat_period_other_series(self): x = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='D')) y = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='M')) expected = Series([x[0], x[1], y[0], y[1]], dtype='object') result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'object') + assert result.dtype == 'object' # non-period x = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='D')) @@ -1799,14 +2044,14 @@ def test_concat_period_series(self): expected = Series([x[0], x[1], y[0], y[1]], dtype='object') result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'object') + assert result.dtype == 'object' x = Series(pd.PeriodIndex(['2015-11-01', '2015-12-01'], freq='D')) y = Series(['A', 'B']) expected = Series([x[0], x[1], y[0], y[1]], dtype='object') result = concat([x, y], ignore_index=True) tm.assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'object') + assert result.dtype == 'object' def test_concat_empty_series(self): # GH 11082 @@ -1831,12 +2076,27 @@ def test_concat_empty_series(self): columns=['x', 0]) tm.assert_frame_equal(res, exp) + @pytest.mark.parametrize('tz', [None, 'UTC']) + @pytest.mark.parametrize('values', [[], [1, 2, 3]]) + def test_concat_empty_series_timelike(self, tz, values): + # GH 18447 + + first = Series([], dtype='M8[ns]').dt.tz_localize(tz) + second = Series(values) + expected = DataFrame( + {0: pd.Series([pd.NaT] * len(values), + dtype='M8[ns]' + ).dt.tz_localize(tz), + 1: values}) + result = concat([first, second], axis=1) + assert_frame_equal(result, expected) + def test_default_index(self): # is_series and ignore_index s1 = pd.Series([1, 2, 3], name='x') s2 = pd.Series([4, 5, 6], name='y') res = pd.concat([s1, s2], axis=1, ignore_index=True) - self.assertIsInstance(res.columns, pd.RangeIndex) + assert isinstance(res.columns, pd.RangeIndex) exp = pd.DataFrame([[1, 4], [2, 5], [3, 6]]) # use check_index_type=True to check the result have # RangeIndex (default index) @@ -1847,7 +2107,7 @@ def test_default_index(self): s1 = pd.Series([1, 2, 3]) s2 = pd.Series([4, 5, 6]) res = pd.concat([s1, s2], axis=1, ignore_index=False) - self.assertIsInstance(res.columns, pd.RangeIndex) + assert isinstance(res.columns, pd.RangeIndex) exp = pd.DataFrame([[1, 4], [2, 5], [3, 6]]) exp.columns = pd.RangeIndex(2) tm.assert_frame_equal(res, exp, check_index_type=True, @@ -1875,8 +2135,8 @@ def test_concat_multiindex_rangeindex(self): df = DataFrame(np.random.randn(9, 2)) df.index = MultiIndex(levels=[pd.RangeIndex(3), pd.RangeIndex(3)], - labels=[np.repeat(np.arange(3), 3), - np.tile(np.arange(3), 3)]) + codes=[np.repeat(np.arange(3), 3), + np.tile(np.arange(3), 3)]) res = concat([df.iloc[[2, 3, 4], :], df.iloc[[5], :]]) exp = df.iloc[[2, 3, 4, 5], :] @@ -1895,7 +2155,7 @@ def test_concat_multiindex_dfs_with_deepcopy(self): expected_index = pd.MultiIndex(levels=[['s1', 's2'], ['a'], ['b', 'c']], - labels=[[0, 1], [0, 0], [0, 1]], + codes=[[0, 1], [0, 0], [0, 1]], names=['testname', None, None]) expected = pd.DataFrame([[0], [1]], index=expected_index) result_copy = pd.concat(deepcopy(example_dict), names=['testname']) @@ -1903,9 +2163,246 @@ def test_concat_multiindex_dfs_with_deepcopy(self): result_no_copy = pd.concat(example_dict, names=['testname']) tm.assert_frame_equal(result_no_copy, expected) + def test_categorical_concat_append(self): + cat = Categorical(["a", "b"], categories=["a", "b"]) + vals = [1, 2] + df = DataFrame({"cats": cat, "vals": vals}) + cat2 = Categorical(["a", "b", "a", "b"], categories=["a", "b"]) + vals2 = [1, 2, 1, 2] + exp = DataFrame({"cats": cat2, "vals": vals2}, + index=Index([0, 1, 0, 1])) + + tm.assert_frame_equal(pd.concat([df, df]), exp) + tm.assert_frame_equal(df.append(df), exp) + + # GH 13524 can concat different categories + cat3 = Categorical(["a", "b"], categories=["a", "b", "c"]) + vals3 = [1, 2] + df_different_categories = DataFrame({"cats": cat3, "vals": vals3}) + + res = pd.concat([df, df_different_categories], ignore_index=True) + exp = DataFrame({"cats": list('abab'), "vals": [1, 2, 1, 2]}) + tm.assert_frame_equal(res, exp) + + res = df.append(df_different_categories, ignore_index=True) + tm.assert_frame_equal(res, exp) + + def test_categorical_concat_dtypes(self): + + # GH8143 + index = ['cat', 'obj', 'num'] + cat = Categorical(['a', 'b', 'c']) + obj = Series(['a', 'b', 'c']) + num = Series([1, 2, 3]) + df = pd.concat([Series(cat), obj, num], axis=1, keys=index) + + result = df.dtypes == 'object' + expected = Series([False, True, False], index=index) + tm.assert_series_equal(result, expected) + + result = df.dtypes == 'int64' + expected = Series([False, False, True], index=index) + tm.assert_series_equal(result, expected) + + result = df.dtypes == 'category' + expected = Series([True, False, False], index=index) + tm.assert_series_equal(result, expected) + + def test_categorical_concat(self, sort): + # See GH 10177 + df1 = DataFrame(np.arange(18, dtype='int64').reshape(6, 3), + columns=["a", "b", "c"]) + + df2 = DataFrame(np.arange(14, dtype='int64').reshape(7, 2), + columns=["a", "c"]) + + cat_values = ["one", "one", "two", "one", "two", "two", "one"] + df2['h'] = Series(Categorical(cat_values)) + + res = pd.concat((df1, df2), axis=0, ignore_index=True, sort=sort) + exp = DataFrame({'a': [0, 3, 6, 9, 12, 15, 0, 2, 4, 6, 8, 10, 12], + 'b': [1, 4, 7, 10, 13, 16, np.nan, np.nan, np.nan, + np.nan, np.nan, np.nan, np.nan], + 'c': [2, 5, 8, 11, 14, 17, 1, 3, 5, 7, 9, 11, 13], + 'h': [None] * 6 + cat_values}) + tm.assert_frame_equal(res, exp) + + def test_categorical_concat_gh7864(self): + # GH 7864 + # make sure ordering is preserverd + df = DataFrame({"id": [1, 2, 3, 4, 5, 6], "raw_grade": list('abbaae')}) + df["grade"] = Categorical(df["raw_grade"]) + df['grade'].cat.set_categories(['e', 'a', 'b']) + + df1 = df[0:3] + df2 = df[3:] + + tm.assert_index_equal(df['grade'].cat.categories, + df1['grade'].cat.categories) + tm.assert_index_equal(df['grade'].cat.categories, + df2['grade'].cat.categories) + + dfx = pd.concat([df1, df2]) + tm.assert_index_equal(df['grade'].cat.categories, + dfx['grade'].cat.categories) + + dfa = df1.append(df2) + tm.assert_index_equal(df['grade'].cat.categories, + dfa['grade'].cat.categories) + + def test_categorical_concat_preserve(self): + + # GH 8641 series concat not preserving category dtype + # GH 13524 can concat different categories + s = Series(list('abc'), dtype='category') + s2 = Series(list('abd'), dtype='category') + + exp = Series(list('abcabd')) + res = pd.concat([s, s2], ignore_index=True) + tm.assert_series_equal(res, exp) + + exp = Series(list('abcabc'), dtype='category') + res = pd.concat([s, s], ignore_index=True) + tm.assert_series_equal(res, exp) + + exp = Series(list('abcabc'), index=[0, 1, 2, 0, 1, 2], + dtype='category') + res = pd.concat([s, s]) + tm.assert_series_equal(res, exp) + + a = Series(np.arange(6, dtype='int64')) + b = Series(list('aabbca')) + + df2 = DataFrame({'A': a, + 'B': b.astype(CategoricalDtype(list('cab')))}) + res = pd.concat([df2, df2]) + exp = DataFrame( + {'A': pd.concat([a, a]), + 'B': pd.concat([b, b]).astype(CategoricalDtype(list('cab')))}) + tm.assert_frame_equal(res, exp) + + def test_categorical_index_preserver(self): + + a = Series(np.arange(6, dtype='int64')) + b = Series(list('aabbca')) + + df2 = DataFrame({'A': a, + 'B': b.astype(CategoricalDtype(list('cab'))) + }).set_index('B') + result = pd.concat([df2, df2]) + expected = DataFrame( + {'A': pd.concat([a, a]), + 'B': pd.concat([b, b]).astype(CategoricalDtype(list('cab'))) + }).set_index('B') + tm.assert_frame_equal(result, expected) + + # wrong catgories + df3 = DataFrame({'A': a, 'B': Categorical(b, categories=list('abe')) + }).set_index('B') + msg = "categories must match existing categories when appending" + with pytest.raises(TypeError, match=msg): + pd.concat([df2, df3]) + + def test_concat_categoricalindex(self): + # GH 16111, categories that aren't lexsorted + categories = [9, 0, 1, 2, 3] + + a = pd.Series(1, index=pd.CategoricalIndex([9, 0], + categories=categories)) + b = pd.Series(2, index=pd.CategoricalIndex([0, 1], + categories=categories)) + c = pd.Series(3, index=pd.CategoricalIndex([1, 2], + categories=categories)) + + result = pd.concat([a, b, c], axis=1) + + exp_idx = pd.CategoricalIndex([9, 0, 1, 2], categories=categories) + exp = pd.DataFrame({0: [1, 1, np.nan, np.nan], + 1: [np.nan, 2, 2, np.nan], + 2: [np.nan, np.nan, 3, 3]}, + columns=[0, 1, 2], + index=exp_idx) + tm.assert_frame_equal(result, exp) + + def test_concat_order(self): + # GH 17344 + dfs = [pd.DataFrame(index=range(3), columns=['a', 1, None])] + dfs += [pd.DataFrame(index=range(3), columns=[None, 1, 'a']) + for i in range(100)] + + result = pd.concat(dfs, sort=True).columns + + if PY2: + # Different sort order between incomparable objects between + # python 2 and python3 via Index.union. + expected = dfs[1].columns + else: + expected = dfs[0].columns + tm.assert_index_equal(result, expected) + + def test_concat_datetime_timezone(self): + # GH 18523 + idx1 = pd.date_range('2011-01-01', periods=3, freq='H', + tz='Europe/Paris') + idx2 = pd.date_range(start=idx1[0], end=idx1[-1], freq='H') + df1 = pd.DataFrame({'a': [1, 2, 3]}, index=idx1) + df2 = pd.DataFrame({'b': [1, 2, 3]}, index=idx2) + result = pd.concat([df1, df2], axis=1) + + exp_idx = DatetimeIndex(['2011-01-01 00:00:00+01:00', + '2011-01-01 01:00:00+01:00', + '2011-01-01 02:00:00+01:00'], + freq='H' + ).tz_convert('UTC').tz_convert('Europe/Paris') + + expected = pd.DataFrame([[1, 1], [2, 2], [3, 3]], + index=exp_idx, columns=['a', 'b']) + + tm.assert_frame_equal(result, expected) + + idx3 = pd.date_range('2011-01-01', periods=3, + freq='H', tz='Asia/Tokyo') + df3 = pd.DataFrame({'b': [1, 2, 3]}, index=idx3) + result = pd.concat([df1, df3], axis=1) + + exp_idx = DatetimeIndex(['2010-12-31 15:00:00+00:00', + '2010-12-31 16:00:00+00:00', + '2010-12-31 17:00:00+00:00', + '2010-12-31 23:00:00+00:00', + '2011-01-01 00:00:00+00:00', + '2011-01-01 01:00:00+00:00'] + ) + + expected = pd.DataFrame([[np.nan, 1], [np.nan, 2], [np.nan, 3], + [1, np.nan], [2, np.nan], [3, np.nan]], + index=exp_idx, columns=['a', 'b']) + + tm.assert_frame_equal(result, expected) + + # GH 13783: Concat after resample + result = pd.concat([df1.resample('H').mean(), + df2.resample('H').mean()], sort=True) + expected = pd.DataFrame({'a': [1, 2, 3] + [np.nan] * 3, + 'b': [np.nan] * 3 + [1, 2, 3]}, + index=idx1.append(idx1)) + tm.assert_frame_equal(result, expected) + + @pytest.mark.skipif(PY2, reason="Unhashable Decimal dtype") + def test_concat_different_extension_dtypes_upcasts(self): + a = pd.Series(pd.core.arrays.integer_array([1, 2])) + b = pd.Series(to_decimal([1, 2])) + + result = pd.concat([a, b], ignore_index=True) + expected = pd.Series([ + 1, 2, + Decimal(1), Decimal(2) + ], dtype=object) + tm.assert_series_equal(result, expected) + @pytest.mark.parametrize('pdt', [pd.Series, pd.DataFrame, pd.Panel]) @pytest.mark.parametrize('dt', np.sctypes['float']) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_concat_no_unnecessary_upcast(dt, pdt): # GH 13247 dims = pdt().ndim @@ -1918,10 +2415,149 @@ def test_concat_no_unnecessary_upcast(dt, pdt): @pytest.mark.parametrize('pdt', [pd.Series, pd.DataFrame, pd.Panel]) @pytest.mark.parametrize('dt', np.sctypes['int']) +@pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") def test_concat_will_upcast(dt, pdt): - dims = pdt().ndim - dfs = [pdt(np.array([1], dtype=dt, ndmin=dims)), - pdt(np.array([np.nan], ndmin=dims)), - pdt(np.array([5], dtype=dt, ndmin=dims))] - x = pd.concat(dfs) - assert x.values.dtype == 'float64' + with catch_warnings(record=True): + dims = pdt().ndim + dfs = [pdt(np.array([1], dtype=dt, ndmin=dims)), + pdt(np.array([np.nan], ndmin=dims)), + pdt(np.array([5], dtype=dt, ndmin=dims))] + x = pd.concat(dfs) + assert x.values.dtype == 'float64' + + +def test_concat_empty_and_non_empty_frame_regression(): + # GH 18178 regression test + df1 = pd.DataFrame({'foo': [1]}) + df2 = pd.DataFrame({'foo': []}) + expected = pd.DataFrame({'foo': [1.0]}) + result = pd.concat([df1, df2]) + assert_frame_equal(result, expected) + + +def test_concat_empty_and_non_empty_series_regression(): + # GH 18187 regression test + s1 = pd.Series([1]) + s2 = pd.Series([]) + expected = s1 + result = pd.concat([s1, s2]) + tm.assert_series_equal(result, expected) + + +def test_concat_sorts_columns(sort_with_none): + # GH-4588 + df1 = pd.DataFrame({"a": [1, 2], "b": [1, 2]}, columns=['b', 'a']) + df2 = pd.DataFrame({"a": [3, 4], "c": [5, 6]}) + + # for sort=True/None + expected = pd.DataFrame({"a": [1, 2, 3, 4], + "b": [1, 2, None, None], + "c": [None, None, 5, 6]}, + columns=['a', 'b', 'c']) + + if sort_with_none is False: + expected = expected[['b', 'a', 'c']] + + if sort_with_none is None: + # only warn if not explicitly specified + ctx = tm.assert_produces_warning(FutureWarning) + else: + ctx = tm.assert_produces_warning(None) + + # default + with ctx: + result = pd.concat([df1, df2], ignore_index=True, sort=sort_with_none) + tm.assert_frame_equal(result, expected) + + +def test_concat_sorts_index(sort_with_none): + df1 = pd.DataFrame({"a": [1, 2, 3]}, index=['c', 'a', 'b']) + df2 = pd.DataFrame({"b": [1, 2]}, index=['a', 'b']) + + # For True/None + expected = pd.DataFrame({"a": [2, 3, 1], "b": [1, 2, None]}, + index=['a', 'b', 'c'], + columns=['a', 'b']) + if sort_with_none is False: + expected = expected.loc[['c', 'a', 'b']] + + if sort_with_none is None: + # only warn if not explicitly specified + ctx = tm.assert_produces_warning(FutureWarning) + else: + ctx = tm.assert_produces_warning(None) + + # Warn and sort by default + with ctx: + result = pd.concat([df1, df2], axis=1, sort=sort_with_none) + tm.assert_frame_equal(result, expected) + + +def test_concat_inner_sort(sort_with_none): + # https://github.com/pandas-dev/pandas/pull/20613 + df1 = pd.DataFrame({"a": [1, 2], "b": [1, 2], "c": [1, 2]}, + columns=['b', 'a', 'c']) + df2 = pd.DataFrame({"a": [1, 2], 'b': [3, 4]}, index=[3, 4]) + + with tm.assert_produces_warning(None): + # unset sort should *not* warn for inner join + # since that never sorted + result = pd.concat([df1, df2], sort=sort_with_none, + join='inner', + ignore_index=True) + + expected = pd.DataFrame({"b": [1, 2, 3, 4], "a": [1, 2, 1, 2]}, + columns=['b', 'a']) + if sort_with_none is True: + expected = expected[['a', 'b']] + tm.assert_frame_equal(result, expected) + + +def test_concat_aligned_sort(): + # GH-4588 + df = pd.DataFrame({"c": [1, 2], "b": [3, 4], 'a': [5, 6]}, + columns=['c', 'b', 'a']) + result = pd.concat([df, df], sort=True, ignore_index=True) + expected = pd.DataFrame({'a': [5, 6, 5, 6], 'b': [3, 4, 3, 4], + 'c': [1, 2, 1, 2]}, + columns=['a', 'b', 'c']) + tm.assert_frame_equal(result, expected) + + result = pd.concat([df, df[['c', 'b']]], join='inner', sort=True, + ignore_index=True) + expected = expected[['b', 'c']] + tm.assert_frame_equal(result, expected) + + +def test_concat_aligned_sort_does_not_raise(): + # GH-4588 + # We catch TypeErrors from sorting internally and do not re-raise. + df = pd.DataFrame({1: [1, 2], "a": [3, 4]}, columns=[1, 'a']) + expected = pd.DataFrame({1: [1, 2, 1, 2], 'a': [3, 4, 3, 4]}, + columns=[1, 'a']) + result = pd.concat([df, df], ignore_index=True, sort=True) + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("s1name,s2name", [ + (np.int64(190), (43, 0)), (190, (43, 0))]) +def test_concat_series_name_npscalar_tuple(s1name, s2name): + # GH21015 + s1 = pd.Series({'a': 1, 'b': 2}, name=s1name) + s2 = pd.Series({'c': 5, 'd': 6}, name=s2name) + result = pd.concat([s1, s2]) + expected = pd.Series({'a': 1, 'b': 2, 'c': 5, 'd': 6}) + tm.assert_series_equal(result, expected) + + +def test_concat_categorical_tz(): + # GH-23816 + a = pd.Series(pd.date_range('2017-01-01', periods=2, tz='US/Pacific')) + b = pd.Series(['a', 'b'], dtype='category') + result = pd.concat([a, b], ignore_index=True) + expected = pd.Series([ + pd.Timestamp('2017-01-01', tz="US/Pacific"), + pd.Timestamp('2017-01-02', tz="US/Pacific"), + 'a', 'b' + ]) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/reshape/test_cut.py b/pandas/tests/reshape/test_cut.py new file mode 100644 index 0000000000000..6833460fa515b --- /dev/null +++ b/pandas/tests/reshape/test_cut.py @@ -0,0 +1,458 @@ +import numpy as np +import pytest + +import pandas as pd +from pandas import ( + Categorical, DataFrame, DatetimeIndex, Index, Interval, IntervalIndex, + Series, TimedeltaIndex, Timestamp, cut, date_range, isna, qcut, + timedelta_range, to_datetime) +from pandas.api.types import CategoricalDtype as CDT +import pandas.core.reshape.tile as tmod +import pandas.util.testing as tm + + +def test_simple(): + data = np.ones(5, dtype="int64") + result = cut(data, 4, labels=False) + + expected = np.array([1, 1, 1, 1, 1]) + tm.assert_numpy_array_equal(result, expected, check_dtype=False) + + +def test_bins(): + data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1]) + result, bins = cut(data, 3, retbins=True) + + intervals = IntervalIndex.from_breaks(bins.round(3)) + intervals = intervals.take([0, 0, 0, 1, 2, 0]) + expected = Categorical(intervals, ordered=True) + + tm.assert_categorical_equal(result, expected) + tm.assert_almost_equal(bins, np.array([0.1905, 3.36666667, + 6.53333333, 9.7])) + + +def test_right(): + data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) + result, bins = cut(data, 4, right=True, retbins=True) + + intervals = IntervalIndex.from_breaks(bins.round(3)) + expected = Categorical(intervals, ordered=True) + expected = expected.take([0, 0, 0, 2, 3, 0, 0]) + + tm.assert_categorical_equal(result, expected) + tm.assert_almost_equal(bins, np.array([0.1905, 2.575, 4.95, 7.325, 9.7])) + + +def test_no_right(): + data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) + result, bins = cut(data, 4, right=False, retbins=True) + + intervals = IntervalIndex.from_breaks(bins.round(3), closed="left") + intervals = intervals.take([0, 0, 0, 2, 3, 0, 1]) + expected = Categorical(intervals, ordered=True) + + tm.assert_categorical_equal(result, expected) + tm.assert_almost_equal(bins, np.array([0.2, 2.575, 4.95, 7.325, 9.7095])) + + +def test_array_like(): + data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] + result, bins = cut(data, 3, retbins=True) + + intervals = IntervalIndex.from_breaks(bins.round(3)) + intervals = intervals.take([0, 0, 0, 1, 2, 0]) + expected = Categorical(intervals, ordered=True) + + tm.assert_categorical_equal(result, expected) + tm.assert_almost_equal(bins, np.array([0.1905, 3.36666667, + 6.53333333, 9.7])) + + +def test_bins_from_interval_index(): + c = cut(range(5), 3) + expected = c + result = cut(range(5), bins=expected.categories) + tm.assert_categorical_equal(result, expected) + + expected = Categorical.from_codes(np.append(c.codes, -1), + categories=c.categories, + ordered=True) + result = cut(range(6), bins=expected.categories) + tm.assert_categorical_equal(result, expected) + + +def test_bins_from_interval_index_doc_example(): + # Make sure we preserve the bins. + ages = np.array([10, 15, 13, 12, 23, 25, 28, 59, 60]) + c = cut(ages, bins=[0, 18, 35, 70]) + expected = IntervalIndex.from_tuples([(0, 18), (18, 35), (35, 70)]) + tm.assert_index_equal(c.categories, expected) + + result = cut([25, 20, 50], bins=c.categories) + tm.assert_index_equal(result.categories, expected) + tm.assert_numpy_array_equal(result.codes, + np.array([1, 1, 2], dtype="int8")) + + +def test_bins_not_overlapping_from_interval_index(): + # see gh-23980 + msg = "Overlapping IntervalIndex is not accepted" + ii = IntervalIndex.from_tuples([(0, 10), (2, 12), (4, 14)]) + + with pytest.raises(ValueError, match=msg): + cut([5, 6], bins=ii) + + +def test_bins_not_monotonic(): + msg = "bins must increase monotonically" + data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] + + with pytest.raises(ValueError, match=msg): + cut(data, [0.1, 1.5, 1, 10]) + + +def test_wrong_num_labels(): + msg = "Bin labels must be one fewer than the number of bin edges" + data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] + + with pytest.raises(ValueError, match=msg): + cut(data, [0, 1, 10], labels=["foo", "bar", "baz"]) + + +@pytest.mark.parametrize("x,bins,msg", [ + ([], 2, "Cannot cut empty array"), + ([1, 2, 3], 0.5, "`bins` should be a positive integer") +]) +def test_cut_corner(x, bins, msg): + with pytest.raises(ValueError, match=msg): + cut(x, bins) + + +@pytest.mark.parametrize("arg", [2, np.eye(2), DataFrame(np.eye(2))]) +@pytest.mark.parametrize("cut_func", [cut, qcut]) +def test_cut_not_1d_arg(arg, cut_func): + msg = "Input array must be 1 dimensional" + with pytest.raises(ValueError, match=msg): + cut_func(arg, 2) + + +@pytest.mark.parametrize('data', [ + [0, 1, 2, 3, 4, np.inf], + [-np.inf, 0, 1, 2, 3, 4], + [-np.inf, 0, 1, 2, 3, 4, np.inf]]) +def test_int_bins_with_inf(data): + # GH 24314 + msg = 'cannot specify integer `bins` when input data contains infinity' + with pytest.raises(ValueError, match=msg): + cut(data, bins=3) + + +def test_cut_out_of_range_more(): + # see gh-1511 + name = "x" + + ser = Series([0, -1, 0, 1, -3], name=name) + ind = cut(ser, [0, 1], labels=False) + + exp = Series([np.nan, np.nan, np.nan, 0, np.nan], name=name) + tm.assert_series_equal(ind, exp) + + +@pytest.mark.parametrize("right,breaks,closed", [ + (True, [-1e-3, 0.25, 0.5, 0.75, 1], "right"), + (False, [0, 0.25, 0.5, 0.75, 1 + 1e-3], "left") +]) +def test_labels(right, breaks, closed): + arr = np.tile(np.arange(0, 1.01, 0.1), 4) + + result, bins = cut(arr, 4, retbins=True, right=right) + ex_levels = IntervalIndex.from_breaks(breaks, closed=closed) + tm.assert_index_equal(result.categories, ex_levels) + + +def test_cut_pass_series_name_to_factor(): + name = "foo" + ser = Series(np.random.randn(100), name=name) + + factor = cut(ser, 4) + assert factor.name == name + + +def test_label_precision(): + arr = np.arange(0, 0.73, 0.01) + result = cut(arr, 4, precision=2) + + ex_levels = IntervalIndex.from_breaks([-0.00072, 0.18, 0.36, 0.54, 0.72]) + tm.assert_index_equal(result.categories, ex_levels) + + +@pytest.mark.parametrize("labels", [None, False]) +def test_na_handling(labels): + arr = np.arange(0, 0.75, 0.01) + arr[::3] = np.nan + + result = cut(arr, 4, labels=labels) + result = np.asarray(result) + + expected = np.where(isna(arr), np.nan, result) + tm.assert_almost_equal(result, expected) + + +def test_inf_handling(): + data = np.arange(6) + data_ser = Series(data, dtype="int64") + + bins = [-np.inf, 2, 4, np.inf] + result = cut(data, bins) + result_ser = cut(data_ser, bins) + + ex_uniques = IntervalIndex.from_breaks(bins) + tm.assert_index_equal(result.categories, ex_uniques) + + assert result[5] == Interval(4, np.inf) + assert result[0] == Interval(-np.inf, 2) + assert result_ser[5] == Interval(4, np.inf) + assert result_ser[0] == Interval(-np.inf, 2) + + +def test_cut_out_of_bounds(): + arr = np.random.randn(100) + result = cut(arr, [-1, 0, 1]) + + mask = isna(result) + ex_mask = (arr < -1) | (arr > 1) + tm.assert_numpy_array_equal(mask, ex_mask) + + +@pytest.mark.parametrize("get_labels,get_expected", [ + (lambda labels: labels, + lambda labels: Categorical(["Medium"] + 4 * ["Small"] + + ["Medium", "Large"], + categories=labels, ordered=True)), + (lambda labels: Categorical.from_codes([0, 1, 2], labels), + lambda labels: Categorical.from_codes([1] + 4 * [0] + [1, 2], labels)) +]) +def test_cut_pass_labels(get_labels, get_expected): + bins = [0, 25, 50, 100] + arr = [50, 5, 10, 15, 20, 30, 70] + labels = ["Small", "Medium", "Large"] + + result = cut(arr, bins, labels=get_labels(labels)) + tm.assert_categorical_equal(result, get_expected(labels)) + + +def test_cut_pass_labels_compat(): + # see gh-16459 + arr = [50, 5, 10, 15, 20, 30, 70] + labels = ["Good", "Medium", "Bad"] + + result = cut(arr, 3, labels=labels) + exp = cut(arr, 3, labels=Categorical(labels, categories=labels, + ordered=True)) + tm.assert_categorical_equal(result, exp) + + +@pytest.mark.parametrize("x", [np.arange(11.), np.arange(11.) / 1e10]) +def test_round_frac_just_works(x): + # It works. + cut(x, 2) + + +@pytest.mark.parametrize("val,precision,expected", [ + (-117.9998, 3, -118), + (117.9998, 3, 118), + (117.9998, 2, 118), + (0.000123456, 2, 0.00012) +]) +def test_round_frac(val, precision, expected): + # see gh-1979 + result = tmod._round_frac(val, precision=precision) + assert result == expected + + +def test_cut_return_intervals(): + ser = Series([0, 1, 2, 3, 4, 5, 6, 7, 8]) + result = cut(ser, 3) + + exp_bins = np.linspace(0, 8, num=4).round(3) + exp_bins[0] -= 0.008 + + expected = Series(IntervalIndex.from_breaks(exp_bins, closed="right").take( + [0, 0, 0, 1, 1, 1, 2, 2, 2])).astype(CDT(ordered=True)) + tm.assert_series_equal(result, expected) + + +def test_series_ret_bins(): + # see gh-8589 + ser = Series(np.arange(4)) + result, bins = cut(ser, 2, retbins=True) + + expected = Series(IntervalIndex.from_breaks( + [-0.003, 1.5, 3], closed="right").repeat(2)).astype(CDT(ordered=True)) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("kwargs,msg", [ + (dict(duplicates="drop"), None), + (dict(), "Bin edges must be unique"), + (dict(duplicates="raise"), "Bin edges must be unique"), + (dict(duplicates="foo"), "invalid value for 'duplicates' parameter") +]) +def test_cut_duplicates_bin(kwargs, msg): + # see gh-20947 + bins = [0, 2, 4, 6, 10, 10] + values = Series(np.array([1, 3, 5, 7, 9]), index=["a", "b", "c", "d", "e"]) + + if msg is not None: + with pytest.raises(ValueError, match=msg): + cut(values, bins, **kwargs) + else: + result = cut(values, bins, **kwargs) + expected = cut(values, pd.unique(bins)) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("data", [9.0, -9.0, 0.0]) +@pytest.mark.parametrize("length", [1, 2]) +def test_single_bin(data, length): + # see gh-14652, gh-15428 + ser = Series([data] * length) + result = cut(ser, 1, labels=False) + + expected = Series([0] * length) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + "array_1_writeable,array_2_writeable", + [(True, True), (True, False), (False, False)]) +def test_cut_read_only(array_1_writeable, array_2_writeable): + # issue 18773 + array_1 = np.arange(0, 100, 10) + array_1.flags.writeable = array_1_writeable + + array_2 = np.arange(0, 100, 10) + array_2.flags.writeable = array_2_writeable + + hundred_elements = np.arange(100) + tm.assert_categorical_equal(cut(hundred_elements, array_1), + cut(hundred_elements, array_2)) + + +@pytest.mark.parametrize("conv", [ + lambda v: Timestamp(v), + lambda v: to_datetime(v), + lambda v: np.datetime64(v), + lambda v: Timestamp(v).to_pydatetime(), +]) +def test_datetime_bin(conv): + data = [np.datetime64("2012-12-13"), np.datetime64("2012-12-15")] + bin_data = ["2012-12-12", "2012-12-14", "2012-12-16"] + + expected = Series(IntervalIndex([ + Interval(Timestamp(bin_data[0]), Timestamp(bin_data[1])), + Interval(Timestamp(bin_data[1]), Timestamp(bin_data[2]))])).astype( + CDT(ordered=True)) + + bins = [conv(v) for v in bin_data] + result = Series(cut(data, bins=bins)) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("data", [ + to_datetime(Series(["2013-01-01", "2013-01-02", "2013-01-03"])), + [np.datetime64("2013-01-01"), np.datetime64("2013-01-02"), + np.datetime64("2013-01-03")], + np.array([np.datetime64("2013-01-01"), np.datetime64("2013-01-02"), + np.datetime64("2013-01-03")]), + DatetimeIndex(["2013-01-01", "2013-01-02", "2013-01-03"]) +]) +def test_datetime_cut(data): + # see gh-14714 + # + # Testing time data when it comes in various collection types. + result, _ = cut(data, 3, retbins=True) + expected = Series(IntervalIndex([ + Interval(Timestamp("2012-12-31 23:57:07.200000"), + Timestamp("2013-01-01 16:00:00")), + Interval(Timestamp("2013-01-01 16:00:00"), + Timestamp("2013-01-02 08:00:00")), + Interval(Timestamp("2013-01-02 08:00:00"), + Timestamp("2013-01-03 00:00:00"))])).astype(CDT(ordered=True)) + tm.assert_series_equal(Series(result), expected) + + +@pytest.mark.parametrize("bins", [ + 3, [Timestamp("2013-01-01 04:57:07.200000"), + Timestamp("2013-01-01 21:00:00"), + Timestamp("2013-01-02 13:00:00"), + Timestamp("2013-01-03 05:00:00")]]) +@pytest.mark.parametrize("box", [list, np.array, Index, Series]) +def test_datetime_tz_cut(bins, box): + # see gh-19872 + tz = "US/Eastern" + s = Series(date_range("20130101", periods=3, tz=tz)) + + if not isinstance(bins, int): + bins = box(bins) + + result = cut(s, bins) + expected = Series(IntervalIndex([ + Interval(Timestamp("2012-12-31 23:57:07.200000", tz=tz), + Timestamp("2013-01-01 16:00:00", tz=tz)), + Interval(Timestamp("2013-01-01 16:00:00", tz=tz), + Timestamp("2013-01-02 08:00:00", tz=tz)), + Interval(Timestamp("2013-01-02 08:00:00", tz=tz), + Timestamp("2013-01-03 00:00:00", tz=tz))])).astype( + CDT(ordered=True)) + tm.assert_series_equal(result, expected) + + +def test_datetime_nan_error(): + msg = "bins must be of datetime64 dtype" + + with pytest.raises(ValueError, match=msg): + cut(date_range("20130101", periods=3), bins=[0, 2, 4]) + + +def test_datetime_nan_mask(): + result = cut(date_range("20130102", periods=5), + bins=date_range("20130101", periods=2)) + + mask = result.categories.isna() + tm.assert_numpy_array_equal(mask, np.array([False])) + + mask = result.isna() + tm.assert_numpy_array_equal(mask, np.array([False, True, True, + True, True])) + + +@pytest.mark.parametrize("tz", [None, "UTC", "US/Pacific"]) +def test_datetime_cut_roundtrip(tz): + # see gh-19891 + ser = Series(date_range("20180101", periods=3, tz=tz)) + result, result_bins = cut(ser, 2, retbins=True) + + expected = cut(ser, result_bins) + tm.assert_series_equal(result, expected) + + expected_bins = DatetimeIndex(["2017-12-31 23:57:07.200000", + "2018-01-02 00:00:00", + "2018-01-03 00:00:00"]) + expected_bins = expected_bins.tz_localize(tz) + tm.assert_index_equal(result_bins, expected_bins) + + +def test_timedelta_cut_roundtrip(): + # see gh-19891 + ser = Series(timedelta_range("1day", periods=3)) + result, result_bins = cut(ser, 2, retbins=True) + + expected = cut(ser, result_bins) + tm.assert_series_equal(result, expected) + + expected_bins = TimedeltaIndex(["0 days 23:57:07.200000", + "2 days 00:00:00", + "3 days 00:00:00"]) + tm.assert_index_equal(result_bins, expected_bins) diff --git a/pandas/tests/reshape/test_melt.py b/pandas/tests/reshape/test_melt.py new file mode 100644 index 0000000000000..6bd1958633e25 --- /dev/null +++ b/pandas/tests/reshape/test_melt.py @@ -0,0 +1,718 @@ +# -*- coding: utf-8 -*- +# pylint: disable-msg=W0612,E1101 + +import numpy as np +from numpy import nan +import pytest + +from pandas.compat import range + +import pandas as pd +from pandas import DataFrame, lreshape, melt, wide_to_long +import pandas.util.testing as tm + + +class TestMelt(object): + + def setup_method(self, method): + self.df = tm.makeTimeDataFrame()[:10] + self.df['id1'] = (self.df['A'] > 0).astype(np.int64) + self.df['id2'] = (self.df['B'] > 0).astype(np.int64) + + self.var_name = 'var' + self.value_name = 'val' + + self.df1 = pd.DataFrame([[1.067683, -1.110463, 0.20867 + ], [-1.321405, 0.368915, -1.055342], + [-0.807333, 0.08298, -0.873361]]) + self.df1.columns = [list('ABC'), list('abc')] + self.df1.columns.names = ['CAP', 'low'] + + def test_top_level_method(self): + result = melt(self.df) + assert result.columns.tolist() == ['variable', 'value'] + + def test_method_signatures(self): + tm.assert_frame_equal(self.df.melt(), + melt(self.df)) + + tm.assert_frame_equal(self.df.melt(id_vars=['id1', 'id2'], + value_vars=['A', 'B']), + melt(self.df, + id_vars=['id1', 'id2'], + value_vars=['A', 'B'])) + + tm.assert_frame_equal(self.df.melt(var_name=self.var_name, + value_name=self.value_name), + melt(self.df, + var_name=self.var_name, + value_name=self.value_name)) + + tm.assert_frame_equal(self.df1.melt(col_level=0), + melt(self.df1, col_level=0)) + + def test_default_col_names(self): + result = self.df.melt() + assert result.columns.tolist() == ['variable', 'value'] + + result1 = self.df.melt(id_vars=['id1']) + assert result1.columns.tolist() == ['id1', 'variable', 'value'] + + result2 = self.df.melt(id_vars=['id1', 'id2']) + assert result2.columns.tolist() == ['id1', 'id2', 'variable', 'value'] + + def test_value_vars(self): + result3 = self.df.melt(id_vars=['id1', 'id2'], value_vars='A') + assert len(result3) == 10 + + result4 = self.df.melt(id_vars=['id1', 'id2'], value_vars=['A', 'B']) + expected4 = DataFrame({'id1': self.df['id1'].tolist() * 2, + 'id2': self.df['id2'].tolist() * 2, + 'variable': ['A'] * 10 + ['B'] * 10, + 'value': (self.df['A'].tolist() + + self.df['B'].tolist())}, + columns=['id1', 'id2', 'variable', 'value']) + tm.assert_frame_equal(result4, expected4) + + def test_value_vars_types(self): + # GH 15348 + expected = DataFrame({'id1': self.df['id1'].tolist() * 2, + 'id2': self.df['id2'].tolist() * 2, + 'variable': ['A'] * 10 + ['B'] * 10, + 'value': (self.df['A'].tolist() + + self.df['B'].tolist())}, + columns=['id1', 'id2', 'variable', 'value']) + + for type_ in (tuple, list, np.array): + result = self.df.melt(id_vars=['id1', 'id2'], + value_vars=type_(('A', 'B'))) + tm.assert_frame_equal(result, expected) + + def test_vars_work_with_multiindex(self): + expected = DataFrame({ + ('A', 'a'): self.df1[('A', 'a')], + 'CAP': ['B'] * len(self.df1), + 'low': ['b'] * len(self.df1), + 'value': self.df1[('B', 'b')], + }, columns=[('A', 'a'), 'CAP', 'low', 'value']) + + result = self.df1.melt(id_vars=[('A', 'a')], value_vars=[('B', 'b')]) + tm.assert_frame_equal(result, expected) + + def test_single_vars_work_with_multiindex(self): + expected = DataFrame({ + 'A': {0: 1.067683, 1: -1.321405, 2: -0.807333}, + 'CAP': {0: 'B', 1: 'B', 2: 'B'}, + 'value': {0: -1.110463, 1: 0.368915, 2: 0.08298}}) + result = self.df1.melt(['A'], ['B'], col_level=0) + tm.assert_frame_equal(result, expected) + + def test_tuple_vars_fail_with_multiindex(self): + # melt should fail with an informative error message if + # the columns have a MultiIndex and a tuple is passed + # for id_vars or value_vars. + tuple_a = ('A', 'a') + list_a = [tuple_a] + tuple_b = ('B', 'b') + list_b = [tuple_b] + + msg = (r"(id|value)_vars must be a list of tuples when columns are" + " a MultiIndex") + for id_vars, value_vars in ((tuple_a, list_b), (list_a, tuple_b), + (tuple_a, tuple_b)): + with pytest.raises(ValueError, match=msg): + self.df1.melt(id_vars=id_vars, value_vars=value_vars) + + def test_custom_var_name(self): + result5 = self.df.melt(var_name=self.var_name) + assert result5.columns.tolist() == ['var', 'value'] + + result6 = self.df.melt(id_vars=['id1'], var_name=self.var_name) + assert result6.columns.tolist() == ['id1', 'var', 'value'] + + result7 = self.df.melt(id_vars=['id1', 'id2'], var_name=self.var_name) + assert result7.columns.tolist() == ['id1', 'id2', 'var', 'value'] + + result8 = self.df.melt(id_vars=['id1', 'id2'], value_vars='A', + var_name=self.var_name) + assert result8.columns.tolist() == ['id1', 'id2', 'var', 'value'] + + result9 = self.df.melt(id_vars=['id1', 'id2'], value_vars=['A', 'B'], + var_name=self.var_name) + expected9 = DataFrame({'id1': self.df['id1'].tolist() * 2, + 'id2': self.df['id2'].tolist() * 2, + self.var_name: ['A'] * 10 + ['B'] * 10, + 'value': (self.df['A'].tolist() + + self.df['B'].tolist())}, + columns=['id1', 'id2', self.var_name, 'value']) + tm.assert_frame_equal(result9, expected9) + + def test_custom_value_name(self): + result10 = self.df.melt(value_name=self.value_name) + assert result10.columns.tolist() == ['variable', 'val'] + + result11 = self.df.melt(id_vars=['id1'], value_name=self.value_name) + assert result11.columns.tolist() == ['id1', 'variable', 'val'] + + result12 = self.df.melt(id_vars=['id1', 'id2'], + value_name=self.value_name) + assert result12.columns.tolist() == ['id1', 'id2', 'variable', 'val'] + + result13 = self.df.melt(id_vars=['id1', 'id2'], value_vars='A', + value_name=self.value_name) + assert result13.columns.tolist() == ['id1', 'id2', 'variable', 'val'] + + result14 = self.df.melt(id_vars=['id1', 'id2'], value_vars=['A', 'B'], + value_name=self.value_name) + expected14 = DataFrame({'id1': self.df['id1'].tolist() * 2, + 'id2': self.df['id2'].tolist() * 2, + 'variable': ['A'] * 10 + ['B'] * 10, + self.value_name: (self.df['A'].tolist() + + self.df['B'].tolist())}, + columns=['id1', 'id2', 'variable', + self.value_name]) + tm.assert_frame_equal(result14, expected14) + + def test_custom_var_and_value_name(self): + + result15 = self.df.melt(var_name=self.var_name, + value_name=self.value_name) + assert result15.columns.tolist() == ['var', 'val'] + + result16 = self.df.melt(id_vars=['id1'], var_name=self.var_name, + value_name=self.value_name) + assert result16.columns.tolist() == ['id1', 'var', 'val'] + + result17 = self.df.melt(id_vars=['id1', 'id2'], + var_name=self.var_name, + value_name=self.value_name) + assert result17.columns.tolist() == ['id1', 'id2', 'var', 'val'] + + result18 = self.df.melt(id_vars=['id1', 'id2'], value_vars='A', + var_name=self.var_name, + value_name=self.value_name) + assert result18.columns.tolist() == ['id1', 'id2', 'var', 'val'] + + result19 = self.df.melt(id_vars=['id1', 'id2'], value_vars=['A', 'B'], + var_name=self.var_name, + value_name=self.value_name) + expected19 = DataFrame({'id1': self.df['id1'].tolist() * 2, + 'id2': self.df['id2'].tolist() * 2, + self.var_name: ['A'] * 10 + ['B'] * 10, + self.value_name: (self.df['A'].tolist() + + self.df['B'].tolist())}, + columns=['id1', 'id2', self.var_name, + self.value_name]) + tm.assert_frame_equal(result19, expected19) + + df20 = self.df.copy() + df20.columns.name = 'foo' + result20 = df20.melt() + assert result20.columns.tolist() == ['foo', 'value'] + + def test_col_level(self): + res1 = self.df1.melt(col_level=0) + res2 = self.df1.melt(col_level='CAP') + assert res1.columns.tolist() == ['CAP', 'value'] + assert res2.columns.tolist() == ['CAP', 'value'] + + def test_multiindex(self): + res = self.df1.melt() + assert res.columns.tolist() == ['CAP', 'low', 'value'] + + @pytest.mark.parametrize("col", [ + pd.Series(pd.date_range('2010', periods=5, tz='US/Pacific')), + pd.Series(["a", "b", "c", "a", "d"], dtype="category"), + pd.Series([0, 1, 0, 0, 0])]) + def test_pandas_dtypes(self, col): + # GH 15785 + df = DataFrame({'klass': range(5), + 'col': col, + 'attr1': [1, 0, 0, 0, 0], + 'attr2': col}) + expected_value = pd.concat([pd.Series([1, 0, 0, 0, 0]), col], + ignore_index=True) + result = melt(df, id_vars=['klass', 'col'], var_name='attribute', + value_name='value') + expected = DataFrame({0: list(range(5)) * 2, + 1: pd.concat([col] * 2, ignore_index=True), + 2: ['attr1'] * 5 + ['attr2'] * 5, + 3: expected_value}) + expected.columns = ['klass', 'col', 'attribute', 'value'] + tm.assert_frame_equal(result, expected) + + def test_melt_missing_columns_raises(self): + # GH-23575 + # This test is to ensure that pandas raises an error if melting is + # attempted with column names absent from the dataframe + + # Generate data + df = pd.DataFrame(np.random.randn(5, 4), columns=list('abcd')) + + # Try to melt with missing `value_vars` column name + msg = "The following '{Var}' are not present in the DataFrame: {Col}" + with pytest.raises( + KeyError, + match=msg.format(Var='value_vars', Col="\\['C'\\]")): + df.melt(['a', 'b'], ['C', 'd']) + + # Try to melt with missing `id_vars` column name + with pytest.raises( + KeyError, + match=msg.format(Var='id_vars', Col="\\['A'\\]")): + df.melt(['A', 'b'], ['c', 'd']) + + # Multiple missing + with pytest.raises( + KeyError, + match=msg.format(Var='id_vars', + Col="\\['not_here', 'or_there'\\]")): + df.melt(['a', 'b', 'not_here', 'or_there'], ['c', 'd']) + + # Multiindex melt fails if column is missing from multilevel melt + multi = df.copy() + multi.columns = [list('ABCD'), list('abcd')] + with pytest.raises( + KeyError, + match=msg.format(Var='id_vars', + Col="\\['E'\\]")): + multi.melt([('E', 'a')], [('B', 'b')]) + # Multiindex fails if column is missing from single level melt + with pytest.raises( + KeyError, + match=msg.format(Var='value_vars', + Col="\\['F'\\]")): + multi.melt(['A'], ['F'], col_level=0) + + +class TestLreshape(object): + + def test_pairs(self): + data = {'birthdt': ['08jan2009', '20dec2008', '30dec2008', '21dec2008', + '11jan2009'], + 'birthwt': [1766, 3301, 1454, 3139, 4133], + 'id': [101, 102, 103, 104, 105], + 'sex': ['Male', 'Female', 'Female', 'Female', 'Female'], + 'visitdt1': ['11jan2009', '22dec2008', '04jan2009', + '29dec2008', '20jan2009'], + 'visitdt2': + ['21jan2009', nan, '22jan2009', '31dec2008', '03feb2009'], + 'visitdt3': ['05feb2009', nan, nan, '02jan2009', '15feb2009'], + 'wt1': [1823, 3338, 1549, 3298, 4306], + 'wt2': [2011.0, nan, 1892.0, 3338.0, 4575.0], + 'wt3': [2293.0, nan, nan, 3377.0, 4805.0]} + + df = DataFrame(data) + + spec = {'visitdt': ['visitdt%d' % i for i in range(1, 4)], + 'wt': ['wt%d' % i for i in range(1, 4)]} + result = lreshape(df, spec) + + exp_data = {'birthdt': + ['08jan2009', '20dec2008', '30dec2008', '21dec2008', + '11jan2009', '08jan2009', '30dec2008', '21dec2008', + '11jan2009', '08jan2009', '21dec2008', '11jan2009'], + 'birthwt': [1766, 3301, 1454, 3139, 4133, 1766, 1454, 3139, + 4133, 1766, 3139, 4133], + 'id': [101, 102, 103, 104, 105, 101, 103, 104, 105, 101, + 104, 105], + 'sex': ['Male', 'Female', 'Female', 'Female', 'Female', + 'Male', 'Female', 'Female', 'Female', 'Male', + 'Female', 'Female'], + 'visitdt': ['11jan2009', '22dec2008', '04jan2009', + '29dec2008', '20jan2009', '21jan2009', + '22jan2009', '31dec2008', '03feb2009', + '05feb2009', '02jan2009', '15feb2009'], + 'wt': [1823.0, 3338.0, 1549.0, 3298.0, 4306.0, 2011.0, + 1892.0, 3338.0, 4575.0, 2293.0, 3377.0, 4805.0]} + exp = DataFrame(exp_data, columns=result.columns) + tm.assert_frame_equal(result, exp) + + result = lreshape(df, spec, dropna=False) + exp_data = {'birthdt': + ['08jan2009', '20dec2008', '30dec2008', '21dec2008', + '11jan2009', '08jan2009', '20dec2008', '30dec2008', + '21dec2008', '11jan2009', '08jan2009', '20dec2008', + '30dec2008', '21dec2008', '11jan2009'], + 'birthwt': [1766, 3301, 1454, 3139, 4133, 1766, 3301, 1454, + 3139, 4133, 1766, 3301, 1454, 3139, 4133], + 'id': [101, 102, 103, 104, 105, 101, 102, 103, 104, 105, + 101, 102, 103, 104, 105], + 'sex': ['Male', 'Female', 'Female', 'Female', 'Female', + 'Male', 'Female', 'Female', 'Female', 'Female', + 'Male', 'Female', 'Female', 'Female', 'Female'], + 'visitdt': ['11jan2009', '22dec2008', '04jan2009', + '29dec2008', '20jan2009', '21jan2009', nan, + '22jan2009', '31dec2008', '03feb2009', + '05feb2009', nan, nan, '02jan2009', + '15feb2009'], + 'wt': [1823.0, 3338.0, 1549.0, 3298.0, 4306.0, 2011.0, nan, + 1892.0, 3338.0, 4575.0, 2293.0, nan, nan, 3377.0, + 4805.0]} + exp = DataFrame(exp_data, columns=result.columns) + tm.assert_frame_equal(result, exp) + + spec = {'visitdt': ['visitdt%d' % i for i in range(1, 3)], + 'wt': ['wt%d' % i for i in range(1, 4)]} + msg = "All column lists must be same length" + with pytest.raises(ValueError, match=msg): + lreshape(df, spec) + + +class TestWideToLong(object): + + def test_simple(self): + np.random.seed(123) + x = np.random.randn(3) + df = pd.DataFrame({"A1970": {0: "a", + 1: "b", + 2: "c"}, + "A1980": {0: "d", + 1: "e", + 2: "f"}, + "B1970": {0: 2.5, + 1: 1.2, + 2: .7}, + "B1980": {0: 3.2, + 1: 1.3, + 2: .1}, + "X": dict(zip( + range(3), x))}) + df["id"] = df.index + exp_data = {"X": x.tolist() + x.tolist(), + "A": ['a', 'b', 'c', 'd', 'e', 'f'], + "B": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], + "year": [1970, 1970, 1970, 1980, 1980, 1980], + "id": [0, 1, 2, 0, 1, 2]} + expected = DataFrame(exp_data) + expected = expected.set_index(['id', 'year'])[["X", "A", "B"]] + result = wide_to_long(df, ["A", "B"], i="id", j="year") + tm.assert_frame_equal(result, expected) + + def test_stubs(self): + # GH9204 + df = pd.DataFrame([[0, 1, 2, 3, 8], [4, 5, 6, 7, 9]]) + df.columns = ['id', 'inc1', 'inc2', 'edu1', 'edu2'] + stubs = ['inc', 'edu'] + + # TODO: unused? + df_long = pd.wide_to_long(df, stubs, i='id', j='age') # noqa + + assert stubs == ['inc', 'edu'] + + def test_separating_character(self): + # GH14779 + np.random.seed(123) + x = np.random.randn(3) + df = pd.DataFrame({"A.1970": {0: "a", + 1: "b", + 2: "c"}, + "A.1980": {0: "d", + 1: "e", + 2: "f"}, + "B.1970": {0: 2.5, + 1: 1.2, + 2: .7}, + "B.1980": {0: 3.2, + 1: 1.3, + 2: .1}, + "X": dict(zip( + range(3), x))}) + df["id"] = df.index + exp_data = {"X": x.tolist() + x.tolist(), + "A": ['a', 'b', 'c', 'd', 'e', 'f'], + "B": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], + "year": [1970, 1970, 1970, 1980, 1980, 1980], + "id": [0, 1, 2, 0, 1, 2]} + expected = DataFrame(exp_data) + expected = expected.set_index(['id', 'year'])[["X", "A", "B"]] + result = wide_to_long(df, ["A", "B"], i="id", j="year", sep=".") + tm.assert_frame_equal(result, expected) + + def test_escapable_characters(self): + np.random.seed(123) + x = np.random.randn(3) + df = pd.DataFrame({"A(quarterly)1970": {0: "a", + 1: "b", + 2: "c"}, + "A(quarterly)1980": {0: "d", + 1: "e", + 2: "f"}, + "B(quarterly)1970": {0: 2.5, + 1: 1.2, + 2: .7}, + "B(quarterly)1980": {0: 3.2, + 1: 1.3, + 2: .1}, + "X": dict(zip( + range(3), x))}) + df["id"] = df.index + exp_data = {"X": x.tolist() + x.tolist(), + "A(quarterly)": ['a', 'b', 'c', 'd', 'e', 'f'], + "B(quarterly)": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], + "year": [1970, 1970, 1970, 1980, 1980, 1980], + "id": [0, 1, 2, 0, 1, 2]} + expected = DataFrame(exp_data) + expected = expected.set_index( + ['id', 'year'])[["X", "A(quarterly)", "B(quarterly)"]] + result = wide_to_long(df, ["A(quarterly)", "B(quarterly)"], + i="id", j="year") + tm.assert_frame_equal(result, expected) + + def test_unbalanced(self): + # test that we can have a varying amount of time variables + df = pd.DataFrame({'A2010': [1.0, 2.0], + 'A2011': [3.0, 4.0], + 'B2010': [5.0, 6.0], + 'X': ['X1', 'X2']}) + df['id'] = df.index + exp_data = {'X': ['X1', 'X1', 'X2', 'X2'], + 'A': [1.0, 3.0, 2.0, 4.0], + 'B': [5.0, np.nan, 6.0, np.nan], + 'id': [0, 0, 1, 1], + 'year': [2010, 2011, 2010, 2011]} + expected = pd.DataFrame(exp_data) + expected = expected.set_index(['id', 'year'])[["X", "A", "B"]] + result = wide_to_long(df, ['A', 'B'], i='id', j='year') + tm.assert_frame_equal(result, expected) + + def test_character_overlap(self): + # Test we handle overlapping characters in both id_vars and value_vars + df = pd.DataFrame({ + 'A11': ['a11', 'a22', 'a33'], + 'A12': ['a21', 'a22', 'a23'], + 'B11': ['b11', 'b12', 'b13'], + 'B12': ['b21', 'b22', 'b23'], + 'BB11': [1, 2, 3], + 'BB12': [4, 5, 6], + 'BBBX': [91, 92, 93], + 'BBBZ': [91, 92, 93] + }) + df['id'] = df.index + expected = pd.DataFrame({ + 'BBBX': [91, 92, 93, 91, 92, 93], + 'BBBZ': [91, 92, 93, 91, 92, 93], + 'A': ['a11', 'a22', 'a33', 'a21', 'a22', 'a23'], + 'B': ['b11', 'b12', 'b13', 'b21', 'b22', 'b23'], + 'BB': [1, 2, 3, 4, 5, 6], + 'id': [0, 1, 2, 0, 1, 2], + 'year': [11, 11, 11, 12, 12, 12]}) + expected = expected.set_index(['id', 'year'])[ + ['BBBX', 'BBBZ', 'A', 'B', 'BB']] + result = wide_to_long(df, ['A', 'B', 'BB'], i='id', j='year') + tm.assert_frame_equal(result.sort_index(axis=1), + expected.sort_index(axis=1)) + + def test_invalid_separator(self): + # if an invalid separator is supplied a empty data frame is returned + sep = 'nope!' + df = pd.DataFrame({'A2010': [1.0, 2.0], + 'A2011': [3.0, 4.0], + 'B2010': [5.0, 6.0], + 'X': ['X1', 'X2']}) + df['id'] = df.index + exp_data = {'X': '', + 'A2010': [], + 'A2011': [], + 'B2010': [], + 'id': [], + 'year': [], + 'A': [], + 'B': []} + expected = pd.DataFrame(exp_data).astype({'year': 'int'}) + expected = expected.set_index(['id', 'year'])[[ + 'X', 'A2010', 'A2011', 'B2010', 'A', 'B']] + expected.index.set_levels([0, 1], level=0, inplace=True) + result = wide_to_long(df, ['A', 'B'], i='id', j='year', sep=sep) + tm.assert_frame_equal(result.sort_index(axis=1), + expected.sort_index(axis=1)) + + def test_num_string_disambiguation(self): + # Test that we can disambiguate number value_vars from + # string value_vars + df = pd.DataFrame({ + 'A11': ['a11', 'a22', 'a33'], + 'A12': ['a21', 'a22', 'a23'], + 'B11': ['b11', 'b12', 'b13'], + 'B12': ['b21', 'b22', 'b23'], + 'BB11': [1, 2, 3], + 'BB12': [4, 5, 6], + 'Arating': [91, 92, 93], + 'Arating_old': [91, 92, 93] + }) + df['id'] = df.index + expected = pd.DataFrame({ + 'Arating': [91, 92, 93, 91, 92, 93], + 'Arating_old': [91, 92, 93, 91, 92, 93], + 'A': ['a11', 'a22', 'a33', 'a21', 'a22', 'a23'], + 'B': ['b11', 'b12', 'b13', 'b21', 'b22', 'b23'], + 'BB': [1, 2, 3, 4, 5, 6], + 'id': [0, 1, 2, 0, 1, 2], + 'year': [11, 11, 11, 12, 12, 12]}) + expected = expected.set_index(['id', 'year'])[ + ['Arating', 'Arating_old', 'A', 'B', 'BB']] + result = wide_to_long(df, ['A', 'B', 'BB'], i='id', j='year') + tm.assert_frame_equal(result.sort_index(axis=1), + expected.sort_index(axis=1)) + + def test_invalid_suffixtype(self): + # If all stubs names end with a string, but a numeric suffix is + # assumed, an empty data frame is returned + df = pd.DataFrame({'Aone': [1.0, 2.0], + 'Atwo': [3.0, 4.0], + 'Bone': [5.0, 6.0], + 'X': ['X1', 'X2']}) + df['id'] = df.index + exp_data = {'X': '', + 'Aone': [], + 'Atwo': [], + 'Bone': [], + 'id': [], + 'year': [], + 'A': [], + 'B': []} + expected = pd.DataFrame(exp_data).astype({'year': 'int'}) + + expected = expected.set_index(['id', 'year']) + expected.index.set_levels([0, 1], level=0, inplace=True) + result = wide_to_long(df, ['A', 'B'], i='id', j='year') + tm.assert_frame_equal(result.sort_index(axis=1), + expected.sort_index(axis=1)) + + def test_multiple_id_columns(self): + # Taken from http://www.ats.ucla.edu/stat/stata/modules/reshapel.htm + df = pd.DataFrame({ + 'famid': [1, 1, 1, 2, 2, 2, 3, 3, 3], + 'birth': [1, 2, 3, 1, 2, 3, 1, 2, 3], + 'ht1': [2.8, 2.9, 2.2, 2, 1.8, 1.9, 2.2, 2.3, 2.1], + 'ht2': [3.4, 3.8, 2.9, 3.2, 2.8, 2.4, 3.3, 3.4, 2.9] + }) + expected = pd.DataFrame({ + 'ht': [2.8, 3.4, 2.9, 3.8, 2.2, 2.9, 2.0, 3.2, 1.8, + 2.8, 1.9, 2.4, 2.2, 3.3, 2.3, 3.4, 2.1, 2.9], + 'famid': [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3], + 'birth': [1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3], + 'age': [1, 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 2, 1, 2, 1, 2] + }) + expected = expected.set_index(['famid', 'birth', 'age'])[['ht']] + result = wide_to_long(df, 'ht', i=['famid', 'birth'], j='age') + tm.assert_frame_equal(result, expected) + + def test_non_unique_idvars(self): + # GH16382 + # Raise an error message if non unique id vars (i) are passed + df = pd.DataFrame({ + 'A_A1': [1, 2, 3, 4, 5], + 'B_B1': [1, 2, 3, 4, 5], + 'x': [1, 1, 1, 1, 1] + }) + msg = "the id variables need to uniquely identify each row" + with pytest.raises(ValueError, match=msg): + wide_to_long(df, ['A_A', 'B_B'], i='x', j='colname') + + def test_cast_j_int(self): + df = pd.DataFrame({ + 'actor_1': ['CCH Pounder', 'Johnny Depp', 'Christoph Waltz'], + 'actor_2': ['Joel David Moore', 'Orlando Bloom', 'Rory Kinnear'], + 'actor_fb_likes_1': [1000.0, 40000.0, 11000.0], + 'actor_fb_likes_2': [936.0, 5000.0, 393.0], + 'title': ['Avatar', "Pirates of the Caribbean", 'Spectre']}) + + expected = pd.DataFrame({ + 'actor': ['CCH Pounder', + 'Johnny Depp', + 'Christoph Waltz', + 'Joel David Moore', + 'Orlando Bloom', + 'Rory Kinnear'], + 'actor_fb_likes': [1000.0, 40000.0, 11000.0, 936.0, 5000.0, 393.0], + 'num': [1, 1, 1, 2, 2, 2], + 'title': ['Avatar', + 'Pirates of the Caribbean', + 'Spectre', + 'Avatar', + 'Pirates of the Caribbean', + 'Spectre']}).set_index(['title', 'num']) + result = wide_to_long(df, ['actor', 'actor_fb_likes'], + i='title', j='num', sep='_') + + tm.assert_frame_equal(result, expected) + + def test_identical_stubnames(self): + df = pd.DataFrame({'A2010': [1.0, 2.0], + 'A2011': [3.0, 4.0], + 'B2010': [5.0, 6.0], + 'A': ['X1', 'X2']}) + msg = "stubname can't be identical to a column name" + with pytest.raises(ValueError, match=msg): + wide_to_long(df, ['A', 'B'], i='A', j='colname') + + def test_nonnumeric_suffix(self): + df = pd.DataFrame({'treatment_placebo': [1.0, 2.0], + 'treatment_test': [3.0, 4.0], + 'result_placebo': [5.0, 6.0], + 'A': ['X1', 'X2']}) + expected = pd.DataFrame({ + 'A': ['X1', 'X1', 'X2', 'X2'], + 'colname': ['placebo', 'test', 'placebo', 'test'], + 'result': [5.0, np.nan, 6.0, np.nan], + 'treatment': [1.0, 3.0, 2.0, 4.0]}) + expected = expected.set_index(['A', 'colname']) + result = wide_to_long(df, ['result', 'treatment'], + i='A', j='colname', suffix='[a-z]+', sep='_') + tm.assert_frame_equal(result, expected) + + def test_mixed_type_suffix(self): + df = pd.DataFrame({ + 'A': ['X1', 'X2'], + 'result_1': [0, 9], + 'result_foo': [5.0, 6.0], + 'treatment_1': [1.0, 2.0], + 'treatment_foo': [3.0, 4.0]}) + expected = pd.DataFrame({ + 'A': ['X1', 'X2', 'X1', 'X2'], + 'colname': ['1', '1', 'foo', 'foo'], + 'result': [0.0, 9.0, 5.0, 6.0], + 'treatment': [1.0, 2.0, 3.0, 4.0]}).set_index(['A', 'colname']) + result = wide_to_long(df, ['result', 'treatment'], + i='A', j='colname', suffix='.+', sep='_') + tm.assert_frame_equal(result, expected) + + def test_float_suffix(self): + df = pd.DataFrame({ + 'treatment_1.1': [1.0, 2.0], + 'treatment_2.1': [3.0, 4.0], + 'result_1.2': [5.0, 6.0], + 'result_1': [0, 9], + 'A': ['X1', 'X2']}) + expected = pd.DataFrame({ + 'A': ['X1', 'X1', 'X1', 'X1', 'X2', 'X2', 'X2', 'X2'], + 'colname': [1, 1.1, 1.2, 2.1, 1, 1.1, 1.2, 2.1], + 'result': [0.0, np.nan, 5.0, np.nan, 9.0, np.nan, 6.0, np.nan], + 'treatment': [np.nan, 1.0, np.nan, 3.0, np.nan, 2.0, np.nan, 4.0]}) + expected = expected.set_index(['A', 'colname']) + result = wide_to_long(df, ['result', 'treatment'], + i='A', j='colname', suffix='[0-9.]+', sep='_') + tm.assert_frame_equal(result, expected) + + def test_col_substring_of_stubname(self): + # GH22468 + # Don't raise ValueError when a column name is a substring + # of a stubname that's been passed as a string + wide_data = {'node_id': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}, + 'A': {0: 0.80, 1: 0.0, 2: 0.25, 3: 1.0, 4: 0.81}, + 'PA0': {0: 0.74, 1: 0.56, 2: 0.56, 3: 0.98, 4: 0.6}, + 'PA1': {0: 0.77, 1: 0.64, 2: 0.52, 3: 0.98, 4: 0.67}, + 'PA3': {0: 0.34, 1: 0.70, 2: 0.52, 3: 0.98, 4: 0.67} + } + wide_df = pd.DataFrame.from_dict(wide_data) + expected = pd.wide_to_long(wide_df, + stubnames=['PA'], + i=['node_id', 'A'], + j='time') + result = pd.wide_to_long(wide_df, + stubnames='PA', + i=['node_id', 'A'], + j='time') + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/tools/test_pivot.py b/pandas/tests/reshape/test_pivot.py similarity index 67% rename from pandas/tests/tools/test_pivot.py rename to pandas/tests/reshape/test_pivot.py index 4502f232c6d9c..e4fbb204af533 100644 --- a/pandas/tests/tools/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -1,19 +1,30 @@ -from datetime import datetime, date, timedelta +# -*- coding: utf-8 -*- + +from collections import OrderedDict +from datetime import date, datetime, timedelta import numpy as np +import pytest + +from pandas.compat import product, range import pandas as pd -from pandas import (DataFrame, Series, Index, MultiIndex, - Grouper, date_range, concat) -from pandas.tools.pivot import pivot_table, crosstab -from pandas.compat import range, product +from pandas import ( + Categorical, DataFrame, Grouper, Index, MultiIndex, Series, concat, + date_range) +from pandas.api.types import CategoricalDtype as CDT +from pandas.core.reshape.pivot import crosstab, pivot_table import pandas.util.testing as tm -from pandas.tseries.util import pivot_annual, isleapyear -class TestPivotTable(tm.TestCase): +@pytest.fixture(params=[True, False]) +def dropna(request): + return request.param + - def setUp(self): +class TestPivotTable(object): + + def setup_method(self, method): self.data = DataFrame({'A': ['foo', 'foo', 'foo', 'foo', 'bar', 'bar', 'bar', 'bar', 'foo', 'foo', 'foo'], @@ -41,14 +52,14 @@ def test_pivot_table(self): pivot_table(self.data, values='D', index=index) if len(index) > 1: - self.assertEqual(table.index.names, tuple(index)) + assert table.index.names == tuple(index) else: - self.assertEqual(table.index.name, index[0]) + assert table.index.name == index[0] if len(columns) > 1: - self.assertEqual(table.columns.names, columns) + assert table.columns.names == columns else: - self.assertEqual(table.columns.name, columns[0]) + assert table.columns.name == columns[0] expected = self.data.groupby( index + [columns])['D'].agg(np.mean).unstack() @@ -86,7 +97,25 @@ def test_pivot_table_dropna(self): tm.assert_index_equal(pv_col.columns, m) tm.assert_index_equal(pv_ind.index, m) - def test_pivot_table_dropna_categoricals(self): + def test_pivot_table_categorical(self): + + cat1 = Categorical(["a", "a", "b", "b"], + categories=["a", "b", "z"], ordered=True) + cat2 = Categorical(["c", "d", "c", "d"], + categories=["c", "d", "y"], ordered=True) + df = DataFrame({"A": cat1, "B": cat2, "values": [1, 2, 3, 4]}) + result = pd.pivot_table(df, values='values', index=['A', 'B'], + dropna=True) + + exp_index = pd.MultiIndex.from_arrays( + [cat1, cat2], + names=['A', 'B']) + expected = DataFrame( + {'values': [1, 2, 3, 4]}, + index=exp_index) + tm.assert_frame_equal(result, expected) + + def test_pivot_table_dropna_categoricals(self, dropna): # GH 15193 categories = ['a', 'b', 'c', 'd'] @@ -94,30 +123,61 @@ def test_pivot_table_dropna_categoricals(self): 'B': [1, 2, 3, 1, 2, 3, 1, 2, 3], 'C': range(0, 9)}) - df['A'] = df['A'].astype('category', ordered=False, - categories=categories) - result_true = df.pivot_table(index='B', columns='A', values='C', - dropna=True) + df['A'] = df['A'].astype(CDT(categories, ordered=False)) + result = df.pivot_table(index='B', columns='A', values='C', + dropna=dropna) expected_columns = Series(['a', 'b', 'c'], name='A') - expected_columns = expected_columns.astype('category', ordered=False, - categories=categories) + expected_columns = expected_columns.astype( + CDT(categories, ordered=False)) expected_index = Series([1, 2, 3], name='B') - expected_true = DataFrame([[0.0, 3.0, 6.0], - [1.0, 4.0, 7.0], - [2.0, 5.0, 8.0]], - index=expected_index, - columns=expected_columns,) - tm.assert_frame_equal(expected_true, result_true) - - result_false = df.pivot_table(index='B', columns='A', values='C', - dropna=False) - expected_columns = Series(['a', 'b', 'c', 'd'], name='A') - expected_false = DataFrame([[0.0, 3.0, 6.0, np.NaN], - [1.0, 4.0, 7.0, np.NaN], - [2.0, 5.0, 8.0, np.NaN]], - index=expected_index, - columns=expected_columns,) - tm.assert_frame_equal(expected_false, result_false) + expected = DataFrame([[0, 3, 6], + [1, 4, 7], + [2, 5, 8]], + index=expected_index, + columns=expected_columns,) + if not dropna: + # add back the non observed to compare + expected = expected.reindex( + columns=Categorical(categories)).astype('float') + + tm.assert_frame_equal(result, expected) + + def test_pivot_with_non_observable_dropna(self, dropna): + # gh-21133 + df = pd.DataFrame( + {'A': pd.Categorical([np.nan, 'low', 'high', 'low', 'high'], + categories=['low', 'high'], + ordered=True), + 'B': range(5)}) + + result = df.pivot_table(index='A', values='B', dropna=dropna) + expected = pd.DataFrame( + {'B': [2, 3]}, + index=pd.Index( + pd.Categorical.from_codes([0, 1], + categories=['low', 'high'], + ordered=True), + name='A')) + + tm.assert_frame_equal(result, expected) + + # gh-21378 + df = pd.DataFrame( + {'A': pd.Categorical(['left', 'low', 'high', 'low', 'high'], + categories=['low', 'high', 'left'], + ordered=True), + 'B': range(5)}) + + result = df.pivot_table(index='A', values='B', dropna=dropna) + expected = pd.DataFrame( + {'B': [2, 3, 0]}, + index=pd.Index( + pd.Categorical.from_codes([0, 1, 2], + categories=['low', 'high', 'left'], + ordered=True), + name='A')) + + tm.assert_frame_equal(result, expected) def test_pass_array(self): result = self.data.pivot_table( @@ -144,7 +204,7 @@ def test_pivot_dtypes(self): # can convert dtypes f = DataFrame({'a': ['cat', 'bat', 'cat', 'bat'], 'v': [ 1, 2, 3, 4], 'i': ['a', 'b', 'a', 'b']}) - self.assertEqual(f.dtypes['v'], 'int64') + assert f.dtypes['v'] == 'int64' z = pivot_table(f, values='v', index=['a'], columns=[ 'i'], fill_value=0, aggfunc=np.sum) @@ -155,7 +215,7 @@ def test_pivot_dtypes(self): # cannot convert dtypes f = DataFrame({'a': ['cat', 'bat', 'cat', 'bat'], 'v': [ 1.5, 2.5, 3.5, 4.5], 'i': ['a', 'b', 'a', 'b']}) - self.assertEqual(f.dtypes['v'], 'float64') + assert f.dtypes['v'] == 'float64' z = pivot_table(f, values='v', index=['a'], columns=[ 'i'], fill_value=0, aggfunc=np.mean) @@ -163,6 +223,24 @@ def test_pivot_dtypes(self): expected = Series(dict(float64=2)) tm.assert_series_equal(result, expected) + @pytest.mark.parametrize('columns,values', + [('bool1', ['float1', 'float2']), + ('bool1', ['float1', 'float2', 'bool1']), + ('bool2', ['float1', 'float2', 'bool1'])]) + def test_pivot_preserve_dtypes(self, columns, values): + # GH 7142 regression test + v = np.arange(5, dtype=np.float64) + df = DataFrame({'float1': v, 'float2': v + 2.0, + 'bool1': v <= 2, 'bool2': v <= 3}) + + df_res = df.reset_index().pivot_table( + index='index', columns=columns, values=values) + + result = dict(df_res.dtypes) + expected = {col: np.dtype('O') if col[0].startswith('b') + else np.dtype('float64') for col in df_res} + assert result == expected + def test_pivot_no_values(self): # GH 14380 idx = pd.DatetimeIndex(['2011-01-01', '2011-02-01', '2011-01-02', @@ -223,13 +301,17 @@ def test_pivot_multi_functions(self): expected = concat([means, stds], keys=['mean', 'std'], axis=1) tm.assert_frame_equal(result, expected) - def test_pivot_index_with_nan(self): + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_index_with_nan(self, method): # GH 3588 nan = np.nan df = DataFrame({'a': ['R1', 'R2', nan, 'R4'], 'b': ['C1', 'C2', 'C3', 'C4'], 'c': [10, 15, 17, 20]}) - result = df.pivot('a', 'b', 'c') + if method: + result = df.pivot('a', 'b', 'c') + else: + result = pd.pivot(df, 'a', 'b', 'c') expected = DataFrame([[nan, nan, 17, nan], [10, nan, nan, nan], [nan, 15, nan, nan], [nan, nan, nan, 20]], index=Index([nan, 'R1', 'R2', 'R4'], name='a'), @@ -244,15 +326,23 @@ def test_pivot_index_with_nan(self): df.loc[1, 'a'] = df.loc[3, 'a'] = nan df.loc[1, 'b'] = df.loc[4, 'b'] = nan - pv = df.pivot('a', 'b', 'c') - self.assertEqual(pv.notnull().values.sum(), len(df)) + if method: + pv = df.pivot('a', 'b', 'c') + else: + pv = pd.pivot(df, 'a', 'b', 'c') + assert pv.notna().values.sum() == len(df) for _, row in df.iterrows(): - self.assertEqual(pv.loc[row['a'], row['b']], row['c']) + assert pv.loc[row['a'], row['b']] == row['c'] - tm.assert_frame_equal(df.pivot('b', 'a', 'c'), pv.T) + if method: + result = df.pivot('b', 'a', 'c') + else: + result = pd.pivot(df, 'b', 'a', 'c') + tm.assert_frame_equal(result, pv.T) - def test_pivot_with_tz(self): + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_with_tz(self, method): # GH 5878 df = DataFrame({'dt1': [datetime(2013, 1, 1, 9, 0), datetime(2013, 1, 2, 9, 0), @@ -280,7 +370,10 @@ def test_pivot_with_tz(self): tz='US/Pacific'), columns=exp_col) - pv = df.pivot(index='dt1', columns='dt2') + if method: + pv = df.pivot(index='dt1', columns='dt2') + else: + pv = pd.pivot(df, index='dt1', columns='dt2') tm.assert_frame_equal(pv, expected) expected = DataFrame([[0, 2], [1, 3]], @@ -293,10 +386,14 @@ def test_pivot_with_tz(self): name='dt2', tz='Asia/Tokyo')) - pv = df.pivot(index='dt1', columns='dt2', values='data1') + if method: + pv = df.pivot(index='dt1', columns='dt2', values='data1') + else: + pv = pd.pivot(df, index='dt1', columns='dt2', values='data1') tm.assert_frame_equal(pv, expected) - def test_pivot_periods(self): + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_periods(self, method): df = DataFrame({'p1': [pd.Period('2013-01-01', 'D'), pd.Period('2013-01-02', 'D'), pd.Period('2013-01-01', 'D'), @@ -316,8 +413,10 @@ def test_pivot_periods(self): index=pd.PeriodIndex(['2013-01-01', '2013-01-02'], name='p1', freq='D'), columns=exp_col) - - pv = df.pivot(index='p1', columns='p2') + if method: + pv = df.pivot(index='p1', columns='p2') + else: + pv = pd.pivot(df, index='p1', columns='p2') tm.assert_frame_equal(pv, expected) expected = DataFrame([[0, 2], [1, 3]], @@ -325,10 +424,115 @@ def test_pivot_periods(self): name='p1', freq='D'), columns=pd.PeriodIndex(['2013-01', '2013-02'], name='p2', freq='M')) - - pv = df.pivot(index='p1', columns='p2', values='data1') + if method: + pv = df.pivot(index='p1', columns='p2', values='data1') + else: + pv = pd.pivot(df, index='p1', columns='p2', values='data1') tm.assert_frame_equal(pv, expected) + @pytest.mark.parametrize('values', [ + ['baz', 'zoo'], np.array(['baz', 'zoo']), + pd.Series(['baz', 'zoo']), pd.Index(['baz', 'zoo']) + ]) + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_with_list_like_values(self, values, method): + # issue #17160 + df = pd.DataFrame({'foo': ['one', 'one', 'one', 'two', 'two', 'two'], + 'bar': ['A', 'B', 'C', 'A', 'B', 'C'], + 'baz': [1, 2, 3, 4, 5, 6], + 'zoo': ['x', 'y', 'z', 'q', 'w', 't']}) + + if method: + result = df.pivot(index='foo', columns='bar', values=values) + else: + result = pd.pivot(df, index='foo', columns='bar', values=values) + + data = [[1, 2, 3, 'x', 'y', 'z'], + [4, 5, 6, 'q', 'w', 't']] + index = Index(data=['one', 'two'], name='foo') + columns = MultiIndex(levels=[['baz', 'zoo'], ['A', 'B', 'C']], + codes=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]], + names=[None, 'bar']) + expected = DataFrame(data=data, index=index, + columns=columns, dtype='object') + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('values', [ + ['bar', 'baz'], np.array(['bar', 'baz']), + pd.Series(['bar', 'baz']), pd.Index(['bar', 'baz']) + ]) + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_with_list_like_values_nans(self, values, method): + # issue #17160 + df = pd.DataFrame({'foo': ['one', 'one', 'one', 'two', 'two', 'two'], + 'bar': ['A', 'B', 'C', 'A', 'B', 'C'], + 'baz': [1, 2, 3, 4, 5, 6], + 'zoo': ['x', 'y', 'z', 'q', 'w', 't']}) + + if method: + result = df.pivot(index='zoo', columns='foo', values=values) + else: + result = pd.pivot(df, index='zoo', columns='foo', values=values) + + data = [[np.nan, 'A', np.nan, 4], + [np.nan, 'C', np.nan, 6], + [np.nan, 'B', np.nan, 5], + ['A', np.nan, 1, np.nan], + ['B', np.nan, 2, np.nan], + ['C', np.nan, 3, np.nan]] + index = Index(data=['q', 't', 'w', 'x', 'y', 'z'], name='zoo') + columns = MultiIndex(levels=[['bar', 'baz'], ['one', 'two']], + codes=[[0, 0, 1, 1], [0, 1, 0, 1]], + names=[None, 'foo']) + expected = DataFrame(data=data, index=index, + columns=columns, dtype='object') + tm.assert_frame_equal(result, expected) + + @pytest.mark.xfail(reason='MultiIndexed unstack with tuple names fails' + 'with KeyError GH#19966') + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_with_multiindex(self, method): + # issue #17160 + index = Index(data=[0, 1, 2, 3, 4, 5]) + data = [['one', 'A', 1, 'x'], + ['one', 'B', 2, 'y'], + ['one', 'C', 3, 'z'], + ['two', 'A', 4, 'q'], + ['two', 'B', 5, 'w'], + ['two', 'C', 6, 't']] + columns = MultiIndex(levels=[['bar', 'baz'], ['first', 'second']], + codes=[[0, 0, 1, 1], [0, 1, 0, 1]]) + df = DataFrame(data=data, index=index, columns=columns, dtype='object') + if method: + result = df.pivot(index=('bar', 'first'), + columns=('bar', 'second'), + values=('baz', 'first')) + else: + result = pd.pivot(df, + index=('bar', 'first'), + columns=('bar', 'second'), + values=('baz', 'first')) + + data = {'A': Series([1, 4], index=['one', 'two']), + 'B': Series([2, 5], index=['one', 'two']), + 'C': Series([3, 6], index=['one', 'two'])} + expected = DataFrame(data) + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('method', [True, False]) + def test_pivot_with_tuple_of_values(self, method): + # issue #17160 + df = pd.DataFrame({'foo': ['one', 'one', 'one', 'two', 'two', 'two'], + 'bar': ['A', 'B', 'C', 'A', 'B', 'C'], + 'baz': [1, 2, 3, 4, 5, 6], + 'zoo': ['x', 'y', 'z', 'q', 'w', 't']}) + with pytest.raises(KeyError, match=r"^\('bar', 'baz'\)$"): + # tuple is seen as a single column name + if method: + df.pivot(index='zoo', columns='foo', values=('bar', 'baz')) + else: + pd.pivot(df, index='zoo', columns='foo', values=('bar', 'baz')) + def test_margins(self): def _check_output(result, values_col, index=['A', 'B'], columns=['C'], @@ -337,7 +541,7 @@ def _check_output(result, values_col, index=['A', 'B'], expected_col_margins = self.data.groupby(index)[values_col].mean() tm.assert_series_equal(col_margins, expected_col_margins, check_names=False) - self.assertEqual(col_margins.name, margins_col) + assert col_margins.name == margins_col result = result.sort_index() index_margins = result.loc[(margins_col, '')].iloc[:-1] @@ -345,11 +549,11 @@ def _check_output(result, values_col, index=['A', 'B'], expected_ix_margins = self.data.groupby(columns)[values_col].mean() tm.assert_series_equal(index_margins, expected_ix_margins, check_names=False) - self.assertEqual(index_margins.name, (margins_col, '')) + assert index_margins.name == (margins_col, '') grand_total_margins = result.loc[(margins_col, ''), margins_col] expected_total_margins = self.data[values_col].mean() - self.assertEqual(grand_total_margins, expected_total_margins) + assert grand_total_margins == expected_total_margins # column specified result = self.data.pivot_table(values='D', index=['A', 'B'], @@ -378,64 +582,53 @@ def _check_output(result, values_col, index=['A', 'B'], aggfunc=np.mean) for value_col in table.columns: totals = table.loc[('All', ''), value_col] - self.assertEqual(totals, self.data[value_col].mean()) + assert totals == self.data[value_col].mean() # no rows rtable = self.data.pivot_table(columns=['AA', 'BB'], margins=True, aggfunc=np.mean) - tm.assertIsInstance(rtable, Series) + assert isinstance(rtable, Series) table = self.data.pivot_table(index=['AA', 'BB'], margins=True, aggfunc='mean') for item in ['DD', 'EE', 'FF']: totals = table.loc[('All', ''), item] - self.assertEqual(totals, self.data[item].mean()) - - # issue number #8349: pivot_table with margins and dictionary aggfunc - data = [ - {'JOB': 'Worker', 'NAME': 'Bob', 'YEAR': 2013, - 'MONTH': 12, 'DAYS': 3, 'SALARY': 17}, - {'JOB': 'Employ', 'NAME': - 'Mary', 'YEAR': 2013, 'MONTH': 12, 'DAYS': 5, 'SALARY': 23}, - {'JOB': 'Worker', 'NAME': 'Bob', 'YEAR': 2014, - 'MONTH': 1, 'DAYS': 10, 'SALARY': 100}, - {'JOB': 'Worker', 'NAME': 'Bob', 'YEAR': 2014, - 'MONTH': 1, 'DAYS': 11, 'SALARY': 110}, - {'JOB': 'Employ', 'NAME': 'Mary', 'YEAR': 2014, - 'MONTH': 1, 'DAYS': 15, 'SALARY': 200}, - {'JOB': 'Worker', 'NAME': 'Bob', 'YEAR': 2014, - 'MONTH': 2, 'DAYS': 8, 'SALARY': 80}, - {'JOB': 'Employ', 'NAME': 'Mary', 'YEAR': 2014, - 'MONTH': 2, 'DAYS': 5, 'SALARY': 190}, - ] + assert totals == self.data[item].mean() - df = DataFrame(data) + def test_margins_dtype(self): + # GH 17013 + + df = self.data.copy() + df[['D', 'E', 'F']] = np.arange(len(df) * 3).reshape(len(df), 3) - df = df.set_index(['JOB', 'NAME', 'YEAR', 'MONTH'], drop=False, - append=False) + mi_val = list(product(['bar', 'foo'], ['one', 'two'])) + [('All', '')] + mi = MultiIndex.from_tuples(mi_val, names=('A', 'B')) + expected = DataFrame({'dull': [12, 21, 3, 9, 45], + 'shiny': [33, 0, 36, 51, 120]}, + index=mi).rename_axis('C', axis=1) + expected['All'] = expected['dull'] + expected['shiny'] - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = df.pivot_table(index=['JOB', 'NAME'], - columns=['YEAR', 'MONTH'], - values=['DAYS', 'SALARY'], - aggfunc={'DAYS': 'mean', 'SALARY': 'sum'}, - margins=True) + result = df.pivot_table(values='D', index=['A', 'B'], + columns='C', margins=True, + aggfunc=np.sum, fill_value=0) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - expected = df.pivot_table(index=['JOB', 'NAME'], - columns=['YEAR', 'MONTH'], - values=['DAYS'], - aggfunc='mean', margins=True) + tm.assert_frame_equal(expected, result) - tm.assert_frame_equal(result['DAYS'], expected['DAYS']) + @pytest.mark.xfail(reason='GH#17035 (len of floats is casted back to ' + 'floats)') + def test_margins_dtype_len(self): + mi_val = list(product(['bar', 'foo'], ['one', 'two'])) + [('All', '')] + mi = MultiIndex.from_tuples(mi_val, names=('A', 'B')) + expected = DataFrame({'dull': [1, 1, 2, 1, 5], + 'shiny': [2, 0, 2, 2, 6]}, + index=mi).rename_axis('C', axis=1) + expected['All'] = expected['dull'] + expected['shiny'] - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - expected = df.pivot_table(index=['JOB', 'NAME'], - columns=['YEAR', 'MONTH'], - values=['SALARY'], - aggfunc='sum', margins=True) + result = self.data.pivot_table(values='D', index=['A', 'B'], + columns='C', margins=True, + aggfunc=len, fill_value=0) - tm.assert_frame_equal(result['SALARY'], expected['SALARY']) + tm.assert_frame_equal(expected, result) def test_pivot_integer_columns(self): # caused by upstream bug in unstack @@ -510,10 +703,10 @@ def test_pivot_columns_lexsorted(self): columns=['Index', 'Symbol', 'Year'], aggfunc='mean') - self.assertTrue(pivoted.columns.is_monotonic) + assert pivoted.columns.is_monotonic def test_pivot_complex_aggfunc(self): - f = {'D': ['std'], 'E': ['sum']} + f = OrderedDict([('D', ['std']), ('E', ['sum'])]) expected = self.data.groupby(['A', 'B']).agg(f).unstack('B') result = self.data.pivot_table(index='A', columns='B', aggfunc=f) @@ -524,21 +717,21 @@ def test_margins_no_values_no_cols(self): result = self.data[['A', 'B']].pivot_table( index=['A', 'B'], aggfunc=len, margins=True) result_list = result.tolist() - self.assertEqual(sum(result_list[:-1]), result_list[-1]) + assert sum(result_list[:-1]) == result_list[-1] def test_margins_no_values_two_rows(self): # Regression test on pivot table: no values passed but rows are a # multi-index result = self.data[['A', 'B', 'C']].pivot_table( index=['A', 'B'], columns='C', aggfunc=len, margins=True) - self.assertEqual(result.All.tolist(), [3.0, 1.0, 4.0, 3.0, 11.0]) + assert result.All.tolist() == [3.0, 1.0, 4.0, 3.0, 11.0] def test_margins_no_values_one_row_one_col(self): # Regression test on pivot table: no values passed but row and col # defined result = self.data[['A', 'B']].pivot_table( index='A', columns='B', aggfunc=len, margins=True) - self.assertEqual(result.All.tolist(), [4.0, 7.0, 11.0]) + assert result.All.tolist() == [4.0, 7.0, 11.0] def test_margins_no_values_two_row_two_cols(self): # Regression test on pivot table: no values passed but rows and cols @@ -547,26 +740,29 @@ def test_margins_no_values_two_row_two_cols(self): 'e', 'f', 'g', 'h', 'i', 'j', 'k'] result = self.data[['A', 'B', 'C', 'D']].pivot_table( index=['A', 'B'], columns=['C', 'D'], aggfunc=len, margins=True) - self.assertEqual(result.All.tolist(), [3.0, 1.0, 4.0, 3.0, 11.0]) - - def test_pivot_table_with_margins_set_margin_name(self): - # GH 3335 - for margin_name in ['foo', 'one', 666, None, ['a', 'b']]: - with self.assertRaises(ValueError): - # multi-index index - pivot_table(self.data, values='D', index=['A', 'B'], - columns=['C'], margins=True, - margins_name=margin_name) - with self.assertRaises(ValueError): - # multi-index column - pivot_table(self.data, values='D', index=['C'], - columns=['A', 'B'], margins=True, - margins_name=margin_name) - with self.assertRaises(ValueError): - # non-multi-index index/column - pivot_table(self.data, values='D', index=['A'], - columns=['B'], margins=True, - margins_name=margin_name) + assert result.All.tolist() == [3.0, 1.0, 4.0, 3.0, 11.0] + + @pytest.mark.parametrize( + 'margin_name', ['foo', 'one', 666, None, ['a', 'b']]) + def test_pivot_table_with_margins_set_margin_name(self, margin_name): + # see gh-3335 + msg = (r'Conflicting name "{}" in margins|' + "margins_name argument must be a string").format(margin_name) + with pytest.raises(ValueError, match=msg): + # multi-index index + pivot_table(self.data, values='D', index=['A', 'B'], + columns=['C'], margins=True, + margins_name=margin_name) + with pytest.raises(ValueError, match=msg): + # multi-index column + pivot_table(self.data, values='D', index=['C'], + columns=['A', 'B'], margins=True, + margins_name=margin_name) + with pytest.raises(ValueError, match=msg): + # non-multi-index index/column + pivot_table(self.data, values='D', index=['A'], + columns=['B'], margins=True, + margins_name=margin_name) def test_pivot_timegrouper(self): df = DataFrame({ @@ -625,13 +821,14 @@ def test_pivot_timegrouper(self): values='Quantity', aggfunc=np.sum) tm.assert_frame_equal(result, expected.T) - self.assertRaises(KeyError, lambda: pivot_table( - df, index=Grouper(freq='6MS', key='foo'), - columns='Buyer', values='Quantity', aggfunc=np.sum)) - self.assertRaises(KeyError, lambda: pivot_table( - df, index='Buyer', - columns=Grouper(freq='6MS', key='foo'), - values='Quantity', aggfunc=np.sum)) + msg = "'The grouper name foo is not found'" + with pytest.raises(KeyError, match=msg): + pivot_table(df, index=Grouper(freq='6MS', key='foo'), + columns='Buyer', values='Quantity', aggfunc=np.sum) + with pytest.raises(KeyError, match=msg): + pivot_table(df, index='Buyer', + columns=Grouper(freq='6MS', key='foo'), + values='Quantity', aggfunc=np.sum) # passing the level df = df.set_index('Date') @@ -645,13 +842,14 @@ def test_pivot_timegrouper(self): values='Quantity', aggfunc=np.sum) tm.assert_frame_equal(result, expected.T) - self.assertRaises(ValueError, lambda: pivot_table( - df, index=Grouper(freq='6MS', level='foo'), - columns='Buyer', values='Quantity', aggfunc=np.sum)) - self.assertRaises(ValueError, lambda: pivot_table( - df, index='Buyer', - columns=Grouper(freq='6MS', level='foo'), - values='Quantity', aggfunc=np.sum)) + msg = "The level foo is not valid" + with pytest.raises(ValueError, match=msg): + pivot_table(df, index=Grouper(freq='6MS', level='foo'), + columns='Buyer', values='Quantity', aggfunc=np.sum) + with pytest.raises(ValueError, match=msg): + pivot_table(df, index='Buyer', + columns=Grouper(freq='6MS', level='foo'), + values='Quantity', aggfunc=np.sum) # double grouper df = DataFrame({ @@ -831,6 +1029,40 @@ def test_pivot_dtaccessor(self): index=['X', 'Y'], columns=exp_col) tm.assert_frame_equal(result, expected) + def test_daily(self): + rng = date_range('1/1/2000', '12/31/2004', freq='D') + ts = Series(np.random.randn(len(rng)), index=rng) + + annual = pivot_table(DataFrame(ts), index=ts.index.year, + columns=ts.index.dayofyear) + annual.columns = annual.columns.droplevel(0) + + doy = np.asarray(ts.index.dayofyear) + + for i in range(1, 367): + subset = ts[doy == i] + subset.index = subset.index.year + + result = annual[i].dropna() + tm.assert_series_equal(result, subset, check_names=False) + assert result.name == i + + def test_monthly(self): + rng = date_range('1/1/2000', '12/31/2004', freq='M') + ts = Series(np.random.randn(len(rng)), index=rng) + + annual = pivot_table(pd.DataFrame(ts), index=ts.index.year, + columns=ts.index.month) + annual.columns = annual.columns.droplevel(0) + + month = ts.index.month + for i in range(1, 13): + subset = ts[month == i] + subset.index = subset.index.year + result = annual[i].dropna() + tm.assert_series_equal(result, subset, check_names=False) + assert result.name == i + def test_pivot_table_with_iterator_values(self): # GH 12017 aggs = {'D': 'sum', 'E': 'mean'} @@ -872,7 +1104,9 @@ def test_pivot_table_margins_name_with_aggfunc_list(self): expected = pd.DataFrame(table.values, index=ix, columns=cols) tm.assert_frame_equal(table, expected) - def test_categorical_margins(self): + @pytest.mark.xfail(reason='GH#17035 (np.mean of ints is casted back to ' + 'ints)') + def test_categorical_margins(self, observed): # GH 10989 df = pd.DataFrame({'x': np.arange(8), 'y': np.arange(8) // 4, @@ -882,23 +1116,33 @@ def test_categorical_margins(self): expected.index = Index([0, 1, 'All'], name='y') expected.columns = Index([0, 1, 'All'], name='z') - data = df.copy() - table = data.pivot_table('x', 'y', 'z', margins=True) + table = df.pivot_table('x', 'y', 'z', dropna=observed, margins=True) tm.assert_frame_equal(table, expected) - data = df.copy() - data.y = data.y.astype('category') - data.z = data.z.astype('category') - table = data.pivot_table('x', 'y', 'z', margins=True) + @pytest.mark.xfail(reason='GH#17035 (np.mean of ints is casted back to ' + 'ints)') + def test_categorical_margins_category(self, observed): + df = pd.DataFrame({'x': np.arange(8), + 'y': np.arange(8) // 4, + 'z': np.arange(8) % 2}) + + expected = pd.DataFrame([[1.0, 2.0, 1.5], [5, 6, 5.5], [3, 4, 3.5]]) + expected.index = Index([0, 1, 'All'], name='y') + expected.columns = Index([0, 1, 'All'], name='z') + + df.y = df.y.astype('category') + df.z = df.z.astype('category') + table = df.pivot_table('x', 'y', 'z', dropna=observed, margins=True) tm.assert_frame_equal(table, expected) - def test_categorical_aggfunc(self): + def test_categorical_aggfunc(self, observed): # GH 9534 df = pd.DataFrame({"C1": ["A", "B", "C", "C"], "C2": ["a", "a", "b", "b"], "V": [1, 2, 3, 4]}) df["C1"] = df["C1"].astype("category") - result = df.pivot_table("V", index="C1", columns="C2", aggfunc="count") + result = df.pivot_table("V", index="C1", columns="C2", + dropna=observed, aggfunc="count") expected_index = pd.CategoricalIndex(['A', 'B', 'C'], categories=['A', 'B', 'C'], @@ -913,7 +1157,7 @@ def test_categorical_aggfunc(self): columns=expected_columns) tm.assert_frame_equal(result, expected) - def test_categorical_pivot_index_ordering(self): + def test_categorical_pivot_index_ordering(self, observed): # GH 8731 df = pd.DataFrame({'Sales': [100, 120, 220], 'Month': ['January', 'January', 'January'], @@ -925,24 +1169,130 @@ def test_categorical_pivot_index_ordering(self): result = df.pivot_table(values='Sales', index='Month', columns='Year', + dropna=observed, aggfunc='sum') expected_columns = pd.Int64Index([2013, 2014], name='Year') - expected_index = pd.CategoricalIndex(months, + expected_index = pd.CategoricalIndex(['January'], categories=months, ordered=False, name='Month') - expected_data = np.empty((12, 2)) - expected_data.fill(np.nan) - expected_data[0, :] = [320., 120.] - expected = pd.DataFrame(expected_data, + expected = pd.DataFrame([[320, 120]], index=expected_index, columns=expected_columns) + if not observed: + result = result.dropna().astype(np.int64) + + tm.assert_frame_equal(result, expected) + + def test_pivot_table_not_series(self): + # GH 4386 + # pivot_table always returns a DataFrame + # when values is not list like and columns is None + # and aggfunc is not instance of list + df = DataFrame({'col1': [3, 4, 5], + 'col2': ['C', 'D', 'E'], + 'col3': [1, 3, 9]}) + + result = df.pivot_table('col1', index=['col3', 'col2'], aggfunc=np.sum) + m = MultiIndex.from_arrays([[1, 3, 9], + ['C', 'D', 'E']], + names=['col3', 'col2']) + expected = DataFrame([3, 4, 5], + index=m, columns=['col1']) + + tm.assert_frame_equal(result, expected) + + result = df.pivot_table( + 'col1', index='col3', columns='col2', aggfunc=np.sum + ) + expected = DataFrame([[3, np.NaN, np.NaN], + [np.NaN, 4, np.NaN], + [np.NaN, np.NaN, 5]], + index=Index([1, 3, 9], name='col3'), + columns=Index(['C', 'D', 'E'], name='col2')) + tm.assert_frame_equal(result, expected) + result = df.pivot_table('col1', index='col3', aggfunc=[np.sum]) + m = MultiIndex.from_arrays([['sum'], + ['col1']]) + expected = DataFrame([3, 4, 5], + index=Index([1, 3, 9], name='col3'), + columns=m) -class TestCrosstab(tm.TestCase): + tm.assert_frame_equal(result, expected) - def setUp(self): + def test_pivot_margins_name_unicode(self): + # issue #13292 + greek = u'\u0394\u03bf\u03ba\u03b9\u03bc\u03ae' + frame = pd.DataFrame({'foo': [1, 2, 3]}) + table = pd.pivot_table(frame, index=['foo'], aggfunc=len, margins=True, + margins_name=greek) + index = pd.Index([1, 2, 3, greek], dtype='object', name='foo') + expected = pd.DataFrame(index=index) + tm.assert_frame_equal(table, expected) + + def test_pivot_string_as_func(self): + # GH #18713 + # for correctness purposes + data = DataFrame({'A': ['foo', 'foo', 'foo', 'foo', 'bar', 'bar', + 'bar', 'bar', 'foo', 'foo', 'foo'], + 'B': ['one', 'one', 'one', 'two', 'one', 'one', + 'one', 'two', 'two', 'two', 'one'], + 'C': range(11)}) + + result = pivot_table(data, index='A', columns='B', aggfunc='sum') + mi = MultiIndex(levels=[['C'], ['one', 'two']], + codes=[[0, 0], [0, 1]], names=[None, 'B']) + expected = DataFrame({('C', 'one'): {'bar': 15, 'foo': 13}, + ('C', 'two'): {'bar': 7, 'foo': 20}}, + columns=mi).rename_axis('A') + tm.assert_frame_equal(result, expected) + + result = pivot_table(data, index='A', columns='B', + aggfunc=['sum', 'mean']) + mi = MultiIndex(levels=[['sum', 'mean'], ['C'], ['one', 'two']], + codes=[[0, 0, 1, 1], [0, 0, 0, 0], [0, 1, 0, 1]], + names=[None, None, 'B']) + expected = DataFrame({('mean', 'C', 'one'): {'bar': 5.0, 'foo': 3.25}, + ('mean', 'C', 'two'): {'bar': 7.0, + 'foo': 6.666666666666667}, + ('sum', 'C', 'one'): {'bar': 15, 'foo': 13}, + ('sum', 'C', 'two'): {'bar': 7, 'foo': 20}}, + columns=mi).rename_axis('A') + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('f, f_numpy', + [('sum', np.sum), + ('mean', np.mean), + ('std', np.std), + (['sum', 'mean'], [np.sum, np.mean]), + (['sum', 'std'], [np.sum, np.std]), + (['std', 'mean'], [np.std, np.mean])]) + def test_pivot_string_func_vs_func(self, f, f_numpy): + # GH #18713 + # for consistency purposes + result = pivot_table(self.data, index='A', columns='B', aggfunc=f) + expected = pivot_table(self.data, index='A', columns='B', + aggfunc=f_numpy) + tm.assert_frame_equal(result, expected) + + @pytest.mark.slow + def test_pivot_number_of_levels_larger_than_int32(self): + # GH 20601 + df = DataFrame({'ind1': np.arange(2 ** 16), + 'ind2': np.arange(2 ** 16), + 'count': 0}) + + msg = "Unstacked DataFrame is too big, causing int32 overflow" + with pytest.raises(ValueError, match=msg): + df.pivot_table(index='ind1', columns='ind2', + values='count', aggfunc='count') + + +class TestCrosstab(object): + + def setup_method(self, method): df = DataFrame({'A': ['foo', 'foo', 'foo', 'foo', 'bar', 'bar', 'bar', 'bar', 'foo', 'foo', 'foo'], @@ -995,8 +1345,24 @@ def test_crosstab_ndarray(self): # assign arbitrary names result = crosstab(self.df['A'].values, self.df['C'].values) - self.assertEqual(result.index.name, 'row_0') - self.assertEqual(result.columns.name, 'col_0') + assert result.index.name == 'row_0' + assert result.columns.name == 'col_0' + + def test_crosstab_non_aligned(self): + # GH 17005 + a = pd.Series([0, 1, 1], index=['a', 'b', 'c']) + b = pd.Series([3, 4, 3, 4, 3], index=['a', 'b', 'c', 'd', 'f']) + c = np.array([3, 4, 3]) + + expected = pd.DataFrame([[1, 0], [1, 1]], + index=Index([0, 1], name='row_0'), + columns=Index([3, 4], name='col_0')) + + result = crosstab(a, b) + tm.assert_frame_equal(result, expected) + + result = crosstab(a, c) + tm.assert_frame_equal(result, expected) def test_crosstab_margins(self): a = np.random.randint(0, 7, size=100) @@ -1008,8 +1374,8 @@ def test_crosstab_margins(self): result = crosstab(a, [b, c], rownames=['a'], colnames=('b', 'c'), margins=True) - self.assertEqual(result.index.names, ('a',)) - self.assertEqual(result.columns.names, ['b', 'c']) + assert result.index.names == ('a',) + assert result.columns.names == ['b', 'c'] all_cols = result['All', ''] exp_cols = df.groupby(['a']).size().astype('i8') @@ -1029,6 +1395,44 @@ def test_crosstab_margins(self): exp_rows = exp_rows.fillna(0).astype(np.int64) tm.assert_series_equal(all_rows, exp_rows) + def test_crosstab_margins_set_margin_name(self): + # GH 15972 + a = np.random.randint(0, 7, size=100) + b = np.random.randint(0, 3, size=100) + c = np.random.randint(0, 5, size=100) + + df = DataFrame({'a': a, 'b': b, 'c': c}) + + result = crosstab(a, [b, c], rownames=['a'], colnames=('b', 'c'), + margins=True, margins_name='TOTAL') + + assert result.index.names == ('a',) + assert result.columns.names == ['b', 'c'] + + all_cols = result['TOTAL', ''] + exp_cols = df.groupby(['a']).size().astype('i8') + # to keep index.name + exp_margin = Series([len(df)], index=Index(['TOTAL'], name='a')) + exp_cols = exp_cols.append(exp_margin) + exp_cols.name = ('TOTAL', '') + + tm.assert_series_equal(all_cols, exp_cols) + + all_rows = result.loc['TOTAL'] + exp_rows = df.groupby(['b', 'c']).size().astype('i8') + exp_rows = exp_rows.append(Series([len(df)], index=[('TOTAL', '')])) + exp_rows.name = 'TOTAL' + + exp_rows = exp_rows.reindex(all_rows.index) + exp_rows = exp_rows.fillna(0).astype(np.int64) + tm.assert_series_equal(all_rows, exp_rows) + + msg = "margins_name argument must be a string" + for margins_name in [666, None, ['a', 'b']]: + with pytest.raises(ValueError, match=msg): + crosstab(a, [b, c], rownames=['a'], colnames=('b', 'c'), + margins=True, margins_name=margins_name) + def test_crosstab_pass_values(self): a = np.random.randint(0, 7, size=100) b = np.random.randint(0, 3, size=100) @@ -1189,12 +1593,14 @@ def test_crosstab_normalize(self): index=pd.Index([1, 2, 'All'], name='a', dtype='object'), - columns=pd.Index([3, 4], name='b')) + columns=pd.Index([3, 4], name='b', + dtype='object')) col_normal_margins = pd.DataFrame([[0.5, 0, 0.2], [0.5, 1.0, 0.8]], index=pd.Index([1, 2], name='a', dtype='object'), columns=pd.Index([3, 4, 'All'], - name='b')) + name='b', + dtype='object')) all_normal_margins = pd.DataFrame([[0.2, 0, 0.2], [0.2, 0.6, 0.8], @@ -1203,11 +1609,13 @@ def test_crosstab_normalize(self): name='a', dtype='object'), columns=pd.Index([3, 4, 'All'], - name='b')) + name='b', + dtype='object')) tm.assert_frame_equal(pd.crosstab(df.a, df.b, normalize='index', margins=True), row_normal_margins) tm.assert_frame_equal(pd.crosstab(df.a, df.b, normalize='columns', - margins=True), col_normal_margins) + margins=True), + col_normal_margins) tm.assert_frame_equal(pd.crosstab(df.a, df.b, normalize=True, margins=True), all_normal_margins) @@ -1279,22 +1687,22 @@ def test_crosstab_errors(self): 'c': [1, 1, np.nan, 1, 1]}) error = 'values cannot be used without an aggfunc.' - with tm.assertRaisesRegexp(ValueError, error): + with pytest.raises(ValueError, match=error): pd.crosstab(df.a, df.b, values=df.c) error = 'aggfunc cannot be used without values' - with tm.assertRaisesRegexp(ValueError, error): + with pytest.raises(ValueError, match=error): pd.crosstab(df.a, df.b, aggfunc=np.mean) error = 'Not a valid normalize argument' - with tm.assertRaisesRegexp(ValueError, error): + with pytest.raises(ValueError, match=error): pd.crosstab(df.a, df.b, normalize='42') - with tm.assertRaisesRegexp(ValueError, error): + with pytest.raises(ValueError, match=error): pd.crosstab(df.a, df.b, normalize=42) error = 'Not a valid margins argument' - with tm.assertRaisesRegexp(ValueError, error): + with pytest.raises(ValueError, match=error): pd.crosstab(df.a, df.b, normalize='all', margins=42) def test_crosstab_with_categorial_columns(self): @@ -1333,8 +1741,8 @@ def test_crosstab_with_numpy_size(self): values=df['D']) expected_index = pd.MultiIndex(levels=[['All', 'one', 'three', 'two'], ['', 'A', 'B', 'C']], - labels=[[1, 1, 1, 2, 2, 2, 3, 3, 3, 0], - [1, 2, 3, 1, 2, 3, 1, 2, 3, 0]], + codes=[[1, 1, 1, 2, 2, 2, 3, 3, 3, 0], + [1, 2, 3, 1, 2, 3, 1, 2, 3, 0]], names=['A', 'B']) expected_column = pd.Index(['bar', 'foo', 'All'], dtype='object', @@ -1354,105 +1762,37 @@ def test_crosstab_with_numpy_size(self): columns=expected_column) tm.assert_frame_equal(result, expected) + def test_crosstab_dup_index_names(self): + # GH 13279 + s = pd.Series(range(3), name='foo') -class TestPivotAnnual(tm.TestCase): - """ - New pandas of scikits.timeseries pivot_annual - """ - - def test_daily(self): - rng = date_range('1/1/2000', '12/31/2004', freq='D') - ts = Series(np.random.randn(len(rng)), index=rng) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - annual = pivot_annual(ts, 'D') - - doy = np.asarray(ts.index.dayofyear) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - doy[(~isleapyear(ts.index.year)) & (doy >= 60)] += 1 - - for i in range(1, 367): - subset = ts[doy == i] - subset.index = [x.year for x in subset.index] - - result = annual[i].dropna() - tm.assert_series_equal(result, subset, check_names=False) - self.assertEqual(result.name, i) - - # check leap days - leaps = ts[(ts.index.month == 2) & (ts.index.day == 29)] - day = leaps.index.dayofyear[0] - leaps.index = leaps.index.year - leaps.name = 60 - tm.assert_series_equal(annual[day].dropna(), leaps) - - def test_hourly(self): - rng_hourly = date_range('1/1/1994', periods=(18 * 8760 + 4 * 24), - freq='H') - data_hourly = np.random.randint(100, 350, rng_hourly.size) - ts_hourly = Series(data_hourly, index=rng_hourly) - - grouped = ts_hourly.groupby(ts_hourly.index.year) - hoy = grouped.apply(lambda x: x.reset_index(drop=True)) - hoy = hoy.index.droplevel(0).values - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - hoy[~isleapyear(ts_hourly.index.year) & (hoy >= 1416)] += 24 - hoy += 1 - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - annual = pivot_annual(ts_hourly) - - ts_hourly = ts_hourly.astype(float) - for i in [1, 1416, 1417, 1418, 1439, 1440, 1441, 8784]: - subset = ts_hourly[hoy == i] - subset.index = [x.year for x in subset.index] - - result = annual[i].dropna() - tm.assert_series_equal(result, subset, check_names=False) - self.assertEqual(result.name, i) - - leaps = ts_hourly[(ts_hourly.index.month == 2) & ( - ts_hourly.index.day == 29) & (ts_hourly.index.hour == 0)] - hour = leaps.index.dayofyear[0] * 24 - 23 - leaps.index = leaps.index.year - leaps.name = 1417 - tm.assert_series_equal(annual[hour].dropna(), leaps) - - def test_weekly(self): - pass - - def test_monthly(self): - rng = date_range('1/1/2000', '12/31/2004', freq='M') - ts = Series(np.random.randn(len(rng)), index=rng) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - annual = pivot_annual(ts, 'M') - - month = ts.index.month - for i in range(1, 13): - subset = ts[month == i] - subset.index = [x.year for x in subset.index] - result = annual[i].dropna() - tm.assert_series_equal(result, subset, check_names=False) - self.assertEqual(result.name, i) - - def test_period_monthly(self): - pass - - def test_period_daily(self): - pass + result = pd.crosstab(s, s) + expected_index = pd.Index(range(3), name='foo') + expected = pd.DataFrame(np.eye(3, dtype=np.int64), + index=expected_index, + columns=expected_index) + tm.assert_frame_equal(result, expected) - def test_period_weekly(self): - pass + @pytest.mark.parametrize("names", [['a', ('b', 'c')], + [('a', 'b'), 'c']]) + def test_crosstab_tuple_name(self, names): + s1 = pd.Series(range(3), name=names[0]) + s2 = pd.Series(range(1, 4), name=names[1]) - def test_isleapyear_deprecate(self): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertTrue(isleapyear(2000)) + mi = pd.MultiIndex.from_arrays([range(3), range(1, 4)], names=names) + expected = pd.Series(1, index=mi).unstack(1, fill_value=0) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertFalse(isleapyear(2001)) + result = pd.crosstab(s1, s2) + tm.assert_frame_equal(result, expected) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertTrue(isleapyear(2004)) + def test_crosstab_unsorted_order(self): + df = pd.DataFrame({"b": [3, 1, 2], 'a': [5, 4, 6]}, + index=['C', 'A', 'B']) + result = pd.crosstab(df.index, [df.b, df.a]) + e_idx = pd.Index(['A', 'B', 'C'], name='row_0') + e_columns = pd.MultiIndex.from_tuples([(1, 4), (2, 6), (3, 5)], + names=['b', 'a']) + expected = pd.DataFrame([[1, 0, 0], [0, 1, 0], [0, 0, 1]], + index=e_idx, + columns=e_columns) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/reshape/test_qcut.py b/pandas/tests/reshape/test_qcut.py new file mode 100644 index 0000000000000..997df7fd7aa4c --- /dev/null +++ b/pandas/tests/reshape/test_qcut.py @@ -0,0 +1,199 @@ +import os + +import numpy as np +import pytest + +from pandas.compat import zip + +from pandas import ( + Categorical, DatetimeIndex, Interval, IntervalIndex, NaT, Series, + TimedeltaIndex, Timestamp, cut, date_range, isna, qcut, timedelta_range) +from pandas.api.types import CategoricalDtype as CDT +from pandas.core.algorithms import quantile +import pandas.util.testing as tm + +from pandas.tseries.offsets import Day, Nano + + +def test_qcut(): + arr = np.random.randn(1000) + + # We store the bins as Index that have been + # rounded to comparisons are a bit tricky. + labels, bins = qcut(arr, 4, retbins=True) + ex_bins = quantile(arr, [0, .25, .5, .75, 1.]) + + result = labels.categories.left.values + assert np.allclose(result, ex_bins[:-1], atol=1e-2) + + result = labels.categories.right.values + assert np.allclose(result, ex_bins[1:], atol=1e-2) + + ex_levels = cut(arr, ex_bins, include_lowest=True) + tm.assert_categorical_equal(labels, ex_levels) + + +def test_qcut_bounds(): + arr = np.random.randn(1000) + + factor = qcut(arr, 10, labels=False) + assert len(np.unique(factor)) == 10 + + +def test_qcut_specify_quantiles(): + arr = np.random.randn(100) + factor = qcut(arr, [0, .25, .5, .75, 1.]) + + expected = qcut(arr, 4) + tm.assert_categorical_equal(factor, expected) + + +def test_qcut_all_bins_same(): + with pytest.raises(ValueError, match="edges.*unique"): + qcut([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 3) + + +def test_qcut_include_lowest(): + values = np.arange(10) + ii = qcut(values, 4) + + ex_levels = IntervalIndex([Interval(-0.001, 2.25), Interval(2.25, 4.5), + Interval(4.5, 6.75), Interval(6.75, 9)]) + tm.assert_index_equal(ii.categories, ex_levels) + + +def test_qcut_nas(): + arr = np.random.randn(100) + arr[:20] = np.nan + + result = qcut(arr, 4) + assert isna(result[:20]).all() + + +def test_qcut_index(): + result = qcut([0, 2], 2) + intervals = [Interval(-0.001, 1), Interval(1, 2)] + + expected = Categorical(intervals, ordered=True) + tm.assert_categorical_equal(result, expected) + + +def test_qcut_binning_issues(datapath): + # see gh-1978, gh-1979 + cut_file = datapath(os.path.join("reshape", "data", "cut_data.csv")) + arr = np.loadtxt(cut_file) + result = qcut(arr, 20) + + starts = [] + ends = [] + + for lev in np.unique(result): + s = lev.left + e = lev.right + assert s != e + + starts.append(float(s)) + ends.append(float(e)) + + for (sp, sn), (ep, en) in zip(zip(starts[:-1], starts[1:]), + zip(ends[:-1], ends[1:])): + assert sp < sn + assert ep < en + assert ep <= sn + + +def test_qcut_return_intervals(): + ser = Series([0, 1, 2, 3, 4, 5, 6, 7, 8]) + res = qcut(ser, [0, 0.333, 0.666, 1]) + + exp_levels = np.array([Interval(-0.001, 2.664), + Interval(2.664, 5.328), Interval(5.328, 8)]) + exp = Series(exp_levels.take([0, 0, 0, 1, 1, 1, 2, 2, 2])).astype( + CDT(ordered=True)) + tm.assert_series_equal(res, exp) + + +@pytest.mark.parametrize("kwargs,msg", [ + (dict(duplicates="drop"), None), + (dict(), "Bin edges must be unique"), + (dict(duplicates="raise"), "Bin edges must be unique"), + (dict(duplicates="foo"), "invalid value for 'duplicates' parameter") +]) +def test_qcut_duplicates_bin(kwargs, msg): + # see gh-7751 + values = [0, 0, 0, 0, 1, 2, 3] + + if msg is not None: + with pytest.raises(ValueError, match=msg): + qcut(values, 3, **kwargs) + else: + result = qcut(values, 3, **kwargs) + expected = IntervalIndex([Interval(-0.001, 1), Interval(1, 3)]) + tm.assert_index_equal(result.categories, expected) + + +@pytest.mark.parametrize("data,start,end", [ + (9.0, 8.999, 9.0), + (0.0, -0.001, 0.0), + (-9.0, -9.001, -9.0), +]) +@pytest.mark.parametrize("length", [1, 2]) +@pytest.mark.parametrize("labels", [None, False]) +def test_single_quantile(data, start, end, length, labels): + # see gh-15431 + ser = Series([data] * length) + result = qcut(ser, 1, labels=labels) + + if labels is None: + intervals = IntervalIndex([Interval(start, end)] * + length, closed="right") + expected = Series(intervals).astype(CDT(ordered=True)) + else: + expected = Series([0] * length) + + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("ser", [ + Series(DatetimeIndex(["20180101", NaT, "20180103"])), + Series(TimedeltaIndex(["0 days", NaT, "2 days"]))], + ids=lambda x: str(x.dtype)) +def test_qcut_nat(ser): + # see gh-19768 + intervals = IntervalIndex.from_tuples([ + (ser[0] - Nano(), ser[2] - Day()), + np.nan, (ser[2] - Day(), ser[2])]) + expected = Series(Categorical(intervals, ordered=True)) + + result = qcut(ser, 2) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("bins", [3, np.linspace(0, 1, 4)]) +def test_datetime_tz_qcut(bins): + # see gh-19872 + tz = "US/Eastern" + ser = Series(date_range("20130101", periods=3, tz=tz)) + + result = qcut(ser, bins) + expected = Series(IntervalIndex([ + Interval(Timestamp("2012-12-31 23:59:59.999999999", tz=tz), + Timestamp("2013-01-01 16:00:00", tz=tz)), + Interval(Timestamp("2013-01-01 16:00:00", tz=tz), + Timestamp("2013-01-02 08:00:00", tz=tz)), + Interval(Timestamp("2013-01-02 08:00:00", tz=tz), + Timestamp("2013-01-03 00:00:00", tz=tz))])).astype( + CDT(ordered=True)) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("arg,expected_bins", [ + [timedelta_range("1day", periods=3), + TimedeltaIndex(["1 days", "2 days", "3 days"])], + [date_range("20180101", periods=3), + DatetimeIndex(["2018-01-01", "2018-01-02", "2018-01-03"])]]) +def test_date_like_qcut_bins(arg, expected_bins): + # see gh-19891 + ser = Series(arg) + result, result_bins = qcut(ser, 2, retbins=True) + tm.assert_index_equal(result_bins, expected_bins) diff --git a/pandas/tests/reshape/test_reshape.py b/pandas/tests/reshape/test_reshape.py new file mode 100644 index 0000000000000..a5b6cffd1d86c --- /dev/null +++ b/pandas/tests/reshape/test_reshape.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# pylint: disable-msg=W0612,E1101 + +from collections import OrderedDict + +import numpy as np +from numpy import nan +import pytest + +from pandas.compat import u + +from pandas.core.dtypes.common import is_integer_dtype + +import pandas as pd +from pandas import Categorical, DataFrame, Index, Series, get_dummies +from pandas.core.sparse.api import SparseArray, SparseDtype +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal + + +class TestGetDummies(object): + + @pytest.fixture + def df(self): + return DataFrame({'A': ['a', 'b', 'a'], + 'B': ['b', 'b', 'c'], + 'C': [1, 2, 3]}) + + @pytest.fixture(params=['uint8', 'i8', np.float64, bool, None]) + def dtype(self, request): + return np.dtype(request.param) + + @pytest.fixture(params=['dense', 'sparse']) + def sparse(self, request): + # params are strings to simplify reading test results, + # e.g. TestGetDummies::test_basic[uint8-sparse] instead of [uint8-True] + return request.param == 'sparse' + + def effective_dtype(self, dtype): + if dtype is None: + return np.uint8 + return dtype + + def test_raises_on_dtype_object(self, df): + with pytest.raises(ValueError): + get_dummies(df, dtype='object') + + def test_basic(self, sparse, dtype): + s_list = list('abc') + s_series = Series(s_list) + s_series_index = Series(s_list, list('ABC')) + + expected = DataFrame({'a': [1, 0, 0], + 'b': [0, 1, 0], + 'c': [0, 0, 1]}, + dtype=self.effective_dtype(dtype)) + if sparse: + expected = expected.apply(pd.SparseArray, fill_value=0.0) + result = get_dummies(s_list, sparse=sparse, dtype=dtype) + assert_frame_equal(result, expected) + + result = get_dummies(s_series, sparse=sparse, dtype=dtype) + assert_frame_equal(result, expected) + + expected.index = list('ABC') + result = get_dummies(s_series_index, sparse=sparse, dtype=dtype) + assert_frame_equal(result, expected) + + def test_basic_types(self, sparse, dtype): + # GH 10531 + s_list = list('abc') + s_series = Series(s_list) + s_df = DataFrame({'a': [0, 1, 0, 1, 2], + 'b': ['A', 'A', 'B', 'C', 'C'], + 'c': [2, 3, 3, 3, 2]}) + + expected = DataFrame({'a': [1, 0, 0], + 'b': [0, 1, 0], + 'c': [0, 0, 1]}, + dtype=self.effective_dtype(dtype), + columns=list('abc')) + if sparse: + if is_integer_dtype(dtype): + fill_value = 0 + elif dtype == bool: + fill_value = False + else: + fill_value = 0.0 + + expected = expected.apply(SparseArray, fill_value=fill_value) + result = get_dummies(s_list, sparse=sparse, dtype=dtype) + tm.assert_frame_equal(result, expected) + + result = get_dummies(s_series, sparse=sparse, dtype=dtype) + tm.assert_frame_equal(result, expected) + + result = get_dummies(s_df, columns=s_df.columns, + sparse=sparse, dtype=dtype) + if sparse: + dtype_name = 'Sparse[{}, {}]'.format( + self.effective_dtype(dtype).name, + fill_value + ) + else: + dtype_name = self.effective_dtype(dtype).name + + expected = Series({dtype_name: 8}) + tm.assert_series_equal(result.get_dtype_counts(), expected) + + result = get_dummies(s_df, columns=['a'], sparse=sparse, dtype=dtype) + + expected_counts = {'int64': 1, 'object': 1} + expected_counts[dtype_name] = 3 + expected_counts.get(dtype_name, 0) + + expected = Series(expected_counts).sort_index() + tm.assert_series_equal(result.get_dtype_counts().sort_index(), + expected) + + def test_just_na(self, sparse): + just_na_list = [np.nan] + just_na_series = Series(just_na_list) + just_na_series_index = Series(just_na_list, index=['A']) + + res_list = get_dummies(just_na_list, sparse=sparse) + res_series = get_dummies(just_na_series, sparse=sparse) + res_series_index = get_dummies(just_na_series_index, sparse=sparse) + + assert res_list.empty + assert res_series.empty + assert res_series_index.empty + + assert res_list.index.tolist() == [0] + assert res_series.index.tolist() == [0] + assert res_series_index.index.tolist() == ['A'] + + def test_include_na(self, sparse, dtype): + s = ['a', 'b', np.nan] + res = get_dummies(s, sparse=sparse, dtype=dtype) + exp = DataFrame({'a': [1, 0, 0], + 'b': [0, 1, 0]}, + dtype=self.effective_dtype(dtype)) + if sparse: + exp = exp.apply(pd.SparseArray, fill_value=0.0) + assert_frame_equal(res, exp) + + # Sparse dataframes do not allow nan labelled columns, see #GH8822 + res_na = get_dummies(s, dummy_na=True, sparse=sparse, dtype=dtype) + exp_na = DataFrame({nan: [0, 0, 1], + 'a': [1, 0, 0], + 'b': [0, 1, 0]}, + dtype=self.effective_dtype(dtype)) + exp_na = exp_na.reindex(['a', 'b', nan], axis=1) + # hack (NaN handling in assert_index_equal) + exp_na.columns = res_na.columns + if sparse: + exp_na = exp_na.apply(pd.SparseArray, fill_value=0.0) + assert_frame_equal(res_na, exp_na) + + res_just_na = get_dummies([nan], dummy_na=True, + sparse=sparse, dtype=dtype) + exp_just_na = DataFrame(Series(1, index=[0]), columns=[nan], + dtype=self.effective_dtype(dtype)) + tm.assert_numpy_array_equal(res_just_na.values, exp_just_na.values) + + def test_unicode(self, sparse): + # See GH 6885 - get_dummies chokes on unicode values + import unicodedata + e = 'e' + eacute = unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE') + s = [e, eacute, eacute] + res = get_dummies(s, prefix='letter', sparse=sparse) + exp = DataFrame({'letter_e': [1, 0, 0], + u('letter_%s') % eacute: [0, 1, 1]}, + dtype=np.uint8) + if sparse: + exp = exp.apply(pd.SparseArray, fill_value=0) + assert_frame_equal(res, exp) + + def test_dataframe_dummies_all_obj(self, df, sparse): + df = df[['A', 'B']] + result = get_dummies(df, sparse=sparse) + expected = DataFrame({'A_a': [1, 0, 1], + 'A_b': [0, 1, 0], + 'B_b': [1, 1, 0], + 'B_c': [0, 0, 1]}, + dtype=np.uint8) + if sparse: + expected = pd.DataFrame({ + "A_a": pd.SparseArray([1, 0, 1], dtype='uint8'), + "A_b": pd.SparseArray([0, 1, 0], dtype='uint8'), + "B_b": pd.SparseArray([1, 1, 0], dtype='uint8'), + "B_c": pd.SparseArray([0, 0, 1], dtype='uint8'), + }) + + assert_frame_equal(result, expected) + + def test_dataframe_dummies_mix_default(self, df, sparse, dtype): + result = get_dummies(df, sparse=sparse, dtype=dtype) + if sparse: + arr = SparseArray + typ = SparseDtype(dtype, 0) + else: + arr = np.array + typ = dtype + expected = DataFrame({'C': [1, 2, 3], + 'A_a': arr([1, 0, 1], dtype=typ), + 'A_b': arr([0, 1, 0], dtype=typ), + 'B_b': arr([1, 1, 0], dtype=typ), + 'B_c': arr([0, 0, 1], dtype=typ)}) + expected = expected[['C', 'A_a', 'A_b', 'B_b', 'B_c']] + assert_frame_equal(result, expected) + + def test_dataframe_dummies_prefix_list(self, df, sparse): + prefixes = ['from_A', 'from_B'] + result = get_dummies(df, prefix=prefixes, sparse=sparse) + expected = DataFrame({'C': [1, 2, 3], + 'from_A_a': [1, 0, 1], + 'from_A_b': [0, 1, 0], + 'from_B_b': [1, 1, 0], + 'from_B_c': [0, 0, 1]}, + dtype=np.uint8) + expected[['C']] = df[['C']] + cols = ['from_A_a', 'from_A_b', 'from_B_b', 'from_B_c'] + expected = expected[['C'] + cols] + + typ = pd.SparseArray if sparse else pd.Series + expected[cols] = expected[cols].apply(lambda x: typ(x)) + assert_frame_equal(result, expected) + + def test_dataframe_dummies_prefix_str(self, df, sparse): + # not that you should do this... + result = get_dummies(df, prefix='bad', sparse=sparse) + bad_columns = ['bad_a', 'bad_b', 'bad_b', 'bad_c'] + expected = DataFrame([[1, 1, 0, 1, 0], + [2, 0, 1, 1, 0], + [3, 1, 0, 0, 1]], + columns=['C'] + bad_columns, + dtype=np.uint8) + expected = expected.astype({"C": np.int64}) + if sparse: + # work around astyping & assigning with duplicate columns + # https://github.com/pandas-dev/pandas/issues/14427 + expected = pd.concat([ + pd.Series([1, 2, 3], name='C'), + pd.Series([1, 0, 1], name='bad_a', dtype='Sparse[uint8]'), + pd.Series([0, 1, 0], name='bad_b', dtype='Sparse[uint8]'), + pd.Series([1, 1, 0], name='bad_b', dtype='Sparse[uint8]'), + pd.Series([0, 0, 1], name='bad_c', dtype='Sparse[uint8]'), + ], axis=1) + + assert_frame_equal(result, expected) + + def test_dataframe_dummies_subset(self, df, sparse): + result = get_dummies(df, prefix=['from_A'], columns=['A'], + sparse=sparse) + expected = DataFrame({'B': ['b', 'b', 'c'], + 'C': [1, 2, 3], + 'from_A_a': [1, 0, 1], + 'from_A_b': [0, 1, 0]}, dtype=np.uint8) + expected[['C']] = df[['C']] + if sparse: + cols = ['from_A_a', 'from_A_b'] + expected[cols] = expected[cols].apply(lambda x: pd.SparseSeries(x)) + assert_frame_equal(result, expected) + + def test_dataframe_dummies_prefix_sep(self, df, sparse): + result = get_dummies(df, prefix_sep='..', sparse=sparse) + expected = DataFrame({'C': [1, 2, 3], + 'A..a': [1, 0, 1], + 'A..b': [0, 1, 0], + 'B..b': [1, 1, 0], + 'B..c': [0, 0, 1]}, + dtype=np.uint8) + expected[['C']] = df[['C']] + expected = expected[['C', 'A..a', 'A..b', 'B..b', 'B..c']] + if sparse: + cols = ['A..a', 'A..b', 'B..b', 'B..c'] + expected[cols] = expected[cols].apply(lambda x: pd.SparseSeries(x)) + + assert_frame_equal(result, expected) + + result = get_dummies(df, prefix_sep=['..', '__'], sparse=sparse) + expected = expected.rename(columns={'B..b': 'B__b', 'B..c': 'B__c'}) + assert_frame_equal(result, expected) + + result = get_dummies(df, prefix_sep={'A': '..', 'B': '__'}, + sparse=sparse) + assert_frame_equal(result, expected) + + def test_dataframe_dummies_prefix_bad_length(self, df, sparse): + with pytest.raises(ValueError): + get_dummies(df, prefix=['too few'], sparse=sparse) + + def test_dataframe_dummies_prefix_sep_bad_length(self, df, sparse): + with pytest.raises(ValueError): + get_dummies(df, prefix_sep=['bad'], sparse=sparse) + + def test_dataframe_dummies_prefix_dict(self, sparse): + prefixes = {'A': 'from_A', 'B': 'from_B'} + df = DataFrame({'C': [1, 2, 3], + 'A': ['a', 'b', 'a'], + 'B': ['b', 'b', 'c']}) + result = get_dummies(df, prefix=prefixes, sparse=sparse) + + expected = DataFrame({'C': [1, 2, 3], + 'from_A_a': [1, 0, 1], + 'from_A_b': [0, 1, 0], + 'from_B_b': [1, 1, 0], + 'from_B_c': [0, 0, 1]}) + + columns = ['from_A_a', 'from_A_b', 'from_B_b', 'from_B_c'] + expected[columns] = expected[columns].astype(np.uint8) + if sparse: + expected[columns] = expected[columns].apply( + lambda x: pd.SparseSeries(x) + ) + + assert_frame_equal(result, expected) + + def test_dataframe_dummies_with_na(self, df, sparse, dtype): + df.loc[3, :] = [np.nan, np.nan, np.nan] + result = get_dummies(df, dummy_na=True, + sparse=sparse, dtype=dtype).sort_index(axis=1) + + if sparse: + arr = SparseArray + typ = SparseDtype(dtype, 0) + else: + arr = np.array + typ = dtype + + expected = DataFrame({'C': [1, 2, 3, np.nan], + 'A_a': arr([1, 0, 1, 0], dtype=typ), + 'A_b': arr([0, 1, 0, 0], dtype=typ), + 'A_nan': arr([0, 0, 0, 1], dtype=typ), + 'B_b': arr([1, 1, 0, 0], dtype=typ), + 'B_c': arr([0, 0, 1, 0], dtype=typ), + 'B_nan': arr([0, 0, 0, 1], dtype=typ) + }).sort_index(axis=1) + + assert_frame_equal(result, expected) + + result = get_dummies(df, dummy_na=False, sparse=sparse, dtype=dtype) + expected = expected[['C', 'A_a', 'A_b', 'B_b', 'B_c']] + assert_frame_equal(result, expected) + + def test_dataframe_dummies_with_categorical(self, df, sparse, dtype): + df['cat'] = pd.Categorical(['x', 'y', 'y']) + result = get_dummies(df, sparse=sparse, dtype=dtype).sort_index(axis=1) + if sparse: + arr = SparseArray + typ = SparseDtype(dtype, 0) + else: + arr = np.array + typ = dtype + + expected = DataFrame({'C': [1, 2, 3], + 'A_a': arr([1, 0, 1], dtype=typ), + 'A_b': arr([0, 1, 0], dtype=typ), + 'B_b': arr([1, 1, 0], dtype=typ), + 'B_c': arr([0, 0, 1], dtype=typ), + 'cat_x': arr([1, 0, 0], dtype=typ), + 'cat_y': arr([0, 1, 1], dtype=typ) + }).sort_index(axis=1) + + assert_frame_equal(result, expected) + + @pytest.mark.parametrize('get_dummies_kwargs,expected', [ + ({'data': pd.DataFrame(({u'ä': ['a']}))}, + pd.DataFrame({u'ä_a': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'ä']})}, + pd.DataFrame({u'x_ä': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'a']}), 'prefix':u'ä'}, + pd.DataFrame({u'ä_a': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'a']}), 'prefix_sep':u'ä'}, + pd.DataFrame({u'xäa': [1]}, dtype=np.uint8))]) + def test_dataframe_dummies_unicode(self, get_dummies_kwargs, expected): + # GH22084 pd.get_dummies incorrectly encodes unicode characters + # in dataframe column names + result = get_dummies(**get_dummies_kwargs) + assert_frame_equal(result, expected) + + def test_basic_drop_first(self, sparse): + # GH12402 Add a new parameter `drop_first` to avoid collinearity + # Basic case + s_list = list('abc') + s_series = Series(s_list) + s_series_index = Series(s_list, list('ABC')) + + expected = DataFrame({'b': [0, 1, 0], + 'c': [0, 0, 1]}, + dtype=np.uint8) + + result = get_dummies(s_list, drop_first=True, sparse=sparse) + if sparse: + expected = expected.apply(pd.SparseArray, fill_value=0) + assert_frame_equal(result, expected) + + result = get_dummies(s_series, drop_first=True, sparse=sparse) + assert_frame_equal(result, expected) + + expected.index = list('ABC') + result = get_dummies(s_series_index, drop_first=True, sparse=sparse) + assert_frame_equal(result, expected) + + def test_basic_drop_first_one_level(self, sparse): + # Test the case that categorical variable only has one level. + s_list = list('aaa') + s_series = Series(s_list) + s_series_index = Series(s_list, list('ABC')) + + expected = DataFrame(index=np.arange(3)) + + result = get_dummies(s_list, drop_first=True, sparse=sparse) + assert_frame_equal(result, expected) + + result = get_dummies(s_series, drop_first=True, sparse=sparse) + assert_frame_equal(result, expected) + + expected = DataFrame(index=list('ABC')) + result = get_dummies(s_series_index, drop_first=True, sparse=sparse) + assert_frame_equal(result, expected) + + def test_basic_drop_first_NA(self, sparse): + # Test NA handling together with drop_first + s_NA = ['a', 'b', np.nan] + res = get_dummies(s_NA, drop_first=True, sparse=sparse) + exp = DataFrame({'b': [0, 1, 0]}, dtype=np.uint8) + if sparse: + exp = exp.apply(pd.SparseArray, fill_value=0) + + assert_frame_equal(res, exp) + + res_na = get_dummies(s_NA, dummy_na=True, drop_first=True, + sparse=sparse) + exp_na = DataFrame( + {'b': [0, 1, 0], + nan: [0, 0, 1]}, + dtype=np.uint8).reindex(['b', nan], axis=1) + if sparse: + exp_na = exp_na.apply(pd.SparseArray, fill_value=0) + assert_frame_equal(res_na, exp_na) + + res_just_na = get_dummies([nan], dummy_na=True, drop_first=True, + sparse=sparse) + exp_just_na = DataFrame(index=np.arange(1)) + assert_frame_equal(res_just_na, exp_just_na) + + def test_dataframe_dummies_drop_first(self, df, sparse): + df = df[['A', 'B']] + result = get_dummies(df, drop_first=True, sparse=sparse) + expected = DataFrame({'A_b': [0, 1, 0], + 'B_c': [0, 0, 1]}, + dtype=np.uint8) + if sparse: + expected = expected.apply(pd.SparseArray, fill_value=0) + assert_frame_equal(result, expected) + + def test_dataframe_dummies_drop_first_with_categorical( + self, df, sparse, dtype): + df['cat'] = pd.Categorical(['x', 'y', 'y']) + result = get_dummies(df, drop_first=True, sparse=sparse) + expected = DataFrame({'C': [1, 2, 3], + 'A_b': [0, 1, 0], + 'B_c': [0, 0, 1], + 'cat_y': [0, 1, 1]}) + cols = ['A_b', 'B_c', 'cat_y'] + expected[cols] = expected[cols].astype(np.uint8) + expected = expected[['C', 'A_b', 'B_c', 'cat_y']] + if sparse: + for col in cols: + expected[col] = pd.SparseSeries(expected[col]) + assert_frame_equal(result, expected) + + def test_dataframe_dummies_drop_first_with_na(self, df, sparse): + df.loc[3, :] = [np.nan, np.nan, np.nan] + result = get_dummies(df, dummy_na=True, drop_first=True, + sparse=sparse).sort_index(axis=1) + expected = DataFrame({'C': [1, 2, 3, np.nan], + 'A_b': [0, 1, 0, 0], + 'A_nan': [0, 0, 0, 1], + 'B_c': [0, 0, 1, 0], + 'B_nan': [0, 0, 0, 1]}) + cols = ['A_b', 'A_nan', 'B_c', 'B_nan'] + expected[cols] = expected[cols].astype(np.uint8) + expected = expected.sort_index(axis=1) + if sparse: + for col in cols: + expected[col] = pd.SparseSeries(expected[col]) + + assert_frame_equal(result, expected) + + result = get_dummies(df, dummy_na=False, drop_first=True, + sparse=sparse) + expected = expected[['C', 'A_b', 'B_c']] + assert_frame_equal(result, expected) + + def test_int_int(self): + data = Series([1, 2, 1]) + result = pd.get_dummies(data) + expected = DataFrame([[1, 0], + [0, 1], + [1, 0]], + columns=[1, 2], + dtype=np.uint8) + tm.assert_frame_equal(result, expected) + + data = Series(pd.Categorical(['a', 'b', 'a'])) + result = pd.get_dummies(data) + expected = DataFrame([[1, 0], + [0, 1], + [1, 0]], + columns=pd.Categorical(['a', 'b']), + dtype=np.uint8) + tm.assert_frame_equal(result, expected) + + def test_int_df(self, dtype): + data = DataFrame( + {'A': [1, 2, 1], + 'B': pd.Categorical(['a', 'b', 'a']), + 'C': [1, 2, 1], + 'D': [1., 2., 1.] + } + ) + columns = ['C', 'D', 'A_1', 'A_2', 'B_a', 'B_b'] + expected = DataFrame([ + [1, 1., 1, 0, 1, 0], + [2, 2., 0, 1, 0, 1], + [1, 1., 1, 0, 1, 0] + ], columns=columns) + expected[columns[2:]] = expected[columns[2:]].astype(dtype) + result = pd.get_dummies(data, columns=['A', 'B'], dtype=dtype) + tm.assert_frame_equal(result, expected) + + def test_dataframe_dummies_preserve_categorical_dtype(self, dtype): + # GH13854 + for ordered in [False, True]: + cat = pd.Categorical(list("xy"), categories=list("xyz"), + ordered=ordered) + result = get_dummies(cat, dtype=dtype) + + data = np.array([[1, 0, 0], [0, 1, 0]], + dtype=self.effective_dtype(dtype)) + cols = pd.CategoricalIndex(cat.categories, + categories=cat.categories, + ordered=ordered) + expected = DataFrame(data, columns=cols, + dtype=self.effective_dtype(dtype)) + + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize('sparse', [True, False]) + def test_get_dummies_dont_sparsify_all_columns(self, sparse): + # GH18914 + df = DataFrame.from_dict(OrderedDict([('GDP', [1, 2]), + ('Nation', ['AB', 'CD'])])) + df = get_dummies(df, columns=['Nation'], sparse=sparse) + df2 = df.reindex(columns=['GDP']) + + tm.assert_frame_equal(df[['GDP']], df2) + + def test_get_dummies_duplicate_columns(self, df): + # GH20839 + df.columns = ["A", "A", "A"] + result = get_dummies(df).sort_index(axis=1) + + expected = DataFrame([[1, 1, 0, 1, 0], + [2, 0, 1, 1, 0], + [3, 1, 0, 0, 1]], + columns=['A', 'A_a', 'A_b', 'A_b', 'A_c'], + dtype=np.uint8).sort_index(axis=1) + + expected = expected.astype({"A": np.int64}) + + tm.assert_frame_equal(result, expected) + + +class TestCategoricalReshape(object): + + def test_reshaping_multi_index_categorical(self): + + # construct a MultiIndexed DataFrame formerly created + # via `tm.makePanel().to_frame()` + cols = ['ItemA', 'ItemB', 'ItemC'] + data = {c: tm.makeTimeDataFrame() for c in cols} + df = pd.concat({c: data[c].stack() for c in data}, axis='columns') + df.index.names = ['major', 'minor'] + df['str'] = 'foo' + + dti = df.index.levels[0] + + df['category'] = df['str'].astype('category') + result = df['category'].unstack() + + c = Categorical(['foo'] * len(dti)) + expected = DataFrame({'A': c.copy(), + 'B': c.copy(), + 'C': c.copy(), + 'D': c.copy()}, + columns=Index(list('ABCD'), name='minor'), + index=dti) + tm.assert_frame_equal(result, expected) + + +class TestMakeAxisDummies(object): + + def test_preserve_categorical_dtype(self): + # GH13854 + for ordered in [False, True]: + cidx = pd.CategoricalIndex(list("xyz"), ordered=ordered) + midx = pd.MultiIndex(levels=[['a'], cidx], + codes=[[0, 0], [0, 1]]) + df = DataFrame([[10, 11]], index=midx) + + expected = DataFrame([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], + index=midx, columns=cidx) + + from pandas.core.reshape.reshape import make_axis_dummies + result = make_axis_dummies(df) + tm.assert_frame_equal(result, expected) + + result = make_axis_dummies(df, transform=lambda x: x) + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/tools/test_union_categoricals.py b/pandas/tests/reshape/test_union_categoricals.py similarity index 90% rename from pandas/tests/tools/test_union_categoricals.py rename to pandas/tests/reshape/test_union_categoricals.py index 299b60f2a00b0..9b2b8bf9ed49f 100644 --- a/pandas/tests/tools/test_union_categoricals.py +++ b/pandas/tests/reshape/test_union_categoricals.py @@ -1,11 +1,14 @@ import numpy as np +import pytest + +from pandas.core.dtypes.concat import union_categoricals + import pandas as pd -from pandas import Categorical, Series, CategoricalIndex -from pandas.types.concat import union_categoricals +from pandas import Categorical, CategoricalIndex, Series from pandas.util import testing as tm -class TestUnionCategoricals(tm.TestCase): +class TestUnionCategoricals(object): def test_union_categorical(self): # GH 13361 @@ -56,11 +59,11 @@ def test_union_categorical(self): s = Categorical([0, 1.2, 2]) s2 = Categorical([2, 3, 4]) msg = 'dtype of categories must be the same' - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([s, s2]) msg = 'No Categoricals to union' - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): union_categoricals([]) def test_union_categoricals_nan(self): @@ -88,7 +91,8 @@ def test_union_categoricals_nan(self): tm.assert_categorical_equal(res, exp) # all NaN - res = union_categoricals([pd.Categorical([np.nan, np.nan]), + res = union_categoricals([pd.Categorical(np.array([np.nan, np.nan], + dtype=object)), pd.Categorical(['X'])]) exp = Categorical([np.nan, np.nan, 'X']) tm.assert_categorical_equal(res, exp) @@ -105,17 +109,11 @@ def test_union_categoricals_empty(self): exp = Categorical([]) tm.assert_categorical_equal(res, exp) - res = union_categoricals([pd.Categorical([]), - pd.Categorical([1.0])]) - exp = Categorical([1.0]) + res = union_categoricals([Categorical([]), + Categorical(['1'])]) + exp = Categorical(['1']) tm.assert_categorical_equal(res, exp) - # to make dtype equal - nanc = pd.Categorical(np.array([np.nan], dtype=np.float64)) - res = union_categoricals([nanc, - pd.Categorical([])]) - tm.assert_categorical_equal(res, nanc) - def test_union_categorical_same_category(self): # check fastpath c1 = Categorical([1, 2, 3, 4], categories=[1, 2, 3, 4]) @@ -132,12 +130,21 @@ def test_union_categorical_same_category(self): categories=['x', 'y', 'z']) tm.assert_categorical_equal(res, exp) + def test_union_categorical_same_categories_different_order(self): + # https://github.com/pandas-dev/pandas/issues/19096 + c1 = Categorical(['a', 'b', 'c'], categories=['a', 'b', 'c']) + c2 = Categorical(['a', 'b', 'c'], categories=['b', 'a', 'c']) + result = union_categoricals([c1, c2]) + expected = Categorical(['a', 'b', 'c', 'a', 'b', 'c'], + categories=['a', 'b', 'c']) + tm.assert_categorical_equal(result, expected) + def test_union_categoricals_ordered(self): c1 = Categorical([1, 2, 3], ordered=True) c2 = Categorical([1, 2, 3], ordered=False) msg = 'Categorical.ordered must be the same' - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([c1, c2]) res = union_categoricals([c1, c1]) @@ -155,7 +162,7 @@ def test_union_categoricals_ordered(self): c2 = Categorical([1, 2, 3], categories=[3, 2, 1], ordered=True) msg = "to union ordered Categoricals, all categories must be the same" - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([c1, c2]) def test_union_categoricals_ignore_order(self): @@ -168,7 +175,7 @@ def test_union_categoricals_ignore_order(self): tm.assert_categorical_equal(res, exp) msg = 'Categorical.ordered must be the same' - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([c1, c2], ignore_order=False) res = union_categoricals([c1, c1], ignore_order=True) @@ -206,10 +213,10 @@ def test_union_categoricals_ignore_order(self): tm.assert_categorical_equal(result, expected) msg = "to union ordered Categoricals, all categories must be the same" - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([c1, c2], ignore_order=False) - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): union_categoricals([c1, c2]) def test_union_categoricals_sort(self): @@ -254,7 +261,7 @@ def test_union_categoricals_sort(self): c1 = Categorical([np.nan]) c2 = Categorical([np.nan]) result = union_categoricals([c1, c2], sort_categories=True) - expected = Categorical([np.nan, np.nan], categories=[]) + expected = Categorical([np.nan, np.nan]) tm.assert_categorical_equal(result, expected) c1 = Categorical([]) @@ -265,7 +272,7 @@ def test_union_categoricals_sort(self): c1 = Categorical(['b', 'a'], categories=['b', 'a', 'c'], ordered=True) c2 = Categorical(['a', 'c'], categories=['b', 'a', 'c'], ordered=True) - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): union_categoricals([c1, c2], sort_categories=True) def test_union_categoricals_sort_false(self): @@ -303,7 +310,7 @@ def test_union_categoricals_sort_false(self): c1 = Categorical([np.nan]) c2 = Categorical([np.nan]) result = union_categoricals([c1, c2], sort_categories=False) - expected = Categorical([np.nan, np.nan], categories=[]) + expected = Categorical([np.nan, np.nan]) tm.assert_categorical_equal(result, expected) c1 = Categorical([]) @@ -335,5 +342,5 @@ def test_union_categorical_unwrap(self): result = union_categoricals([c1, c2]) tm.assert_categorical_equal(result, expected) - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): union_categoricals([c1, ['a', 'b', 'c']]) diff --git a/pandas/tests/reshape/test_util.py b/pandas/tests/reshape/test_util.py new file mode 100644 index 0000000000000..a8d9e7a775442 --- /dev/null +++ b/pandas/tests/reshape/test_util.py @@ -0,0 +1,53 @@ +import numpy as np +import pytest + +from pandas import Index, date_range +from pandas.core.reshape.util import cartesian_product +import pandas.util.testing as tm + + +class TestCartesianProduct(object): + + def test_simple(self): + x, y = list('ABC'), [1, 22] + result1, result2 = cartesian_product([x, y]) + expected1 = np.array(['A', 'A', 'B', 'B', 'C', 'C']) + expected2 = np.array([1, 22, 1, 22, 1, 22]) + tm.assert_numpy_array_equal(result1, expected1) + tm.assert_numpy_array_equal(result2, expected2) + + def test_datetimeindex(self): + # regression test for GitHub issue #6439 + # make sure that the ordering on datetimeindex is consistent + x = date_range('2000-01-01', periods=2) + result1, result2 = [Index(y).day for y in cartesian_product([x, x])] + expected1 = Index([1, 1, 2, 2]) + expected2 = Index([1, 2, 1, 2]) + tm.assert_index_equal(result1, expected1) + tm.assert_index_equal(result2, expected2) + + def test_empty(self): + # product of empty factors + X = [[], [0, 1], []] + Y = [[], [], ['a', 'b', 'c']] + for x, y in zip(X, Y): + expected1 = np.array([], dtype=np.asarray(x).dtype) + expected2 = np.array([], dtype=np.asarray(y).dtype) + result1, result2 = cartesian_product([x, y]) + tm.assert_numpy_array_equal(result1, expected1) + tm.assert_numpy_array_equal(result2, expected2) + + # empty product (empty input): + result = cartesian_product([]) + expected = [] + assert result == expected + + @pytest.mark.parametrize("X", [ + 1, [1], [1, 2], [[1], 2], + 'a', ['a'], ['a', 'b'], [['a'], 'b'] + ]) + def test_invalid_input(self, X): + msg = "Input must be a list-like of list-likes" + + with pytest.raises(TypeError, match=msg): + cartesian_product(X=X) diff --git a/pandas/tests/scalar/interval/__init__.py b/pandas/tests/scalar/interval/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/scalar/interval/test_interval.py b/pandas/tests/scalar/interval/test_interval.py new file mode 100644 index 0000000000000..432f44725e2ba --- /dev/null +++ b/pandas/tests/scalar/interval/test_interval.py @@ -0,0 +1,225 @@ +from __future__ import division + +import numpy as np +import pytest + +from pandas import Interval, Timedelta, Timestamp +import pandas.core.common as com + + +@pytest.fixture +def interval(): + return Interval(0, 1) + + +class TestInterval(object): + + def test_properties(self, interval): + assert interval.closed == 'right' + assert interval.left == 0 + assert interval.right == 1 + assert interval.mid == 0.5 + + def test_repr(self, interval): + assert repr(interval) == "Interval(0, 1, closed='right')" + assert str(interval) == "(0, 1]" + + interval_left = Interval(0, 1, closed='left') + assert repr(interval_left) == "Interval(0, 1, closed='left')" + assert str(interval_left) == "[0, 1)" + + def test_contains(self, interval): + assert 0.5 in interval + assert 1 in interval + assert 0 not in interval + + msg = "__contains__ not defined for two intervals" + with pytest.raises(TypeError, match=msg): + interval in interval + + interval_both = Interval(0, 1, closed='both') + assert 0 in interval_both + assert 1 in interval_both + + interval_neither = Interval(0, 1, closed='neither') + assert 0 not in interval_neither + assert 0.5 in interval_neither + assert 1 not in interval_neither + + def test_equal(self): + assert Interval(0, 1) == Interval(0, 1, closed='right') + assert Interval(0, 1) != Interval(0, 1, closed='left') + assert Interval(0, 1) != 0 + + def test_comparison(self): + with pytest.raises(TypeError, match='unorderable types'): + Interval(0, 1) < 2 + + assert Interval(0, 1) < Interval(1, 2) + assert Interval(0, 1) < Interval(0, 2) + assert Interval(0, 1) < Interval(0.5, 1.5) + assert Interval(0, 1) <= Interval(0, 1) + assert Interval(0, 1) > Interval(-1, 2) + assert Interval(0, 1) >= Interval(0, 1) + + def test_hash(self, interval): + # should not raise + hash(interval) + + @pytest.mark.parametrize('left, right, expected', [ + (0, 5, 5), + (-2, 5.5, 7.5), + (10, 10, 0), + (10, np.inf, np.inf), + (-np.inf, -5, np.inf), + (-np.inf, np.inf, np.inf), + (Timedelta('0 days'), Timedelta('5 days'), Timedelta('5 days')), + (Timedelta('10 days'), Timedelta('10 days'), Timedelta('0 days')), + (Timedelta('1H10M'), Timedelta('5H5M'), Timedelta('3H55M')), + (Timedelta('5S'), Timedelta('1H'), Timedelta('59M55S'))]) + def test_length(self, left, right, expected): + # GH 18789 + iv = Interval(left, right) + result = iv.length + assert result == expected + + @pytest.mark.parametrize('left, right, expected', [ + ('2017-01-01', '2017-01-06', '5 days'), + ('2017-01-01', '2017-01-01 12:00:00', '12 hours'), + ('2017-01-01 12:00', '2017-01-01 12:00:00', '0 days'), + ('2017-01-01 12:01', '2017-01-05 17:31:00', '4 days 5 hours 30 min')]) + @pytest.mark.parametrize('tz', (None, 'UTC', 'CET', 'US/Eastern')) + def test_length_timestamp(self, tz, left, right, expected): + # GH 18789 + iv = Interval(Timestamp(left, tz=tz), Timestamp(right, tz=tz)) + result = iv.length + expected = Timedelta(expected) + assert result == expected + + @pytest.mark.parametrize('left, right', [ + ('a', 'z'), + (('a', 'b'), ('c', 'd')), + (list('AB'), list('ab')), + (Interval(0, 1), Interval(1, 2))]) + def test_length_errors(self, left, right): + # GH 18789 + iv = Interval(left, right) + msg = 'cannot compute length between .* and .*' + with pytest.raises(TypeError, match=msg): + iv.length + + def test_math_add(self, closed): + interval = Interval(0, 1, closed=closed) + expected = Interval(1, 2, closed=closed) + + result = interval + 1 + assert result == expected + + result = 1 + interval + assert result == expected + + result = interval + result += 1 + assert result == expected + + msg = r"unsupported operand type\(s\) for \+" + with pytest.raises(TypeError, match=msg): + interval + interval + + with pytest.raises(TypeError, match=msg): + interval + 'foo' + + def test_math_sub(self, closed): + interval = Interval(0, 1, closed=closed) + expected = Interval(-1, 0, closed=closed) + + result = interval - 1 + assert result == expected + + result = interval + result -= 1 + assert result == expected + + msg = r"unsupported operand type\(s\) for -" + with pytest.raises(TypeError, match=msg): + interval - interval + + with pytest.raises(TypeError, match=msg): + interval - 'foo' + + def test_math_mult(self, closed): + interval = Interval(0, 1, closed=closed) + expected = Interval(0, 2, closed=closed) + + result = interval * 2 + assert result == expected + + result = 2 * interval + assert result == expected + + result = interval + result *= 2 + assert result == expected + + msg = r"unsupported operand type\(s\) for \*" + with pytest.raises(TypeError, match=msg): + interval * interval + + msg = r"can\'t multiply sequence by non-int" + with pytest.raises(TypeError, match=msg): + interval * 'foo' + + def test_math_div(self, closed): + interval = Interval(0, 1, closed=closed) + expected = Interval(0, 0.5, closed=closed) + + result = interval / 2.0 + assert result == expected + + result = interval + result /= 2.0 + assert result == expected + + msg = r"unsupported operand type\(s\) for /" + with pytest.raises(TypeError, match=msg): + interval / interval + + with pytest.raises(TypeError, match=msg): + interval / 'foo' + + def test_math_floordiv(self, closed): + interval = Interval(1, 2, closed=closed) + expected = Interval(0, 1, closed=closed) + + result = interval // 2 + assert result == expected + + result = interval + result //= 2 + assert result == expected + + msg = r"unsupported operand type\(s\) for //" + with pytest.raises(TypeError, match=msg): + interval // interval + + with pytest.raises(TypeError, match=msg): + interval // 'foo' + + def test_constructor_errors(self): + msg = "invalid option for 'closed': foo" + with pytest.raises(ValueError, match=msg): + Interval(0, 1, closed='foo') + + msg = 'left side of interval must be <= right side' + with pytest.raises(ValueError, match=msg): + Interval(1, 0) + + @pytest.mark.parametrize('tz_left, tz_right', [ + (None, 'UTC'), ('UTC', None), ('UTC', 'US/Eastern')]) + def test_constructor_errors_tz(self, tz_left, tz_right): + # GH 18538 + left = Timestamp('2017-01-01', tz=tz_left) + right = Timestamp('2017-01-02', tz=tz_right) + error = TypeError if com._any_none(tz_left, tz_right) else ValueError + with pytest.raises(error): + Interval(left, right) diff --git a/pandas/tests/scalar/interval/test_ops.py b/pandas/tests/scalar/interval/test_ops.py new file mode 100644 index 0000000000000..869ff205c2f51 --- /dev/null +++ b/pandas/tests/scalar/interval/test_ops.py @@ -0,0 +1,60 @@ +"""Tests for Interval-Interval operations, such as overlaps, contains, etc.""" +import pytest + +from pandas import Interval, Timedelta, Timestamp + + +@pytest.fixture(params=[ + (Timedelta('0 days'), Timedelta('1 day')), + (Timestamp('2018-01-01'), Timedelta('1 day')), + (0, 1)], ids=lambda x: type(x[0]).__name__) +def start_shift(request): + """ + Fixture for generating intervals of types from a start value and a shift + value that can be added to start to generate an endpoint + """ + return request.param + + +class TestOverlaps(object): + + def test_overlaps_self(self, start_shift, closed): + start, shift = start_shift + interval = Interval(start, start + shift, closed) + assert interval.overlaps(interval) + + def test_overlaps_nested(self, start_shift, closed, other_closed): + start, shift = start_shift + interval1 = Interval(start, start + 3 * shift, other_closed) + interval2 = Interval(start + shift, start + 2 * shift, closed) + + # nested intervals should always overlap + assert interval1.overlaps(interval2) + + def test_overlaps_disjoint(self, start_shift, closed, other_closed): + start, shift = start_shift + interval1 = Interval(start, start + shift, other_closed) + interval2 = Interval(start + 2 * shift, start + 3 * shift, closed) + + # disjoint intervals should never overlap + assert not interval1.overlaps(interval2) + + def test_overlaps_endpoint(self, start_shift, closed, other_closed): + start, shift = start_shift + interval1 = Interval(start, start + shift, other_closed) + interval2 = Interval(start + shift, start + 2 * shift, closed) + + # overlap if shared endpoint is closed for both (overlap at a point) + result = interval1.overlaps(interval2) + expected = interval1.closed_right and interval2.closed_left + assert result == expected + + @pytest.mark.parametrize('other', [ + 10, True, 'foo', Timedelta('1 day'), Timestamp('2018-01-01')], + ids=lambda x: type(x).__name__) + def test_overlaps_invalid_type(self, other): + interval = Interval(0, 1) + msg = '`other` must be an Interval, got {other}'.format( + other=type(other).__name__) + with pytest.raises(TypeError, match=msg): + interval.overlaps(other) diff --git a/pandas/tests/scalar/period/__init__.py b/pandas/tests/scalar/period/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/scalar/test_period_asfreq.py b/pandas/tests/scalar/period/test_asfreq.py similarity index 57% rename from pandas/tests/scalar/test_period_asfreq.py rename to pandas/tests/scalar/period/test_asfreq.py index d311fef8a826d..f46f2da6c076d 100644 --- a/pandas/tests/scalar/test_period_asfreq.py +++ b/pandas/tests/scalar/period/test_asfreq.py @@ -1,21 +1,52 @@ -import pandas as pd +import pytest + +from pandas._libs.tslibs.frequencies import ( + INVALID_FREQ_ERR_MSG, _period_code_map) +from pandas.errors import OutOfBoundsDatetime + from pandas import Period, offsets -from pandas.util import testing as tm -from pandas.tseries.frequencies import _period_code_map -class TestFreqConversion(tm.TestCase): - "Test frequency conversion of date objects" +class TestFreqConversion(object): + """Test frequency conversion of date objects""" + @pytest.mark.parametrize('freq', ['A', 'Q', 'M', 'W', 'B', 'D']) + def test_asfreq_near_zero(self, freq): + # GH#19643, GH#19650 + per = Period('0001-01-01', freq=freq) + tup1 = (per.year, per.hour, per.day) + + prev = per - 1 + assert prev.ordinal == per.ordinal - 1 + tup2 = (prev.year, prev.month, prev.day) + assert tup2 < tup1 + + def test_asfreq_near_zero_weekly(self): + # GH#19834 + per1 = Period('0001-01-01', 'D') + 6 + per2 = Period('0001-01-01', 'D') - 6 + week1 = per1.asfreq('W') + week2 = per2.asfreq('W') + assert week1 != week2 + assert week1.asfreq('D', 'E') >= per1 + assert week2.asfreq('D', 'S') <= per2 + + @pytest.mark.xfail(reason='GH#19643 period_helper asfreq functions fail ' + 'to check for overflows') + def test_to_timestamp_out_of_bounds(self): + # GH#19643, currently gives Timestamp('1754-08-30 22:43:41.128654848') + per = Period('0001-01-01', freq='B') + with pytest.raises(OutOfBoundsDatetime): + per.to_timestamp() def test_asfreq_corner(self): val = Period(freq='A', year=2007) result1 = val.asfreq('5t') result2 = val.asfreq('t') expected = Period('2007-12-31 23:59', freq='t') - self.assertEqual(result1.ordinal, expected.ordinal) - self.assertEqual(result1.freqstr, '5T') - self.assertEqual(result2.ordinal, expected.ordinal) - self.assertEqual(result2.freqstr, 'T') + assert result1.ordinal == expected.ordinal + assert result1.freqstr == '5T' + assert result2.ordinal == expected.ordinal + assert result2.freqstr == 'T' def test_conv_annual(self): # frequency conversion tests: from Annual Frequency @@ -55,35 +86,35 @@ def test_conv_annual(self): ival_ANOV_to_D_end = Period(freq='D', year=2007, month=11, day=30) ival_ANOV_to_D_start = Period(freq='D', year=2006, month=12, day=1) - self.assertEqual(ival_A.asfreq('Q', 'S'), ival_A_to_Q_start) - self.assertEqual(ival_A.asfreq('Q', 'e'), ival_A_to_Q_end) - self.assertEqual(ival_A.asfreq('M', 's'), ival_A_to_M_start) - self.assertEqual(ival_A.asfreq('M', 'E'), ival_A_to_M_end) - self.assertEqual(ival_A.asfreq('W', 'S'), ival_A_to_W_start) - self.assertEqual(ival_A.asfreq('W', 'E'), ival_A_to_W_end) - self.assertEqual(ival_A.asfreq('B', 'S'), ival_A_to_B_start) - self.assertEqual(ival_A.asfreq('B', 'E'), ival_A_to_B_end) - self.assertEqual(ival_A.asfreq('D', 'S'), ival_A_to_D_start) - self.assertEqual(ival_A.asfreq('D', 'E'), ival_A_to_D_end) - self.assertEqual(ival_A.asfreq('H', 'S'), ival_A_to_H_start) - self.assertEqual(ival_A.asfreq('H', 'E'), ival_A_to_H_end) - self.assertEqual(ival_A.asfreq('min', 'S'), ival_A_to_T_start) - self.assertEqual(ival_A.asfreq('min', 'E'), ival_A_to_T_end) - self.assertEqual(ival_A.asfreq('T', 'S'), ival_A_to_T_start) - self.assertEqual(ival_A.asfreq('T', 'E'), ival_A_to_T_end) - self.assertEqual(ival_A.asfreq('S', 'S'), ival_A_to_S_start) - self.assertEqual(ival_A.asfreq('S', 'E'), ival_A_to_S_end) - - self.assertEqual(ival_AJAN.asfreq('D', 'S'), ival_AJAN_to_D_start) - self.assertEqual(ival_AJAN.asfreq('D', 'E'), ival_AJAN_to_D_end) - - self.assertEqual(ival_AJUN.asfreq('D', 'S'), ival_AJUN_to_D_start) - self.assertEqual(ival_AJUN.asfreq('D', 'E'), ival_AJUN_to_D_end) - - self.assertEqual(ival_ANOV.asfreq('D', 'S'), ival_ANOV_to_D_start) - self.assertEqual(ival_ANOV.asfreq('D', 'E'), ival_ANOV_to_D_end) - - self.assertEqual(ival_A.asfreq('A'), ival_A) + assert ival_A.asfreq('Q', 'S') == ival_A_to_Q_start + assert ival_A.asfreq('Q', 'e') == ival_A_to_Q_end + assert ival_A.asfreq('M', 's') == ival_A_to_M_start + assert ival_A.asfreq('M', 'E') == ival_A_to_M_end + assert ival_A.asfreq('W', 'S') == ival_A_to_W_start + assert ival_A.asfreq('W', 'E') == ival_A_to_W_end + assert ival_A.asfreq('B', 'S') == ival_A_to_B_start + assert ival_A.asfreq('B', 'E') == ival_A_to_B_end + assert ival_A.asfreq('D', 'S') == ival_A_to_D_start + assert ival_A.asfreq('D', 'E') == ival_A_to_D_end + assert ival_A.asfreq('H', 'S') == ival_A_to_H_start + assert ival_A.asfreq('H', 'E') == ival_A_to_H_end + assert ival_A.asfreq('min', 'S') == ival_A_to_T_start + assert ival_A.asfreq('min', 'E') == ival_A_to_T_end + assert ival_A.asfreq('T', 'S') == ival_A_to_T_start + assert ival_A.asfreq('T', 'E') == ival_A_to_T_end + assert ival_A.asfreq('S', 'S') == ival_A_to_S_start + assert ival_A.asfreq('S', 'E') == ival_A_to_S_end + + assert ival_AJAN.asfreq('D', 'S') == ival_AJAN_to_D_start + assert ival_AJAN.asfreq('D', 'E') == ival_AJAN_to_D_end + + assert ival_AJUN.asfreq('D', 'S') == ival_AJUN_to_D_start + assert ival_AJUN.asfreq('D', 'E') == ival_AJUN_to_D_end + + assert ival_ANOV.asfreq('D', 'S') == ival_ANOV_to_D_start + assert ival_ANOV.asfreq('D', 'E') == ival_ANOV_to_D_end + + assert ival_A.asfreq('A') == ival_A def test_conv_quarterly(self): # frequency conversion tests: from Quarterly Frequency @@ -120,30 +151,30 @@ def test_conv_quarterly(self): ival_QEJUN_to_D_start = Period(freq='D', year=2006, month=7, day=1) ival_QEJUN_to_D_end = Period(freq='D', year=2006, month=9, day=30) - self.assertEqual(ival_Q.asfreq('A'), ival_Q_to_A) - self.assertEqual(ival_Q_end_of_year.asfreq('A'), ival_Q_to_A) - - self.assertEqual(ival_Q.asfreq('M', 'S'), ival_Q_to_M_start) - self.assertEqual(ival_Q.asfreq('M', 'E'), ival_Q_to_M_end) - self.assertEqual(ival_Q.asfreq('W', 'S'), ival_Q_to_W_start) - self.assertEqual(ival_Q.asfreq('W', 'E'), ival_Q_to_W_end) - self.assertEqual(ival_Q.asfreq('B', 'S'), ival_Q_to_B_start) - self.assertEqual(ival_Q.asfreq('B', 'E'), ival_Q_to_B_end) - self.assertEqual(ival_Q.asfreq('D', 'S'), ival_Q_to_D_start) - self.assertEqual(ival_Q.asfreq('D', 'E'), ival_Q_to_D_end) - self.assertEqual(ival_Q.asfreq('H', 'S'), ival_Q_to_H_start) - self.assertEqual(ival_Q.asfreq('H', 'E'), ival_Q_to_H_end) - self.assertEqual(ival_Q.asfreq('Min', 'S'), ival_Q_to_T_start) - self.assertEqual(ival_Q.asfreq('Min', 'E'), ival_Q_to_T_end) - self.assertEqual(ival_Q.asfreq('S', 'S'), ival_Q_to_S_start) - self.assertEqual(ival_Q.asfreq('S', 'E'), ival_Q_to_S_end) - - self.assertEqual(ival_QEJAN.asfreq('D', 'S'), ival_QEJAN_to_D_start) - self.assertEqual(ival_QEJAN.asfreq('D', 'E'), ival_QEJAN_to_D_end) - self.assertEqual(ival_QEJUN.asfreq('D', 'S'), ival_QEJUN_to_D_start) - self.assertEqual(ival_QEJUN.asfreq('D', 'E'), ival_QEJUN_to_D_end) - - self.assertEqual(ival_Q.asfreq('Q'), ival_Q) + assert ival_Q.asfreq('A') == ival_Q_to_A + assert ival_Q_end_of_year.asfreq('A') == ival_Q_to_A + + assert ival_Q.asfreq('M', 'S') == ival_Q_to_M_start + assert ival_Q.asfreq('M', 'E') == ival_Q_to_M_end + assert ival_Q.asfreq('W', 'S') == ival_Q_to_W_start + assert ival_Q.asfreq('W', 'E') == ival_Q_to_W_end + assert ival_Q.asfreq('B', 'S') == ival_Q_to_B_start + assert ival_Q.asfreq('B', 'E') == ival_Q_to_B_end + assert ival_Q.asfreq('D', 'S') == ival_Q_to_D_start + assert ival_Q.asfreq('D', 'E') == ival_Q_to_D_end + assert ival_Q.asfreq('H', 'S') == ival_Q_to_H_start + assert ival_Q.asfreq('H', 'E') == ival_Q_to_H_end + assert ival_Q.asfreq('Min', 'S') == ival_Q_to_T_start + assert ival_Q.asfreq('Min', 'E') == ival_Q_to_T_end + assert ival_Q.asfreq('S', 'S') == ival_Q_to_S_start + assert ival_Q.asfreq('S', 'E') == ival_Q_to_S_end + + assert ival_QEJAN.asfreq('D', 'S') == ival_QEJAN_to_D_start + assert ival_QEJAN.asfreq('D', 'E') == ival_QEJAN_to_D_end + assert ival_QEJUN.asfreq('D', 'S') == ival_QEJUN_to_D_start + assert ival_QEJUN.asfreq('D', 'E') == ival_QEJUN_to_D_end + + assert ival_Q.asfreq('Q') == ival_Q def test_conv_monthly(self): # frequency conversion tests: from Monthly Frequency @@ -170,25 +201,25 @@ def test_conv_monthly(self): ival_M_to_S_end = Period(freq='S', year=2007, month=1, day=31, hour=23, minute=59, second=59) - self.assertEqual(ival_M.asfreq('A'), ival_M_to_A) - self.assertEqual(ival_M_end_of_year.asfreq('A'), ival_M_to_A) - self.assertEqual(ival_M.asfreq('Q'), ival_M_to_Q) - self.assertEqual(ival_M_end_of_quarter.asfreq('Q'), ival_M_to_Q) - - self.assertEqual(ival_M.asfreq('W', 'S'), ival_M_to_W_start) - self.assertEqual(ival_M.asfreq('W', 'E'), ival_M_to_W_end) - self.assertEqual(ival_M.asfreq('B', 'S'), ival_M_to_B_start) - self.assertEqual(ival_M.asfreq('B', 'E'), ival_M_to_B_end) - self.assertEqual(ival_M.asfreq('D', 'S'), ival_M_to_D_start) - self.assertEqual(ival_M.asfreq('D', 'E'), ival_M_to_D_end) - self.assertEqual(ival_M.asfreq('H', 'S'), ival_M_to_H_start) - self.assertEqual(ival_M.asfreq('H', 'E'), ival_M_to_H_end) - self.assertEqual(ival_M.asfreq('Min', 'S'), ival_M_to_T_start) - self.assertEqual(ival_M.asfreq('Min', 'E'), ival_M_to_T_end) - self.assertEqual(ival_M.asfreq('S', 'S'), ival_M_to_S_start) - self.assertEqual(ival_M.asfreq('S', 'E'), ival_M_to_S_end) - - self.assertEqual(ival_M.asfreq('M'), ival_M) + assert ival_M.asfreq('A') == ival_M_to_A + assert ival_M_end_of_year.asfreq('A') == ival_M_to_A + assert ival_M.asfreq('Q') == ival_M_to_Q + assert ival_M_end_of_quarter.asfreq('Q') == ival_M_to_Q + + assert ival_M.asfreq('W', 'S') == ival_M_to_W_start + assert ival_M.asfreq('W', 'E') == ival_M_to_W_end + assert ival_M.asfreq('B', 'S') == ival_M_to_B_start + assert ival_M.asfreq('B', 'E') == ival_M_to_B_end + assert ival_M.asfreq('D', 'S') == ival_M_to_D_start + assert ival_M.asfreq('D', 'E') == ival_M_to_D_end + assert ival_M.asfreq('H', 'S') == ival_M_to_H_start + assert ival_M.asfreq('H', 'E') == ival_M_to_H_end + assert ival_M.asfreq('Min', 'S') == ival_M_to_T_start + assert ival_M.asfreq('Min', 'E') == ival_M_to_T_end + assert ival_M.asfreq('S', 'S') == ival_M_to_S_start + assert ival_M.asfreq('S', 'E') == ival_M_to_S_end + + assert ival_M.asfreq('M') == ival_M def test_conv_weekly(self): # frequency conversion tests: from Weekly Frequency @@ -254,67 +285,66 @@ def test_conv_weekly(self): ival_W_to_S_end = Period(freq='S', year=2007, month=1, day=7, hour=23, minute=59, second=59) - self.assertEqual(ival_W.asfreq('A'), ival_W_to_A) - self.assertEqual(ival_W_end_of_year.asfreq('A'), - ival_W_to_A_end_of_year) - self.assertEqual(ival_W.asfreq('Q'), ival_W_to_Q) - self.assertEqual(ival_W_end_of_quarter.asfreq('Q'), - ival_W_to_Q_end_of_quarter) - self.assertEqual(ival_W.asfreq('M'), ival_W_to_M) - self.assertEqual(ival_W_end_of_month.asfreq('M'), - ival_W_to_M_end_of_month) - - self.assertEqual(ival_W.asfreq('B', 'S'), ival_W_to_B_start) - self.assertEqual(ival_W.asfreq('B', 'E'), ival_W_to_B_end) - - self.assertEqual(ival_W.asfreq('D', 'S'), ival_W_to_D_start) - self.assertEqual(ival_W.asfreq('D', 'E'), ival_W_to_D_end) - - self.assertEqual(ival_WSUN.asfreq('D', 'S'), ival_WSUN_to_D_start) - self.assertEqual(ival_WSUN.asfreq('D', 'E'), ival_WSUN_to_D_end) - self.assertEqual(ival_WSAT.asfreq('D', 'S'), ival_WSAT_to_D_start) - self.assertEqual(ival_WSAT.asfreq('D', 'E'), ival_WSAT_to_D_end) - self.assertEqual(ival_WFRI.asfreq('D', 'S'), ival_WFRI_to_D_start) - self.assertEqual(ival_WFRI.asfreq('D', 'E'), ival_WFRI_to_D_end) - self.assertEqual(ival_WTHU.asfreq('D', 'S'), ival_WTHU_to_D_start) - self.assertEqual(ival_WTHU.asfreq('D', 'E'), ival_WTHU_to_D_end) - self.assertEqual(ival_WWED.asfreq('D', 'S'), ival_WWED_to_D_start) - self.assertEqual(ival_WWED.asfreq('D', 'E'), ival_WWED_to_D_end) - self.assertEqual(ival_WTUE.asfreq('D', 'S'), ival_WTUE_to_D_start) - self.assertEqual(ival_WTUE.asfreq('D', 'E'), ival_WTUE_to_D_end) - self.assertEqual(ival_WMON.asfreq('D', 'S'), ival_WMON_to_D_start) - self.assertEqual(ival_WMON.asfreq('D', 'E'), ival_WMON_to_D_end) - - self.assertEqual(ival_W.asfreq('H', 'S'), ival_W_to_H_start) - self.assertEqual(ival_W.asfreq('H', 'E'), ival_W_to_H_end) - self.assertEqual(ival_W.asfreq('Min', 'S'), ival_W_to_T_start) - self.assertEqual(ival_W.asfreq('Min', 'E'), ival_W_to_T_end) - self.assertEqual(ival_W.asfreq('S', 'S'), ival_W_to_S_start) - self.assertEqual(ival_W.asfreq('S', 'E'), ival_W_to_S_end) - - self.assertEqual(ival_W.asfreq('W'), ival_W) - - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR - with self.assertRaisesRegexp(ValueError, msg): + assert ival_W.asfreq('A') == ival_W_to_A + assert ival_W_end_of_year.asfreq('A') == ival_W_to_A_end_of_year + + assert ival_W.asfreq('Q') == ival_W_to_Q + assert ival_W_end_of_quarter.asfreq('Q') == ival_W_to_Q_end_of_quarter + + assert ival_W.asfreq('M') == ival_W_to_M + assert ival_W_end_of_month.asfreq('M') == ival_W_to_M_end_of_month + + assert ival_W.asfreq('B', 'S') == ival_W_to_B_start + assert ival_W.asfreq('B', 'E') == ival_W_to_B_end + + assert ival_W.asfreq('D', 'S') == ival_W_to_D_start + assert ival_W.asfreq('D', 'E') == ival_W_to_D_end + + assert ival_WSUN.asfreq('D', 'S') == ival_WSUN_to_D_start + assert ival_WSUN.asfreq('D', 'E') == ival_WSUN_to_D_end + assert ival_WSAT.asfreq('D', 'S') == ival_WSAT_to_D_start + assert ival_WSAT.asfreq('D', 'E') == ival_WSAT_to_D_end + assert ival_WFRI.asfreq('D', 'S') == ival_WFRI_to_D_start + assert ival_WFRI.asfreq('D', 'E') == ival_WFRI_to_D_end + assert ival_WTHU.asfreq('D', 'S') == ival_WTHU_to_D_start + assert ival_WTHU.asfreq('D', 'E') == ival_WTHU_to_D_end + assert ival_WWED.asfreq('D', 'S') == ival_WWED_to_D_start + assert ival_WWED.asfreq('D', 'E') == ival_WWED_to_D_end + assert ival_WTUE.asfreq('D', 'S') == ival_WTUE_to_D_start + assert ival_WTUE.asfreq('D', 'E') == ival_WTUE_to_D_end + assert ival_WMON.asfreq('D', 'S') == ival_WMON_to_D_start + assert ival_WMON.asfreq('D', 'E') == ival_WMON_to_D_end + + assert ival_W.asfreq('H', 'S') == ival_W_to_H_start + assert ival_W.asfreq('H', 'E') == ival_W_to_H_end + assert ival_W.asfreq('Min', 'S') == ival_W_to_T_start + assert ival_W.asfreq('Min', 'E') == ival_W_to_T_end + assert ival_W.asfreq('S', 'S') == ival_W_to_S_start + assert ival_W.asfreq('S', 'E') == ival_W_to_S_end + + assert ival_W.asfreq('W') == ival_W + + msg = INVALID_FREQ_ERR_MSG + with pytest.raises(ValueError, match=msg): ival_W.asfreq('WK') def test_conv_weekly_legacy(self): # frequency conversion tests: from Weekly Frequency - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR - with self.assertRaisesRegexp(ValueError, msg): + msg = INVALID_FREQ_ERR_MSG + with pytest.raises(ValueError, match=msg): Period(freq='WK', year=2007, month=1, day=1) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-SAT', year=2007, month=1, day=6) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-FRI', year=2007, month=1, day=5) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-THU', year=2007, month=1, day=4) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-WED', year=2007, month=1, day=3) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-TUE', year=2007, month=1, day=2) - with self.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): Period(freq='WK-MON', year=2007, month=1, day=1) def test_conv_business(self): @@ -342,25 +372,25 @@ def test_conv_business(self): ival_B_to_S_end = Period(freq='S', year=2007, month=1, day=1, hour=23, minute=59, second=59) - self.assertEqual(ival_B.asfreq('A'), ival_B_to_A) - self.assertEqual(ival_B_end_of_year.asfreq('A'), ival_B_to_A) - self.assertEqual(ival_B.asfreq('Q'), ival_B_to_Q) - self.assertEqual(ival_B_end_of_quarter.asfreq('Q'), ival_B_to_Q) - self.assertEqual(ival_B.asfreq('M'), ival_B_to_M) - self.assertEqual(ival_B_end_of_month.asfreq('M'), ival_B_to_M) - self.assertEqual(ival_B.asfreq('W'), ival_B_to_W) - self.assertEqual(ival_B_end_of_week.asfreq('W'), ival_B_to_W) + assert ival_B.asfreq('A') == ival_B_to_A + assert ival_B_end_of_year.asfreq('A') == ival_B_to_A + assert ival_B.asfreq('Q') == ival_B_to_Q + assert ival_B_end_of_quarter.asfreq('Q') == ival_B_to_Q + assert ival_B.asfreq('M') == ival_B_to_M + assert ival_B_end_of_month.asfreq('M') == ival_B_to_M + assert ival_B.asfreq('W') == ival_B_to_W + assert ival_B_end_of_week.asfreq('W') == ival_B_to_W - self.assertEqual(ival_B.asfreq('D'), ival_B_to_D) + assert ival_B.asfreq('D') == ival_B_to_D - self.assertEqual(ival_B.asfreq('H', 'S'), ival_B_to_H_start) - self.assertEqual(ival_B.asfreq('H', 'E'), ival_B_to_H_end) - self.assertEqual(ival_B.asfreq('Min', 'S'), ival_B_to_T_start) - self.assertEqual(ival_B.asfreq('Min', 'E'), ival_B_to_T_end) - self.assertEqual(ival_B.asfreq('S', 'S'), ival_B_to_S_start) - self.assertEqual(ival_B.asfreq('S', 'E'), ival_B_to_S_end) + assert ival_B.asfreq('H', 'S') == ival_B_to_H_start + assert ival_B.asfreq('H', 'E') == ival_B_to_H_end + assert ival_B.asfreq('Min', 'S') == ival_B_to_T_start + assert ival_B.asfreq('Min', 'E') == ival_B_to_T_end + assert ival_B.asfreq('S', 'S') == ival_B_to_S_start + assert ival_B.asfreq('S', 'E') == ival_B_to_S_end - self.assertEqual(ival_B.asfreq('B'), ival_B) + assert ival_B.asfreq('B') == ival_B def test_conv_daily(self): # frequency conversion tests: from Business Frequency" @@ -405,39 +435,36 @@ def test_conv_daily(self): ival_D_to_S_end = Period(freq='S', year=2007, month=1, day=1, hour=23, minute=59, second=59) - self.assertEqual(ival_D.asfreq('A'), ival_D_to_A) - - self.assertEqual(ival_D_end_of_quarter.asfreq('A-JAN'), - ival_Deoq_to_AJAN) - self.assertEqual(ival_D_end_of_quarter.asfreq('A-JUN'), - ival_Deoq_to_AJUN) - self.assertEqual(ival_D_end_of_quarter.asfreq('A-DEC'), - ival_Deoq_to_ADEC) - - self.assertEqual(ival_D_end_of_year.asfreq('A'), ival_D_to_A) - self.assertEqual(ival_D_end_of_quarter.asfreq('Q'), ival_D_to_QEDEC) - self.assertEqual(ival_D.asfreq("Q-JAN"), ival_D_to_QEJAN) - self.assertEqual(ival_D.asfreq("Q-JUN"), ival_D_to_QEJUN) - self.assertEqual(ival_D.asfreq("Q-DEC"), ival_D_to_QEDEC) - self.assertEqual(ival_D.asfreq('M'), ival_D_to_M) - self.assertEqual(ival_D_end_of_month.asfreq('M'), ival_D_to_M) - self.assertEqual(ival_D.asfreq('W'), ival_D_to_W) - self.assertEqual(ival_D_end_of_week.asfreq('W'), ival_D_to_W) - - self.assertEqual(ival_D_friday.asfreq('B'), ival_B_friday) - self.assertEqual(ival_D_saturday.asfreq('B', 'S'), ival_B_friday) - self.assertEqual(ival_D_saturday.asfreq('B', 'E'), ival_B_monday) - self.assertEqual(ival_D_sunday.asfreq('B', 'S'), ival_B_friday) - self.assertEqual(ival_D_sunday.asfreq('B', 'E'), ival_B_monday) - - self.assertEqual(ival_D.asfreq('H', 'S'), ival_D_to_H_start) - self.assertEqual(ival_D.asfreq('H', 'E'), ival_D_to_H_end) - self.assertEqual(ival_D.asfreq('Min', 'S'), ival_D_to_T_start) - self.assertEqual(ival_D.asfreq('Min', 'E'), ival_D_to_T_end) - self.assertEqual(ival_D.asfreq('S', 'S'), ival_D_to_S_start) - self.assertEqual(ival_D.asfreq('S', 'E'), ival_D_to_S_end) - - self.assertEqual(ival_D.asfreq('D'), ival_D) + assert ival_D.asfreq('A') == ival_D_to_A + + assert ival_D_end_of_quarter.asfreq('A-JAN') == ival_Deoq_to_AJAN + assert ival_D_end_of_quarter.asfreq('A-JUN') == ival_Deoq_to_AJUN + assert ival_D_end_of_quarter.asfreq('A-DEC') == ival_Deoq_to_ADEC + + assert ival_D_end_of_year.asfreq('A') == ival_D_to_A + assert ival_D_end_of_quarter.asfreq('Q') == ival_D_to_QEDEC + assert ival_D.asfreq("Q-JAN") == ival_D_to_QEJAN + assert ival_D.asfreq("Q-JUN") == ival_D_to_QEJUN + assert ival_D.asfreq("Q-DEC") == ival_D_to_QEDEC + assert ival_D.asfreq('M') == ival_D_to_M + assert ival_D_end_of_month.asfreq('M') == ival_D_to_M + assert ival_D.asfreq('W') == ival_D_to_W + assert ival_D_end_of_week.asfreq('W') == ival_D_to_W + + assert ival_D_friday.asfreq('B') == ival_B_friday + assert ival_D_saturday.asfreq('B', 'S') == ival_B_friday + assert ival_D_saturday.asfreq('B', 'E') == ival_B_monday + assert ival_D_sunday.asfreq('B', 'S') == ival_B_friday + assert ival_D_sunday.asfreq('B', 'E') == ival_B_monday + + assert ival_D.asfreq('H', 'S') == ival_D_to_H_start + assert ival_D.asfreq('H', 'E') == ival_D_to_H_end + assert ival_D.asfreq('Min', 'S') == ival_D_to_T_start + assert ival_D.asfreq('Min', 'E') == ival_D_to_T_end + assert ival_D.asfreq('S', 'S') == ival_D_to_S_start + assert ival_D.asfreq('S', 'E') == ival_D_to_S_end + + assert ival_D.asfreq('D') == ival_D def test_conv_hourly(self): # frequency conversion tests: from Hourly Frequency" @@ -472,25 +499,25 @@ def test_conv_hourly(self): ival_H_to_S_end = Period(freq='S', year=2007, month=1, day=1, hour=0, minute=59, second=59) - self.assertEqual(ival_H.asfreq('A'), ival_H_to_A) - self.assertEqual(ival_H_end_of_year.asfreq('A'), ival_H_to_A) - self.assertEqual(ival_H.asfreq('Q'), ival_H_to_Q) - self.assertEqual(ival_H_end_of_quarter.asfreq('Q'), ival_H_to_Q) - self.assertEqual(ival_H.asfreq('M'), ival_H_to_M) - self.assertEqual(ival_H_end_of_month.asfreq('M'), ival_H_to_M) - self.assertEqual(ival_H.asfreq('W'), ival_H_to_W) - self.assertEqual(ival_H_end_of_week.asfreq('W'), ival_H_to_W) - self.assertEqual(ival_H.asfreq('D'), ival_H_to_D) - self.assertEqual(ival_H_end_of_day.asfreq('D'), ival_H_to_D) - self.assertEqual(ival_H.asfreq('B'), ival_H_to_B) - self.assertEqual(ival_H_end_of_bus.asfreq('B'), ival_H_to_B) - - self.assertEqual(ival_H.asfreq('Min', 'S'), ival_H_to_T_start) - self.assertEqual(ival_H.asfreq('Min', 'E'), ival_H_to_T_end) - self.assertEqual(ival_H.asfreq('S', 'S'), ival_H_to_S_start) - self.assertEqual(ival_H.asfreq('S', 'E'), ival_H_to_S_end) - - self.assertEqual(ival_H.asfreq('H'), ival_H) + assert ival_H.asfreq('A') == ival_H_to_A + assert ival_H_end_of_year.asfreq('A') == ival_H_to_A + assert ival_H.asfreq('Q') == ival_H_to_Q + assert ival_H_end_of_quarter.asfreq('Q') == ival_H_to_Q + assert ival_H.asfreq('M') == ival_H_to_M + assert ival_H_end_of_month.asfreq('M') == ival_H_to_M + assert ival_H.asfreq('W') == ival_H_to_W + assert ival_H_end_of_week.asfreq('W') == ival_H_to_W + assert ival_H.asfreq('D') == ival_H_to_D + assert ival_H_end_of_day.asfreq('D') == ival_H_to_D + assert ival_H.asfreq('B') == ival_H_to_B + assert ival_H_end_of_bus.asfreq('B') == ival_H_to_B + + assert ival_H.asfreq('Min', 'S') == ival_H_to_T_start + assert ival_H.asfreq('Min', 'E') == ival_H_to_T_end + assert ival_H.asfreq('S', 'S') == ival_H_to_S_start + assert ival_H.asfreq('S', 'E') == ival_H_to_S_end + + assert ival_H.asfreq('H') == ival_H def test_conv_minutely(self): # frequency conversion tests: from Minutely Frequency" @@ -525,25 +552,25 @@ def test_conv_minutely(self): ival_T_to_S_end = Period(freq='S', year=2007, month=1, day=1, hour=0, minute=0, second=59) - self.assertEqual(ival_T.asfreq('A'), ival_T_to_A) - self.assertEqual(ival_T_end_of_year.asfreq('A'), ival_T_to_A) - self.assertEqual(ival_T.asfreq('Q'), ival_T_to_Q) - self.assertEqual(ival_T_end_of_quarter.asfreq('Q'), ival_T_to_Q) - self.assertEqual(ival_T.asfreq('M'), ival_T_to_M) - self.assertEqual(ival_T_end_of_month.asfreq('M'), ival_T_to_M) - self.assertEqual(ival_T.asfreq('W'), ival_T_to_W) - self.assertEqual(ival_T_end_of_week.asfreq('W'), ival_T_to_W) - self.assertEqual(ival_T.asfreq('D'), ival_T_to_D) - self.assertEqual(ival_T_end_of_day.asfreq('D'), ival_T_to_D) - self.assertEqual(ival_T.asfreq('B'), ival_T_to_B) - self.assertEqual(ival_T_end_of_bus.asfreq('B'), ival_T_to_B) - self.assertEqual(ival_T.asfreq('H'), ival_T_to_H) - self.assertEqual(ival_T_end_of_hour.asfreq('H'), ival_T_to_H) - - self.assertEqual(ival_T.asfreq('S', 'S'), ival_T_to_S_start) - self.assertEqual(ival_T.asfreq('S', 'E'), ival_T_to_S_end) - - self.assertEqual(ival_T.asfreq('Min'), ival_T) + assert ival_T.asfreq('A') == ival_T_to_A + assert ival_T_end_of_year.asfreq('A') == ival_T_to_A + assert ival_T.asfreq('Q') == ival_T_to_Q + assert ival_T_end_of_quarter.asfreq('Q') == ival_T_to_Q + assert ival_T.asfreq('M') == ival_T_to_M + assert ival_T_end_of_month.asfreq('M') == ival_T_to_M + assert ival_T.asfreq('W') == ival_T_to_W + assert ival_T_end_of_week.asfreq('W') == ival_T_to_W + assert ival_T.asfreq('D') == ival_T_to_D + assert ival_T_end_of_day.asfreq('D') == ival_T_to_D + assert ival_T.asfreq('B') == ival_T_to_B + assert ival_T_end_of_bus.asfreq('B') == ival_T_to_B + assert ival_T.asfreq('H') == ival_T_to_H + assert ival_T_end_of_hour.asfreq('H') == ival_T_to_H + + assert ival_T.asfreq('S', 'S') == ival_T_to_S_start + assert ival_T.asfreq('S', 'E') == ival_T_to_S_end + + assert ival_T.asfreq('Min') == ival_T def test_conv_secondly(self): # frequency conversion tests: from Secondly Frequency" @@ -577,24 +604,24 @@ def test_conv_secondly(self): ival_S_to_T = Period(freq='Min', year=2007, month=1, day=1, hour=0, minute=0) - self.assertEqual(ival_S.asfreq('A'), ival_S_to_A) - self.assertEqual(ival_S_end_of_year.asfreq('A'), ival_S_to_A) - self.assertEqual(ival_S.asfreq('Q'), ival_S_to_Q) - self.assertEqual(ival_S_end_of_quarter.asfreq('Q'), ival_S_to_Q) - self.assertEqual(ival_S.asfreq('M'), ival_S_to_M) - self.assertEqual(ival_S_end_of_month.asfreq('M'), ival_S_to_M) - self.assertEqual(ival_S.asfreq('W'), ival_S_to_W) - self.assertEqual(ival_S_end_of_week.asfreq('W'), ival_S_to_W) - self.assertEqual(ival_S.asfreq('D'), ival_S_to_D) - self.assertEqual(ival_S_end_of_day.asfreq('D'), ival_S_to_D) - self.assertEqual(ival_S.asfreq('B'), ival_S_to_B) - self.assertEqual(ival_S_end_of_bus.asfreq('B'), ival_S_to_B) - self.assertEqual(ival_S.asfreq('H'), ival_S_to_H) - self.assertEqual(ival_S_end_of_hour.asfreq('H'), ival_S_to_H) - self.assertEqual(ival_S.asfreq('Min'), ival_S_to_T) - self.assertEqual(ival_S_end_of_minute.asfreq('Min'), ival_S_to_T) - - self.assertEqual(ival_S.asfreq('S'), ival_S) + assert ival_S.asfreq('A') == ival_S_to_A + assert ival_S_end_of_year.asfreq('A') == ival_S_to_A + assert ival_S.asfreq('Q') == ival_S_to_Q + assert ival_S_end_of_quarter.asfreq('Q') == ival_S_to_Q + assert ival_S.asfreq('M') == ival_S_to_M + assert ival_S_end_of_month.asfreq('M') == ival_S_to_M + assert ival_S.asfreq('W') == ival_S_to_W + assert ival_S_end_of_week.asfreq('W') == ival_S_to_W + assert ival_S.asfreq('D') == ival_S_to_D + assert ival_S_end_of_day.asfreq('D') == ival_S_to_D + assert ival_S.asfreq('B') == ival_S_to_B + assert ival_S_end_of_bus.asfreq('B') == ival_S_to_B + assert ival_S.asfreq('H') == ival_S_to_H + assert ival_S_end_of_hour.asfreq('H') == ival_S_to_H + assert ival_S.asfreq('Min') == ival_S_to_T + assert ival_S_end_of_minute.asfreq('Min') == ival_S_to_T + + assert ival_S.asfreq('S') == ival_S def test_asfreq_mult(self): # normal freq to mult freq @@ -604,17 +631,17 @@ def test_asfreq_mult(self): result = p.asfreq(freq) expected = Period('2007', freq='3A') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq # ordinal will not change for freq in ['3A', offsets.YearEnd(3)]: result = p.asfreq(freq, how='S') expected = Period('2007', freq='3A') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq # mult freq to normal freq p = Period(freq='3A', year=2007) @@ -623,49 +650,49 @@ def test_asfreq_mult(self): result = p.asfreq(freq) expected = Period('2009', freq='A') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq # ordinal will not change for freq in ['A', offsets.YearEnd()]: result = p.asfreq(freq, how='S') expected = Period('2007', freq='A') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq p = Period(freq='A', year=2007) for freq in ['2M', offsets.MonthEnd(2)]: result = p.asfreq(freq) expected = Period('2007-12', freq='2M') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq for freq in ['2M', offsets.MonthEnd(2)]: result = p.asfreq(freq, how='S') expected = Period('2007-01', freq='2M') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq p = Period(freq='3A', year=2007) for freq in ['2M', offsets.MonthEnd(2)]: result = p.asfreq(freq) expected = Period('2009-12', freq='2M') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq for freq in ['2M', offsets.MonthEnd(2)]: result = p.asfreq(freq, how='S') expected = Period('2007-01', freq='2M') - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq def test_asfreq_combined(self): # normal freq to combined freq @@ -675,9 +702,9 @@ def test_asfreq_combined(self): expected = Period('2007', freq='25H') for freq, how in zip(['1D1H', '1H1D'], ['E', 'S']): result = p.asfreq(freq, how=how) - self.assertEqual(result, expected) - self.assertEqual(result.ordinal, expected.ordinal) - self.assertEqual(result.freq, expected.freq) + assert result == expected + assert result.ordinal == expected.ordinal + assert result.freq == expected.freq # combined freq to normal freq p1 = Period(freq='1D1H', year=2007) @@ -687,35 +714,34 @@ def test_asfreq_combined(self): result1 = p1.asfreq('H') result2 = p2.asfreq('H') expected = Period('2007-01-02', freq='H') - self.assertEqual(result1, expected) - self.assertEqual(result1.ordinal, expected.ordinal) - self.assertEqual(result1.freq, expected.freq) - self.assertEqual(result2, expected) - self.assertEqual(result2.ordinal, expected.ordinal) - self.assertEqual(result2.freq, expected.freq) + assert result1 == expected + assert result1.ordinal == expected.ordinal + assert result1.freq == expected.freq + assert result2 == expected + assert result2.ordinal == expected.ordinal + assert result2.freq == expected.freq # ordinal will not change result1 = p1.asfreq('H', how='S') result2 = p2.asfreq('H', how='S') expected = Period('2007-01-01', freq='H') - self.assertEqual(result1, expected) - self.assertEqual(result1.ordinal, expected.ordinal) - self.assertEqual(result1.freq, expected.freq) - self.assertEqual(result2, expected) - self.assertEqual(result2.ordinal, expected.ordinal) - self.assertEqual(result2.freq, expected.freq) + assert result1 == expected + assert result1.ordinal == expected.ordinal + assert result1.freq == expected.freq + assert result2 == expected + assert result2.ordinal == expected.ordinal + assert result2.freq == expected.freq def test_asfreq_MS(self): initial = Period("2013") - self.assertEqual(initial.asfreq(freq="M", how="S"), - Period('2013-01', 'M')) + assert initial.asfreq(freq="M", how="S") == Period('2013-01', 'M') - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR - with self.assertRaisesRegexp(ValueError, msg): + msg = INVALID_FREQ_ERR_MSG + with pytest.raises(ValueError, match=msg): initial.asfreq(freq="MS", how="S") - with tm.assertRaisesRegexp(ValueError, msg): - pd.Period('2013-01', 'MS') + with pytest.raises(ValueError, match=msg): + Period('2013-01', 'MS') - self.assertTrue(_period_code_map.get("MS") is None) + assert _period_code_map.get("MS") is None diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py new file mode 100644 index 0000000000000..8ca19745055a3 --- /dev/null +++ b/pandas/tests/scalar/period/test_period.py @@ -0,0 +1,1498 @@ +from datetime import date, datetime, timedelta + +import numpy as np +import pytest +import pytz + +from pandas._libs.tslibs import iNaT, period as libperiod +from pandas._libs.tslibs.ccalendar import DAYS, MONTHS +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG +from pandas._libs.tslibs.parsing import DateParseError +from pandas._libs.tslibs.period import IncompatibleFrequency +from pandas._libs.tslibs.timezones import dateutil_gettz, maybe_get_tz +from pandas.compat import iteritems, text_type +from pandas.compat.numpy import np_datetime64_compat + +import pandas as pd +from pandas import NaT, Period, Timedelta, Timestamp, offsets +import pandas.core.indexes.period as period +import pandas.util.testing as tm + + +class TestPeriodConstruction(object): + def test_construction(self): + i1 = Period('1/1/2005', freq='M') + i2 = Period('Jan 2005') + + assert i1 == i2 + + i1 = Period('2005', freq='A') + i2 = Period('2005') + i3 = Period('2005', freq='a') + + assert i1 == i2 + assert i1 == i3 + + i4 = Period('2005', freq='M') + i5 = Period('2005', freq='m') + + msg = r"Input has different freq=M from Period\(freq=A-DEC\)" + with pytest.raises(IncompatibleFrequency, match=msg): + i1 != i4 + assert i4 == i5 + + i1 = Period.now('Q') + i2 = Period(datetime.now(), freq='Q') + i3 = Period.now('q') + + assert i1 == i2 + assert i1 == i3 + + i1 = Period('1982', freq='min') + i2 = Period('1982', freq='MIN') + assert i1 == i2 + i2 = Period('1982', freq=('Min', 1)) + assert i1 == i2 + + i1 = Period(year=2005, month=3, day=1, freq='D') + i2 = Period('3/1/2005', freq='D') + assert i1 == i2 + + i3 = Period(year=2005, month=3, day=1, freq='d') + assert i1 == i3 + + i1 = Period('2007-01-01 09:00:00.001') + expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1000), freq='L') + assert i1 == expected + + expected = Period(np_datetime64_compat( + '2007-01-01 09:00:00.001Z'), freq='L') + assert i1 == expected + + i1 = Period('2007-01-01 09:00:00.00101') + expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1010), freq='U') + assert i1 == expected + + expected = Period(np_datetime64_compat('2007-01-01 09:00:00.00101Z'), + freq='U') + assert i1 == expected + + msg = "Must supply freq for ordinal value" + with pytest.raises(ValueError, match=msg): + Period(ordinal=200701) + + with pytest.raises(ValueError, match="Invalid frequency: X"): + Period('2007-1-1', freq='X') + + def test_construction_bday(self): + + # Biz day construction, roll forward if non-weekday + i1 = Period('3/10/12', freq='B') + i2 = Period('3/10/12', freq='D') + assert i1 == i2.asfreq('B') + i2 = Period('3/11/12', freq='D') + assert i1 == i2.asfreq('B') + i2 = Period('3/12/12', freq='D') + assert i1 == i2.asfreq('B') + + i3 = Period('3/10/12', freq='b') + assert i1 == i3 + + i1 = Period(year=2012, month=3, day=10, freq='B') + i2 = Period('3/12/12', freq='B') + assert i1 == i2 + + def test_construction_quarter(self): + + i1 = Period(year=2005, quarter=1, freq='Q') + i2 = Period('1/1/2005', freq='Q') + assert i1 == i2 + + i1 = Period(year=2005, quarter=3, freq='Q') + i2 = Period('9/1/2005', freq='Q') + assert i1 == i2 + + i1 = Period('2005Q1') + i2 = Period(year=2005, quarter=1, freq='Q') + i3 = Period('2005q1') + assert i1 == i2 + assert i1 == i3 + + i1 = Period('05Q1') + assert i1 == i2 + lower = Period('05q1') + assert i1 == lower + + i1 = Period('1Q2005') + assert i1 == i2 + lower = Period('1q2005') + assert i1 == lower + + i1 = Period('1Q05') + assert i1 == i2 + lower = Period('1q05') + assert i1 == lower + + i1 = Period('4Q1984') + assert i1.year == 1984 + lower = Period('4q1984') + assert i1 == lower + + def test_construction_month(self): + + expected = Period('2007-01', freq='M') + i1 = Period('200701', freq='M') + assert i1 == expected + + i1 = Period('200701', freq='M') + assert i1 == expected + + i1 = Period(200701, freq='M') + assert i1 == expected + + i1 = Period(ordinal=200701, freq='M') + assert i1.year == 18695 + + i1 = Period(datetime(2007, 1, 1), freq='M') + i2 = Period('200701', freq='M') + assert i1 == i2 + + i1 = Period(date(2007, 1, 1), freq='M') + i2 = Period(datetime(2007, 1, 1), freq='M') + i3 = Period(np.datetime64('2007-01-01'), freq='M') + i4 = Period(np_datetime64_compat('2007-01-01 00:00:00Z'), freq='M') + i5 = Period(np_datetime64_compat('2007-01-01 00:00:00.000Z'), freq='M') + assert i1 == i2 + assert i1 == i3 + assert i1 == i4 + assert i1 == i5 + + def test_period_constructor_offsets(self): + assert (Period('1/1/2005', freq=offsets.MonthEnd()) == + Period('1/1/2005', freq='M')) + assert (Period('2005', freq=offsets.YearEnd()) == + Period('2005', freq='A')) + assert (Period('2005', freq=offsets.MonthEnd()) == + Period('2005', freq='M')) + assert (Period('3/10/12', freq=offsets.BusinessDay()) == + Period('3/10/12', freq='B')) + assert (Period('3/10/12', freq=offsets.Day()) == + Period('3/10/12', freq='D')) + + assert (Period(year=2005, quarter=1, + freq=offsets.QuarterEnd(startingMonth=12)) == + Period(year=2005, quarter=1, freq='Q')) + assert (Period(year=2005, quarter=2, + freq=offsets.QuarterEnd(startingMonth=12)) == + Period(year=2005, quarter=2, freq='Q')) + + assert (Period(year=2005, month=3, day=1, freq=offsets.Day()) == + Period(year=2005, month=3, day=1, freq='D')) + assert (Period(year=2012, month=3, day=10, freq=offsets.BDay()) == + Period(year=2012, month=3, day=10, freq='B')) + + expected = Period('2005-03-01', freq='3D') + assert (Period(year=2005, month=3, day=1, + freq=offsets.Day(3)) == expected) + assert Period(year=2005, month=3, day=1, freq='3D') == expected + + assert (Period(year=2012, month=3, day=10, + freq=offsets.BDay(3)) == + Period(year=2012, month=3, day=10, freq='3B')) + + assert (Period(200701, freq=offsets.MonthEnd()) == + Period(200701, freq='M')) + + i1 = Period(ordinal=200701, freq=offsets.MonthEnd()) + i2 = Period(ordinal=200701, freq='M') + assert i1 == i2 + assert i1.year == 18695 + assert i2.year == 18695 + + i1 = Period(datetime(2007, 1, 1), freq='M') + i2 = Period('200701', freq='M') + assert i1 == i2 + + i1 = Period(date(2007, 1, 1), freq='M') + i2 = Period(datetime(2007, 1, 1), freq='M') + i3 = Period(np.datetime64('2007-01-01'), freq='M') + i4 = Period(np_datetime64_compat('2007-01-01 00:00:00Z'), freq='M') + i5 = Period(np_datetime64_compat('2007-01-01 00:00:00.000Z'), freq='M') + assert i1 == i2 + assert i1 == i3 + assert i1 == i4 + assert i1 == i5 + + i1 = Period('2007-01-01 09:00:00.001') + expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1000), freq='L') + assert i1 == expected + + expected = Period(np_datetime64_compat( + '2007-01-01 09:00:00.001Z'), freq='L') + assert i1 == expected + + i1 = Period('2007-01-01 09:00:00.00101') + expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1010), freq='U') + assert i1 == expected + + expected = Period(np_datetime64_compat('2007-01-01 09:00:00.00101Z'), + freq='U') + assert i1 == expected + + def test_invalid_arguments(self): + with pytest.raises(ValueError): + Period(datetime.now()) + with pytest.raises(ValueError): + Period(datetime.now().date()) + + with pytest.raises(ValueError): + Period(1.6, freq='D') + with pytest.raises(ValueError): + Period(ordinal=1.6, freq='D') + with pytest.raises(ValueError): + Period(ordinal=2, value=1, freq='D') + + with pytest.raises(ValueError): + Period(month=1) + + with pytest.raises(ValueError): + Period('-2000', 'A') + with pytest.raises(DateParseError): + Period('0', 'A') + with pytest.raises(DateParseError): + Period('1/1/-2000', 'A') + + def test_constructor_corner(self): + expected = Period('2007-01', freq='2M') + assert Period(year=2007, month=1, freq='2M') == expected + + assert Period(None) is NaT + + p = Period('2007-01-01', freq='D') + + result = Period(p, freq='A') + exp = Period('2007', freq='A') + assert result == exp + + def test_constructor_infer_freq(self): + p = Period('2007-01-01') + assert p.freq == 'D' + + p = Period('2007-01-01 07') + assert p.freq == 'H' + + p = Period('2007-01-01 07:10') + assert p.freq == 'T' + + p = Period('2007-01-01 07:10:15') + assert p.freq == 'S' + + p = Period('2007-01-01 07:10:15.123') + assert p.freq == 'L' + + p = Period('2007-01-01 07:10:15.123000') + assert p.freq == 'L' + + p = Period('2007-01-01 07:10:15.123400') + assert p.freq == 'U' + + def test_multiples(self): + result1 = Period('1989', freq='2A') + result2 = Period('1989', freq='A') + assert result1.ordinal == result2.ordinal + assert result1.freqstr == '2A-DEC' + assert result2.freqstr == 'A-DEC' + assert result1.freq == offsets.YearEnd(2) + assert result2.freq == offsets.YearEnd() + + assert (result1 + 1).ordinal == result1.ordinal + 2 + assert (1 + result1).ordinal == result1.ordinal + 2 + assert (result1 - 1).ordinal == result2.ordinal - 2 + assert (-1 + result1).ordinal == result2.ordinal - 2 + + @pytest.mark.parametrize('month', MONTHS) + def test_period_cons_quarterly(self, month): + # bugs in scikits.timeseries + freq = 'Q-%s' % month + exp = Period('1989Q3', freq=freq) + assert '1989Q3' in str(exp) + stamp = exp.to_timestamp('D', how='end') + p = Period(stamp, freq=freq) + assert p == exp + + stamp = exp.to_timestamp('3D', how='end') + p = Period(stamp, freq=freq) + assert p == exp + + @pytest.mark.parametrize('month', MONTHS) + def test_period_cons_annual(self, month): + # bugs in scikits.timeseries + freq = 'A-%s' % month + exp = Period('1989', freq=freq) + stamp = exp.to_timestamp('D', how='end') + timedelta(days=30) + p = Period(stamp, freq=freq) + + assert p == exp + 1 + assert isinstance(p, Period) + + @pytest.mark.parametrize('day', DAYS) + @pytest.mark.parametrize('num', range(10, 17)) + def test_period_cons_weekly(self, num, day): + daystr = '2011-02-%d' % num + freq = 'W-%s' % day + + result = Period(daystr, freq=freq) + expected = Period(daystr, freq='D').asfreq(freq) + assert result == expected + assert isinstance(result, Period) + + def test_period_from_ordinal(self): + p = Period('2011-01', freq='M') + res = Period._from_ordinal(p.ordinal, freq='M') + assert p == res + assert isinstance(res, Period) + + def test_period_cons_nat(self): + p = Period('NaT', freq='M') + assert p is NaT + + p = Period('nat', freq='W-SUN') + assert p is NaT + + p = Period(iNaT, freq='D') + assert p is NaT + + p = Period(iNaT, freq='3D') + assert p is NaT + + p = Period(iNaT, freq='1D1H') + assert p is NaT + + p = Period('NaT') + assert p is NaT + + p = Period(iNaT) + assert p is NaT + + def test_period_cons_mult(self): + p1 = Period('2011-01', freq='3M') + p2 = Period('2011-01', freq='M') + assert p1.ordinal == p2.ordinal + + assert p1.freq == offsets.MonthEnd(3) + assert p1.freqstr == '3M' + + assert p2.freq == offsets.MonthEnd() + assert p2.freqstr == 'M' + + result = p1 + 1 + assert result.ordinal == (p2 + 3).ordinal + + assert result.freq == p1.freq + assert result.freqstr == '3M' + + result = p1 - 1 + assert result.ordinal == (p2 - 3).ordinal + assert result.freq == p1.freq + assert result.freqstr == '3M' + + msg = ('Frequency must be positive, because it' + ' represents span: -3M') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='-3M') + + msg = ('Frequency must be positive, because it' ' represents span: 0M') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='0M') + + def test_period_cons_combined(self): + p = [(Period('2011-01', freq='1D1H'), + Period('2011-01', freq='1H1D'), + Period('2011-01', freq='H')), + (Period(ordinal=1, freq='1D1H'), + Period(ordinal=1, freq='1H1D'), + Period(ordinal=1, freq='H'))] + + for p1, p2, p3 in p: + assert p1.ordinal == p3.ordinal + assert p2.ordinal == p3.ordinal + + assert p1.freq == offsets.Hour(25) + assert p1.freqstr == '25H' + + assert p2.freq == offsets.Hour(25) + assert p2.freqstr == '25H' + + assert p3.freq == offsets.Hour() + assert p3.freqstr == 'H' + + result = p1 + 1 + assert result.ordinal == (p3 + 25).ordinal + assert result.freq == p1.freq + assert result.freqstr == '25H' + + result = p2 + 1 + assert result.ordinal == (p3 + 25).ordinal + assert result.freq == p2.freq + assert result.freqstr == '25H' + + result = p1 - 1 + assert result.ordinal == (p3 - 25).ordinal + assert result.freq == p1.freq + assert result.freqstr == '25H' + + result = p2 - 1 + assert result.ordinal == (p3 - 25).ordinal + assert result.freq == p2.freq + assert result.freqstr == '25H' + + msg = ('Frequency must be positive, because it' + ' represents span: -25H') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='-1D1H') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='-1H1D') + with pytest.raises(ValueError, match=msg): + Period(ordinal=1, freq='-1D1H') + with pytest.raises(ValueError, match=msg): + Period(ordinal=1, freq='-1H1D') + + msg = ('Frequency must be positive, because it' + ' represents span: 0D') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='0D0H') + with pytest.raises(ValueError, match=msg): + Period(ordinal=1, freq='0D0H') + + # You can only combine together day and intraday offsets + msg = ('Invalid frequency: 1W1D') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='1W1D') + msg = ('Invalid frequency: 1D1W') + with pytest.raises(ValueError, match=msg): + Period('2011-01', freq='1D1W') + + +class TestPeriodMethods(object): + def test_round_trip(self): + p = Period('2000Q1') + new_p = tm.round_trip_pickle(p) + assert new_p == p + + def test_hash(self): + assert (hash(Period('2011-01', freq='M')) == + hash(Period('2011-01', freq='M'))) + + assert (hash(Period('2011-01-01', freq='D')) != + hash(Period('2011-01', freq='M'))) + + assert (hash(Period('2011-01', freq='3M')) != + hash(Period('2011-01', freq='2M'))) + + assert (hash(Period('2011-01', freq='M')) != + hash(Period('2011-02', freq='M'))) + + # -------------------------------------------------------------- + # to_timestamp + + @pytest.mark.parametrize('tzstr', ['Europe/Brussels', + 'Asia/Tokyo', 'US/Pacific']) + def test_to_timestamp_tz_arg(self, tzstr): + p = Period('1/1/2005', freq='M').to_timestamp(tz=tzstr) + exp = Timestamp('1/1/2005', tz='UTC').tz_convert(tzstr) + exp_zone = pytz.timezone(tzstr).normalize(p) + + assert p == exp + assert p.tz == exp_zone.tzinfo + assert p.tz == exp.tz + + p = Period('1/1/2005', freq='3H').to_timestamp(tz=tzstr) + exp = Timestamp('1/1/2005', tz='UTC').tz_convert(tzstr) + exp_zone = pytz.timezone(tzstr).normalize(p) + + assert p == exp + assert p.tz == exp_zone.tzinfo + assert p.tz == exp.tz + + p = Period('1/1/2005', freq='A').to_timestamp(freq='A', tz=tzstr) + exp = Timestamp('31/12/2005', tz='UTC').tz_convert(tzstr) + exp_zone = pytz.timezone(tzstr).normalize(p) + + assert p == exp + assert p.tz == exp_zone.tzinfo + assert p.tz == exp.tz + + p = Period('1/1/2005', freq='A').to_timestamp(freq='3H', tz=tzstr) + exp = Timestamp('1/1/2005', tz='UTC').tz_convert(tzstr) + exp_zone = pytz.timezone(tzstr).normalize(p) + + assert p == exp + assert p.tz == exp_zone.tzinfo + assert p.tz == exp.tz + + @pytest.mark.parametrize('tzstr', ['dateutil/Europe/Brussels', + 'dateutil/Asia/Tokyo', + 'dateutil/US/Pacific']) + def test_to_timestamp_tz_arg_dateutil(self, tzstr): + tz = maybe_get_tz(tzstr) + p = Period('1/1/2005', freq='M').to_timestamp(tz=tz) + exp = Timestamp('1/1/2005', tz='UTC').tz_convert(tzstr) + assert p == exp + assert p.tz == dateutil_gettz(tzstr.split('/', 1)[1]) + assert p.tz == exp.tz + + p = Period('1/1/2005', freq='M').to_timestamp(freq='3H', tz=tz) + exp = Timestamp('1/1/2005', tz='UTC').tz_convert(tzstr) + assert p == exp + assert p.tz == dateutil_gettz(tzstr.split('/', 1)[1]) + assert p.tz == exp.tz + + def test_to_timestamp_tz_arg_dateutil_from_string(self): + p = Period('1/1/2005', + freq='M').to_timestamp(tz='dateutil/Europe/Brussels') + assert p.tz == dateutil_gettz('Europe/Brussels') + + def test_to_timestamp_mult(self): + p = Period('2011-01', freq='M') + assert p.to_timestamp(how='S') == Timestamp('2011-01-01') + expected = Timestamp('2011-02-01') - Timedelta(1, 'ns') + assert p.to_timestamp(how='E') == expected + + p = Period('2011-01', freq='3M') + assert p.to_timestamp(how='S') == Timestamp('2011-01-01') + expected = Timestamp('2011-04-01') - Timedelta(1, 'ns') + assert p.to_timestamp(how='E') == expected + + def test_to_timestamp(self): + p = Period('1982', freq='A') + start_ts = p.to_timestamp(how='S') + aliases = ['s', 'StarT', 'BEGIn'] + for a in aliases: + assert start_ts == p.to_timestamp('D', how=a) + # freq with mult should not affect to the result + assert start_ts == p.to_timestamp('3D', how=a) + + end_ts = p.to_timestamp(how='E') + aliases = ['e', 'end', 'FINIsH'] + for a in aliases: + assert end_ts == p.to_timestamp('D', how=a) + assert end_ts == p.to_timestamp('3D', how=a) + + from_lst = ['A', 'Q', 'M', 'W', 'B', 'D', 'H', 'Min', 'S'] + + def _ex(p): + return Timestamp((p + p.freq).start_time.value - 1) + + for i, fcode in enumerate(from_lst): + p = Period('1982', freq=fcode) + result = p.to_timestamp().to_period(fcode) + assert result == p + + assert p.start_time == p.to_timestamp(how='S') + + assert p.end_time == _ex(p) + + # Frequency other than daily + + p = Period('1985', freq='A') + + result = p.to_timestamp('H', how='end') + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') + assert result == expected + result = p.to_timestamp('3H', how='end') + assert result == expected + + result = p.to_timestamp('T', how='end') + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') + assert result == expected + result = p.to_timestamp('2T', how='end') + assert result == expected + + result = p.to_timestamp(how='end') + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') + assert result == expected + + expected = datetime(1985, 1, 1) + result = p.to_timestamp('H', how='start') + assert result == expected + result = p.to_timestamp('T', how='start') + assert result == expected + result = p.to_timestamp('S', how='start') + assert result == expected + result = p.to_timestamp('3H', how='start') + assert result == expected + result = p.to_timestamp('5S', how='start') + assert result == expected + + # -------------------------------------------------------------- + # Rendering: __repr__, strftime, etc + + def test_repr(self): + p = Period('Jan-2000') + assert '2000-01' in repr(p) + + p = Period('2000-12-15') + assert '2000-12-15' in repr(p) + + def test_repr_nat(self): + p = Period('nat', freq='M') + assert repr(NaT) in repr(p) + + def test_millisecond_repr(self): + p = Period('2000-01-01 12:15:02.123') + + assert repr(p) == "Period('2000-01-01 12:15:02.123', 'L')" + + def test_microsecond_repr(self): + p = Period('2000-01-01 12:15:02.123567') + + assert repr(p) == "Period('2000-01-01 12:15:02.123567', 'U')" + + def test_strftime(self): + # GH#3363 + p = Period('2000-1-1 12:34:12', freq='S') + res = p.strftime('%Y-%m-%d %H:%M:%S') + assert res == '2000-01-01 12:34:12' + assert isinstance(res, text_type) + + +class TestPeriodProperties(object): + "Test properties such as year, month, weekday, etc...." + + @pytest.mark.parametrize('freq', ['A', 'M', 'D', 'H']) + def test_is_leap_year(self, freq): + # GH 13727 + p = Period('2000-01-01 00:00:00', freq=freq) + assert p.is_leap_year + assert isinstance(p.is_leap_year, bool) + + p = Period('1999-01-01 00:00:00', freq=freq) + assert not p.is_leap_year + + p = Period('2004-01-01 00:00:00', freq=freq) + assert p.is_leap_year + + p = Period('2100-01-01 00:00:00', freq=freq) + assert not p.is_leap_year + + def test_quarterly_negative_ordinals(self): + p = Period(ordinal=-1, freq='Q-DEC') + assert p.year == 1969 + assert p.quarter == 4 + assert isinstance(p, Period) + + p = Period(ordinal=-2, freq='Q-DEC') + assert p.year == 1969 + assert p.quarter == 3 + assert isinstance(p, Period) + + p = Period(ordinal=-2, freq='M') + assert p.year == 1969 + assert p.month == 11 + assert isinstance(p, Period) + + def test_freq_str(self): + i1 = Period('1982', freq='Min') + assert i1.freq == offsets.Minute() + assert i1.freqstr == 'T' + + def test_period_deprecated_freq(self): + cases = {"M": ["MTH", "MONTH", "MONTHLY", "Mth", "month", "monthly"], + "B": ["BUS", "BUSINESS", "BUSINESSLY", "WEEKDAY", "bus"], + "D": ["DAY", "DLY", "DAILY", "Day", "Dly", "Daily"], + "H": ["HR", "HOUR", "HRLY", "HOURLY", "hr", "Hour", "HRly"], + "T": ["minute", "MINUTE", "MINUTELY", "minutely"], + "S": ["sec", "SEC", "SECOND", "SECONDLY", "second"], + "L": ["MILLISECOND", "MILLISECONDLY", "millisecond"], + "U": ["MICROSECOND", "MICROSECONDLY", "microsecond"], + "N": ["NANOSECOND", "NANOSECONDLY", "nanosecond"]} + + msg = INVALID_FREQ_ERR_MSG + for exp, freqs in iteritems(cases): + for freq in freqs: + with pytest.raises(ValueError, match=msg): + Period('2016-03-01 09:00', freq=freq) + with pytest.raises(ValueError, match=msg): + Period(ordinal=1, freq=freq) + + # check supported freq-aliases still works + p1 = Period('2016-03-01 09:00', freq=exp) + p2 = Period(ordinal=1, freq=exp) + assert isinstance(p1, Period) + assert isinstance(p2, Period) + + def test_start_time(self): + freq_lst = ['A', 'Q', 'M', 'D', 'H', 'T', 'S'] + xp = datetime(2012, 1, 1) + for f in freq_lst: + p = Period('2012', freq=f) + assert p.start_time == xp + assert Period('2012', freq='B').start_time == datetime(2012, 1, 2) + assert Period('2012', freq='W').start_time == datetime(2011, 12, 26) + + def test_end_time(self): + p = Period('2012', freq='A') + + def _ex(*args): + return Timestamp(Timestamp(datetime(*args)).value - 1) + + xp = _ex(2013, 1, 1) + assert xp == p.end_time + + p = Period('2012', freq='Q') + xp = _ex(2012, 4, 1) + assert xp == p.end_time + + p = Period('2012', freq='M') + xp = _ex(2012, 2, 1) + assert xp == p.end_time + + p = Period('2012', freq='D') + xp = _ex(2012, 1, 2) + assert xp == p.end_time + + p = Period('2012', freq='H') + xp = _ex(2012, 1, 1, 1) + assert xp == p.end_time + + p = Period('2012', freq='B') + xp = _ex(2012, 1, 3) + assert xp == p.end_time + + p = Period('2012', freq='W') + xp = _ex(2012, 1, 2) + assert xp == p.end_time + + # Test for GH 11738 + p = Period('2012', freq='15D') + xp = _ex(2012, 1, 16) + assert xp == p.end_time + + p = Period('2012', freq='1D1H') + xp = _ex(2012, 1, 2, 1) + assert xp == p.end_time + + p = Period('2012', freq='1H1D') + xp = _ex(2012, 1, 2, 1) + assert xp == p.end_time + + def test_anchor_week_end_time(self): + def _ex(*args): + return Timestamp(Timestamp(datetime(*args)).value - 1) + + p = Period('2013-1-1', 'W-SAT') + xp = _ex(2013, 1, 6) + assert p.end_time == xp + + def test_properties_annually(self): + # Test properties on Periods with annually frequency. + a_date = Period(freq='A', year=2007) + assert a_date.year == 2007 + + def test_properties_quarterly(self): + # Test properties on Periods with daily frequency. + qedec_date = Period(freq="Q-DEC", year=2007, quarter=1) + qejan_date = Period(freq="Q-JAN", year=2007, quarter=1) + qejun_date = Period(freq="Q-JUN", year=2007, quarter=1) + # + for x in range(3): + for qd in (qedec_date, qejan_date, qejun_date): + assert (qd + x).qyear == 2007 + assert (qd + x).quarter == x + 1 + + def test_properties_monthly(self): + # Test properties on Periods with daily frequency. + m_date = Period(freq='M', year=2007, month=1) + for x in range(11): + m_ival_x = m_date + x + assert m_ival_x.year == 2007 + if 1 <= x + 1 <= 3: + assert m_ival_x.quarter == 1 + elif 4 <= x + 1 <= 6: + assert m_ival_x.quarter == 2 + elif 7 <= x + 1 <= 9: + assert m_ival_x.quarter == 3 + elif 10 <= x + 1 <= 12: + assert m_ival_x.quarter == 4 + assert m_ival_x.month == x + 1 + + def test_properties_weekly(self): + # Test properties on Periods with daily frequency. + w_date = Period(freq='W', year=2007, month=1, day=7) + # + assert w_date.year == 2007 + assert w_date.quarter == 1 + assert w_date.month == 1 + assert w_date.week == 1 + assert (w_date - 1).week == 52 + assert w_date.days_in_month == 31 + assert Period(freq='W', year=2012, + month=2, day=1).days_in_month == 29 + + def test_properties_weekly_legacy(self): + # Test properties on Periods with daily frequency. + w_date = Period(freq='W', year=2007, month=1, day=7) + assert w_date.year == 2007 + assert w_date.quarter == 1 + assert w_date.month == 1 + assert w_date.week == 1 + assert (w_date - 1).week == 52 + assert w_date.days_in_month == 31 + + exp = Period(freq='W', year=2012, month=2, day=1) + assert exp.days_in_month == 29 + + msg = INVALID_FREQ_ERR_MSG + with pytest.raises(ValueError, match=msg): + Period(freq='WK', year=2007, month=1, day=7) + + def test_properties_daily(self): + # Test properties on Periods with daily frequency. + b_date = Period(freq='B', year=2007, month=1, day=1) + # + assert b_date.year == 2007 + assert b_date.quarter == 1 + assert b_date.month == 1 + assert b_date.day == 1 + assert b_date.weekday == 0 + assert b_date.dayofyear == 1 + assert b_date.days_in_month == 31 + assert Period(freq='B', year=2012, + month=2, day=1).days_in_month == 29 + + d_date = Period(freq='D', year=2007, month=1, day=1) + + assert d_date.year == 2007 + assert d_date.quarter == 1 + assert d_date.month == 1 + assert d_date.day == 1 + assert d_date.weekday == 0 + assert d_date.dayofyear == 1 + assert d_date.days_in_month == 31 + assert Period(freq='D', year=2012, month=2, + day=1).days_in_month == 29 + + def test_properties_hourly(self): + # Test properties on Periods with hourly frequency. + h_date1 = Period(freq='H', year=2007, month=1, day=1, hour=0) + h_date2 = Period(freq='2H', year=2007, month=1, day=1, hour=0) + + for h_date in [h_date1, h_date2]: + assert h_date.year == 2007 + assert h_date.quarter == 1 + assert h_date.month == 1 + assert h_date.day == 1 + assert h_date.weekday == 0 + assert h_date.dayofyear == 1 + assert h_date.hour == 0 + assert h_date.days_in_month == 31 + assert Period(freq='H', year=2012, month=2, day=1, + hour=0).days_in_month == 29 + + def test_properties_minutely(self): + # Test properties on Periods with minutely frequency. + t_date = Period(freq='Min', year=2007, month=1, day=1, hour=0, + minute=0) + # + assert t_date.quarter == 1 + assert t_date.month == 1 + assert t_date.day == 1 + assert t_date.weekday == 0 + assert t_date.dayofyear == 1 + assert t_date.hour == 0 + assert t_date.minute == 0 + assert t_date.days_in_month == 31 + assert Period(freq='D', year=2012, month=2, day=1, hour=0, + minute=0).days_in_month == 29 + + def test_properties_secondly(self): + # Test properties on Periods with secondly frequency. + s_date = Period(freq='Min', year=2007, month=1, day=1, hour=0, + minute=0, second=0) + # + assert s_date.year == 2007 + assert s_date.quarter == 1 + assert s_date.month == 1 + assert s_date.day == 1 + assert s_date.weekday == 0 + assert s_date.dayofyear == 1 + assert s_date.hour == 0 + assert s_date.minute == 0 + assert s_date.second == 0 + assert s_date.days_in_month == 31 + assert Period(freq='Min', year=2012, month=2, day=1, hour=0, + minute=0, second=0).days_in_month == 29 + + +class TestPeriodField(object): + + def test_get_period_field_array_raises_on_out_of_range(self): + msg = "Buffer dtype mismatch, expected 'int64_t' but got 'double'" + with pytest.raises(ValueError, match=msg): + libperiod.get_period_field_arr(-1, np.empty(1), 0) + + +class TestComparisons(object): + + def setup_method(self, method): + self.january1 = Period('2000-01', 'M') + self.january2 = Period('2000-01', 'M') + self.february = Period('2000-02', 'M') + self.march = Period('2000-03', 'M') + self.day = Period('2012-01-01', 'D') + + def test_equal(self): + assert self.january1 == self.january2 + + def test_equal_Raises_Value(self): + with pytest.raises(period.IncompatibleFrequency): + self.january1 == self.day + + def test_notEqual(self): + assert self.january1 != 1 + assert self.january1 != self.february + + def test_greater(self): + assert self.february > self.january1 + + def test_greater_Raises_Value(self): + with pytest.raises(period.IncompatibleFrequency): + self.january1 > self.day + + def test_greater_Raises_Type(self): + with pytest.raises(TypeError): + self.january1 > 1 + + def test_greaterEqual(self): + assert self.january1 >= self.january2 + + def test_greaterEqual_Raises_Value(self): + with pytest.raises(period.IncompatibleFrequency): + self.january1 >= self.day + + with pytest.raises(TypeError): + print(self.january1 >= 1) + + def test_smallerEqual(self): + assert self.january1 <= self.january2 + + def test_smallerEqual_Raises_Value(self): + with pytest.raises(period.IncompatibleFrequency): + self.january1 <= self.day + + def test_smallerEqual_Raises_Type(self): + with pytest.raises(TypeError): + self.january1 <= 1 + + def test_smaller(self): + assert self.january1 < self.february + + def test_smaller_Raises_Value(self): + with pytest.raises(period.IncompatibleFrequency): + self.january1 < self.day + + def test_smaller_Raises_Type(self): + with pytest.raises(TypeError): + self.january1 < 1 + + def test_sort(self): + periods = [self.march, self.january1, self.february] + correctPeriods = [self.january1, self.february, self.march] + assert sorted(periods) == correctPeriods + + def test_period_nat_comp(self): + p_nat = Period('NaT', freq='D') + p = Period('2011-01-01', freq='D') + + nat = Timestamp('NaT') + t = Timestamp('2011-01-01') + # confirm Period('NaT') work identical with Timestamp('NaT') + for left, right in [(p_nat, p), (p, p_nat), (p_nat, p_nat), (nat, t), + (t, nat), (nat, nat)]: + assert not left < right + assert not left > right + assert not left == right + assert left != right + assert not left <= right + assert not left >= right + + +class TestArithmetic(object): + + def test_sub_delta(self): + left, right = Period('2011', freq='A'), Period('2007', freq='A') + result = left - right + assert result == 4 * right.freq + + with pytest.raises(period.IncompatibleFrequency): + left - Period('2007-01', freq='M') + + def test_add_integer(self): + per1 = Period(freq='D', year=2008, month=1, day=1) + per2 = Period(freq='D', year=2008, month=1, day=2) + assert per1 + 1 == per2 + assert 1 + per1 == per2 + + def test_add_sub_nat(self): + # GH#13071 + p = Period('2011-01', freq='M') + assert p + NaT is NaT + assert NaT + p is NaT + assert p - NaT is NaT + assert NaT - p is NaT + + p = Period('NaT', freq='M') + assert p + NaT is NaT + assert NaT + p is NaT + assert p - NaT is NaT + assert NaT - p is NaT + + def test_add_invalid(self): + # GH#4731 + per1 = Period(freq='D', year=2008, month=1, day=1) + per2 = Period(freq='D', year=2008, month=1, day=2) + + msg = r"unsupported operand type\(s\)" + with pytest.raises(TypeError, match=msg): + per1 + "str" + with pytest.raises(TypeError, match=msg): + "str" + per1 + with pytest.raises(TypeError, match=msg): + per1 + per2 + + boxes = [lambda x: x, lambda x: pd.Series([x]), lambda x: pd.Index([x])] + ids = ['identity', 'Series', 'Index'] + + @pytest.mark.parametrize('lbox', boxes, ids=ids) + @pytest.mark.parametrize('rbox', boxes, ids=ids) + def test_add_timestamp_raises(self, rbox, lbox): + # GH#17983 + ts = Timestamp('2017') + per = Period('2017', freq='M') + + # We may get a different message depending on which class raises + # the error. + msg = (r"cannot add|unsupported operand|" + r"can only operate on a|incompatible type|" + r"ufunc add cannot use operands") + with pytest.raises(TypeError, match=msg): + lbox(ts) + rbox(per) + + with pytest.raises(TypeError, match=msg): + lbox(per) + rbox(ts) + + with pytest.raises(TypeError, match=msg): + lbox(per) + rbox(per) + + def test_sub(self): + per1 = Period('2011-01-01', freq='D') + per2 = Period('2011-01-15', freq='D') + + off = per1.freq + assert per1 - per2 == -14 * off + assert per2 - per1 == 14 * off + + msg = r"Input has different freq=M from Period\(freq=D\)" + with pytest.raises(period.IncompatibleFrequency, match=msg): + per1 - Period('2011-02', freq='M') + + @pytest.mark.parametrize('n', [1, 2, 3, 4]) + def test_sub_n_gt_1_ticks(self, tick_classes, n): + # GH 23878 + p1 = pd.Period('19910905', freq=tick_classes(n)) + p2 = pd.Period('19920406', freq=tick_classes(n)) + + expected = (pd.Period(str(p2), freq=p2.freq.base) + - pd.Period(str(p1), freq=p1.freq.base)) + + assert (p2 - p1) == expected + + @pytest.mark.parametrize('normalize', [True, False]) + @pytest.mark.parametrize('n', [1, 2, 3, 4]) + @pytest.mark.parametrize('offset, kwd_name', [ + (pd.offsets.YearEnd, 'month'), + (pd.offsets.QuarterEnd, 'startingMonth'), + (pd.offsets.MonthEnd, None), + (pd.offsets.Week, 'weekday') + ]) + def test_sub_n_gt_1_offsets(self, offset, kwd_name, n, normalize): + # GH 23878 + kwds = {kwd_name: 3} if kwd_name is not None else {} + p1_d = '19910905' + p2_d = '19920406' + p1 = pd.Period(p1_d, freq=offset(n, normalize, **kwds)) + p2 = pd.Period(p2_d, freq=offset(n, normalize, **kwds)) + + expected = (pd.Period(p2_d, freq=p2.freq.base) + - pd.Period(p1_d, freq=p1.freq.base)) + + assert (p2 - p1) == expected + + def test_add_offset(self): + # freq is DateOffset + for freq in ['A', '2A', '3A']: + p = Period('2011', freq=freq) + exp = Period('2013', freq=freq) + assert p + offsets.YearEnd(2) == exp + assert offsets.YearEnd(2) + p == exp + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + with pytest.raises(period.IncompatibleFrequency): + p + o + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + with pytest.raises(period.IncompatibleFrequency): + o + p + + for freq in ['M', '2M', '3M']: + p = Period('2011-03', freq=freq) + exp = Period('2011-05', freq=freq) + assert p + offsets.MonthEnd(2) == exp + assert offsets.MonthEnd(2) + p == exp + + exp = Period('2012-03', freq=freq) + assert p + offsets.MonthEnd(12) == exp + assert offsets.MonthEnd(12) + p == exp + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + with pytest.raises(period.IncompatibleFrequency): + p + o + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + with pytest.raises(period.IncompatibleFrequency): + o + p + + # freq is Tick + for freq in ['D', '2D', '3D']: + p = Period('2011-04-01', freq=freq) + + exp = Period('2011-04-06', freq=freq) + assert p + offsets.Day(5) == exp + assert offsets.Day(5) + p == exp + + exp = Period('2011-04-02', freq=freq) + assert p + offsets.Hour(24) == exp + assert offsets.Hour(24) + p == exp + + exp = Period('2011-04-03', freq=freq) + assert p + np.timedelta64(2, 'D') == exp + with pytest.raises(TypeError): + np.timedelta64(2, 'D') + p + + exp = Period('2011-04-02', freq=freq) + assert p + np.timedelta64(3600 * 24, 's') == exp + with pytest.raises(TypeError): + np.timedelta64(3600 * 24, 's') + p + + exp = Period('2011-03-30', freq=freq) + assert p + timedelta(-2) == exp + assert timedelta(-2) + p == exp + + exp = Period('2011-04-03', freq=freq) + assert p + timedelta(hours=48) == exp + assert timedelta(hours=48) + p == exp + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(4, 'h'), + timedelta(hours=23)]: + with pytest.raises(period.IncompatibleFrequency): + p + o + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + with pytest.raises(period.IncompatibleFrequency): + o + p + + for freq in ['H', '2H', '3H']: + p = Period('2011-04-01 09:00', freq=freq) + + exp = Period('2011-04-03 09:00', freq=freq) + assert p + offsets.Day(2) == exp + assert offsets.Day(2) + p == exp + + exp = Period('2011-04-01 12:00', freq=freq) + assert p + offsets.Hour(3) == exp + assert offsets.Hour(3) + p == exp + + exp = Period('2011-04-01 12:00', freq=freq) + assert p + np.timedelta64(3, 'h') == exp + with pytest.raises(TypeError): + np.timedelta64(3, 'h') + p + + exp = Period('2011-04-01 10:00', freq=freq) + assert p + np.timedelta64(3600, 's') == exp + with pytest.raises(TypeError): + np.timedelta64(3600, 's') + p + + exp = Period('2011-04-01 11:00', freq=freq) + assert p + timedelta(minutes=120) == exp + assert timedelta(minutes=120) + p == exp + + exp = Period('2011-04-05 12:00', freq=freq) + assert p + timedelta(days=4, minutes=180) == exp + assert timedelta(days=4, minutes=180) + p == exp + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(3200, 's'), + timedelta(hours=23, minutes=30)]: + with pytest.raises(period.IncompatibleFrequency): + p + o + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + with pytest.raises(period.IncompatibleFrequency): + o + p + + def test_add_offset_nat(self): + # freq is DateOffset + for freq in ['A', '2A', '3A']: + p = Period('NaT', freq=freq) + for o in [offsets.YearEnd(2)]: + assert p + o is NaT + assert o + p is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + for freq in ['M', '2M', '3M']: + p = Period('NaT', freq=freq) + for o in [offsets.MonthEnd(2), offsets.MonthEnd(12)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + # freq is Tick + for freq in ['D', '2D', '3D']: + p = Period('NaT', freq=freq) + for o in [offsets.Day(5), offsets.Hour(24), np.timedelta64(2, 'D'), + np.timedelta64(3600 * 24, 's'), timedelta(-2), + timedelta(hours=48)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(4, 'h'), + timedelta(hours=23)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + for freq in ['H', '2H', '3H']: + p = Period('NaT', freq=freq) + for o in [offsets.Day(2), offsets.Hour(3), np.timedelta64(3, 'h'), + np.timedelta64(3600, 's'), timedelta(minutes=120), + timedelta(days=4, minutes=180)]: + assert p + o is NaT + + if not isinstance(o, np.timedelta64): + assert o + p is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(3200, 's'), + timedelta(hours=23, minutes=30)]: + assert p + o is NaT + + if isinstance(o, np.timedelta64): + with pytest.raises(TypeError): + o + p + else: + assert o + p is NaT + + def test_sub_offset(self): + # freq is DateOffset + for freq in ['A', '2A', '3A']: + p = Period('2011', freq=freq) + assert p - offsets.YearEnd(2) == Period('2009', freq=freq) + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + with pytest.raises(period.IncompatibleFrequency): + p - o + + for freq in ['M', '2M', '3M']: + p = Period('2011-03', freq=freq) + assert p - offsets.MonthEnd(2) == Period('2011-01', freq=freq) + assert p - offsets.MonthEnd(12) == Period('2010-03', freq=freq) + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + with pytest.raises(period.IncompatibleFrequency): + p - o + + # freq is Tick + for freq in ['D', '2D', '3D']: + p = Period('2011-04-01', freq=freq) + assert p - offsets.Day(5) == Period('2011-03-27', freq=freq) + assert p - offsets.Hour(24) == Period('2011-03-31', freq=freq) + assert p - np.timedelta64(2, 'D') == Period( + '2011-03-30', freq=freq) + assert p - np.timedelta64(3600 * 24, 's') == Period( + '2011-03-31', freq=freq) + assert p - timedelta(-2) == Period('2011-04-03', freq=freq) + assert p - timedelta(hours=48) == Period('2011-03-30', freq=freq) + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(4, 'h'), + timedelta(hours=23)]: + with pytest.raises(period.IncompatibleFrequency): + p - o + + for freq in ['H', '2H', '3H']: + p = Period('2011-04-01 09:00', freq=freq) + assert p - offsets.Day(2) == Period('2011-03-30 09:00', freq=freq) + assert p - offsets.Hour(3) == Period('2011-04-01 06:00', freq=freq) + assert p - np.timedelta64(3, 'h') == Period( + '2011-04-01 06:00', freq=freq) + assert p - np.timedelta64(3600, 's') == Period( + '2011-04-01 08:00', freq=freq) + assert p - timedelta(minutes=120) == Period( + '2011-04-01 07:00', freq=freq) + assert p - timedelta(days=4, minutes=180) == Period( + '2011-03-28 06:00', freq=freq) + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(3200, 's'), + timedelta(hours=23, minutes=30)]: + with pytest.raises(period.IncompatibleFrequency): + p - o + + def test_sub_offset_nat(self): + # freq is DateOffset + for freq in ['A', '2A', '3A']: + p = Period('NaT', freq=freq) + for o in [offsets.YearEnd(2)]: + assert p - o is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + assert p - o is NaT + + for freq in ['M', '2M', '3M']: + p = Period('NaT', freq=freq) + for o in [offsets.MonthEnd(2), offsets.MonthEnd(12)]: + assert p - o is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(365, 'D'), + timedelta(365)]: + assert p - o is NaT + + # freq is Tick + for freq in ['D', '2D', '3D']: + p = Period('NaT', freq=freq) + for o in [offsets.Day(5), offsets.Hour(24), np.timedelta64(2, 'D'), + np.timedelta64(3600 * 24, 's'), timedelta(-2), + timedelta(hours=48)]: + assert p - o is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(4, 'h'), + timedelta(hours=23)]: + assert p - o is NaT + + for freq in ['H', '2H', '3H']: + p = Period('NaT', freq=freq) + for o in [offsets.Day(2), offsets.Hour(3), np.timedelta64(3, 'h'), + np.timedelta64(3600, 's'), timedelta(minutes=120), + timedelta(days=4, minutes=180)]: + assert p - o is NaT + + for o in [offsets.YearBegin(2), offsets.MonthBegin(1), + offsets.Minute(), np.timedelta64(3200, 's'), + timedelta(hours=23, minutes=30)]: + assert p - o is NaT + + @pytest.mark.parametrize('freq', ['M', '2M', '3M']) + def test_nat_ops(self, freq): + p = Period('NaT', freq=freq) + assert p + 1 is NaT + assert 1 + p is NaT + assert p - 1 is NaT + assert p - Period('2011-01', freq=freq) is NaT + assert Period('2011-01', freq=freq) - p is NaT + + def test_period_ops_offset(self): + p = Period('2011-04-01', freq='D') + result = p + offsets.Day() + exp = Period('2011-04-02', freq='D') + assert result == exp + + result = p - offsets.Day(2) + exp = Period('2011-03-30', freq='D') + assert result == exp + + msg = r"Input cannot be converted to Period\(freq=D\)" + with pytest.raises(period.IncompatibleFrequency, match=msg): + p + offsets.Hour(2) + + with pytest.raises(period.IncompatibleFrequency, match=msg): + p - offsets.Hour(2) + + +def test_period_immutable(): + # see gh-17116 + per = Period('2014Q1') + with pytest.raises(AttributeError): + per.ordinal = 14 + + freq = per.freq + with pytest.raises(AttributeError): + per.freq = 2 * freq + + +# TODO: This doesn't fail on all systems; track down which +@pytest.mark.xfail(reason="Parses as Jan 1, 0007 on some systems", + strict=False) +def test_small_year_parsing(): + per1 = Period('0001-01-07', 'D') + assert per1.year == 1 + assert per1.day == 7 diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index ce2ed237f5559..43747ea8621d9 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -1,26 +1,28 @@ -import pytest - from datetime import datetime, timedelta -import pytz import numpy as np -from pandas import (NaT, Index, Timestamp, Timedelta, Period, - DatetimeIndex, PeriodIndex, - TimedeltaIndex, Series, isnull) +import pytest +import pytz + +from pandas._libs.tslibs import iNaT +import pandas.compat as compat + +from pandas import ( + DatetimeIndex, Index, NaT, Period, Series, Timedelta, TimedeltaIndex, + Timestamp, isna) +from pandas.core.arrays import PeriodArray from pandas.util import testing as tm -from pandas._libs.tslib import iNaT -@pytest.mark.parametrize('nat, idx', [(Timestamp('NaT'), DatetimeIndex), - (Timedelta('NaT'), TimedeltaIndex), - (Period('NaT', freq='M'), PeriodIndex)]) +@pytest.mark.parametrize("nat,idx", [(Timestamp("NaT"), DatetimeIndex), + (Timedelta("NaT"), TimedeltaIndex), + (Period("NaT", freq="M"), PeriodArray)]) def test_nat_fields(nat, idx): for field in idx._field_ops: - # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue result = getattr(NaT, field) @@ -39,210 +41,310 @@ def test_nat_fields(nat, idx): def test_nat_vector_field_access(): - idx = DatetimeIndex(['1/1/2000', None, None, '1/4/2000']) + idx = DatetimeIndex(["1/1/2000", None, None, "1/4/2000"]) for field in DatetimeIndex._field_ops: # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue result = getattr(idx, field) expected = Index([getattr(x, field) for x in idx]) tm.assert_index_equal(result, expected) - s = Series(idx) + ser = Series(idx) for field in DatetimeIndex._field_ops: - # weekday is a property of DTI, but a method # on NaT/Timestamp for compat with datetime - if field == 'weekday': + if field == "weekday": continue - result = getattr(s.dt, field) + result = getattr(ser.dt, field) expected = [getattr(x, field) for x in idx] tm.assert_series_equal(result, Series(expected)) for field in DatetimeIndex._bool_ops: - result = getattr(s.dt, field) + result = getattr(ser.dt, field) expected = [getattr(x, field) for x in idx] tm.assert_series_equal(result, Series(expected)) -@pytest.mark.parametrize('klass', [Timestamp, Timedelta, Period]) -def test_identity(klass): - assert klass(None) is NaT - - result = klass(np.nan) - assert result is NaT - - result = klass(None) - assert result is NaT - - result = klass(iNaT) - assert result is NaT - - result = klass(np.nan) - assert result is NaT - - result = klass(float('nan')) - assert result is NaT - - result = klass(NaT) - assert result is NaT - - result = klass('NaT') - assert result is NaT - - assert isnull(klass('nat')) - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta, Period]) -def test_equality(klass): - - # nat - if klass is not Period: - klass('').value == iNaT - klass('nat').value == iNaT - klass('NAT').value == iNaT - klass(None).value == iNaT - klass(np.nan).value == iNaT - assert isnull(klass('nat')) - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta]) -def test_round_nat(klass): - # GH14940 - ts = klass('nat') - for method in ["round", "floor", "ceil"]: - round_method = getattr(ts, method) - for freq in ["s", "5s", "min", "5min", "h", "5h"]: - assert round_method(freq) is ts - - -def test_NaT_methods(): - # GH 9513 - raise_methods = ['astimezone', 'combine', 'ctime', 'dst', - 'fromordinal', 'fromtimestamp', 'isocalendar', - 'strftime', 'strptime', 'time', 'timestamp', - 'timetuple', 'timetz', 'toordinal', 'tzname', - 'utcfromtimestamp', 'utcnow', 'utcoffset', - 'utctimetuple'] - nat_methods = ['date', 'now', 'replace', 'to_datetime', 'today'] - nan_methods = ['weekday', 'isoweekday'] - - for method in raise_methods: - if hasattr(NaT, method): - with pytest.raises(ValueError): - getattr(NaT, method)() - - for method in nan_methods: - if hasattr(NaT, method): - assert np.isnan(getattr(NaT, method)()) - - for method in nat_methods: - if hasattr(NaT, method): - # see gh-8254 - exp_warning = None - if method == 'to_datetime': - exp_warning = FutureWarning - with tm.assert_produces_warning( - exp_warning, check_stacklevel=False): - assert getattr(NaT, method)() is NaT - - # GH 12300 - assert NaT.isoformat() == 'NaT' - - -@pytest.mark.parametrize('klass', [Timestamp, Timedelta]) -def test_isoformat(klass): - - result = klass('NaT').isoformat() - expected = 'NaT' - assert result == expected - - -def test_nat_arithmetic(): - # GH 6873 - i = 2 - f = 1.5 - - for (left, right) in [(NaT, i), (NaT, f), (NaT, np.nan)]: - assert left / right is NaT - assert left * right is NaT - assert right * left is NaT - with pytest.raises(TypeError): - right / left - - # Timestamp / datetime - t = Timestamp('2014-01-01') - dt = datetime(2014, 1, 1) - for (left, right) in [(NaT, NaT), (NaT, t), (NaT, dt)]: - # NaT __add__ or __sub__ Timestamp-like (or inverse) returns NaT - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - # timedelta-like - # offsets are tested in test_offsets.py - - delta = timedelta(3600) - td = Timedelta('5s') - - for (left, right) in [(NaT, delta), (NaT, td)]: - # NaT + timedelta-like returns NaT - assert right + left is NaT - assert left + right is NaT - assert right - left is NaT - assert left - right is NaT - - # GH 11718 - t_utc = Timestamp('2014-01-01', tz='UTC') - t_tz = Timestamp('2014-01-01', tz='US/Eastern') - dt_tz = pytz.timezone('Asia/Tokyo').localize(dt) - - for (left, right) in [(NaT, t_utc), (NaT, t_tz), - (NaT, dt_tz)]: - # NaT __add__ or __sub__ Timestamp-like (or inverse) returns NaT - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - # int addition / subtraction - for (left, right) in [(NaT, 2), (NaT, 0), (NaT, -3)]: - assert right + left is NaT - assert left + right is NaT - assert left - right is NaT - assert right - left is NaT - - -def test_nat_arithmetic_index(): - # GH 11718 - - dti = DatetimeIndex(['2011-01-01', '2011-01-02'], name='x') - exp = DatetimeIndex([NaT, NaT], name='x') - tm.assert_index_equal(dti + NaT, exp) - tm.assert_index_equal(NaT + dti, exp) - - dti_tz = DatetimeIndex(['2011-01-01', '2011-01-02'], - tz='US/Eastern', name='x') - exp = DatetimeIndex([NaT, NaT], name='x', tz='US/Eastern') - tm.assert_index_equal(dti_tz + NaT, exp) - tm.assert_index_equal(NaT + dti_tz, exp) - - exp = TimedeltaIndex([NaT, NaT], name='x') - for (left, right) in [(NaT, dti), (NaT, dti_tz)]: - tm.assert_index_equal(left - right, exp) - tm.assert_index_equal(right - left, exp) - - # timedelta - tdi = TimedeltaIndex(['1 day', '2 day'], name='x') - exp = DatetimeIndex([NaT, NaT], name='x') - for (left, right) in [(NaT, tdi)]: - tm.assert_index_equal(left + right, exp) - tm.assert_index_equal(right + left, exp) - tm.assert_index_equal(left - right, exp) - tm.assert_index_equal(right - left, exp) +@pytest.mark.parametrize("klass", [Timestamp, Timedelta, Period]) +@pytest.mark.parametrize("value", [None, np.nan, iNaT, float("nan"), + NaT, "NaT", "nat"]) +def test_identity(klass, value): + assert klass(value) is NaT + + +@pytest.mark.parametrize("klass", [Timestamp, Timedelta, Period]) +@pytest.mark.parametrize("value", ["", "nat", "NAT", None, np.nan]) +def test_equality(klass, value): + if klass is Period and value == "": + pytest.skip("Period cannot parse empty string") + + assert klass(value).value == iNaT + + +@pytest.mark.parametrize("klass", [Timestamp, Timedelta]) +@pytest.mark.parametrize("method", ["round", "floor", "ceil"]) +@pytest.mark.parametrize("freq", ["s", "5s", "min", "5min", "h", "5h"]) +def test_round_nat(klass, method, freq): + # see gh-14940 + ts = klass("nat") + + round_method = getattr(ts, method) + assert round_method(freq) is ts + + +@pytest.mark.parametrize("method", [ + "astimezone", "combine", "ctime", "dst", "fromordinal", + "fromtimestamp", "isocalendar", "strftime", "strptime", + "time", "timestamp", "timetuple", "timetz", "toordinal", + "tzname", "utcfromtimestamp", "utcnow", "utcoffset", + "utctimetuple", "timestamp" +]) +def test_nat_methods_raise(method): + # see gh-9513, gh-17329 + msg = "NaTType does not support {method}".format(method=method) + + with pytest.raises(ValueError, match=msg): + getattr(NaT, method)() + + +@pytest.mark.parametrize("method", [ + "weekday", "isoweekday" +]) +def test_nat_methods_nan(method): + # see gh-9513, gh-17329 + assert np.isnan(getattr(NaT, method)()) + + +@pytest.mark.parametrize("method", [ + "date", "now", "replace", "today", + "tz_convert", "tz_localize" +]) +def test_nat_methods_nat(method): + # see gh-8254, gh-9513, gh-17329 + assert getattr(NaT, method)() is NaT + + +@pytest.mark.parametrize("get_nat", [ + lambda x: NaT, + lambda x: Timedelta(x), + lambda x: Timestamp(x) +]) +def test_nat_iso_format(get_nat): + # see gh-12300 + assert get_nat("NaT").isoformat() == "NaT" + + +@pytest.mark.parametrize("klass,expected", [ + (Timestamp, ["freqstr", "normalize", "to_julian_date", "to_period", "tz"]), + (Timedelta, ["components", "delta", "is_populated", "to_pytimedelta", + "to_timedelta64", "view"]) +]) +def test_missing_public_nat_methods(klass, expected): + # see gh-17327 + # + # NaT should have *most* of the Timestamp and Timedelta methods. + # Here, we check which public methods NaT does not have. We + # ignore any missing private methods. + nat_names = dir(NaT) + klass_names = dir(klass) + + missing = [x for x in klass_names if x not in nat_names and + not x.startswith("_")] + missing.sort() + + assert missing == expected + + +def _get_overlap_public_nat_methods(klass, as_tuple=False): + """ + Get overlapping public methods between NaT and another class. + + Parameters + ---------- + klass : type + The class to compare with NaT + as_tuple : bool, default False + Whether to return a list of tuples of the form (klass, method). + + Returns + ------- + overlap : list + """ + nat_names = dir(NaT) + klass_names = dir(klass) + + overlap = [x for x in nat_names if x in klass_names and + not x.startswith("_") and + callable(getattr(klass, x))] + + # Timestamp takes precedence over Timedelta in terms of overlap. + if klass is Timedelta: + ts_names = dir(Timestamp) + overlap = [x for x in overlap if x not in ts_names] + + if as_tuple: + overlap = [(klass, method) for method in overlap] + + overlap.sort() + return overlap + + +@pytest.mark.parametrize("klass,expected", [ + (Timestamp, ["astimezone", "ceil", "combine", "ctime", "date", "day_name", + "dst", "floor", "fromisoformat", "fromordinal", + "fromtimestamp", "isocalendar", "isoformat", "isoweekday", + "month_name", "now", "replace", "round", "strftime", + "strptime", "time", "timestamp", "timetuple", "timetz", + "to_datetime64", "to_numpy", "to_pydatetime", "today", + "toordinal", "tz_convert", "tz_localize", "tzname", + "utcfromtimestamp", "utcnow", "utcoffset", "utctimetuple", + "weekday"]), + (Timedelta, ["total_seconds"]) +]) +def test_overlap_public_nat_methods(klass, expected): + # see gh-17327 + # + # NaT should have *most* of the Timestamp and Timedelta methods. + # In case when Timestamp, Timedelta, and NaT are overlap, the overlap + # is considered to be with Timestamp and NaT, not Timedelta. + + # "fromisoformat" was introduced in 3.7 + if klass is Timestamp and not compat.PY37: + expected.remove("fromisoformat") + + assert _get_overlap_public_nat_methods(klass) == expected + + +@pytest.mark.parametrize("compare", ( + _get_overlap_public_nat_methods(Timestamp, True) + + _get_overlap_public_nat_methods(Timedelta, True)) +) +def test_nat_doc_strings(compare): + # see gh-17327 + # + # The docstrings for overlapping methods should match. + klass, method = compare + klass_doc = getattr(klass, method).__doc__ + + nat_doc = getattr(NaT, method).__doc__ + assert klass_doc == nat_doc + + +_ops = { + "left_plus_right": lambda a, b: a + b, + "right_plus_left": lambda a, b: b + a, + "left_minus_right": lambda a, b: a - b, + "right_minus_left": lambda a, b: b - a, + "left_times_right": lambda a, b: a * b, + "right_times_left": lambda a, b: b * a, + "left_div_right": lambda a, b: a / b, + "right_div_left": lambda a, b: b / a, +} + + +@pytest.mark.parametrize("op_name", list(_ops.keys())) +@pytest.mark.parametrize("value,val_type", [ + (2, "scalar"), + (1.5, "scalar"), + (np.nan, "scalar"), + (timedelta(3600), "timedelta"), + (Timedelta("5s"), "timedelta"), + (datetime(2014, 1, 1), "timestamp"), + (Timestamp("2014-01-01"), "timestamp"), + (Timestamp("2014-01-01", tz="UTC"), "timestamp"), + (Timestamp("2014-01-01", tz="US/Eastern"), "timestamp"), + (pytz.timezone("Asia/Tokyo").localize(datetime(2014, 1, 1)), "timestamp"), +]) +def test_nat_arithmetic_scalar(op_name, value, val_type): + # see gh-6873 + invalid_ops = { + "scalar": {"right_div_left"}, + "timedelta": {"left_times_right", "right_times_left"}, + "timestamp": {"left_times_right", "right_times_left", + "left_div_right", "right_div_left"} + } + + op = _ops[op_name] + + if op_name in invalid_ops.get(val_type, set()): + if (val_type == "timedelta" and "times" in op_name and + isinstance(value, Timedelta)): + msg = "Cannot multiply" + else: + msg = "unsupported operand type" + + with pytest.raises(TypeError, match=msg): + op(NaT, value) + else: + if val_type == "timedelta" and "div" in op_name: + expected = np.nan + else: + expected = NaT + + assert op(NaT, value) is expected + + +@pytest.mark.parametrize("val,expected", [ + (np.nan, NaT), + (NaT, np.nan), + (np.timedelta64("NaT"), np.nan) +]) +def test_nat_rfloordiv_timedelta(val, expected): + # see gh-#18846 + # + # See also test_timedelta.TestTimedeltaArithmetic.test_floordiv + td = Timedelta(hours=3, minutes=4) + assert td // val is expected + + +@pytest.mark.parametrize("op_name", [ + "left_plus_right", "right_plus_left", + "left_minus_right", "right_minus_left" +]) +@pytest.mark.parametrize("value", [ + DatetimeIndex(["2011-01-01", "2011-01-02"], name="x"), + DatetimeIndex(["2011-01-01", "2011-01-02"], name="x"), + TimedeltaIndex(["1 day", "2 day"], name="x"), +]) +def test_nat_arithmetic_index(op_name, value): + # see gh-11718 + exp_name = "x" + exp_data = [NaT] * 2 + + if isinstance(value, DatetimeIndex) and "plus" in op_name: + expected = DatetimeIndex(exp_data, name=exp_name, tz=value.tz) + else: + expected = TimedeltaIndex(exp_data, name=exp_name) + + tm.assert_index_equal(_ops[op_name](NaT, value), expected) + + +@pytest.mark.parametrize("op_name", [ + "left_plus_right", "right_plus_left", + "left_minus_right", "right_minus_left" +]) +@pytest.mark.parametrize("box", [TimedeltaIndex, Series]) +def test_nat_arithmetic_td64_vector(op_name, box): + # see gh-19124 + vec = box(["1 day", "2 day"], dtype="timedelta64[ns]") + box_nat = box([NaT, NaT], dtype="timedelta64[ns]") + tm.assert_equal(_ops[op_name](vec, NaT), box_nat) + + +def test_nat_pinned_docstrings(): + # see gh-17327 + assert NaT.ctime.__doc__ == datetime.ctime.__doc__ + + +def test_to_numpy_alias(): + # GH 24653: alias .to_numpy() for scalars + expected = NaT.to_datetime64() + result = NaT.to_numpy() + + assert isna(expected) and isna(result) diff --git a/pandas/tests/scalar/test_period.py b/pandas/tests/scalar/test_period.py deleted file mode 100644 index 7a15600d6041e..0000000000000 --- a/pandas/tests/scalar/test_period.py +++ /dev/null @@ -1,1419 +0,0 @@ -import numpy as np -from datetime import datetime, date, timedelta - -import pandas as pd -import pandas.util.testing as tm -import pandas.tseries.period as period -from pandas.compat import text_type, iteritems -from pandas.compat.numpy import np_datetime64_compat - -from pandas._libs import tslib, period as libperiod -from pandas import Period, Timestamp, offsets -from pandas.tseries.frequencies import DAYS, MONTHS - - -class TestPeriodProperties(tm.TestCase): - "Test properties such as year, month, weekday, etc...." - - def test_is_leap_year(self): - # GH 13727 - for freq in ['A', 'M', 'D', 'H']: - p = Period('2000-01-01 00:00:00', freq=freq) - self.assertTrue(p.is_leap_year) - self.assertIsInstance(p.is_leap_year, bool) - - p = Period('1999-01-01 00:00:00', freq=freq) - self.assertFalse(p.is_leap_year) - - p = Period('2004-01-01 00:00:00', freq=freq) - self.assertTrue(p.is_leap_year) - - p = Period('2100-01-01 00:00:00', freq=freq) - self.assertFalse(p.is_leap_year) - - def test_quarterly_negative_ordinals(self): - p = Period(ordinal=-1, freq='Q-DEC') - self.assertEqual(p.year, 1969) - self.assertEqual(p.quarter, 4) - self.assertIsInstance(p, Period) - - p = Period(ordinal=-2, freq='Q-DEC') - self.assertEqual(p.year, 1969) - self.assertEqual(p.quarter, 3) - self.assertIsInstance(p, Period) - - p = Period(ordinal=-2, freq='M') - self.assertEqual(p.year, 1969) - self.assertEqual(p.month, 11) - self.assertIsInstance(p, Period) - - def test_period_cons_quarterly(self): - # bugs in scikits.timeseries - for month in MONTHS: - freq = 'Q-%s' % month - exp = Period('1989Q3', freq=freq) - self.assertIn('1989Q3', str(exp)) - stamp = exp.to_timestamp('D', how='end') - p = Period(stamp, freq=freq) - self.assertEqual(p, exp) - - stamp = exp.to_timestamp('3D', how='end') - p = Period(stamp, freq=freq) - self.assertEqual(p, exp) - - def test_period_cons_annual(self): - # bugs in scikits.timeseries - for month in MONTHS: - freq = 'A-%s' % month - exp = Period('1989', freq=freq) - stamp = exp.to_timestamp('D', how='end') + timedelta(days=30) - p = Period(stamp, freq=freq) - self.assertEqual(p, exp + 1) - self.assertIsInstance(p, Period) - - def test_period_cons_weekly(self): - for num in range(10, 17): - daystr = '2011-02-%d' % num - for day in DAYS: - freq = 'W-%s' % day - - result = Period(daystr, freq=freq) - expected = Period(daystr, freq='D').asfreq(freq) - self.assertEqual(result, expected) - self.assertIsInstance(result, Period) - - def test_period_from_ordinal(self): - p = pd.Period('2011-01', freq='M') - res = pd.Period._from_ordinal(p.ordinal, freq='M') - self.assertEqual(p, res) - self.assertIsInstance(res, Period) - - def test_period_cons_nat(self): - p = Period('NaT', freq='M') - self.assertIs(p, pd.NaT) - - p = Period('nat', freq='W-SUN') - self.assertIs(p, pd.NaT) - - p = Period(tslib.iNaT, freq='D') - self.assertIs(p, pd.NaT) - - p = Period(tslib.iNaT, freq='3D') - self.assertIs(p, pd.NaT) - - p = Period(tslib.iNaT, freq='1D1H') - self.assertIs(p, pd.NaT) - - p = Period('NaT') - self.assertIs(p, pd.NaT) - - p = Period(tslib.iNaT) - self.assertIs(p, pd.NaT) - - def test_period_cons_mult(self): - p1 = Period('2011-01', freq='3M') - p2 = Period('2011-01', freq='M') - self.assertEqual(p1.ordinal, p2.ordinal) - - self.assertEqual(p1.freq, offsets.MonthEnd(3)) - self.assertEqual(p1.freqstr, '3M') - - self.assertEqual(p2.freq, offsets.MonthEnd()) - self.assertEqual(p2.freqstr, 'M') - - result = p1 + 1 - self.assertEqual(result.ordinal, (p2 + 3).ordinal) - self.assertEqual(result.freq, p1.freq) - self.assertEqual(result.freqstr, '3M') - - result = p1 - 1 - self.assertEqual(result.ordinal, (p2 - 3).ordinal) - self.assertEqual(result.freq, p1.freq) - self.assertEqual(result.freqstr, '3M') - - msg = ('Frequency must be positive, because it' - ' represents span: -3M') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='-3M') - - msg = ('Frequency must be positive, because it' ' represents span: 0M') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='0M') - - def test_period_cons_combined(self): - p = [(Period('2011-01', freq='1D1H'), - Period('2011-01', freq='1H1D'), - Period('2011-01', freq='H')), - (Period(ordinal=1, freq='1D1H'), - Period(ordinal=1, freq='1H1D'), - Period(ordinal=1, freq='H'))] - - for p1, p2, p3 in p: - self.assertEqual(p1.ordinal, p3.ordinal) - self.assertEqual(p2.ordinal, p3.ordinal) - - self.assertEqual(p1.freq, offsets.Hour(25)) - self.assertEqual(p1.freqstr, '25H') - - self.assertEqual(p2.freq, offsets.Hour(25)) - self.assertEqual(p2.freqstr, '25H') - - self.assertEqual(p3.freq, offsets.Hour()) - self.assertEqual(p3.freqstr, 'H') - - result = p1 + 1 - self.assertEqual(result.ordinal, (p3 + 25).ordinal) - self.assertEqual(result.freq, p1.freq) - self.assertEqual(result.freqstr, '25H') - - result = p2 + 1 - self.assertEqual(result.ordinal, (p3 + 25).ordinal) - self.assertEqual(result.freq, p2.freq) - self.assertEqual(result.freqstr, '25H') - - result = p1 - 1 - self.assertEqual(result.ordinal, (p3 - 25).ordinal) - self.assertEqual(result.freq, p1.freq) - self.assertEqual(result.freqstr, '25H') - - result = p2 - 1 - self.assertEqual(result.ordinal, (p3 - 25).ordinal) - self.assertEqual(result.freq, p2.freq) - self.assertEqual(result.freqstr, '25H') - - msg = ('Frequency must be positive, because it' - ' represents span: -25H') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='-1D1H') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='-1H1D') - with tm.assertRaisesRegexp(ValueError, msg): - Period(ordinal=1, freq='-1D1H') - with tm.assertRaisesRegexp(ValueError, msg): - Period(ordinal=1, freq='-1H1D') - - msg = ('Frequency must be positive, because it' - ' represents span: 0D') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='0D0H') - with tm.assertRaisesRegexp(ValueError, msg): - Period(ordinal=1, freq='0D0H') - - # You can only combine together day and intraday offsets - msg = ('Invalid frequency: 1W1D') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='1W1D') - msg = ('Invalid frequency: 1D1W') - with tm.assertRaisesRegexp(ValueError, msg): - Period('2011-01', freq='1D1W') - - def test_timestamp_tz_arg(self): - tm._skip_if_no_pytz() - import pytz - for case in ['Europe/Brussels', 'Asia/Tokyo', 'US/Pacific']: - p = Period('1/1/2005', freq='M').to_timestamp(tz=case) - exp = Timestamp('1/1/2005', tz='UTC').tz_convert(case) - exp_zone = pytz.timezone(case).normalize(p) - - self.assertEqual(p, exp) - self.assertEqual(p.tz, exp_zone.tzinfo) - self.assertEqual(p.tz, exp.tz) - - p = Period('1/1/2005', freq='3H').to_timestamp(tz=case) - exp = Timestamp('1/1/2005', tz='UTC').tz_convert(case) - exp_zone = pytz.timezone(case).normalize(p) - - self.assertEqual(p, exp) - self.assertEqual(p.tz, exp_zone.tzinfo) - self.assertEqual(p.tz, exp.tz) - - p = Period('1/1/2005', freq='A').to_timestamp(freq='A', tz=case) - exp = Timestamp('31/12/2005', tz='UTC').tz_convert(case) - exp_zone = pytz.timezone(case).normalize(p) - - self.assertEqual(p, exp) - self.assertEqual(p.tz, exp_zone.tzinfo) - self.assertEqual(p.tz, exp.tz) - - p = Period('1/1/2005', freq='A').to_timestamp(freq='3H', tz=case) - exp = Timestamp('1/1/2005', tz='UTC').tz_convert(case) - exp_zone = pytz.timezone(case).normalize(p) - - self.assertEqual(p, exp) - self.assertEqual(p.tz, exp_zone.tzinfo) - self.assertEqual(p.tz, exp.tz) - - def test_timestamp_tz_arg_dateutil(self): - from pandas._libs.tslib import _dateutil_gettz as gettz - from pandas._libs.tslib import maybe_get_tz - for case in ['dateutil/Europe/Brussels', 'dateutil/Asia/Tokyo', - 'dateutil/US/Pacific']: - p = Period('1/1/2005', freq='M').to_timestamp( - tz=maybe_get_tz(case)) - exp = Timestamp('1/1/2005', tz='UTC').tz_convert(case) - self.assertEqual(p, exp) - self.assertEqual(p.tz, gettz(case.split('/', 1)[1])) - self.assertEqual(p.tz, exp.tz) - - p = Period('1/1/2005', - freq='M').to_timestamp(freq='3H', tz=maybe_get_tz(case)) - exp = Timestamp('1/1/2005', tz='UTC').tz_convert(case) - self.assertEqual(p, exp) - self.assertEqual(p.tz, gettz(case.split('/', 1)[1])) - self.assertEqual(p.tz, exp.tz) - - def test_timestamp_tz_arg_dateutil_from_string(self): - from pandas._libs.tslib import _dateutil_gettz as gettz - p = Period('1/1/2005', - freq='M').to_timestamp(tz='dateutil/Europe/Brussels') - self.assertEqual(p.tz, gettz('Europe/Brussels')) - - def test_timestamp_mult(self): - p = pd.Period('2011-01', freq='M') - self.assertEqual(p.to_timestamp(how='S'), pd.Timestamp('2011-01-01')) - self.assertEqual(p.to_timestamp(how='E'), pd.Timestamp('2011-01-31')) - - p = pd.Period('2011-01', freq='3M') - self.assertEqual(p.to_timestamp(how='S'), pd.Timestamp('2011-01-01')) - self.assertEqual(p.to_timestamp(how='E'), pd.Timestamp('2011-03-31')) - - def test_construction(self): - i1 = Period('1/1/2005', freq='M') - i2 = Period('Jan 2005') - - self.assertEqual(i1, i2) - - i1 = Period('2005', freq='A') - i2 = Period('2005') - i3 = Period('2005', freq='a') - - self.assertEqual(i1, i2) - self.assertEqual(i1, i3) - - i4 = Period('2005', freq='M') - i5 = Period('2005', freq='m') - - self.assertRaises(ValueError, i1.__ne__, i4) - self.assertEqual(i4, i5) - - i1 = Period.now('Q') - i2 = Period(datetime.now(), freq='Q') - i3 = Period.now('q') - - self.assertEqual(i1, i2) - self.assertEqual(i1, i3) - - i1 = Period('1982', freq='min') - i2 = Period('1982', freq='MIN') - self.assertEqual(i1, i2) - i2 = Period('1982', freq=('Min', 1)) - self.assertEqual(i1, i2) - - i1 = Period(year=2005, month=3, day=1, freq='D') - i2 = Period('3/1/2005', freq='D') - self.assertEqual(i1, i2) - - i3 = Period(year=2005, month=3, day=1, freq='d') - self.assertEqual(i1, i3) - - i1 = Period('2007-01-01 09:00:00.001') - expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1000), freq='L') - self.assertEqual(i1, expected) - - expected = Period(np_datetime64_compat( - '2007-01-01 09:00:00.001Z'), freq='L') - self.assertEqual(i1, expected) - - i1 = Period('2007-01-01 09:00:00.00101') - expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1010), freq='U') - self.assertEqual(i1, expected) - - expected = Period(np_datetime64_compat('2007-01-01 09:00:00.00101Z'), - freq='U') - self.assertEqual(i1, expected) - - self.assertRaises(ValueError, Period, ordinal=200701) - - self.assertRaises(ValueError, Period, '2007-1-1', freq='X') - - def test_construction_bday(self): - - # Biz day construction, roll forward if non-weekday - i1 = Period('3/10/12', freq='B') - i2 = Period('3/10/12', freq='D') - self.assertEqual(i1, i2.asfreq('B')) - i2 = Period('3/11/12', freq='D') - self.assertEqual(i1, i2.asfreq('B')) - i2 = Period('3/12/12', freq='D') - self.assertEqual(i1, i2.asfreq('B')) - - i3 = Period('3/10/12', freq='b') - self.assertEqual(i1, i3) - - i1 = Period(year=2012, month=3, day=10, freq='B') - i2 = Period('3/12/12', freq='B') - self.assertEqual(i1, i2) - - def test_construction_quarter(self): - - i1 = Period(year=2005, quarter=1, freq='Q') - i2 = Period('1/1/2005', freq='Q') - self.assertEqual(i1, i2) - - i1 = Period(year=2005, quarter=3, freq='Q') - i2 = Period('9/1/2005', freq='Q') - self.assertEqual(i1, i2) - - i1 = Period('2005Q1') - i2 = Period(year=2005, quarter=1, freq='Q') - i3 = Period('2005q1') - self.assertEqual(i1, i2) - self.assertEqual(i1, i3) - - i1 = Period('05Q1') - self.assertEqual(i1, i2) - lower = Period('05q1') - self.assertEqual(i1, lower) - - i1 = Period('1Q2005') - self.assertEqual(i1, i2) - lower = Period('1q2005') - self.assertEqual(i1, lower) - - i1 = Period('1Q05') - self.assertEqual(i1, i2) - lower = Period('1q05') - self.assertEqual(i1, lower) - - i1 = Period('4Q1984') - self.assertEqual(i1.year, 1984) - lower = Period('4q1984') - self.assertEqual(i1, lower) - - def test_construction_month(self): - - expected = Period('2007-01', freq='M') - i1 = Period('200701', freq='M') - self.assertEqual(i1, expected) - - i1 = Period('200701', freq='M') - self.assertEqual(i1, expected) - - i1 = Period(200701, freq='M') - self.assertEqual(i1, expected) - - i1 = Period(ordinal=200701, freq='M') - self.assertEqual(i1.year, 18695) - - i1 = Period(datetime(2007, 1, 1), freq='M') - i2 = Period('200701', freq='M') - self.assertEqual(i1, i2) - - i1 = Period(date(2007, 1, 1), freq='M') - i2 = Period(datetime(2007, 1, 1), freq='M') - i3 = Period(np.datetime64('2007-01-01'), freq='M') - i4 = Period(np_datetime64_compat('2007-01-01 00:00:00Z'), freq='M') - i5 = Period(np_datetime64_compat('2007-01-01 00:00:00.000Z'), freq='M') - self.assertEqual(i1, i2) - self.assertEqual(i1, i3) - self.assertEqual(i1, i4) - self.assertEqual(i1, i5) - - def test_period_constructor_offsets(self): - self.assertEqual(Period('1/1/2005', freq=offsets.MonthEnd()), - Period('1/1/2005', freq='M')) - self.assertEqual(Period('2005', freq=offsets.YearEnd()), - Period('2005', freq='A')) - self.assertEqual(Period('2005', freq=offsets.MonthEnd()), - Period('2005', freq='M')) - self.assertEqual(Period('3/10/12', freq=offsets.BusinessDay()), - Period('3/10/12', freq='B')) - self.assertEqual(Period('3/10/12', freq=offsets.Day()), - Period('3/10/12', freq='D')) - - self.assertEqual(Period(year=2005, quarter=1, - freq=offsets.QuarterEnd(startingMonth=12)), - Period(year=2005, quarter=1, freq='Q')) - self.assertEqual(Period(year=2005, quarter=2, - freq=offsets.QuarterEnd(startingMonth=12)), - Period(year=2005, quarter=2, freq='Q')) - - self.assertEqual(Period(year=2005, month=3, day=1, freq=offsets.Day()), - Period(year=2005, month=3, day=1, freq='D')) - self.assertEqual(Period(year=2012, month=3, day=10, - freq=offsets.BDay()), - Period(year=2012, month=3, day=10, freq='B')) - - expected = Period('2005-03-01', freq='3D') - self.assertEqual(Period(year=2005, month=3, day=1, - freq=offsets.Day(3)), expected) - self.assertEqual(Period(year=2005, month=3, day=1, freq='3D'), - expected) - - self.assertEqual(Period(year=2012, month=3, day=10, - freq=offsets.BDay(3)), - Period(year=2012, month=3, day=10, freq='3B')) - - self.assertEqual(Period(200701, freq=offsets.MonthEnd()), - Period(200701, freq='M')) - - i1 = Period(ordinal=200701, freq=offsets.MonthEnd()) - i2 = Period(ordinal=200701, freq='M') - self.assertEqual(i1, i2) - self.assertEqual(i1.year, 18695) - self.assertEqual(i2.year, 18695) - - i1 = Period(datetime(2007, 1, 1), freq='M') - i2 = Period('200701', freq='M') - self.assertEqual(i1, i2) - - i1 = Period(date(2007, 1, 1), freq='M') - i2 = Period(datetime(2007, 1, 1), freq='M') - i3 = Period(np.datetime64('2007-01-01'), freq='M') - i4 = Period(np_datetime64_compat('2007-01-01 00:00:00Z'), freq='M') - i5 = Period(np_datetime64_compat('2007-01-01 00:00:00.000Z'), freq='M') - self.assertEqual(i1, i2) - self.assertEqual(i1, i3) - self.assertEqual(i1, i4) - self.assertEqual(i1, i5) - - i1 = Period('2007-01-01 09:00:00.001') - expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1000), freq='L') - self.assertEqual(i1, expected) - - expected = Period(np_datetime64_compat( - '2007-01-01 09:00:00.001Z'), freq='L') - self.assertEqual(i1, expected) - - i1 = Period('2007-01-01 09:00:00.00101') - expected = Period(datetime(2007, 1, 1, 9, 0, 0, 1010), freq='U') - self.assertEqual(i1, expected) - - expected = Period(np_datetime64_compat('2007-01-01 09:00:00.00101Z'), - freq='U') - self.assertEqual(i1, expected) - - self.assertRaises(ValueError, Period, ordinal=200701) - - self.assertRaises(ValueError, Period, '2007-1-1', freq='X') - - def test_freq_str(self): - i1 = Period('1982', freq='Min') - self.assertEqual(i1.freq, offsets.Minute()) - self.assertEqual(i1.freqstr, 'T') - - def test_period_deprecated_freq(self): - cases = {"M": ["MTH", "MONTH", "MONTHLY", "Mth", "month", "monthly"], - "B": ["BUS", "BUSINESS", "BUSINESSLY", "WEEKDAY", "bus"], - "D": ["DAY", "DLY", "DAILY", "Day", "Dly", "Daily"], - "H": ["HR", "HOUR", "HRLY", "HOURLY", "hr", "Hour", "HRly"], - "T": ["minute", "MINUTE", "MINUTELY", "minutely"], - "S": ["sec", "SEC", "SECOND", "SECONDLY", "second"], - "L": ["MILLISECOND", "MILLISECONDLY", "millisecond"], - "U": ["MICROSECOND", "MICROSECONDLY", "microsecond"], - "N": ["NANOSECOND", "NANOSECONDLY", "nanosecond"]} - - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR - for exp, freqs in iteritems(cases): - for freq in freqs: - with self.assertRaisesRegexp(ValueError, msg): - Period('2016-03-01 09:00', freq=freq) - with self.assertRaisesRegexp(ValueError, msg): - Period(ordinal=1, freq=freq) - - # check supported freq-aliases still works - p1 = Period('2016-03-01 09:00', freq=exp) - p2 = Period(ordinal=1, freq=exp) - tm.assertIsInstance(p1, Period) - tm.assertIsInstance(p2, Period) - - def test_hash(self): - self.assertEqual(hash(Period('2011-01', freq='M')), - hash(Period('2011-01', freq='M'))) - - self.assertNotEqual(hash(Period('2011-01-01', freq='D')), - hash(Period('2011-01', freq='M'))) - - self.assertNotEqual(hash(Period('2011-01', freq='3M')), - hash(Period('2011-01', freq='2M'))) - - self.assertNotEqual(hash(Period('2011-01', freq='M')), - hash(Period('2011-02', freq='M'))) - - def test_repr(self): - p = Period('Jan-2000') - self.assertIn('2000-01', repr(p)) - - p = Period('2000-12-15') - self.assertIn('2000-12-15', repr(p)) - - def test_repr_nat(self): - p = Period('nat', freq='M') - self.assertIn(repr(tslib.NaT), repr(p)) - - def test_millisecond_repr(self): - p = Period('2000-01-01 12:15:02.123') - - self.assertEqual("Period('2000-01-01 12:15:02.123', 'L')", repr(p)) - - def test_microsecond_repr(self): - p = Period('2000-01-01 12:15:02.123567') - - self.assertEqual("Period('2000-01-01 12:15:02.123567', 'U')", repr(p)) - - def test_strftime(self): - p = Period('2000-1-1 12:34:12', freq='S') - res = p.strftime('%Y-%m-%d %H:%M:%S') - self.assertEqual(res, '2000-01-01 12:34:12') - tm.assertIsInstance(res, text_type) # GH3363 - - def test_sub_delta(self): - left, right = Period('2011', freq='A'), Period('2007', freq='A') - result = left - right - self.assertEqual(result, 4) - - with self.assertRaises(period.IncompatibleFrequency): - left - Period('2007-01', freq='M') - - def test_to_timestamp(self): - p = Period('1982', freq='A') - start_ts = p.to_timestamp(how='S') - aliases = ['s', 'StarT', 'BEGIn'] - for a in aliases: - self.assertEqual(start_ts, p.to_timestamp('D', how=a)) - # freq with mult should not affect to the result - self.assertEqual(start_ts, p.to_timestamp('3D', how=a)) - - end_ts = p.to_timestamp(how='E') - aliases = ['e', 'end', 'FINIsH'] - for a in aliases: - self.assertEqual(end_ts, p.to_timestamp('D', how=a)) - self.assertEqual(end_ts, p.to_timestamp('3D', how=a)) - - from_lst = ['A', 'Q', 'M', 'W', 'B', 'D', 'H', 'Min', 'S'] - - def _ex(p): - return Timestamp((p + 1).start_time.value - 1) - - for i, fcode in enumerate(from_lst): - p = Period('1982', freq=fcode) - result = p.to_timestamp().to_period(fcode) - self.assertEqual(result, p) - - self.assertEqual(p.start_time, p.to_timestamp(how='S')) - - self.assertEqual(p.end_time, _ex(p)) - - # Frequency other than daily - - p = Period('1985', freq='A') - - result = p.to_timestamp('H', how='end') - expected = datetime(1985, 12, 31, 23) - self.assertEqual(result, expected) - result = p.to_timestamp('3H', how='end') - self.assertEqual(result, expected) - - result = p.to_timestamp('T', how='end') - expected = datetime(1985, 12, 31, 23, 59) - self.assertEqual(result, expected) - result = p.to_timestamp('2T', how='end') - self.assertEqual(result, expected) - - result = p.to_timestamp(how='end') - expected = datetime(1985, 12, 31) - self.assertEqual(result, expected) - - expected = datetime(1985, 1, 1) - result = p.to_timestamp('H', how='start') - self.assertEqual(result, expected) - result = p.to_timestamp('T', how='start') - self.assertEqual(result, expected) - result = p.to_timestamp('S', how='start') - self.assertEqual(result, expected) - result = p.to_timestamp('3H', how='start') - self.assertEqual(result, expected) - result = p.to_timestamp('5S', how='start') - self.assertEqual(result, expected) - - def test_start_time(self): - freq_lst = ['A', 'Q', 'M', 'D', 'H', 'T', 'S'] - xp = datetime(2012, 1, 1) - for f in freq_lst: - p = Period('2012', freq=f) - self.assertEqual(p.start_time, xp) - self.assertEqual(Period('2012', freq='B').start_time, - datetime(2012, 1, 2)) - self.assertEqual(Period('2012', freq='W').start_time, - datetime(2011, 12, 26)) - - def test_end_time(self): - p = Period('2012', freq='A') - - def _ex(*args): - return Timestamp(Timestamp(datetime(*args)).value - 1) - - xp = _ex(2013, 1, 1) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='Q') - xp = _ex(2012, 4, 1) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='M') - xp = _ex(2012, 2, 1) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='D') - xp = _ex(2012, 1, 2) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='H') - xp = _ex(2012, 1, 1, 1) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='B') - xp = _ex(2012, 1, 3) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='W') - xp = _ex(2012, 1, 2) - self.assertEqual(xp, p.end_time) - - # Test for GH 11738 - p = Period('2012', freq='15D') - xp = _ex(2012, 1, 16) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='1D1H') - xp = _ex(2012, 1, 2, 1) - self.assertEqual(xp, p.end_time) - - p = Period('2012', freq='1H1D') - xp = _ex(2012, 1, 2, 1) - self.assertEqual(xp, p.end_time) - - def test_anchor_week_end_time(self): - def _ex(*args): - return Timestamp(Timestamp(datetime(*args)).value - 1) - - p = Period('2013-1-1', 'W-SAT') - xp = _ex(2013, 1, 6) - self.assertEqual(p.end_time, xp) - - def test_properties_annually(self): - # Test properties on Periods with annually frequency. - a_date = Period(freq='A', year=2007) - self.assertEqual(a_date.year, 2007) - - def test_properties_quarterly(self): - # Test properties on Periods with daily frequency. - qedec_date = Period(freq="Q-DEC", year=2007, quarter=1) - qejan_date = Period(freq="Q-JAN", year=2007, quarter=1) - qejun_date = Period(freq="Q-JUN", year=2007, quarter=1) - # - for x in range(3): - for qd in (qedec_date, qejan_date, qejun_date): - self.assertEqual((qd + x).qyear, 2007) - self.assertEqual((qd + x).quarter, x + 1) - - def test_properties_monthly(self): - # Test properties on Periods with daily frequency. - m_date = Period(freq='M', year=2007, month=1) - for x in range(11): - m_ival_x = m_date + x - self.assertEqual(m_ival_x.year, 2007) - if 1 <= x + 1 <= 3: - self.assertEqual(m_ival_x.quarter, 1) - elif 4 <= x + 1 <= 6: - self.assertEqual(m_ival_x.quarter, 2) - elif 7 <= x + 1 <= 9: - self.assertEqual(m_ival_x.quarter, 3) - elif 10 <= x + 1 <= 12: - self.assertEqual(m_ival_x.quarter, 4) - self.assertEqual(m_ival_x.month, x + 1) - - def test_properties_weekly(self): - # Test properties on Periods with daily frequency. - w_date = Period(freq='W', year=2007, month=1, day=7) - # - self.assertEqual(w_date.year, 2007) - self.assertEqual(w_date.quarter, 1) - self.assertEqual(w_date.month, 1) - self.assertEqual(w_date.week, 1) - self.assertEqual((w_date - 1).week, 52) - self.assertEqual(w_date.days_in_month, 31) - self.assertEqual(Period(freq='W', year=2012, - month=2, day=1).days_in_month, 29) - - def test_properties_weekly_legacy(self): - # Test properties on Periods with daily frequency. - w_date = Period(freq='W', year=2007, month=1, day=7) - self.assertEqual(w_date.year, 2007) - self.assertEqual(w_date.quarter, 1) - self.assertEqual(w_date.month, 1) - self.assertEqual(w_date.week, 1) - self.assertEqual((w_date - 1).week, 52) - self.assertEqual(w_date.days_in_month, 31) - - exp = Period(freq='W', year=2012, month=2, day=1) - self.assertEqual(exp.days_in_month, 29) - - msg = pd.tseries.frequencies._INVALID_FREQ_ERROR - with self.assertRaisesRegexp(ValueError, msg): - Period(freq='WK', year=2007, month=1, day=7) - - def test_properties_daily(self): - # Test properties on Periods with daily frequency. - b_date = Period(freq='B', year=2007, month=1, day=1) - # - self.assertEqual(b_date.year, 2007) - self.assertEqual(b_date.quarter, 1) - self.assertEqual(b_date.month, 1) - self.assertEqual(b_date.day, 1) - self.assertEqual(b_date.weekday, 0) - self.assertEqual(b_date.dayofyear, 1) - self.assertEqual(b_date.days_in_month, 31) - self.assertEqual(Period(freq='B', year=2012, - month=2, day=1).days_in_month, 29) - # - d_date = Period(freq='D', year=2007, month=1, day=1) - # - self.assertEqual(d_date.year, 2007) - self.assertEqual(d_date.quarter, 1) - self.assertEqual(d_date.month, 1) - self.assertEqual(d_date.day, 1) - self.assertEqual(d_date.weekday, 0) - self.assertEqual(d_date.dayofyear, 1) - self.assertEqual(d_date.days_in_month, 31) - self.assertEqual(Period(freq='D', year=2012, month=2, - day=1).days_in_month, 29) - - def test_properties_hourly(self): - # Test properties on Periods with hourly frequency. - h_date1 = Period(freq='H', year=2007, month=1, day=1, hour=0) - h_date2 = Period(freq='2H', year=2007, month=1, day=1, hour=0) - - for h_date in [h_date1, h_date2]: - self.assertEqual(h_date.year, 2007) - self.assertEqual(h_date.quarter, 1) - self.assertEqual(h_date.month, 1) - self.assertEqual(h_date.day, 1) - self.assertEqual(h_date.weekday, 0) - self.assertEqual(h_date.dayofyear, 1) - self.assertEqual(h_date.hour, 0) - self.assertEqual(h_date.days_in_month, 31) - self.assertEqual(Period(freq='H', year=2012, month=2, day=1, - hour=0).days_in_month, 29) - - def test_properties_minutely(self): - # Test properties on Periods with minutely frequency. - t_date = Period(freq='Min', year=2007, month=1, day=1, hour=0, - minute=0) - # - self.assertEqual(t_date.quarter, 1) - self.assertEqual(t_date.month, 1) - self.assertEqual(t_date.day, 1) - self.assertEqual(t_date.weekday, 0) - self.assertEqual(t_date.dayofyear, 1) - self.assertEqual(t_date.hour, 0) - self.assertEqual(t_date.minute, 0) - self.assertEqual(t_date.days_in_month, 31) - self.assertEqual(Period(freq='D', year=2012, month=2, day=1, hour=0, - minute=0).days_in_month, 29) - - def test_properties_secondly(self): - # Test properties on Periods with secondly frequency. - s_date = Period(freq='Min', year=2007, month=1, day=1, hour=0, - minute=0, second=0) - # - self.assertEqual(s_date.year, 2007) - self.assertEqual(s_date.quarter, 1) - self.assertEqual(s_date.month, 1) - self.assertEqual(s_date.day, 1) - self.assertEqual(s_date.weekday, 0) - self.assertEqual(s_date.dayofyear, 1) - self.assertEqual(s_date.hour, 0) - self.assertEqual(s_date.minute, 0) - self.assertEqual(s_date.second, 0) - self.assertEqual(s_date.days_in_month, 31) - self.assertEqual(Period(freq='Min', year=2012, month=2, day=1, hour=0, - minute=0, second=0).days_in_month, 29) - - def test_pnow(self): - - # deprecation, xref #13790 - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - period.pnow('D') - - def test_constructor_corner(self): - expected = Period('2007-01', freq='2M') - self.assertEqual(Period(year=2007, month=1, freq='2M'), expected) - - self.assertRaises(ValueError, Period, datetime.now()) - self.assertRaises(ValueError, Period, datetime.now().date()) - self.assertRaises(ValueError, Period, 1.6, freq='D') - self.assertRaises(ValueError, Period, ordinal=1.6, freq='D') - self.assertRaises(ValueError, Period, ordinal=2, value=1, freq='D') - self.assertIs(Period(None), pd.NaT) - self.assertRaises(ValueError, Period, month=1) - - p = Period('2007-01-01', freq='D') - - result = Period(p, freq='A') - exp = Period('2007', freq='A') - self.assertEqual(result, exp) - - def test_constructor_infer_freq(self): - p = Period('2007-01-01') - self.assertEqual(p.freq, 'D') - - p = Period('2007-01-01 07') - self.assertEqual(p.freq, 'H') - - p = Period('2007-01-01 07:10') - self.assertEqual(p.freq, 'T') - - p = Period('2007-01-01 07:10:15') - self.assertEqual(p.freq, 'S') - - p = Period('2007-01-01 07:10:15.123') - self.assertEqual(p.freq, 'L') - - p = Period('2007-01-01 07:10:15.123000') - self.assertEqual(p.freq, 'L') - - p = Period('2007-01-01 07:10:15.123400') - self.assertEqual(p.freq, 'U') - - def test_badinput(self): - self.assertRaises(ValueError, Period, '-2000', 'A') - self.assertRaises(tslib.DateParseError, Period, '0', 'A') - self.assertRaises(tslib.DateParseError, Period, '1/1/-2000', 'A') - - def test_multiples(self): - result1 = Period('1989', freq='2A') - result2 = Period('1989', freq='A') - self.assertEqual(result1.ordinal, result2.ordinal) - self.assertEqual(result1.freqstr, '2A-DEC') - self.assertEqual(result2.freqstr, 'A-DEC') - self.assertEqual(result1.freq, offsets.YearEnd(2)) - self.assertEqual(result2.freq, offsets.YearEnd()) - - self.assertEqual((result1 + 1).ordinal, result1.ordinal + 2) - self.assertEqual((1 + result1).ordinal, result1.ordinal + 2) - self.assertEqual((result1 - 1).ordinal, result2.ordinal - 2) - self.assertEqual((-1 + result1).ordinal, result2.ordinal - 2) - - def test_round_trip(self): - - p = Period('2000Q1') - new_p = self.round_trip_pickle(p) - self.assertEqual(new_p, p) - - -class TestPeriodField(tm.TestCase): - - def test_get_period_field_raises_on_out_of_range(self): - self.assertRaises(ValueError, libperiod.get_period_field, -1, 0, 0) - - def test_get_period_field_array_raises_on_out_of_range(self): - self.assertRaises(ValueError, libperiod.get_period_field_arr, -1, - np.empty(1), 0) - - -class TestComparisons(tm.TestCase): - - def setUp(self): - self.january1 = Period('2000-01', 'M') - self.january2 = Period('2000-01', 'M') - self.february = Period('2000-02', 'M') - self.march = Period('2000-03', 'M') - self.day = Period('2012-01-01', 'D') - - def test_equal(self): - self.assertEqual(self.january1, self.january2) - - def test_equal_Raises_Value(self): - with tm.assertRaises(period.IncompatibleFrequency): - self.january1 == self.day - - def test_notEqual(self): - self.assertNotEqual(self.january1, 1) - self.assertNotEqual(self.january1, self.february) - - def test_greater(self): - self.assertTrue(self.february > self.january1) - - def test_greater_Raises_Value(self): - with tm.assertRaises(period.IncompatibleFrequency): - self.january1 > self.day - - def test_greater_Raises_Type(self): - with tm.assertRaises(TypeError): - self.january1 > 1 - - def test_greaterEqual(self): - self.assertTrue(self.january1 >= self.january2) - - def test_greaterEqual_Raises_Value(self): - with tm.assertRaises(period.IncompatibleFrequency): - self.january1 >= self.day - - with tm.assertRaises(TypeError): - print(self.january1 >= 1) - - def test_smallerEqual(self): - self.assertTrue(self.january1 <= self.january2) - - def test_smallerEqual_Raises_Value(self): - with tm.assertRaises(period.IncompatibleFrequency): - self.january1 <= self.day - - def test_smallerEqual_Raises_Type(self): - with tm.assertRaises(TypeError): - self.january1 <= 1 - - def test_smaller(self): - self.assertTrue(self.january1 < self.february) - - def test_smaller_Raises_Value(self): - with tm.assertRaises(period.IncompatibleFrequency): - self.january1 < self.day - - def test_smaller_Raises_Type(self): - with tm.assertRaises(TypeError): - self.january1 < 1 - - def test_sort(self): - periods = [self.march, self.january1, self.february] - correctPeriods = [self.january1, self.february, self.march] - self.assertEqual(sorted(periods), correctPeriods) - - def test_period_nat_comp(self): - p_nat = Period('NaT', freq='D') - p = Period('2011-01-01', freq='D') - - nat = pd.Timestamp('NaT') - t = pd.Timestamp('2011-01-01') - # confirm Period('NaT') work identical with Timestamp('NaT') - for left, right in [(p_nat, p), (p, p_nat), (p_nat, p_nat), (nat, t), - (t, nat), (nat, nat)]: - self.assertEqual(left < right, False) - self.assertEqual(left > right, False) - self.assertEqual(left == right, False) - self.assertEqual(left != right, True) - self.assertEqual(left <= right, False) - self.assertEqual(left >= right, False) - - -class TestMethods(tm.TestCase): - - def test_add(self): - dt1 = Period(freq='D', year=2008, month=1, day=1) - dt2 = Period(freq='D', year=2008, month=1, day=2) - self.assertEqual(dt1 + 1, dt2) - self.assertEqual(1 + dt1, dt2) - - def test_add_pdnat(self): - p = pd.Period('2011-01', freq='M') - self.assertIs(p + pd.NaT, pd.NaT) - self.assertIs(pd.NaT + p, pd.NaT) - - p = pd.Period('NaT', freq='M') - self.assertIs(p + pd.NaT, pd.NaT) - self.assertIs(pd.NaT + p, pd.NaT) - - def test_add_raises(self): - # GH 4731 - dt1 = Period(freq='D', year=2008, month=1, day=1) - dt2 = Period(freq='D', year=2008, month=1, day=2) - msg = r"unsupported operand type\(s\)" - with tm.assertRaisesRegexp(TypeError, msg): - dt1 + "str" - - msg = r"unsupported operand type\(s\)" - with tm.assertRaisesRegexp(TypeError, msg): - "str" + dt1 - - with tm.assertRaisesRegexp(TypeError, msg): - dt1 + dt2 - - def test_sub(self): - dt1 = Period('2011-01-01', freq='D') - dt2 = Period('2011-01-15', freq='D') - - self.assertEqual(dt1 - dt2, -14) - self.assertEqual(dt2 - dt1, 14) - - msg = r"Input has different freq=M from Period\(freq=D\)" - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - dt1 - pd.Period('2011-02', freq='M') - - def test_add_offset(self): - # freq is DateOffset - for freq in ['A', '2A', '3A']: - p = Period('2011', freq=freq) - exp = Period('2013', freq=freq) - self.assertEqual(p + offsets.YearEnd(2), exp) - self.assertEqual(offsets.YearEnd(2) + p, exp) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - with tm.assertRaises(period.IncompatibleFrequency): - p + o - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - with tm.assertRaises(period.IncompatibleFrequency): - o + p - - for freq in ['M', '2M', '3M']: - p = Period('2011-03', freq=freq) - exp = Period('2011-05', freq=freq) - self.assertEqual(p + offsets.MonthEnd(2), exp) - self.assertEqual(offsets.MonthEnd(2) + p, exp) - - exp = Period('2012-03', freq=freq) - self.assertEqual(p + offsets.MonthEnd(12), exp) - self.assertEqual(offsets.MonthEnd(12) + p, exp) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - with tm.assertRaises(period.IncompatibleFrequency): - p + o - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - with tm.assertRaises(period.IncompatibleFrequency): - o + p - - # freq is Tick - for freq in ['D', '2D', '3D']: - p = Period('2011-04-01', freq=freq) - - exp = Period('2011-04-06', freq=freq) - self.assertEqual(p + offsets.Day(5), exp) - self.assertEqual(offsets.Day(5) + p, exp) - - exp = Period('2011-04-02', freq=freq) - self.assertEqual(p + offsets.Hour(24), exp) - self.assertEqual(offsets.Hour(24) + p, exp) - - exp = Period('2011-04-03', freq=freq) - self.assertEqual(p + np.timedelta64(2, 'D'), exp) - with tm.assertRaises(TypeError): - np.timedelta64(2, 'D') + p - - exp = Period('2011-04-02', freq=freq) - self.assertEqual(p + np.timedelta64(3600 * 24, 's'), exp) - with tm.assertRaises(TypeError): - np.timedelta64(3600 * 24, 's') + p - - exp = Period('2011-03-30', freq=freq) - self.assertEqual(p + timedelta(-2), exp) - self.assertEqual(timedelta(-2) + p, exp) - - exp = Period('2011-04-03', freq=freq) - self.assertEqual(p + timedelta(hours=48), exp) - self.assertEqual(timedelta(hours=48) + p, exp) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(4, 'h'), - timedelta(hours=23)]: - with tm.assertRaises(period.IncompatibleFrequency): - p + o - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - with tm.assertRaises(period.IncompatibleFrequency): - o + p - - for freq in ['H', '2H', '3H']: - p = Period('2011-04-01 09:00', freq=freq) - - exp = Period('2011-04-03 09:00', freq=freq) - self.assertEqual(p + offsets.Day(2), exp) - self.assertEqual(offsets.Day(2) + p, exp) - - exp = Period('2011-04-01 12:00', freq=freq) - self.assertEqual(p + offsets.Hour(3), exp) - self.assertEqual(offsets.Hour(3) + p, exp) - - exp = Period('2011-04-01 12:00', freq=freq) - self.assertEqual(p + np.timedelta64(3, 'h'), exp) - with tm.assertRaises(TypeError): - np.timedelta64(3, 'h') + p - - exp = Period('2011-04-01 10:00', freq=freq) - self.assertEqual(p + np.timedelta64(3600, 's'), exp) - with tm.assertRaises(TypeError): - np.timedelta64(3600, 's') + p - - exp = Period('2011-04-01 11:00', freq=freq) - self.assertEqual(p + timedelta(minutes=120), exp) - self.assertEqual(timedelta(minutes=120) + p, exp) - - exp = Period('2011-04-05 12:00', freq=freq) - self.assertEqual(p + timedelta(days=4, minutes=180), exp) - self.assertEqual(timedelta(days=4, minutes=180) + p, exp) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(3200, 's'), - timedelta(hours=23, minutes=30)]: - with tm.assertRaises(period.IncompatibleFrequency): - p + o - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - with tm.assertRaises(period.IncompatibleFrequency): - o + p - - def test_add_offset_nat(self): - # freq is DateOffset - for freq in ['A', '2A', '3A']: - p = Period('NaT', freq=freq) - for o in [offsets.YearEnd(2)]: - self.assertIs(p + o, tslib.NaT) - self.assertIs(o + p, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - for freq in ['M', '2M', '3M']: - p = Period('NaT', freq=freq) - for o in [offsets.MonthEnd(2), offsets.MonthEnd(12)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - # freq is Tick - for freq in ['D', '2D', '3D']: - p = Period('NaT', freq=freq) - for o in [offsets.Day(5), offsets.Hour(24), np.timedelta64(2, 'D'), - np.timedelta64(3600 * 24, 's'), timedelta(-2), - timedelta(hours=48)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(4, 'h'), - timedelta(hours=23)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - for freq in ['H', '2H', '3H']: - p = Period('NaT', freq=freq) - for o in [offsets.Day(2), offsets.Hour(3), np.timedelta64(3, 'h'), - np.timedelta64(3600, 's'), timedelta(minutes=120), - timedelta(days=4, minutes=180)]: - self.assertIs(p + o, tslib.NaT) - - if not isinstance(o, np.timedelta64): - self.assertIs(o + p, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(3200, 's'), - timedelta(hours=23, minutes=30)]: - self.assertIs(p + o, tslib.NaT) - - if isinstance(o, np.timedelta64): - with tm.assertRaises(TypeError): - o + p - else: - self.assertIs(o + p, tslib.NaT) - - def test_sub_pdnat(self): - # GH 13071 - p = pd.Period('2011-01', freq='M') - self.assertIs(p - pd.NaT, pd.NaT) - self.assertIs(pd.NaT - p, pd.NaT) - - p = pd.Period('NaT', freq='M') - self.assertIs(p - pd.NaT, pd.NaT) - self.assertIs(pd.NaT - p, pd.NaT) - - def test_sub_offset(self): - # freq is DateOffset - for freq in ['A', '2A', '3A']: - p = Period('2011', freq=freq) - self.assertEqual(p - offsets.YearEnd(2), Period('2009', freq=freq)) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - with tm.assertRaises(period.IncompatibleFrequency): - p - o - - for freq in ['M', '2M', '3M']: - p = Period('2011-03', freq=freq) - self.assertEqual(p - offsets.MonthEnd(2), - Period('2011-01', freq=freq)) - self.assertEqual(p - offsets.MonthEnd(12), - Period('2010-03', freq=freq)) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - with tm.assertRaises(period.IncompatibleFrequency): - p - o - - # freq is Tick - for freq in ['D', '2D', '3D']: - p = Period('2011-04-01', freq=freq) - self.assertEqual(p - offsets.Day(5), - Period('2011-03-27', freq=freq)) - self.assertEqual(p - offsets.Hour(24), - Period('2011-03-31', freq=freq)) - self.assertEqual(p - np.timedelta64(2, 'D'), - Period('2011-03-30', freq=freq)) - self.assertEqual(p - np.timedelta64(3600 * 24, 's'), - Period('2011-03-31', freq=freq)) - self.assertEqual(p - timedelta(-2), - Period('2011-04-03', freq=freq)) - self.assertEqual(p - timedelta(hours=48), - Period('2011-03-30', freq=freq)) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(4, 'h'), - timedelta(hours=23)]: - with tm.assertRaises(period.IncompatibleFrequency): - p - o - - for freq in ['H', '2H', '3H']: - p = Period('2011-04-01 09:00', freq=freq) - self.assertEqual(p - offsets.Day(2), - Period('2011-03-30 09:00', freq=freq)) - self.assertEqual(p - offsets.Hour(3), - Period('2011-04-01 06:00', freq=freq)) - self.assertEqual(p - np.timedelta64(3, 'h'), - Period('2011-04-01 06:00', freq=freq)) - self.assertEqual(p - np.timedelta64(3600, 's'), - Period('2011-04-01 08:00', freq=freq)) - self.assertEqual(p - timedelta(minutes=120), - Period('2011-04-01 07:00', freq=freq)) - self.assertEqual(p - timedelta(days=4, minutes=180), - Period('2011-03-28 06:00', freq=freq)) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(3200, 's'), - timedelta(hours=23, minutes=30)]: - with tm.assertRaises(period.IncompatibleFrequency): - p - o - - def test_sub_offset_nat(self): - # freq is DateOffset - for freq in ['A', '2A', '3A']: - p = Period('NaT', freq=freq) - for o in [offsets.YearEnd(2)]: - self.assertIs(p - o, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - self.assertIs(p - o, tslib.NaT) - - for freq in ['M', '2M', '3M']: - p = Period('NaT', freq=freq) - for o in [offsets.MonthEnd(2), offsets.MonthEnd(12)]: - self.assertIs(p - o, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(365, 'D'), - timedelta(365)]: - self.assertIs(p - o, tslib.NaT) - - # freq is Tick - for freq in ['D', '2D', '3D']: - p = Period('NaT', freq=freq) - for o in [offsets.Day(5), offsets.Hour(24), np.timedelta64(2, 'D'), - np.timedelta64(3600 * 24, 's'), timedelta(-2), - timedelta(hours=48)]: - self.assertIs(p - o, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(4, 'h'), - timedelta(hours=23)]: - self.assertIs(p - o, tslib.NaT) - - for freq in ['H', '2H', '3H']: - p = Period('NaT', freq=freq) - for o in [offsets.Day(2), offsets.Hour(3), np.timedelta64(3, 'h'), - np.timedelta64(3600, 's'), timedelta(minutes=120), - timedelta(days=4, minutes=180)]: - self.assertIs(p - o, tslib.NaT) - - for o in [offsets.YearBegin(2), offsets.MonthBegin(1), - offsets.Minute(), np.timedelta64(3200, 's'), - timedelta(hours=23, minutes=30)]: - self.assertIs(p - o, tslib.NaT) - - def test_nat_ops(self): - for freq in ['M', '2M', '3M']: - p = Period('NaT', freq=freq) - self.assertIs(p + 1, tslib.NaT) - self.assertIs(1 + p, tslib.NaT) - self.assertIs(p - 1, tslib.NaT) - self.assertIs(p - Period('2011-01', freq=freq), tslib.NaT) - self.assertIs(Period('2011-01', freq=freq) - p, tslib.NaT) - - def test_period_ops_offset(self): - p = Period('2011-04-01', freq='D') - result = p + offsets.Day() - exp = pd.Period('2011-04-02', freq='D') - self.assertEqual(result, exp) - - result = p - offsets.Day(2) - exp = pd.Period('2011-03-30', freq='D') - self.assertEqual(result, exp) - - msg = r"Input cannot be converted to Period\(freq=D\)" - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - p + offsets.Hour(2) - - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - p - offsets.Hour(2) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py deleted file mode 100644 index c2b895925b685..0000000000000 --- a/pandas/tests/scalar/test_timedelta.py +++ /dev/null @@ -1,699 +0,0 @@ -""" test the scalar Timedelta """ -import numpy as np -from datetime import timedelta - -import pandas as pd -import pandas.util.testing as tm -from pandas.tseries.timedeltas import _coerce_scalar_to_timedelta_type as ct -from pandas import (Timedelta, TimedeltaIndex, timedelta_range, Series, - to_timedelta, compat) -from pandas._libs.tslib import iNaT, NaTType - - -class TestTimedeltas(tm.TestCase): - _multiprocess_can_split_ = True - - def setUp(self): - pass - - def test_construction(self): - - expected = np.timedelta64(10, 'D').astype('m8[ns]').view('i8') - self.assertEqual(Timedelta(10, unit='d').value, expected) - self.assertEqual(Timedelta(10.0, unit='d').value, expected) - self.assertEqual(Timedelta('10 days').value, expected) - self.assertEqual(Timedelta(days=10).value, expected) - self.assertEqual(Timedelta(days=10.0).value, expected) - - expected += np.timedelta64(10, 's').astype('m8[ns]').view('i8') - self.assertEqual(Timedelta('10 days 00:00:10').value, expected) - self.assertEqual(Timedelta(days=10, seconds=10).value, expected) - self.assertEqual( - Timedelta(days=10, milliseconds=10 * 1000).value, expected) - self.assertEqual( - Timedelta(days=10, microseconds=10 * 1000 * 1000).value, expected) - - # test construction with np dtypes - # GH 8757 - timedelta_kwargs = {'days': 'D', - 'seconds': 's', - 'microseconds': 'us', - 'milliseconds': 'ms', - 'minutes': 'm', - 'hours': 'h', - 'weeks': 'W'} - npdtypes = [np.int64, np.int32, np.int16, np.float64, np.float32, - np.float16] - for npdtype in npdtypes: - for pykwarg, npkwarg in timedelta_kwargs.items(): - expected = np.timedelta64(1, - npkwarg).astype('m8[ns]').view('i8') - self.assertEqual( - Timedelta(**{pykwarg: npdtype(1)}).value, expected) - - # rounding cases - self.assertEqual(Timedelta(82739999850000).value, 82739999850000) - self.assertTrue('0 days 22:58:59.999850' in str(Timedelta( - 82739999850000))) - self.assertEqual(Timedelta(123072001000000).value, 123072001000000) - self.assertTrue('1 days 10:11:12.001' in str(Timedelta( - 123072001000000))) - - # string conversion with/without leading zero - # GH 9570 - self.assertEqual(Timedelta('0:00:00'), timedelta(hours=0)) - self.assertEqual(Timedelta('00:00:00'), timedelta(hours=0)) - self.assertEqual(Timedelta('-1:00:00'), -timedelta(hours=1)) - self.assertEqual(Timedelta('-01:00:00'), -timedelta(hours=1)) - - # more strings & abbrevs - # GH 8190 - self.assertEqual(Timedelta('1 h'), timedelta(hours=1)) - self.assertEqual(Timedelta('1 hour'), timedelta(hours=1)) - self.assertEqual(Timedelta('1 hr'), timedelta(hours=1)) - self.assertEqual(Timedelta('1 hours'), timedelta(hours=1)) - self.assertEqual(Timedelta('-1 hours'), -timedelta(hours=1)) - self.assertEqual(Timedelta('1 m'), timedelta(minutes=1)) - self.assertEqual(Timedelta('1.5 m'), timedelta(seconds=90)) - self.assertEqual(Timedelta('1 minute'), timedelta(minutes=1)) - self.assertEqual(Timedelta('1 minutes'), timedelta(minutes=1)) - self.assertEqual(Timedelta('1 s'), timedelta(seconds=1)) - self.assertEqual(Timedelta('1 second'), timedelta(seconds=1)) - self.assertEqual(Timedelta('1 seconds'), timedelta(seconds=1)) - self.assertEqual(Timedelta('1 ms'), timedelta(milliseconds=1)) - self.assertEqual(Timedelta('1 milli'), timedelta(milliseconds=1)) - self.assertEqual(Timedelta('1 millisecond'), timedelta(milliseconds=1)) - self.assertEqual(Timedelta('1 us'), timedelta(microseconds=1)) - self.assertEqual(Timedelta('1 micros'), timedelta(microseconds=1)) - self.assertEqual(Timedelta('1 microsecond'), timedelta(microseconds=1)) - self.assertEqual(Timedelta('1.5 microsecond'), - Timedelta('00:00:00.000001500')) - self.assertEqual(Timedelta('1 ns'), Timedelta('00:00:00.000000001')) - self.assertEqual(Timedelta('1 nano'), Timedelta('00:00:00.000000001')) - self.assertEqual(Timedelta('1 nanosecond'), - Timedelta('00:00:00.000000001')) - - # combos - self.assertEqual(Timedelta('10 days 1 hour'), - timedelta(days=10, hours=1)) - self.assertEqual(Timedelta('10 days 1 h'), timedelta(days=10, hours=1)) - self.assertEqual(Timedelta('10 days 1 h 1m 1s'), timedelta( - days=10, hours=1, minutes=1, seconds=1)) - self.assertEqual(Timedelta('-10 days 1 h 1m 1s'), - - timedelta(days=10, hours=1, minutes=1, seconds=1)) - self.assertEqual(Timedelta('-10 days 1 h 1m 1s'), - - timedelta(days=10, hours=1, minutes=1, seconds=1)) - self.assertEqual(Timedelta('-10 days 1 h 1m 1s 3us'), - - timedelta(days=10, hours=1, minutes=1, - seconds=1, microseconds=3)) - self.assertEqual(Timedelta('-10 days 1 h 1.5m 1s 3us'), - - timedelta(days=10, hours=1, minutes=1, - seconds=31, microseconds=3)) - - # currently invalid as it has a - on the hhmmdd part (only allowed on - # the days) - self.assertRaises(ValueError, - lambda: Timedelta('-10 days -1 h 1.5m 1s 3us')) - - # only leading neg signs are allowed - self.assertRaises(ValueError, - lambda: Timedelta('10 days -1 h 1.5m 1s 3us')) - - # no units specified - self.assertRaises(ValueError, lambda: Timedelta('3.1415')) - - # invalid construction - tm.assertRaisesRegexp(ValueError, "cannot construct a Timedelta", - lambda: Timedelta()) - tm.assertRaisesRegexp(ValueError, "unit abbreviation w/o a number", - lambda: Timedelta('foo')) - tm.assertRaisesRegexp(ValueError, - "cannot construct a Timedelta from the passed " - "arguments, allowed keywords are ", - lambda: Timedelta(day=10)) - - # roundtripping both for string and value - for v in ['1s', '-1s', '1us', '-1us', '1 day', '-1 day', - '-23:59:59.999999', '-1 days +23:59:59.999999', '-1ns', - '1ns', '-23:59:59.999999999']: - - td = Timedelta(v) - self.assertEqual(Timedelta(td.value), td) - - # str does not normally display nanos - if not td.nanoseconds: - self.assertEqual(Timedelta(str(td)), td) - self.assertEqual(Timedelta(td._repr_base(format='all')), td) - - # floats - expected = np.timedelta64( - 10, 's').astype('m8[ns]').view('i8') + np.timedelta64( - 500, 'ms').astype('m8[ns]').view('i8') - self.assertEqual(Timedelta(10.5, unit='s').value, expected) - - # offset - self.assertEqual(to_timedelta(pd.offsets.Hour(2)), - Timedelta('0 days, 02:00:00')) - self.assertEqual(Timedelta(pd.offsets.Hour(2)), - Timedelta('0 days, 02:00:00')) - self.assertEqual(Timedelta(pd.offsets.Second(2)), - Timedelta('0 days, 00:00:02')) - - # unicode - # GH 11995 - expected = Timedelta('1H') - result = pd.Timedelta(u'1H') - self.assertEqual(result, expected) - self.assertEqual(to_timedelta(pd.offsets.Hour(2)), - Timedelta(u'0 days, 02:00:00')) - - self.assertRaises(ValueError, lambda: Timedelta(u'foo bar')) - - def test_overflow_on_construction(self): - # xref https://github.com/statsmodels/statsmodels/issues/3374 - value = pd.Timedelta('1day').value * 20169940 - self.assertRaises(OverflowError, pd.Timedelta, value) - - def test_total_seconds_scalar(self): - # GH 10939 - rng = Timedelta('1 days, 10:11:12.100123456') - expt = 1 * 86400 + 10 * 3600 + 11 * 60 + 12 + 100123456. / 1e9 - tm.assert_almost_equal(rng.total_seconds(), expt) - - rng = Timedelta(np.nan) - self.assertTrue(np.isnan(rng.total_seconds())) - - def test_repr(self): - - self.assertEqual(repr(Timedelta(10, unit='d')), - "Timedelta('10 days 00:00:00')") - self.assertEqual(repr(Timedelta(10, unit='s')), - "Timedelta('0 days 00:00:10')") - self.assertEqual(repr(Timedelta(10, unit='ms')), - "Timedelta('0 days 00:00:00.010000')") - self.assertEqual(repr(Timedelta(-10, unit='ms')), - "Timedelta('-1 days +23:59:59.990000')") - - def test_conversion(self): - - for td in [Timedelta(10, unit='d'), - Timedelta('1 days, 10:11:12.012345')]: - pydt = td.to_pytimedelta() - self.assertTrue(td == Timedelta(pydt)) - self.assertEqual(td, pydt) - self.assertTrue(isinstance(pydt, timedelta) and not isinstance( - pydt, Timedelta)) - - self.assertEqual(td, np.timedelta64(td.value, 'ns')) - td64 = td.to_timedelta64() - self.assertEqual(td64, np.timedelta64(td.value, 'ns')) - self.assertEqual(td, td64) - self.assertTrue(isinstance(td64, np.timedelta64)) - - # this is NOT equal and cannot be roundtriped (because of the nanos) - td = Timedelta('1 days, 10:11:12.012345678') - self.assertTrue(td != td.to_pytimedelta()) - - def test_freq_conversion(self): - - td = Timedelta('1 days 2 hours 3 ns') - result = td / np.timedelta64(1, 'D') - self.assertEqual(result, td.value / float(86400 * 1e9)) - result = td / np.timedelta64(1, 's') - self.assertEqual(result, td.value / float(1e9)) - result = td / np.timedelta64(1, 'ns') - self.assertEqual(result, td.value) - - def test_fields(self): - def check(value): - # that we are int/long like - self.assertTrue(isinstance(value, (int, compat.long))) - - # compat to datetime.timedelta - rng = to_timedelta('1 days, 10:11:12') - self.assertEqual(rng.days, 1) - self.assertEqual(rng.seconds, 10 * 3600 + 11 * 60 + 12) - self.assertEqual(rng.microseconds, 0) - self.assertEqual(rng.nanoseconds, 0) - - self.assertRaises(AttributeError, lambda: rng.hours) - self.assertRaises(AttributeError, lambda: rng.minutes) - self.assertRaises(AttributeError, lambda: rng.milliseconds) - - # GH 10050 - check(rng.days) - check(rng.seconds) - check(rng.microseconds) - check(rng.nanoseconds) - - td = Timedelta('-1 days, 10:11:12') - self.assertEqual(abs(td), Timedelta('13:48:48')) - self.assertTrue(str(td) == "-1 days +10:11:12") - self.assertEqual(-td, Timedelta('0 days 13:48:48')) - self.assertEqual(-Timedelta('-1 days, 10:11:12').value, 49728000000000) - self.assertEqual(Timedelta('-1 days, 10:11:12').value, -49728000000000) - - rng = to_timedelta('-1 days, 10:11:12.100123456') - self.assertEqual(rng.days, -1) - self.assertEqual(rng.seconds, 10 * 3600 + 11 * 60 + 12) - self.assertEqual(rng.microseconds, 100 * 1000 + 123) - self.assertEqual(rng.nanoseconds, 456) - self.assertRaises(AttributeError, lambda: rng.hours) - self.assertRaises(AttributeError, lambda: rng.minutes) - self.assertRaises(AttributeError, lambda: rng.milliseconds) - - # components - tup = pd.to_timedelta(-1, 'us').components - self.assertEqual(tup.days, -1) - self.assertEqual(tup.hours, 23) - self.assertEqual(tup.minutes, 59) - self.assertEqual(tup.seconds, 59) - self.assertEqual(tup.milliseconds, 999) - self.assertEqual(tup.microseconds, 999) - self.assertEqual(tup.nanoseconds, 0) - - # GH 10050 - check(tup.days) - check(tup.hours) - check(tup.minutes) - check(tup.seconds) - check(tup.milliseconds) - check(tup.microseconds) - check(tup.nanoseconds) - - tup = Timedelta('-1 days 1 us').components - self.assertEqual(tup.days, -2) - self.assertEqual(tup.hours, 23) - self.assertEqual(tup.minutes, 59) - self.assertEqual(tup.seconds, 59) - self.assertEqual(tup.milliseconds, 999) - self.assertEqual(tup.microseconds, 999) - self.assertEqual(tup.nanoseconds, 0) - - def test_nat_converters(self): - self.assertEqual(to_timedelta( - 'nat', box=False).astype('int64'), iNaT) - self.assertEqual(to_timedelta( - 'nan', box=False).astype('int64'), iNaT) - - def testit(unit, transform): - - # array - result = to_timedelta(np.arange(5), unit=unit) - expected = TimedeltaIndex([np.timedelta64(i, transform(unit)) - for i in np.arange(5).tolist()]) - tm.assert_index_equal(result, expected) - - # scalar - result = to_timedelta(2, unit=unit) - expected = Timedelta(np.timedelta64(2, transform(unit)).astype( - 'timedelta64[ns]')) - self.assertEqual(result, expected) - - # validate all units - # GH 6855 - for unit in ['Y', 'M', 'W', 'D', 'y', 'w', 'd']: - testit(unit, lambda x: x.upper()) - for unit in ['days', 'day', 'Day', 'Days']: - testit(unit, lambda x: 'D') - for unit in ['h', 'm', 's', 'ms', 'us', 'ns', 'H', 'S', 'MS', 'US', - 'NS']: - testit(unit, lambda x: x.lower()) - - # offsets - - # m - testit('T', lambda x: 'm') - - # ms - testit('L', lambda x: 'ms') - - def test_numeric_conversions(self): - self.assertEqual(ct(0), np.timedelta64(0, 'ns')) - self.assertEqual(ct(10), np.timedelta64(10, 'ns')) - self.assertEqual(ct(10, unit='ns'), np.timedelta64( - 10, 'ns').astype('m8[ns]')) - - self.assertEqual(ct(10, unit='us'), np.timedelta64( - 10, 'us').astype('m8[ns]')) - self.assertEqual(ct(10, unit='ms'), np.timedelta64( - 10, 'ms').astype('m8[ns]')) - self.assertEqual(ct(10, unit='s'), np.timedelta64( - 10, 's').astype('m8[ns]')) - self.assertEqual(ct(10, unit='d'), np.timedelta64( - 10, 'D').astype('m8[ns]')) - - def test_timedelta_conversions(self): - self.assertEqual(ct(timedelta(seconds=1)), - np.timedelta64(1, 's').astype('m8[ns]')) - self.assertEqual(ct(timedelta(microseconds=1)), - np.timedelta64(1, 'us').astype('m8[ns]')) - self.assertEqual(ct(timedelta(days=1)), - np.timedelta64(1, 'D').astype('m8[ns]')) - - def test_round(self): - - t1 = Timedelta('1 days 02:34:56.789123456') - t2 = Timedelta('-1 days 02:34:56.789123456') - - for (freq, s1, s2) in [('N', t1, t2), - ('U', Timedelta('1 days 02:34:56.789123000'), - Timedelta('-1 days 02:34:56.789123000')), - ('L', Timedelta('1 days 02:34:56.789000000'), - Timedelta('-1 days 02:34:56.789000000')), - ('S', Timedelta('1 days 02:34:57'), - Timedelta('-1 days 02:34:57')), - ('2S', Timedelta('1 days 02:34:56'), - Timedelta('-1 days 02:34:56')), - ('5S', Timedelta('1 days 02:34:55'), - Timedelta('-1 days 02:34:55')), - ('T', Timedelta('1 days 02:35:00'), - Timedelta('-1 days 02:35:00')), - ('12T', Timedelta('1 days 02:36:00'), - Timedelta('-1 days 02:36:00')), - ('H', Timedelta('1 days 03:00:00'), - Timedelta('-1 days 03:00:00')), - ('d', Timedelta('1 days'), - Timedelta('-1 days'))]: - r1 = t1.round(freq) - self.assertEqual(r1, s1) - r2 = t2.round(freq) - self.assertEqual(r2, s2) - - # invalid - for freq in ['Y', 'M', 'foobar']: - self.assertRaises(ValueError, lambda: t1.round(freq)) - - t1 = timedelta_range('1 days', periods=3, freq='1 min 2 s 3 us') - t2 = -1 * t1 - t1a = timedelta_range('1 days', periods=3, freq='1 min 2 s') - t1c = pd.TimedeltaIndex([1, 1, 1], unit='D') - - # note that negative times round DOWN! so don't give whole numbers - for (freq, s1, s2) in [('N', t1, t2), - ('U', t1, t2), - ('L', t1a, - TimedeltaIndex(['-1 days +00:00:00', - '-2 days +23:58:58', - '-2 days +23:57:56'], - dtype='timedelta64[ns]', - freq=None) - ), - ('S', t1a, - TimedeltaIndex(['-1 days +00:00:00', - '-2 days +23:58:58', - '-2 days +23:57:56'], - dtype='timedelta64[ns]', - freq=None) - ), - ('12T', t1c, - TimedeltaIndex(['-1 days', - '-1 days', - '-1 days'], - dtype='timedelta64[ns]', - freq=None) - ), - ('H', t1c, - TimedeltaIndex(['-1 days', - '-1 days', - '-1 days'], - dtype='timedelta64[ns]', - freq=None) - ), - ('d', t1c, - pd.TimedeltaIndex([-1, -1, -1], unit='D') - )]: - - r1 = t1.round(freq) - tm.assert_index_equal(r1, s1) - r2 = t2.round(freq) - tm.assert_index_equal(r2, s2) - - # invalid - for freq in ['Y', 'M', 'foobar']: - self.assertRaises(ValueError, lambda: t1.round(freq)) - - def test_contains(self): - # Checking for any NaT-like objects - # GH 13603 - td = to_timedelta(range(5), unit='d') + pd.offsets.Hour(1) - for v in [pd.NaT, None, float('nan'), np.nan]: - self.assertFalse((v in td)) - - td = to_timedelta([pd.NaT]) - for v in [pd.NaT, None, float('nan'), np.nan]: - self.assertTrue((v in td)) - - def test_identity(self): - - td = Timedelta(10, unit='d') - self.assertTrue(isinstance(td, Timedelta)) - self.assertTrue(isinstance(td, timedelta)) - - def test_short_format_converters(self): - def conv(v): - return v.astype('m8[ns]') - - self.assertEqual(ct('10'), np.timedelta64(10, 'ns')) - self.assertEqual(ct('10ns'), np.timedelta64(10, 'ns')) - self.assertEqual(ct('100'), np.timedelta64(100, 'ns')) - self.assertEqual(ct('100ns'), np.timedelta64(100, 'ns')) - - self.assertEqual(ct('1000'), np.timedelta64(1000, 'ns')) - self.assertEqual(ct('1000ns'), np.timedelta64(1000, 'ns')) - self.assertEqual(ct('1000NS'), np.timedelta64(1000, 'ns')) - - self.assertEqual(ct('10us'), np.timedelta64(10000, 'ns')) - self.assertEqual(ct('100us'), np.timedelta64(100000, 'ns')) - self.assertEqual(ct('1000us'), np.timedelta64(1000000, 'ns')) - self.assertEqual(ct('1000Us'), np.timedelta64(1000000, 'ns')) - self.assertEqual(ct('1000uS'), np.timedelta64(1000000, 'ns')) - - self.assertEqual(ct('1ms'), np.timedelta64(1000000, 'ns')) - self.assertEqual(ct('10ms'), np.timedelta64(10000000, 'ns')) - self.assertEqual(ct('100ms'), np.timedelta64(100000000, 'ns')) - self.assertEqual(ct('1000ms'), np.timedelta64(1000000000, 'ns')) - - self.assertEqual(ct('-1s'), -np.timedelta64(1000000000, 'ns')) - self.assertEqual(ct('1s'), np.timedelta64(1000000000, 'ns')) - self.assertEqual(ct('10s'), np.timedelta64(10000000000, 'ns')) - self.assertEqual(ct('100s'), np.timedelta64(100000000000, 'ns')) - self.assertEqual(ct('1000s'), np.timedelta64(1000000000000, 'ns')) - - self.assertEqual(ct('1d'), conv(np.timedelta64(1, 'D'))) - self.assertEqual(ct('-1d'), -conv(np.timedelta64(1, 'D'))) - self.assertEqual(ct('1D'), conv(np.timedelta64(1, 'D'))) - self.assertEqual(ct('10D'), conv(np.timedelta64(10, 'D'))) - self.assertEqual(ct('100D'), conv(np.timedelta64(100, 'D'))) - self.assertEqual(ct('1000D'), conv(np.timedelta64(1000, 'D'))) - self.assertEqual(ct('10000D'), conv(np.timedelta64(10000, 'D'))) - - # space - self.assertEqual(ct(' 10000D '), conv(np.timedelta64(10000, 'D'))) - self.assertEqual(ct(' - 10000D '), -conv(np.timedelta64(10000, 'D'))) - - # invalid - self.assertRaises(ValueError, ct, '1foo') - self.assertRaises(ValueError, ct, 'foo') - - def test_full_format_converters(self): - def conv(v): - return v.astype('m8[ns]') - - d1 = np.timedelta64(1, 'D') - - self.assertEqual(ct('1days'), conv(d1)) - self.assertEqual(ct('1days,'), conv(d1)) - self.assertEqual(ct('- 1days,'), -conv(d1)) - - self.assertEqual(ct('00:00:01'), conv(np.timedelta64(1, 's'))) - self.assertEqual(ct('06:00:01'), conv( - np.timedelta64(6 * 3600 + 1, 's'))) - self.assertEqual(ct('06:00:01.0'), conv( - np.timedelta64(6 * 3600 + 1, 's'))) - self.assertEqual(ct('06:00:01.01'), conv( - np.timedelta64(1000 * (6 * 3600 + 1) + 10, 'ms'))) - - self.assertEqual(ct('- 1days, 00:00:01'), - conv(-d1 + np.timedelta64(1, 's'))) - self.assertEqual(ct('1days, 06:00:01'), conv( - d1 + np.timedelta64(6 * 3600 + 1, 's'))) - self.assertEqual(ct('1days, 06:00:01.01'), conv( - d1 + np.timedelta64(1000 * (6 * 3600 + 1) + 10, 'ms'))) - - # invalid - self.assertRaises(ValueError, ct, '- 1days, 00') - - def test_overflow(self): - # GH 9442 - s = Series(pd.date_range('20130101', periods=100000, freq='H')) - s[0] += pd.Timedelta('1s 1ms') - - # mean - result = (s - s.min()).mean() - expected = pd.Timedelta((pd.DatetimeIndex((s - s.min())).asi8 / len(s) - ).sum()) - - # the computation is converted to float so might be some loss of - # precision - self.assertTrue(np.allclose(result.value / 1000, expected.value / - 1000)) - - # sum - self.assertRaises(ValueError, lambda: (s - s.min()).sum()) - s1 = s[0:10000] - self.assertRaises(ValueError, lambda: (s1 - s1.min()).sum()) - s2 = s[0:1000] - result = (s2 - s2.min()).sum() - - def test_pickle(self): - - v = Timedelta('1 days 10:11:12.0123456') - v_p = self.round_trip_pickle(v) - self.assertEqual(v, v_p) - - def test_timedelta_hash_equality(self): - # GH 11129 - v = Timedelta(1, 'D') - td = timedelta(days=1) - self.assertEqual(hash(v), hash(td)) - - d = {td: 2} - self.assertEqual(d[v], 2) - - tds = timedelta_range('1 second', periods=20) - self.assertTrue(all(hash(td) == hash(td.to_pytimedelta()) for td in - tds)) - - # python timedeltas drop ns resolution - ns_td = Timedelta(1, 'ns') - self.assertNotEqual(hash(ns_td), hash(ns_td.to_pytimedelta())) - - def test_implementation_limits(self): - min_td = Timedelta(Timedelta.min) - max_td = Timedelta(Timedelta.max) - - # GH 12727 - # timedelta limits correspond to int64 boundaries - self.assertTrue(min_td.value == np.iinfo(np.int64).min + 1) - self.assertTrue(max_td.value == np.iinfo(np.int64).max) - - # Beyond lower limit, a NAT before the Overflow - self.assertIsInstance(min_td - Timedelta(1, 'ns'), - NaTType) - - with tm.assertRaises(OverflowError): - min_td - Timedelta(2, 'ns') - - with tm.assertRaises(OverflowError): - max_td + Timedelta(1, 'ns') - - # Same tests using the internal nanosecond values - td = Timedelta(min_td.value - 1, 'ns') - self.assertIsInstance(td, NaTType) - - with tm.assertRaises(OverflowError): - Timedelta(min_td.value - 2, 'ns') - - with tm.assertRaises(OverflowError): - Timedelta(max_td.value + 1, 'ns') - - def test_timedelta_arithmetic(self): - data = pd.Series(['nat', '32 days'], dtype='timedelta64[ns]') - deltas = [timedelta(days=1), Timedelta(1, unit='D')] - for delta in deltas: - result_method = data.add(delta) - result_operator = data + delta - expected = pd.Series(['nat', '33 days'], dtype='timedelta64[ns]') - tm.assert_series_equal(result_operator, expected) - tm.assert_series_equal(result_method, expected) - - result_method = data.sub(delta) - result_operator = data - delta - expected = pd.Series(['nat', '31 days'], dtype='timedelta64[ns]') - tm.assert_series_equal(result_operator, expected) - tm.assert_series_equal(result_method, expected) - # GH 9396 - result_method = data.div(delta) - result_operator = data / delta - expected = pd.Series([np.nan, 32.], dtype='float64') - tm.assert_series_equal(result_operator, expected) - tm.assert_series_equal(result_method, expected) - - def test_apply_to_timedelta(self): - timedelta_NaT = pd.to_timedelta('NaT') - - list_of_valid_strings = ['00:00:01', '00:00:02'] - a = pd.to_timedelta(list_of_valid_strings) - b = Series(list_of_valid_strings).apply(pd.to_timedelta) - # Can't compare until apply on a Series gives the correct dtype - # assert_series_equal(a, b) - - list_of_strings = ['00:00:01', np.nan, pd.NaT, timedelta_NaT] - - # TODO: unused? - a = pd.to_timedelta(list_of_strings) # noqa - b = Series(list_of_strings).apply(pd.to_timedelta) # noqa - # Can't compare until apply on a Series gives the correct dtype - # assert_series_equal(a, b) - - def test_components(self): - rng = timedelta_range('1 days, 10:11:12', periods=2, freq='s') - rng.components - - # with nat - s = Series(rng) - s[1] = np.nan - - result = s.dt.components - self.assertFalse(result.iloc[0].isnull().all()) - self.assertTrue(result.iloc[1].isnull().all()) - - def test_isoformat(self): - td = Timedelta(days=6, minutes=50, seconds=3, - milliseconds=10, microseconds=10, nanoseconds=12) - expected = 'P6DT0H50M3.010010012S' - result = td.isoformat() - self.assertEqual(result, expected) - - td = Timedelta(days=4, hours=12, minutes=30, seconds=5) - result = td.isoformat() - expected = 'P4DT12H30M5S' - self.assertEqual(result, expected) - - td = Timedelta(nanoseconds=123) - result = td.isoformat() - expected = 'P0DT0H0M0.000000123S' - self.assertEqual(result, expected) - - # trim nano - td = Timedelta(microseconds=10) - result = td.isoformat() - expected = 'P0DT0H0M0.00001S' - self.assertEqual(result, expected) - - # trim micro - td = Timedelta(milliseconds=1) - result = td.isoformat() - expected = 'P0DT0H0M0.001S' - self.assertEqual(result, expected) - - # don't strip every 0 - result = Timedelta(minutes=1).isoformat() - expected = 'P0DT0H1M0S' - self.assertEqual(result, expected) - - def test_ops_error_str(self): - # GH 13624 - td = Timedelta('1 day') - - for l, r in [(td, 'a'), ('a', td)]: - - with tm.assertRaises(TypeError): - l + r - - with tm.assertRaises(TypeError): - l > r - - self.assertFalse(l == r) - self.assertTrue(l != r) diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py deleted file mode 100644 index e39375141ad5f..0000000000000 --- a/pandas/tests/scalar/test_timestamp.py +++ /dev/null @@ -1,1527 +0,0 @@ -""" test the scalar Timestamp """ - -import sys -import operator -import calendar -import numpy as np -from datetime import datetime, timedelta -from distutils.version import LooseVersion - -import pandas.util.testing as tm -from pandas.tseries import offsets, frequencies -from pandas._libs import tslib, period -from pandas._libs.tslib import get_timezone - -from pandas.compat import lrange, long -from pandas.util.testing import assert_series_equal -from pandas.compat.numpy import np_datetime64_compat -from pandas import (Timestamp, date_range, Period, Timedelta, compat, - Series, NaT, DataFrame, DatetimeIndex) -from pandas.tseries.frequencies import (RESO_DAY, RESO_HR, RESO_MIN, RESO_US, - RESO_MS, RESO_SEC) - - -class TestTimestamp(tm.TestCase): - - def test_constructor(self): - base_str = '2014-07-01 09:00' - base_dt = datetime(2014, 7, 1, 9) - base_expected = 1404205200000000000 - - # confirm base representation is correct - import calendar - self.assertEqual(calendar.timegm(base_dt.timetuple()) * 1000000000, - base_expected) - - tests = [(base_str, base_dt, base_expected), - ('2014-07-01 10:00', datetime(2014, 7, 1, 10), - base_expected + 3600 * 1000000000), - ('2014-07-01 09:00:00.000008000', - datetime(2014, 7, 1, 9, 0, 0, 8), - base_expected + 8000), - ('2014-07-01 09:00:00.000000005', - Timestamp('2014-07-01 09:00:00.000000005'), - base_expected + 5)] - - tm._skip_if_no_pytz() - tm._skip_if_no_dateutil() - import pytz - import dateutil - timezones = [(None, 0), ('UTC', 0), (pytz.utc, 0), ('Asia/Tokyo', 9), - ('US/Eastern', -4), ('dateutil/US/Pacific', -7), - (pytz.FixedOffset(-180), -3), - (dateutil.tz.tzoffset(None, 18000), 5)] - - for date_str, date, expected in tests: - for result in [Timestamp(date_str), Timestamp(date)]: - # only with timestring - self.assertEqual(result.value, expected) - self.assertEqual(tslib.pydt_to_i8(result), expected) - - # re-creation shouldn't affect to internal value - result = Timestamp(result) - self.assertEqual(result.value, expected) - self.assertEqual(tslib.pydt_to_i8(result), expected) - - # with timezone - for tz, offset in timezones: - for result in [Timestamp(date_str, tz=tz), Timestamp(date, - tz=tz)]: - expected_tz = expected - offset * 3600 * 1000000000 - self.assertEqual(result.value, expected_tz) - self.assertEqual(tslib.pydt_to_i8(result), expected_tz) - - # should preserve tz - result = Timestamp(result) - self.assertEqual(result.value, expected_tz) - self.assertEqual(tslib.pydt_to_i8(result), expected_tz) - - # should convert to UTC - result = Timestamp(result, tz='UTC') - expected_utc = expected - offset * 3600 * 1000000000 - self.assertEqual(result.value, expected_utc) - self.assertEqual(tslib.pydt_to_i8(result), expected_utc) - - def test_constructor_with_stringoffset(self): - # GH 7833 - base_str = '2014-07-01 11:00:00+02:00' - base_dt = datetime(2014, 7, 1, 9) - base_expected = 1404205200000000000 - - # confirm base representation is correct - import calendar - self.assertEqual(calendar.timegm(base_dt.timetuple()) * 1000000000, - base_expected) - - tests = [(base_str, base_expected), - ('2014-07-01 12:00:00+02:00', - base_expected + 3600 * 1000000000), - ('2014-07-01 11:00:00.000008000+02:00', base_expected + 8000), - ('2014-07-01 11:00:00.000000005+02:00', base_expected + 5)] - - tm._skip_if_no_pytz() - tm._skip_if_no_dateutil() - import pytz - import dateutil - timezones = [(None, 0), ('UTC', 0), (pytz.utc, 0), ('Asia/Tokyo', 9), - ('US/Eastern', -4), ('dateutil/US/Pacific', -7), - (pytz.FixedOffset(-180), -3), - (dateutil.tz.tzoffset(None, 18000), 5)] - - for date_str, expected in tests: - for result in [Timestamp(date_str)]: - # only with timestring - self.assertEqual(result.value, expected) - self.assertEqual(tslib.pydt_to_i8(result), expected) - - # re-creation shouldn't affect to internal value - result = Timestamp(result) - self.assertEqual(result.value, expected) - self.assertEqual(tslib.pydt_to_i8(result), expected) - - # with timezone - for tz, offset in timezones: - result = Timestamp(date_str, tz=tz) - expected_tz = expected - self.assertEqual(result.value, expected_tz) - self.assertEqual(tslib.pydt_to_i8(result), expected_tz) - - # should preserve tz - result = Timestamp(result) - self.assertEqual(result.value, expected_tz) - self.assertEqual(tslib.pydt_to_i8(result), expected_tz) - - # should convert to UTC - result = Timestamp(result, tz='UTC') - expected_utc = expected - self.assertEqual(result.value, expected_utc) - self.assertEqual(tslib.pydt_to_i8(result), expected_utc) - - # This should be 2013-11-01 05:00 in UTC - # converted to Chicago tz - result = Timestamp('2013-11-01 00:00:00-0500', tz='America/Chicago') - self.assertEqual(result.value, Timestamp('2013-11-01 05:00').value) - expected = "Timestamp('2013-11-01 00:00:00-0500', tz='America/Chicago')" # noqa - self.assertEqual(repr(result), expected) - self.assertEqual(result, eval(repr(result))) - - # This should be 2013-11-01 05:00 in UTC - # converted to Tokyo tz (+09:00) - result = Timestamp('2013-11-01 00:00:00-0500', tz='Asia/Tokyo') - self.assertEqual(result.value, Timestamp('2013-11-01 05:00').value) - expected = "Timestamp('2013-11-01 14:00:00+0900', tz='Asia/Tokyo')" - self.assertEqual(repr(result), expected) - self.assertEqual(result, eval(repr(result))) - - # GH11708 - # This should be 2015-11-18 10:00 in UTC - # converted to Asia/Katmandu - result = Timestamp("2015-11-18 15:45:00+05:45", tz="Asia/Katmandu") - self.assertEqual(result.value, Timestamp("2015-11-18 10:00").value) - expected = "Timestamp('2015-11-18 15:45:00+0545', tz='Asia/Katmandu')" - self.assertEqual(repr(result), expected) - self.assertEqual(result, eval(repr(result))) - - # This should be 2015-11-18 10:00 in UTC - # converted to Asia/Kolkata - result = Timestamp("2015-11-18 15:30:00+05:30", tz="Asia/Kolkata") - self.assertEqual(result.value, Timestamp("2015-11-18 10:00").value) - expected = "Timestamp('2015-11-18 15:30:00+0530', tz='Asia/Kolkata')" - self.assertEqual(repr(result), expected) - self.assertEqual(result, eval(repr(result))) - - def test_constructor_invalid(self): - with tm.assertRaisesRegexp(TypeError, 'Cannot convert input'): - Timestamp(slice(2)) - with tm.assertRaisesRegexp(ValueError, 'Cannot convert Period'): - Timestamp(Period('1000-01-01')) - - def test_constructor_positional(self): - # GH 10758 - with tm.assertRaises(TypeError): - Timestamp(2000, 1) - with tm.assertRaises(ValueError): - Timestamp(2000, 0, 1) - with tm.assertRaises(ValueError): - Timestamp(2000, 13, 1) - with tm.assertRaises(ValueError): - Timestamp(2000, 1, 0) - with tm.assertRaises(ValueError): - Timestamp(2000, 1, 32) - - # GH 11630 - self.assertEqual( - repr(Timestamp(2015, 11, 12)), - repr(Timestamp('20151112'))) - - self.assertEqual( - repr(Timestamp(2015, 11, 12, 1, 2, 3, 999999)), - repr(Timestamp('2015-11-12 01:02:03.999999'))) - - def test_constructor_keyword(self): - # GH 10758 - with tm.assertRaises(TypeError): - Timestamp(year=2000, month=1) - with tm.assertRaises(ValueError): - Timestamp(year=2000, month=0, day=1) - with tm.assertRaises(ValueError): - Timestamp(year=2000, month=13, day=1) - with tm.assertRaises(ValueError): - Timestamp(year=2000, month=1, day=0) - with tm.assertRaises(ValueError): - Timestamp(year=2000, month=1, day=32) - - self.assertEqual( - repr(Timestamp(year=2015, month=11, day=12)), - repr(Timestamp('20151112'))) - - self.assertEqual( - repr(Timestamp(year=2015, month=11, day=12, - hour=1, minute=2, second=3, microsecond=999999)), - repr(Timestamp('2015-11-12 01:02:03.999999'))) - - def test_constructor_fromordinal(self): - base = datetime(2000, 1, 1) - - ts = Timestamp.fromordinal(base.toordinal(), freq='D') - self.assertEqual(base, ts) - self.assertEqual(ts.freq, 'D') - self.assertEqual(base.toordinal(), ts.toordinal()) - - ts = Timestamp.fromordinal(base.toordinal(), tz='US/Eastern') - self.assertEqual(Timestamp('2000-01-01', tz='US/Eastern'), ts) - self.assertEqual(base.toordinal(), ts.toordinal()) - - def test_constructor_offset_depr(self): - # GH 12160 - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - ts = Timestamp('2011-01-01', offset='D') - self.assertEqual(ts.freq, 'D') - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - self.assertEqual(ts.offset, 'D') - - msg = "Can only specify freq or offset, not both" - with tm.assertRaisesRegexp(TypeError, msg): - Timestamp('2011-01-01', offset='D', freq='D') - - def test_constructor_offset_depr_fromordinal(self): - # GH 12160 - base = datetime(2000, 1, 1) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - ts = Timestamp.fromordinal(base.toordinal(), offset='D') - self.assertEqual(Timestamp('2000-01-01'), ts) - self.assertEqual(ts.freq, 'D') - self.assertEqual(base.toordinal(), ts.toordinal()) - - msg = "Can only specify freq or offset, not both" - with tm.assertRaisesRegexp(TypeError, msg): - Timestamp.fromordinal(base.toordinal(), offset='D', freq='D') - - def test_conversion(self): - # GH 9255 - ts = Timestamp('2000-01-01') - - result = ts.to_pydatetime() - expected = datetime(2000, 1, 1) - self.assertEqual(result, expected) - self.assertEqual(type(result), type(expected)) - - result = ts.to_datetime64() - expected = np.datetime64(ts.value, 'ns') - self.assertEqual(result, expected) - self.assertEqual(type(result), type(expected)) - self.assertEqual(result.dtype, expected.dtype) - - def test_repr(self): - tm._skip_if_no_pytz() - tm._skip_if_no_dateutil() - - dates = ['2014-03-07', '2014-01-01 09:00', - '2014-01-01 00:00:00.000000001'] - - # dateutil zone change (only matters for repr) - import dateutil - if (dateutil.__version__ >= LooseVersion('2.3') and - (dateutil.__version__ <= LooseVersion('2.4.0') or - dateutil.__version__ >= LooseVersion('2.6.0'))): - timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern', - 'dateutil/US/Pacific'] - else: - timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern', - 'dateutil/America/Los_Angeles'] - - freqs = ['D', 'M', 'S', 'N'] - - for date in dates: - for tz in timezones: - for freq in freqs: - - # avoid to match with timezone name - freq_repr = "'{0}'".format(freq) - if tz.startswith('dateutil'): - tz_repr = tz.replace('dateutil', '') - else: - tz_repr = tz - - date_only = Timestamp(date) - self.assertIn(date, repr(date_only)) - self.assertNotIn(tz_repr, repr(date_only)) - self.assertNotIn(freq_repr, repr(date_only)) - self.assertEqual(date_only, eval(repr(date_only))) - - date_tz = Timestamp(date, tz=tz) - self.assertIn(date, repr(date_tz)) - self.assertIn(tz_repr, repr(date_tz)) - self.assertNotIn(freq_repr, repr(date_tz)) - self.assertEqual(date_tz, eval(repr(date_tz))) - - date_freq = Timestamp(date, freq=freq) - self.assertIn(date, repr(date_freq)) - self.assertNotIn(tz_repr, repr(date_freq)) - self.assertIn(freq_repr, repr(date_freq)) - self.assertEqual(date_freq, eval(repr(date_freq))) - - date_tz_freq = Timestamp(date, tz=tz, freq=freq) - self.assertIn(date, repr(date_tz_freq)) - self.assertIn(tz_repr, repr(date_tz_freq)) - self.assertIn(freq_repr, repr(date_tz_freq)) - self.assertEqual(date_tz_freq, eval(repr(date_tz_freq))) - - # this can cause the tz field to be populated, but it's redundant to - # information in the datestring - tm._skip_if_no_pytz() - import pytz # noqa - date_with_utc_offset = Timestamp('2014-03-13 00:00:00-0400', tz=None) - self.assertIn('2014-03-13 00:00:00-0400', repr(date_with_utc_offset)) - self.assertNotIn('tzoffset', repr(date_with_utc_offset)) - self.assertIn('pytz.FixedOffset(-240)', repr(date_with_utc_offset)) - expr = repr(date_with_utc_offset).replace("'pytz.FixedOffset(-240)'", - 'pytz.FixedOffset(-240)') - self.assertEqual(date_with_utc_offset, eval(expr)) - - def test_bounds_with_different_units(self): - out_of_bounds_dates = ('1677-09-21', '2262-04-12', ) - - time_units = ('D', 'h', 'm', 's', 'ms', 'us') - - for date_string in out_of_bounds_dates: - for unit in time_units: - self.assertRaises(ValueError, Timestamp, np.datetime64( - date_string, dtype='M8[%s]' % unit)) - - in_bounds_dates = ('1677-09-23', '2262-04-11', ) - - for date_string in in_bounds_dates: - for unit in time_units: - Timestamp(np.datetime64(date_string, dtype='M8[%s]' % unit)) - - def test_tz(self): - t = '2014-02-01 09:00' - ts = Timestamp(t) - local = ts.tz_localize('Asia/Tokyo') - self.assertEqual(local.hour, 9) - self.assertEqual(local, Timestamp(t, tz='Asia/Tokyo')) - conv = local.tz_convert('US/Eastern') - self.assertEqual(conv, Timestamp('2014-01-31 19:00', tz='US/Eastern')) - self.assertEqual(conv.hour, 19) - - # preserves nanosecond - ts = Timestamp(t) + offsets.Nano(5) - local = ts.tz_localize('Asia/Tokyo') - self.assertEqual(local.hour, 9) - self.assertEqual(local.nanosecond, 5) - conv = local.tz_convert('US/Eastern') - self.assertEqual(conv.nanosecond, 5) - self.assertEqual(conv.hour, 19) - - def test_tz_localize_ambiguous(self): - - ts = Timestamp('2014-11-02 01:00') - ts_dst = ts.tz_localize('US/Eastern', ambiguous=True) - ts_no_dst = ts.tz_localize('US/Eastern', ambiguous=False) - - rng = date_range('2014-11-02', periods=3, freq='H', tz='US/Eastern') - self.assertEqual(rng[1], ts_dst) - self.assertEqual(rng[2], ts_no_dst) - self.assertRaises(ValueError, ts.tz_localize, 'US/Eastern', - ambiguous='infer') - - # GH 8025 - with tm.assertRaisesRegexp(TypeError, - 'Cannot localize tz-aware Timestamp, use ' - 'tz_convert for conversions'): - Timestamp('2011-01-01', tz='US/Eastern').tz_localize('Asia/Tokyo') - - with tm.assertRaisesRegexp(TypeError, - 'Cannot convert tz-naive Timestamp, use ' - 'tz_localize to localize'): - Timestamp('2011-01-01').tz_convert('Asia/Tokyo') - - def test_tz_localize_nonexistent(self): - # See issue 13057 - from pytz.exceptions import NonExistentTimeError - times = ['2015-03-08 02:00', '2015-03-08 02:30', - '2015-03-29 02:00', '2015-03-29 02:30'] - timezones = ['US/Eastern', 'US/Pacific', - 'Europe/Paris', 'Europe/Belgrade'] - for t, tz in zip(times, timezones): - ts = Timestamp(t) - self.assertRaises(NonExistentTimeError, ts.tz_localize, - tz) - self.assertRaises(NonExistentTimeError, ts.tz_localize, - tz, errors='raise') - self.assertIs(ts.tz_localize(tz, errors='coerce'), - NaT) - - def test_tz_localize_errors_ambiguous(self): - # See issue 13057 - from pytz.exceptions import AmbiguousTimeError - ts = Timestamp('2015-11-1 01:00') - self.assertRaises(AmbiguousTimeError, - ts.tz_localize, 'US/Pacific', errors='coerce') - - def test_tz_localize_roundtrip(self): - for tz in ['UTC', 'Asia/Tokyo', 'US/Eastern', 'dateutil/US/Pacific']: - for t in ['2014-02-01 09:00', '2014-07-08 09:00', - '2014-11-01 17:00', '2014-11-05 00:00']: - ts = Timestamp(t) - localized = ts.tz_localize(tz) - self.assertEqual(localized, Timestamp(t, tz=tz)) - - with tm.assertRaises(TypeError): - localized.tz_localize(tz) - - reset = localized.tz_localize(None) - self.assertEqual(reset, ts) - self.assertTrue(reset.tzinfo is None) - - def test_tz_convert_roundtrip(self): - for tz in ['UTC', 'Asia/Tokyo', 'US/Eastern', 'dateutil/US/Pacific']: - for t in ['2014-02-01 09:00', '2014-07-08 09:00', - '2014-11-01 17:00', '2014-11-05 00:00']: - ts = Timestamp(t, tz='UTC') - converted = ts.tz_convert(tz) - - reset = converted.tz_convert(None) - self.assertEqual(reset, Timestamp(t)) - self.assertTrue(reset.tzinfo is None) - self.assertEqual(reset, - converted.tz_convert('UTC').tz_localize(None)) - - def test_barely_oob_dts(self): - one_us = np.timedelta64(1).astype('timedelta64[us]') - - # By definition we can't go out of bounds in [ns], so we - # convert the datetime64s to [us] so we can go out of bounds - min_ts_us = np.datetime64(Timestamp.min).astype('M8[us]') - max_ts_us = np.datetime64(Timestamp.max).astype('M8[us]') - - # No error for the min/max datetimes - Timestamp(min_ts_us) - Timestamp(max_ts_us) - - # One us less than the minimum is an error - self.assertRaises(ValueError, Timestamp, min_ts_us - one_us) - - # One us more than the maximum is an error - self.assertRaises(ValueError, Timestamp, max_ts_us + one_us) - - def test_utc_z_designator(self): - self.assertEqual(get_timezone( - Timestamp('2014-11-02 01:00Z').tzinfo), 'UTC') - - def test_now(self): - # #9000 - ts_from_string = Timestamp('now') - ts_from_method = Timestamp.now() - ts_datetime = datetime.now() - - ts_from_string_tz = Timestamp('now', tz='US/Eastern') - ts_from_method_tz = Timestamp.now(tz='US/Eastern') - - # Check that the delta between the times is less than 1s (arbitrarily - # small) - delta = Timedelta(seconds=1) - self.assertTrue(abs(ts_from_method - ts_from_string) < delta) - self.assertTrue(abs(ts_datetime - ts_from_method) < delta) - self.assertTrue(abs(ts_from_method_tz - ts_from_string_tz) < delta) - self.assertTrue(abs(ts_from_string_tz.tz_localize(None) - - ts_from_method_tz.tz_localize(None)) < delta) - - def test_today(self): - - ts_from_string = Timestamp('today') - ts_from_method = Timestamp.today() - ts_datetime = datetime.today() - - ts_from_string_tz = Timestamp('today', tz='US/Eastern') - ts_from_method_tz = Timestamp.today(tz='US/Eastern') - - # Check that the delta between the times is less than 1s (arbitrarily - # small) - delta = Timedelta(seconds=1) - self.assertTrue(abs(ts_from_method - ts_from_string) < delta) - self.assertTrue(abs(ts_datetime - ts_from_method) < delta) - self.assertTrue(abs(ts_from_method_tz - ts_from_string_tz) < delta) - self.assertTrue(abs(ts_from_string_tz.tz_localize(None) - - ts_from_method_tz.tz_localize(None)) < delta) - - def test_asm8(self): - np.random.seed(7960929) - ns = [Timestamp.min.value, Timestamp.max.value, 1000, ] - for n in ns: - self.assertEqual(Timestamp(n).asm8.view('i8'), - np.datetime64(n, 'ns').view('i8'), n) - self.assertEqual(Timestamp('nat').asm8.view('i8'), - np.datetime64('nat', 'ns').view('i8')) - - def test_fields(self): - def check(value, equal): - # that we are int/long like - self.assertTrue(isinstance(value, (int, compat.long))) - self.assertEqual(value, equal) - - # GH 10050 - ts = Timestamp('2015-05-10 09:06:03.000100001') - check(ts.year, 2015) - check(ts.month, 5) - check(ts.day, 10) - check(ts.hour, 9) - check(ts.minute, 6) - check(ts.second, 3) - self.assertRaises(AttributeError, lambda: ts.millisecond) - check(ts.microsecond, 100) - check(ts.nanosecond, 1) - check(ts.dayofweek, 6) - check(ts.quarter, 2) - check(ts.dayofyear, 130) - check(ts.week, 19) - check(ts.daysinmonth, 31) - check(ts.daysinmonth, 31) - - # GH 13303 - ts = Timestamp('2014-12-31 23:59:00-05:00', tz='US/Eastern') - check(ts.year, 2014) - check(ts.month, 12) - check(ts.day, 31) - check(ts.hour, 23) - check(ts.minute, 59) - check(ts.second, 0) - self.assertRaises(AttributeError, lambda: ts.millisecond) - check(ts.microsecond, 0) - check(ts.nanosecond, 0) - check(ts.dayofweek, 2) - check(ts.quarter, 4) - check(ts.dayofyear, 365) - check(ts.week, 1) - check(ts.daysinmonth, 31) - - ts = Timestamp('2014-01-01 00:00:00+01:00') - starts = ['is_month_start', 'is_quarter_start', 'is_year_start'] - for start in starts: - self.assertTrue(getattr(ts, start)) - ts = Timestamp('2014-12-31 23:59:59+01:00') - ends = ['is_month_end', 'is_year_end', 'is_quarter_end'] - for end in ends: - self.assertTrue(getattr(ts, end)) - - def test_pprint(self): - # GH12622 - import pprint - nested_obj = {'foo': 1, - 'bar': [{'w': {'a': Timestamp('2011-01-01')}}] * 10} - result = pprint.pformat(nested_obj, width=50) - expected = r"""{'bar': [{'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, - {'w': {'a': Timestamp('2011-01-01 00:00:00')}}], - 'foo': 1}""" - self.assertEqual(result, expected) - - def to_datetime_depr(self): - # see gh-8254 - ts = Timestamp('2011-01-01') - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - expected = datetime(2011, 1, 1) - result = ts.to_datetime() - self.assertEqual(result, expected) - - def to_pydatetime_nonzero_nano(self): - ts = Timestamp('2011-01-01 9:00:00.123456789') - - # Warn the user of data loss (nanoseconds). - with tm.assert_produces_warning(UserWarning, - check_stacklevel=False): - expected = datetime(2011, 1, 1, 9, 0, 0, 123456) - result = ts.to_pydatetime() - self.assertEqual(result, expected) - - def test_round(self): - - # round - dt = Timestamp('20130101 09:10:11') - result = dt.round('D') - expected = Timestamp('20130101') - self.assertEqual(result, expected) - - dt = Timestamp('20130101 19:10:11') - result = dt.round('D') - expected = Timestamp('20130102') - self.assertEqual(result, expected) - - dt = Timestamp('20130201 12:00:00') - result = dt.round('D') - expected = Timestamp('20130202') - self.assertEqual(result, expected) - - dt = Timestamp('20130104 12:00:00') - result = dt.round('D') - expected = Timestamp('20130105') - self.assertEqual(result, expected) - - dt = Timestamp('20130104 12:32:00') - result = dt.round('30Min') - expected = Timestamp('20130104 12:30:00') - self.assertEqual(result, expected) - - dti = date_range('20130101 09:10:11', periods=5) - result = dti.round('D') - expected = date_range('20130101', periods=5) - tm.assert_index_equal(result, expected) - - # floor - dt = Timestamp('20130101 09:10:11') - result = dt.floor('D') - expected = Timestamp('20130101') - self.assertEqual(result, expected) - - # ceil - dt = Timestamp('20130101 09:10:11') - result = dt.ceil('D') - expected = Timestamp('20130102') - self.assertEqual(result, expected) - - # round with tz - dt = Timestamp('20130101 09:10:11', tz='US/Eastern') - result = dt.round('D') - expected = Timestamp('20130101', tz='US/Eastern') - self.assertEqual(result, expected) - - dt = Timestamp('20130101 09:10:11', tz='US/Eastern') - result = dt.round('s') - self.assertEqual(result, dt) - - dti = date_range('20130101 09:10:11', - periods=5).tz_localize('UTC').tz_convert('US/Eastern') - result = dti.round('D') - expected = date_range('20130101', periods=5).tz_localize('US/Eastern') - tm.assert_index_equal(result, expected) - - result = dti.round('s') - tm.assert_index_equal(result, dti) - - # invalid - for freq in ['Y', 'M', 'foobar']: - self.assertRaises(ValueError, lambda: dti.round(freq)) - - # GH 14440 & 15578 - result = Timestamp('2016-10-17 12:00:00.0015').round('ms') - expected = Timestamp('2016-10-17 12:00:00.002000') - self.assertEqual(result, expected) - - result = Timestamp('2016-10-17 12:00:00.00149').round('ms') - expected = Timestamp('2016-10-17 12:00:00.001000') - self.assertEqual(result, expected) - - ts = Timestamp('2016-10-17 12:00:00.0015') - for freq in ['us', 'ns']: - self.assertEqual(ts, ts.round(freq)) - - result = Timestamp('2016-10-17 12:00:00.001501031').round('10ns') - expected = Timestamp('2016-10-17 12:00:00.001501030') - self.assertEqual(result, expected) - - with tm.assert_produces_warning(): - Timestamp('2016-10-17 12:00:00.001501031').round('1010ns') - - def test_round_misc(self): - stamp = Timestamp('2000-01-05 05:09:15.13') - - def _check_round(freq, expected): - result = stamp.round(freq=freq) - self.assertEqual(result, expected) - - for freq, expected in [('D', Timestamp('2000-01-05 00:00:00')), - ('H', Timestamp('2000-01-05 05:00:00')), - ('S', Timestamp('2000-01-05 05:09:15'))]: - _check_round(freq, expected) - - msg = frequencies._INVALID_FREQ_ERROR - with self.assertRaisesRegexp(ValueError, msg): - stamp.round('foo') - - def test_class_ops_pytz(self): - tm._skip_if_no_pytz() - from pytz import timezone - - def compare(x, y): - self.assertEqual(int(Timestamp(x).value / 1e9), - int(Timestamp(y).value / 1e9)) - - compare(Timestamp.now(), datetime.now()) - compare(Timestamp.now('UTC'), datetime.now(timezone('UTC'))) - compare(Timestamp.utcnow(), datetime.utcnow()) - compare(Timestamp.today(), datetime.today()) - current_time = calendar.timegm(datetime.now().utctimetuple()) - compare(Timestamp.utcfromtimestamp(current_time), - datetime.utcfromtimestamp(current_time)) - compare(Timestamp.fromtimestamp(current_time), - datetime.fromtimestamp(current_time)) - - date_component = datetime.utcnow() - time_component = (date_component + timedelta(minutes=10)).time() - compare(Timestamp.combine(date_component, time_component), - datetime.combine(date_component, time_component)) - - def test_class_ops_dateutil(self): - tm._skip_if_no_dateutil() - from dateutil.tz import tzutc - - def compare(x, y): - self.assertEqual(int(np.round(Timestamp(x).value / 1e9)), - int(np.round(Timestamp(y).value / 1e9))) - - compare(Timestamp.now(), datetime.now()) - compare(Timestamp.now('UTC'), datetime.now(tzutc())) - compare(Timestamp.utcnow(), datetime.utcnow()) - compare(Timestamp.today(), datetime.today()) - current_time = calendar.timegm(datetime.now().utctimetuple()) - compare(Timestamp.utcfromtimestamp(current_time), - datetime.utcfromtimestamp(current_time)) - compare(Timestamp.fromtimestamp(current_time), - datetime.fromtimestamp(current_time)) - - date_component = datetime.utcnow() - time_component = (date_component + timedelta(minutes=10)).time() - compare(Timestamp.combine(date_component, time_component), - datetime.combine(date_component, time_component)) - - def test_basics_nanos(self): - val = np.int64(946684800000000000).view('M8[ns]') - stamp = Timestamp(val.view('i8') + 500) - self.assertEqual(stamp.year, 2000) - self.assertEqual(stamp.month, 1) - self.assertEqual(stamp.microsecond, 0) - self.assertEqual(stamp.nanosecond, 500) - - # GH 14415 - val = np.iinfo(np.int64).min + 80000000000000 - stamp = Timestamp(val) - self.assertEqual(stamp.year, 1677) - self.assertEqual(stamp.month, 9) - self.assertEqual(stamp.day, 21) - self.assertEqual(stamp.microsecond, 145224) - self.assertEqual(stamp.nanosecond, 192) - - def test_unit(self): - - def check(val, unit=None, h=1, s=1, us=0): - stamp = Timestamp(val, unit=unit) - self.assertEqual(stamp.year, 2000) - self.assertEqual(stamp.month, 1) - self.assertEqual(stamp.day, 1) - self.assertEqual(stamp.hour, h) - if unit != 'D': - self.assertEqual(stamp.minute, 1) - self.assertEqual(stamp.second, s) - self.assertEqual(stamp.microsecond, us) - else: - self.assertEqual(stamp.minute, 0) - self.assertEqual(stamp.second, 0) - self.assertEqual(stamp.microsecond, 0) - self.assertEqual(stamp.nanosecond, 0) - - ts = Timestamp('20000101 01:01:01') - val = ts.value - days = (ts - Timestamp('1970-01-01')).days - - check(val) - check(val / long(1000), unit='us') - check(val / long(1000000), unit='ms') - check(val / long(1000000000), unit='s') - check(days, unit='D', h=0) - - # using truediv, so these are like floats - if compat.PY3: - check((val + 500000) / long(1000000000), unit='s', us=500) - check((val + 500000000) / long(1000000000), unit='s', us=500000) - check((val + 500000) / long(1000000), unit='ms', us=500) - - # get chopped in py2 - else: - check((val + 500000) / long(1000000000), unit='s') - check((val + 500000000) / long(1000000000), unit='s') - check((val + 500000) / long(1000000), unit='ms') - - # ok - check((val + 500000) / long(1000), unit='us', us=500) - check((val + 500000000) / long(1000000), unit='ms', us=500000) - - # floats - check(val / 1000.0 + 5, unit='us', us=5) - check(val / 1000.0 + 5000, unit='us', us=5000) - check(val / 1000000.0 + 0.5, unit='ms', us=500) - check(val / 1000000.0 + 0.005, unit='ms', us=5) - check(val / 1000000000.0 + 0.5, unit='s', us=500000) - check(days + 0.5, unit='D', h=12) - - def test_roundtrip(self): - - # test value to string and back conversions - # further test accessors - base = Timestamp('20140101 00:00:00') - - result = Timestamp(base.value + Timedelta('5ms').value) - self.assertEqual(result, Timestamp(str(base) + ".005000")) - self.assertEqual(result.microsecond, 5000) - - result = Timestamp(base.value + Timedelta('5us').value) - self.assertEqual(result, Timestamp(str(base) + ".000005")) - self.assertEqual(result.microsecond, 5) - - result = Timestamp(base.value + Timedelta('5ns').value) - self.assertEqual(result, Timestamp(str(base) + ".000000005")) - self.assertEqual(result.nanosecond, 5) - self.assertEqual(result.microsecond, 0) - - result = Timestamp(base.value + Timedelta('6ms 5us').value) - self.assertEqual(result, Timestamp(str(base) + ".006005")) - self.assertEqual(result.microsecond, 5 + 6 * 1000) - - result = Timestamp(base.value + Timedelta('200ms 5us').value) - self.assertEqual(result, Timestamp(str(base) + ".200005")) - self.assertEqual(result.microsecond, 5 + 200 * 1000) - - def test_comparison(self): - # 5-18-2012 00:00:00.000 - stamp = long(1337299200000000000) - - val = Timestamp(stamp) - - self.assertEqual(val, val) - self.assertFalse(val != val) - self.assertFalse(val < val) - self.assertTrue(val <= val) - self.assertFalse(val > val) - self.assertTrue(val >= val) - - other = datetime(2012, 5, 18) - self.assertEqual(val, other) - self.assertFalse(val != other) - self.assertFalse(val < other) - self.assertTrue(val <= other) - self.assertFalse(val > other) - self.assertTrue(val >= other) - - other = Timestamp(stamp + 100) - - self.assertNotEqual(val, other) - self.assertNotEqual(val, other) - self.assertTrue(val < other) - self.assertTrue(val <= other) - self.assertTrue(other > val) - self.assertTrue(other >= val) - - def test_compare_invalid(self): - - # GH 8058 - val = Timestamp('20130101 12:01:02') - self.assertFalse(val == 'foo') - self.assertFalse(val == 10.0) - self.assertFalse(val == 1) - self.assertFalse(val == long(1)) - self.assertFalse(val == []) - self.assertFalse(val == {'foo': 1}) - self.assertFalse(val == np.float64(1)) - self.assertFalse(val == np.int64(1)) - - self.assertTrue(val != 'foo') - self.assertTrue(val != 10.0) - self.assertTrue(val != 1) - self.assertTrue(val != long(1)) - self.assertTrue(val != []) - self.assertTrue(val != {'foo': 1}) - self.assertTrue(val != np.float64(1)) - self.assertTrue(val != np.int64(1)) - - # ops testing - df = DataFrame(np.random.randn(5, 2)) - a = df[0] - b = Series(np.random.randn(5)) - b.name = Timestamp('2000-01-01') - tm.assert_series_equal(a / b, 1 / (b / a)) - - def test_cant_compare_tz_naive_w_aware(self): - tm._skip_if_no_pytz() - # #1404 - a = Timestamp('3/12/2012') - b = Timestamp('3/12/2012', tz='utc') - - self.assertRaises(Exception, a.__eq__, b) - self.assertRaises(Exception, a.__ne__, b) - self.assertRaises(Exception, a.__lt__, b) - self.assertRaises(Exception, a.__gt__, b) - self.assertRaises(Exception, b.__eq__, a) - self.assertRaises(Exception, b.__ne__, a) - self.assertRaises(Exception, b.__lt__, a) - self.assertRaises(Exception, b.__gt__, a) - - if sys.version_info < (3, 3): - self.assertRaises(Exception, a.__eq__, b.to_pydatetime()) - self.assertRaises(Exception, a.to_pydatetime().__eq__, b) - else: - self.assertFalse(a == b.to_pydatetime()) - self.assertFalse(a.to_pydatetime() == b) - - def test_cant_compare_tz_naive_w_aware_explicit_pytz(self): - tm._skip_if_no_pytz() - from pytz import utc - # #1404 - a = Timestamp('3/12/2012') - b = Timestamp('3/12/2012', tz=utc) - - self.assertRaises(Exception, a.__eq__, b) - self.assertRaises(Exception, a.__ne__, b) - self.assertRaises(Exception, a.__lt__, b) - self.assertRaises(Exception, a.__gt__, b) - self.assertRaises(Exception, b.__eq__, a) - self.assertRaises(Exception, b.__ne__, a) - self.assertRaises(Exception, b.__lt__, a) - self.assertRaises(Exception, b.__gt__, a) - - if sys.version_info < (3, 3): - self.assertRaises(Exception, a.__eq__, b.to_pydatetime()) - self.assertRaises(Exception, a.to_pydatetime().__eq__, b) - else: - self.assertFalse(a == b.to_pydatetime()) - self.assertFalse(a.to_pydatetime() == b) - - def test_cant_compare_tz_naive_w_aware_dateutil(self): - tm._skip_if_no_dateutil() - from dateutil.tz import tzutc - utc = tzutc() - # #1404 - a = Timestamp('3/12/2012') - b = Timestamp('3/12/2012', tz=utc) - - self.assertRaises(Exception, a.__eq__, b) - self.assertRaises(Exception, a.__ne__, b) - self.assertRaises(Exception, a.__lt__, b) - self.assertRaises(Exception, a.__gt__, b) - self.assertRaises(Exception, b.__eq__, a) - self.assertRaises(Exception, b.__ne__, a) - self.assertRaises(Exception, b.__lt__, a) - self.assertRaises(Exception, b.__gt__, a) - - if sys.version_info < (3, 3): - self.assertRaises(Exception, a.__eq__, b.to_pydatetime()) - self.assertRaises(Exception, a.to_pydatetime().__eq__, b) - else: - self.assertFalse(a == b.to_pydatetime()) - self.assertFalse(a.to_pydatetime() == b) - - def test_delta_preserve_nanos(self): - val = Timestamp(long(1337299200000000123)) - result = val + timedelta(1) - self.assertEqual(result.nanosecond, val.nanosecond) - - def test_frequency_misc(self): - self.assertEqual(frequencies.get_freq_group('T'), - frequencies.FreqGroup.FR_MIN) - - code, stride = frequencies.get_freq_code(offsets.Hour()) - self.assertEqual(code, frequencies.FreqGroup.FR_HR) - - code, stride = frequencies.get_freq_code((5, 'T')) - self.assertEqual(code, frequencies.FreqGroup.FR_MIN) - self.assertEqual(stride, 5) - - offset = offsets.Hour() - result = frequencies.to_offset(offset) - self.assertEqual(result, offset) - - result = frequencies.to_offset((5, 'T')) - expected = offsets.Minute(5) - self.assertEqual(result, expected) - - self.assertRaises(ValueError, frequencies.get_freq_code, (5, 'baz')) - - self.assertRaises(ValueError, frequencies.to_offset, '100foo') - - self.assertRaises(ValueError, frequencies.to_offset, ('', '')) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = frequencies.get_standard_freq(offsets.Hour()) - self.assertEqual(result, 'H') - - def test_hash_equivalent(self): - d = {datetime(2011, 1, 1): 5} - stamp = Timestamp(datetime(2011, 1, 1)) - self.assertEqual(d[stamp], 5) - - def test_timestamp_compare_scalars(self): - # case where ndim == 0 - lhs = np.datetime64(datetime(2013, 12, 6)) - rhs = Timestamp('now') - nat = Timestamp('nat') - - ops = {'gt': 'lt', - 'lt': 'gt', - 'ge': 'le', - 'le': 'ge', - 'eq': 'eq', - 'ne': 'ne'} - - for left, right in ops.items(): - left_f = getattr(operator, left) - right_f = getattr(operator, right) - expected = left_f(lhs, rhs) - - result = right_f(rhs, lhs) - self.assertEqual(result, expected) - - expected = left_f(rhs, nat) - result = right_f(nat, rhs) - self.assertEqual(result, expected) - - def test_timestamp_compare_series(self): - # make sure we can compare Timestamps on the right AND left hand side - # GH4982 - s = Series(date_range('20010101', periods=10), name='dates') - s_nat = s.copy(deep=True) - - s[0] = Timestamp('nat') - s[3] = Timestamp('nat') - - ops = {'lt': 'gt', 'le': 'ge', 'eq': 'eq', 'ne': 'ne'} - - for left, right in ops.items(): - left_f = getattr(operator, left) - right_f = getattr(operator, right) - - # no nats - expected = left_f(s, Timestamp('20010109')) - result = right_f(Timestamp('20010109'), s) - tm.assert_series_equal(result, expected) - - # nats - expected = left_f(s, Timestamp('nat')) - result = right_f(Timestamp('nat'), s) - tm.assert_series_equal(result, expected) - - # compare to timestamp with series containing nats - expected = left_f(s_nat, Timestamp('20010109')) - result = right_f(Timestamp('20010109'), s_nat) - tm.assert_series_equal(result, expected) - - # compare to nat with series containing nats - expected = left_f(s_nat, Timestamp('nat')) - result = right_f(Timestamp('nat'), s_nat) - tm.assert_series_equal(result, expected) - - def test_is_leap_year(self): - # GH 13727 - for tz in [None, 'UTC', 'US/Eastern', 'Asia/Tokyo']: - dt = Timestamp('2000-01-01 00:00:00', tz=tz) - self.assertTrue(dt.is_leap_year) - self.assertIsInstance(dt.is_leap_year, bool) - - dt = Timestamp('1999-01-01 00:00:00', tz=tz) - self.assertFalse(dt.is_leap_year) - - dt = Timestamp('2004-01-01 00:00:00', tz=tz) - self.assertTrue(dt.is_leap_year) - - dt = Timestamp('2100-01-01 00:00:00', tz=tz) - self.assertFalse(dt.is_leap_year) - - -class TestTimestampNsOperations(tm.TestCase): - - def setUp(self): - self.timestamp = Timestamp(datetime.utcnow()) - - def assert_ns_timedelta(self, modified_timestamp, expected_value): - value = self.timestamp.value - modified_value = modified_timestamp.value - - self.assertEqual(modified_value - value, expected_value) - - def test_timedelta_ns_arithmetic(self): - self.assert_ns_timedelta(self.timestamp + np.timedelta64(-123, 'ns'), - -123) - - def test_timedelta_ns_based_arithmetic(self): - self.assert_ns_timedelta(self.timestamp + np.timedelta64( - 1234567898, 'ns'), 1234567898) - - def test_timedelta_us_arithmetic(self): - self.assert_ns_timedelta(self.timestamp + np.timedelta64(-123, 'us'), - -123000) - - def test_timedelta_ms_arithmetic(self): - time = self.timestamp + np.timedelta64(-123, 'ms') - self.assert_ns_timedelta(time, -123000000) - - def test_nanosecond_string_parsing(self): - ts = Timestamp('2013-05-01 07:15:45.123456789') - # GH 7878 - expected_repr = '2013-05-01 07:15:45.123456789' - expected_value = 1367392545123456789 - self.assertEqual(ts.value, expected_value) - self.assertIn(expected_repr, repr(ts)) - - ts = Timestamp('2013-05-01 07:15:45.123456789+09:00', tz='Asia/Tokyo') - self.assertEqual(ts.value, expected_value - 9 * 3600 * 1000000000) - self.assertIn(expected_repr, repr(ts)) - - ts = Timestamp('2013-05-01 07:15:45.123456789', tz='UTC') - self.assertEqual(ts.value, expected_value) - self.assertIn(expected_repr, repr(ts)) - - ts = Timestamp('2013-05-01 07:15:45.123456789', tz='US/Eastern') - self.assertEqual(ts.value, expected_value + 4 * 3600 * 1000000000) - self.assertIn(expected_repr, repr(ts)) - - # GH 10041 - ts = Timestamp('20130501T071545.123456789') - self.assertEqual(ts.value, expected_value) - self.assertIn(expected_repr, repr(ts)) - - def test_nanosecond_timestamp(self): - # GH 7610 - expected = 1293840000000000005 - t = Timestamp('2011-01-01') + offsets.Nano(5) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000005')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 5) - - t = Timestamp(t) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000005')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 5) - - t = Timestamp(np_datetime64_compat('2011-01-01 00:00:00.000000005Z')) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000005')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 5) - - expected = 1293840000000000010 - t = t + offsets.Nano(5) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000010')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 10) - - t = Timestamp(t) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000010')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 10) - - t = Timestamp(np_datetime64_compat('2011-01-01 00:00:00.000000010Z')) - self.assertEqual(repr(t), "Timestamp('2011-01-01 00:00:00.000000010')") - self.assertEqual(t.value, expected) - self.assertEqual(t.nanosecond, 10) - - -class TestTimestampOps(tm.TestCase): - - def test_timestamp_and_datetime(self): - self.assertEqual((Timestamp(datetime( - 2013, 10, 13)) - datetime(2013, 10, 12)).days, 1) - self.assertEqual((datetime(2013, 10, 12) - - Timestamp(datetime(2013, 10, 13))).days, -1) - - def test_timestamp_and_series(self): - timestamp_series = Series(date_range('2014-03-17', periods=2, freq='D', - tz='US/Eastern')) - first_timestamp = timestamp_series[0] - - delta_series = Series([np.timedelta64(0, 'D'), np.timedelta64(1, 'D')]) - assert_series_equal(timestamp_series - first_timestamp, delta_series) - assert_series_equal(first_timestamp - timestamp_series, -delta_series) - - def test_addition_subtraction_types(self): - # Assert on the types resulting from Timestamp +/- various date/time - # objects - datetime_instance = datetime(2014, 3, 4) - timedelta_instance = timedelta(seconds=1) - # build a timestamp with a frequency, since then it supports - # addition/subtraction of integers - timestamp_instance = date_range(datetime_instance, periods=1, - freq='D')[0] - - self.assertEqual(type(timestamp_instance + 1), Timestamp) - self.assertEqual(type(timestamp_instance - 1), Timestamp) - - # Timestamp + datetime not supported, though subtraction is supported - # and yields timedelta more tests in tseries/base/tests/test_base.py - self.assertEqual( - type(timestamp_instance - datetime_instance), Timedelta) - self.assertEqual( - type(timestamp_instance + timedelta_instance), Timestamp) - self.assertEqual( - type(timestamp_instance - timedelta_instance), Timestamp) - - # Timestamp +/- datetime64 not supported, so not tested (could possibly - # assert error raised?) - timedelta64_instance = np.timedelta64(1, 'D') - self.assertEqual( - type(timestamp_instance + timedelta64_instance), Timestamp) - self.assertEqual( - type(timestamp_instance - timedelta64_instance), Timestamp) - - def test_addition_subtraction_preserve_frequency(self): - timestamp_instance = date_range('2014-03-05', periods=1, freq='D')[0] - timedelta_instance = timedelta(days=1) - original_freq = timestamp_instance.freq - self.assertEqual((timestamp_instance + 1).freq, original_freq) - self.assertEqual((timestamp_instance - 1).freq, original_freq) - self.assertEqual( - (timestamp_instance + timedelta_instance).freq, original_freq) - self.assertEqual( - (timestamp_instance - timedelta_instance).freq, original_freq) - - timedelta64_instance = np.timedelta64(1, 'D') - self.assertEqual( - (timestamp_instance + timedelta64_instance).freq, original_freq) - self.assertEqual( - (timestamp_instance - timedelta64_instance).freq, original_freq) - - def test_resolution(self): - - for freq, expected in zip(['A', 'Q', 'M', 'D', 'H', 'T', - 'S', 'L', 'U'], - [RESO_DAY, RESO_DAY, - RESO_DAY, RESO_DAY, - RESO_HR, RESO_MIN, - RESO_SEC, RESO_MS, - RESO_US]): - for tz in [None, 'Asia/Tokyo', 'US/Eastern', - 'dateutil/US/Eastern']: - idx = date_range(start='2013-04-01', periods=30, freq=freq, - tz=tz) - result = period.resolution(idx.asi8, idx.tz) - self.assertEqual(result, expected) - - -class TestTimestampToJulianDate(tm.TestCase): - - def test_compare_1700(self): - r = Timestamp('1700-06-23').to_julian_date() - self.assertEqual(r, 2342145.5) - - def test_compare_2000(self): - r = Timestamp('2000-04-12').to_julian_date() - self.assertEqual(r, 2451646.5) - - def test_compare_2100(self): - r = Timestamp('2100-08-12').to_julian_date() - self.assertEqual(r, 2488292.5) - - def test_compare_hour01(self): - r = Timestamp('2000-08-12T01:00:00').to_julian_date() - self.assertEqual(r, 2451768.5416666666666666) - - def test_compare_hour13(self): - r = Timestamp('2000-08-12T13:00:00').to_julian_date() - self.assertEqual(r, 2451769.0416666666666666) - - -class TestTimeSeries(tm.TestCase): - - def test_timestamp_to_datetime(self): - tm._skip_if_no_pytz() - rng = date_range('20090415', '20090519', tz='US/Eastern') - - stamp = rng[0] - dtval = stamp.to_pydatetime() - self.assertEqual(stamp, dtval) - self.assertEqual(stamp.tzinfo, dtval.tzinfo) - - def test_timestamp_to_datetime_dateutil(self): - tm._skip_if_no_pytz() - rng = date_range('20090415', '20090519', tz='dateutil/US/Eastern') - - stamp = rng[0] - dtval = stamp.to_pydatetime() - self.assertEqual(stamp, dtval) - self.assertEqual(stamp.tzinfo, dtval.tzinfo) - - def test_timestamp_to_datetime_explicit_pytz(self): - tm._skip_if_no_pytz() - import pytz - rng = date_range('20090415', '20090519', - tz=pytz.timezone('US/Eastern')) - - stamp = rng[0] - dtval = stamp.to_pydatetime() - self.assertEqual(stamp, dtval) - self.assertEqual(stamp.tzinfo, dtval.tzinfo) - - def test_timestamp_to_datetime_explicit_dateutil(self): - tm._skip_if_windows_python_3() - tm._skip_if_no_dateutil() - from pandas._libs.tslib import _dateutil_gettz as gettz - rng = date_range('20090415', '20090519', tz=gettz('US/Eastern')) - - stamp = rng[0] - dtval = stamp.to_pydatetime() - self.assertEqual(stamp, dtval) - self.assertEqual(stamp.tzinfo, dtval.tzinfo) - - def test_timestamp_fields(self): - # extra fields from DatetimeIndex like quarter and week - idx = tm.makeDateIndex(100) - - fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter', - 'days_in_month', 'is_month_start', 'is_month_end', - 'is_quarter_start', 'is_quarter_end', 'is_year_start', - 'is_year_end', 'weekday_name'] - for f in fields: - expected = getattr(idx, f)[-1] - result = getattr(Timestamp(idx[-1]), f) - self.assertEqual(result, expected) - - self.assertEqual(idx.freq, Timestamp(idx[-1], idx.freq).freq) - self.assertEqual(idx.freqstr, Timestamp(idx[-1], idx.freq).freqstr) - - def test_timestamp_date_out_of_range(self): - self.assertRaises(ValueError, Timestamp, '1676-01-01') - self.assertRaises(ValueError, Timestamp, '2263-01-01') - - # 1475 - self.assertRaises(ValueError, DatetimeIndex, ['1400-01-01']) - self.assertRaises(ValueError, DatetimeIndex, [datetime(1400, 1, 1)]) - - def test_timestamp_repr(self): - # pre-1900 - stamp = Timestamp('1850-01-01', tz='US/Eastern') - repr(stamp) - - iso8601 = '1850-01-01 01:23:45.012345' - stamp = Timestamp(iso8601, tz='US/Eastern') - result = repr(stamp) - self.assertIn(iso8601, result) - - def test_timestamp_from_ordinal(self): - - # GH 3042 - dt = datetime(2011, 4, 16, 0, 0) - ts = Timestamp.fromordinal(dt.toordinal()) - self.assertEqual(ts.to_pydatetime(), dt) - - # with a tzinfo - stamp = Timestamp('2011-4-16', tz='US/Eastern') - dt_tz = stamp.to_pydatetime() - ts = Timestamp.fromordinal(dt_tz.toordinal(), tz='US/Eastern') - self.assertEqual(ts.to_pydatetime(), dt_tz) - - def test_timestamp_compare_with_early_datetime(self): - # e.g. datetime.min - stamp = Timestamp('2012-01-01') - - self.assertFalse(stamp == datetime.min) - self.assertFalse(stamp == datetime(1600, 1, 1)) - self.assertFalse(stamp == datetime(2700, 1, 1)) - self.assertNotEqual(stamp, datetime.min) - self.assertNotEqual(stamp, datetime(1600, 1, 1)) - self.assertNotEqual(stamp, datetime(2700, 1, 1)) - self.assertTrue(stamp > datetime(1600, 1, 1)) - self.assertTrue(stamp >= datetime(1600, 1, 1)) - self.assertTrue(stamp < datetime(2700, 1, 1)) - self.assertTrue(stamp <= datetime(2700, 1, 1)) - - def test_timestamp_equality(self): - - # GH 11034 - s = Series([Timestamp('2000-01-29 01:59:00'), 'NaT']) - result = s != s - assert_series_equal(result, Series([False, True])) - result = s != s[0] - assert_series_equal(result, Series([False, True])) - result = s != s[1] - assert_series_equal(result, Series([True, True])) - - result = s == s - assert_series_equal(result, Series([True, False])) - result = s == s[0] - assert_series_equal(result, Series([True, False])) - result = s == s[1] - assert_series_equal(result, Series([False, False])) - - def test_series_box_timestamp(self): - rng = date_range('20090415', '20090519', freq='B') - s = Series(rng) - - tm.assertIsInstance(s[5], Timestamp) - - rng = date_range('20090415', '20090519', freq='B') - s = Series(rng, index=rng) - tm.assertIsInstance(s[5], Timestamp) - - tm.assertIsInstance(s.iat[5], Timestamp) - - def test_frame_setitem_timestamp(self): - # 2155 - columns = DatetimeIndex(start='1/1/2012', end='2/1/2012', - freq=offsets.BDay()) - index = lrange(10) - data = DataFrame(columns=columns, index=index) - t = datetime(2012, 11, 1) - ts = Timestamp(t) - data[ts] = np.nan # works - - def test_to_html_timestamp(self): - rng = date_range('2000-01-01', periods=10) - df = DataFrame(np.random.randn(10, 4), index=rng) - - result = df.to_html() - self.assertIn('2000-01-01', result) - - def test_series_map_box_timestamps(self): - # #2689, #2627 - s = Series(date_range('1/1/2000', periods=10)) - - def f(x): - return (x.hour, x.day, x.month) - - # it works! - s.map(f) - s.apply(f) - DataFrame(s).applymap(f) - - def test_dti_slicing(self): - dti = DatetimeIndex(start='1/1/2005', end='12/1/2005', freq='M') - dti2 = dti[[1, 3, 5]] - - v1 = dti2[0] - v2 = dti2[1] - v3 = dti2[2] - - self.assertEqual(v1, Timestamp('2/28/2005')) - self.assertEqual(v2, Timestamp('4/30/2005')) - self.assertEqual(v3, Timestamp('6/30/2005')) - - # don't carry freq through irregular slicing - self.assertIsNone(dti2.freq) - - def test_woy_boundary(self): - # make sure weeks at year boundaries are correct - d = datetime(2013, 12, 31) - result = Timestamp(d).week - expected = 1 # ISO standard - self.assertEqual(result, expected) - - d = datetime(2008, 12, 28) - result = Timestamp(d).week - expected = 52 # ISO standard - self.assertEqual(result, expected) - - d = datetime(2009, 12, 31) - result = Timestamp(d).week - expected = 53 # ISO standard - self.assertEqual(result, expected) - - d = datetime(2010, 1, 1) - result = Timestamp(d).week - expected = 53 # ISO standard - self.assertEqual(result, expected) - - d = datetime(2010, 1, 3) - result = Timestamp(d).week - expected = 53 # ISO standard - self.assertEqual(result, expected) - - result = np.array([Timestamp(datetime(*args)).week - for args in [(2000, 1, 1), (2000, 1, 2), ( - 2005, 1, 1), (2005, 1, 2)]]) - self.assertTrue((result == [52, 52, 53, 53]).all()) - - -class TestTsUtil(tm.TestCase): - - def test_min_valid(self): - # Ensure that Timestamp.min is a valid Timestamp - Timestamp(Timestamp.min) - - def test_max_valid(self): - # Ensure that Timestamp.max is a valid Timestamp - Timestamp(Timestamp.max) - - def test_to_datetime_bijective(self): - # Ensure that converting to datetime and back only loses precision - # by going from nanoseconds to microseconds. - exp_warning = None if Timestamp.max.nanosecond == 0 else UserWarning - with tm.assert_produces_warning(exp_warning, check_stacklevel=False): - self.assertEqual( - Timestamp(Timestamp.max.to_pydatetime()).value / 1000, - Timestamp.max.value / 1000) - - exp_warning = None if Timestamp.min.nanosecond == 0 else UserWarning - with tm.assert_produces_warning(exp_warning, check_stacklevel=False): - self.assertEqual( - Timestamp(Timestamp.min.to_pydatetime()).value / 1000, - Timestamp.min.value / 1000) diff --git a/pandas/tests/scalar/timedelta/__init__.py b/pandas/tests/scalar/timedelta/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/scalar/timedelta/test_arithmetic.py b/pandas/tests/scalar/timedelta/test_arithmetic.py new file mode 100644 index 0000000000000..b6ad251d598ab --- /dev/null +++ b/pandas/tests/scalar/timedelta/test_arithmetic.py @@ -0,0 +1,691 @@ +# -*- coding: utf-8 -*- +""" +Tests for scalar Timedelta arithmetic ops +""" +from datetime import datetime, timedelta +import operator + +import numpy as np +import pytest + +import pandas as pd +from pandas import NaT, Timedelta, Timestamp +from pandas.core import ops +import pandas.util.testing as tm + + +class TestTimedeltaAdditionSubtraction(object): + """ + Tests for Timedelta methods: + + __add__, __radd__, + __sub__, __rsub__ + """ + @pytest.mark.parametrize('ten_seconds', [ + Timedelta(10, unit='s'), + timedelta(seconds=10), + np.timedelta64(10, 's'), + np.timedelta64(10000000000, 'ns'), + pd.offsets.Second(10)]) + def test_td_add_sub_ten_seconds(self, ten_seconds): + # GH#6808 + base = Timestamp('20130101 09:01:12.123456') + expected_add = Timestamp('20130101 09:01:22.123456') + expected_sub = Timestamp('20130101 09:01:02.123456') + + result = base + ten_seconds + assert result == expected_add + + result = base - ten_seconds + assert result == expected_sub + + @pytest.mark.parametrize('one_day_ten_secs', [ + Timedelta('1 day, 00:00:10'), + Timedelta('1 days, 00:00:10'), + timedelta(days=1, seconds=10), + np.timedelta64(1, 'D') + np.timedelta64(10, 's'), + pd.offsets.Day() + pd.offsets.Second(10)]) + def test_td_add_sub_one_day_ten_seconds(self, one_day_ten_secs): + # GH#6808 + base = Timestamp('20130102 09:01:12.123456') + expected_add = Timestamp('20130103 09:01:22.123456') + expected_sub = Timestamp('20130101 09:01:02.123456') + + result = base + one_day_ten_secs + assert result == expected_add + + result = base - one_day_ten_secs + assert result == expected_sub + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_datetimelike_scalar(self, op): + # GH#19738 + td = Timedelta(10, unit='d') + + result = op(td, datetime(2016, 1, 1)) + if op is operator.add: + # datetime + Timedelta does _not_ call Timedelta.__radd__, + # so we get a datetime back instead of a Timestamp + assert isinstance(result, Timestamp) + assert result == Timestamp(2016, 1, 11) + + result = op(td, Timestamp('2018-01-12 18:09')) + assert isinstance(result, Timestamp) + assert result == Timestamp('2018-01-22 18:09') + + result = op(td, np.datetime64('2018-01-12')) + assert isinstance(result, Timestamp) + assert result == Timestamp('2018-01-22') + + result = op(td, NaT) + assert result is NaT + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_td(self, op): + td = Timedelta(10, unit='d') + + result = op(td, Timedelta(days=10)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=20) + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_pytimedelta(self, op): + td = Timedelta(10, unit='d') + result = op(td, timedelta(days=9)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=19) + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_timedelta64(self, op): + td = Timedelta(10, unit='d') + result = op(td, np.timedelta64(-4, 'D')) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=6) + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_offset(self, op): + td = Timedelta(10, unit='d') + + result = op(td, pd.offsets.Hour(6)) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=10, hours=6) + + def test_td_sub_td(self): + td = Timedelta(10, unit='d') + expected = Timedelta(0, unit='ns') + result = td - td + assert isinstance(result, Timedelta) + assert result == expected + + def test_td_sub_pytimedelta(self): + td = Timedelta(10, unit='d') + expected = Timedelta(0, unit='ns') + + result = td - td.to_pytimedelta() + assert isinstance(result, Timedelta) + assert result == expected + + result = td.to_pytimedelta() - td + assert isinstance(result, Timedelta) + assert result == expected + + def test_td_sub_timedelta64(self): + td = Timedelta(10, unit='d') + expected = Timedelta(0, unit='ns') + + result = td - td.to_timedelta64() + assert isinstance(result, Timedelta) + assert result == expected + + result = td.to_timedelta64() - td + assert isinstance(result, Timedelta) + assert result == expected + + def test_td_sub_nat(self): + # In this context pd.NaT is treated as timedelta-like + td = Timedelta(10, unit='d') + result = td - NaT + assert result is NaT + + def test_td_sub_td64_nat(self): + td = Timedelta(10, unit='d') + td_nat = np.timedelta64('NaT') + + result = td - td_nat + assert result is NaT + + result = td_nat - td + assert result is NaT + + def test_td_sub_offset(self): + td = Timedelta(10, unit='d') + result = td - pd.offsets.Hour(1) + assert isinstance(result, Timedelta) + assert result == Timedelta(239, unit='h') + + def test_td_add_sub_numeric_raises(self): + td = Timedelta(10, unit='d') + for other in [2, 2.0, np.int64(2), np.float64(2)]: + with pytest.raises(TypeError): + td + other + with pytest.raises(TypeError): + other + td + with pytest.raises(TypeError): + td - other + with pytest.raises(TypeError): + other - td + + def test_td_rsub_nat(self): + td = Timedelta(10, unit='d') + result = NaT - td + assert result is NaT + + result = np.datetime64('NaT') - td + assert result is NaT + + def test_td_rsub_offset(self): + result = pd.offsets.Hour(1) - Timedelta(10, unit='d') + assert isinstance(result, Timedelta) + assert result == Timedelta(-239, unit='h') + + def test_td_sub_timedeltalike_object_dtype_array(self): + # GH#21980 + arr = np.array([Timestamp('20130101 9:01'), + Timestamp('20121230 9:02')]) + exp = np.array([Timestamp('20121231 9:01'), + Timestamp('20121229 9:02')]) + res = arr - Timedelta('1D') + tm.assert_numpy_array_equal(res, exp) + + def test_td_sub_mixed_most_timedeltalike_object_dtype_array(self): + # GH#21980 + now = Timestamp.now() + arr = np.array([now, + Timedelta('1D'), + np.timedelta64(2, 'h')]) + exp = np.array([now - Timedelta('1D'), + Timedelta('0D'), + np.timedelta64(2, 'h') - Timedelta('1D')]) + res = arr - Timedelta('1D') + tm.assert_numpy_array_equal(res, exp) + + def test_td_rsub_mixed_most_timedeltalike_object_dtype_array(self): + # GH#21980 + now = Timestamp.now() + arr = np.array([now, + Timedelta('1D'), + np.timedelta64(2, 'h')]) + with pytest.raises(TypeError): + Timedelta('1D') - arr + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_timedeltalike_object_dtype_array(self, op): + # GH#21980 + arr = np.array([Timestamp('20130101 9:01'), + Timestamp('20121230 9:02')]) + exp = np.array([Timestamp('20130102 9:01'), + Timestamp('20121231 9:02')]) + res = op(arr, Timedelta('1D')) + tm.assert_numpy_array_equal(res, exp) + + @pytest.mark.parametrize('op', [operator.add, ops.radd]) + def test_td_add_mixed_timedeltalike_object_dtype_array(self, op): + # GH#21980 + now = Timestamp.now() + arr = np.array([now, + Timedelta('1D')]) + exp = np.array([now + Timedelta('1D'), + Timedelta('2D')]) + res = op(arr, Timedelta('1D')) + tm.assert_numpy_array_equal(res, exp) + + +class TestTimedeltaMultiplicationDivision(object): + """ + Tests for Timedelta methods: + + __mul__, __rmul__, + __div__, __rdiv__, + __truediv__, __rtruediv__, + __floordiv__, __rfloordiv__, + __mod__, __rmod__, + __divmod__, __rdivmod__ + """ + + # --------------------------------------------------------------- + # Timedelta.__mul__, __rmul__ + + @pytest.mark.parametrize('td_nat', [NaT, + np.timedelta64('NaT', 'ns'), + np.timedelta64('NaT')]) + @pytest.mark.parametrize('op', [operator.mul, ops.rmul]) + def test_td_mul_nat(self, op, td_nat): + # GH#19819 + td = Timedelta(10, unit='d') + with pytest.raises(TypeError): + op(td, td_nat) + + @pytest.mark.parametrize('nan', [np.nan, np.float64('NaN'), float('nan')]) + @pytest.mark.parametrize('op', [operator.mul, ops.rmul]) + def test_td_mul_nan(self, op, nan): + # np.float64('NaN') has a 'dtype' attr, avoid treating as array + td = Timedelta(10, unit='d') + result = op(td, nan) + assert result is NaT + + @pytest.mark.parametrize('op', [operator.mul, ops.rmul]) + def test_td_mul_scalar(self, op): + # GH#19738 + td = Timedelta(minutes=3) + + result = op(td, 2) + assert result == Timedelta(minutes=6) + + result = op(td, 1.5) + assert result == Timedelta(minutes=4, seconds=30) + + assert op(td, np.nan) is NaT + + assert op(-1, td).value == -1 * td.value + assert op(-1.0, td).value == -1.0 * td.value + + with pytest.raises(TypeError): + # timedelta * datetime is gibberish + op(td, Timestamp(2016, 1, 2)) + + with pytest.raises(TypeError): + # invalid multiply with another timedelta + op(td, td) + + # --------------------------------------------------------------- + # Timedelta.__div__, __truediv__ + + def test_td_div_timedeltalike_scalar(self): + # GH#19738 + td = Timedelta(10, unit='d') + + result = td / pd.offsets.Hour(1) + assert result == 240 + + assert td / td == 1 + assert td / np.timedelta64(60, 'h') == 4 + + assert np.isnan(td / NaT) + + def test_td_div_numeric_scalar(self): + # GH#19738 + td = Timedelta(10, unit='d') + + result = td / 2 + assert isinstance(result, Timedelta) + assert result == Timedelta(days=5) + + result = td / 5.0 + assert isinstance(result, Timedelta) + assert result == Timedelta(days=2) + + @pytest.mark.parametrize('nan', [np.nan, np.float64('NaN'), float('nan')]) + def test_td_div_nan(self, nan): + # np.float64('NaN') has a 'dtype' attr, avoid treating as array + td = Timedelta(10, unit='d') + result = td / nan + assert result is NaT + + result = td // nan + assert result is NaT + + # --------------------------------------------------------------- + # Timedelta.__rdiv__ + + def test_td_rdiv_timedeltalike_scalar(self): + # GH#19738 + td = Timedelta(10, unit='d') + result = pd.offsets.Hour(1) / td + assert result == 1 / 240.0 + + assert np.timedelta64(60, 'h') / td == 0.25 + + # --------------------------------------------------------------- + # Timedelta.__floordiv__ + + def test_td_floordiv_timedeltalike_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + scalar = Timedelta(hours=3, minutes=3) + + assert td // scalar == 1 + assert -td // scalar.to_pytimedelta() == -2 + assert (2 * td) // scalar.to_timedelta64() == 2 + + def test_td_floordiv_null_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + + assert td // np.nan is NaT + assert np.isnan(td // NaT) + assert np.isnan(td // np.timedelta64('NaT')) + + def test_td_floordiv_offsets(self): + # GH#19738 + td = Timedelta(hours=3, minutes=4) + assert td // pd.offsets.Hour(1) == 3 + assert td // pd.offsets.Minute(2) == 92 + + def test_td_floordiv_invalid_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + + with pytest.raises(TypeError): + td // np.datetime64('2016-01-01', dtype='datetime64[us]') + + def test_td_floordiv_numeric_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + + expected = Timedelta(hours=1, minutes=32) + assert td // 2 == expected + assert td // 2.0 == expected + assert td // np.float64(2.0) == expected + assert td // np.int32(2.0) == expected + assert td // np.uint8(2.0) == expected + + def test_td_floordiv_timedeltalike_array(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + scalar = Timedelta(hours=3, minutes=3) + + # Array-like others + assert td // np.array(scalar.to_timedelta64()) == 1 + + res = (3 * td) // np.array([scalar.to_timedelta64()]) + expected = np.array([3], dtype=np.int64) + tm.assert_numpy_array_equal(res, expected) + + res = (10 * td) // np.array([scalar.to_timedelta64(), + np.timedelta64('NaT')]) + expected = np.array([10, np.nan]) + tm.assert_numpy_array_equal(res, expected) + + def test_td_floordiv_numeric_series(self): + # GH#18846 + td = Timedelta(hours=3, minutes=4) + ser = pd.Series([1], dtype=np.int64) + res = td // ser + assert res.dtype.kind == 'm' + + # --------------------------------------------------------------- + # Timedelta.__rfloordiv__ + + def test_td_rfloordiv_timedeltalike_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + scalar = Timedelta(hours=3, minutes=4) + + # scalar others + # x // Timedelta is defined only for timedelta-like x. int-like, + # float-like, and date-like, in particular, should all either + # a) raise TypeError directly or + # b) return NotImplemented, following which the reversed + # operation will raise TypeError. + assert td.__rfloordiv__(scalar) == 1 + assert (-td).__rfloordiv__(scalar.to_pytimedelta()) == -2 + assert (2 * td).__rfloordiv__(scalar.to_timedelta64()) == 0 + + def test_td_rfloordiv_null_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + + assert np.isnan(td.__rfloordiv__(NaT)) + assert np.isnan(td.__rfloordiv__(np.timedelta64('NaT'))) + + def test_td_rfloordiv_offsets(self): + # GH#19738 + assert pd.offsets.Hour(1) // Timedelta(minutes=25) == 2 + + def test_td_rfloordiv_invalid_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + + dt64 = np.datetime64('2016-01-01', dtype='datetime64[us]') + with pytest.raises(TypeError): + td.__rfloordiv__(dt64) + + def test_td_rfloordiv_numeric_scalar(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + + assert td.__rfloordiv__(np.nan) is NotImplemented + assert td.__rfloordiv__(3.5) is NotImplemented + assert td.__rfloordiv__(2) is NotImplemented + + with pytest.raises(TypeError): + td.__rfloordiv__(np.float64(2.0)) + with pytest.raises(TypeError): + td.__rfloordiv__(np.uint8(9)) + with tm.assert_produces_warning(FutureWarning): + # GH-19761: Change to TypeError. + td.__rfloordiv__(np.int32(2.0)) + + def test_td_rfloordiv_timedeltalike_array(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + scalar = Timedelta(hours=3, minutes=4) + + # Array-like others + assert td.__rfloordiv__(np.array(scalar.to_timedelta64())) == 1 + + res = td.__rfloordiv__(np.array([(3 * scalar).to_timedelta64()])) + expected = np.array([3], dtype=np.int64) + tm.assert_numpy_array_equal(res, expected) + + arr = np.array([(10 * scalar).to_timedelta64(), + np.timedelta64('NaT')]) + res = td.__rfloordiv__(arr) + expected = np.array([10, np.nan]) + tm.assert_numpy_array_equal(res, expected) + + def test_td_rfloordiv_numeric_series(self): + # GH#18846 + td = Timedelta(hours=3, minutes=3) + ser = pd.Series([1], dtype=np.int64) + res = td.__rfloordiv__(ser) + assert res is NotImplemented + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # TODO: GH-19761. Change to TypeError. + ser // td + + # ---------------------------------------------------------------- + # Timedelta.__mod__, __rmod__ + + def test_mod_timedeltalike(self): + # GH#19365 + td = Timedelta(hours=37) + + # Timedelta-like others + result = td % Timedelta(hours=6) + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=1) + + result = td % timedelta(minutes=60) + assert isinstance(result, Timedelta) + assert result == Timedelta(0) + + result = td % NaT + assert result is NaT + + def test_mod_timedelta64_nat(self): + # GH#19365 + td = Timedelta(hours=37) + + result = td % np.timedelta64('NaT', 'ns') + assert result is NaT + + def test_mod_timedelta64(self): + # GH#19365 + td = Timedelta(hours=37) + + result = td % np.timedelta64(2, 'h') + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=1) + + def test_mod_offset(self): + # GH#19365 + td = Timedelta(hours=37) + + result = td % pd.offsets.Hour(5) + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=2) + + def test_mod_numeric(self): + # GH#19365 + td = Timedelta(hours=37) + + # Numeric Others + result = td % 2 + assert isinstance(result, Timedelta) + assert result == Timedelta(0) + + result = td % 1e12 + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=3, seconds=20) + + result = td % int(1e12) + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=3, seconds=20) + + def test_mod_invalid(self): + # GH#19365 + td = Timedelta(hours=37) + + with pytest.raises(TypeError): + td % Timestamp('2018-01-22') + + with pytest.raises(TypeError): + td % [] + + def test_rmod_pytimedelta(self): + # GH#19365 + td = Timedelta(minutes=3) + + result = timedelta(minutes=4) % td + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=1) + + def test_rmod_timedelta64(self): + # GH#19365 + td = Timedelta(minutes=3) + result = np.timedelta64(5, 'm') % td + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=2) + + def test_rmod_invalid(self): + # GH#19365 + td = Timedelta(minutes=3) + + with pytest.raises(TypeError): + Timestamp('2018-01-22') % td + + with pytest.raises(TypeError): + 15 % td + + with pytest.raises(TypeError): + 16.0 % td + + with pytest.raises(TypeError): + np.array([22, 24]) % td + + # ---------------------------------------------------------------- + # Timedelta.__divmod__, __rdivmod__ + + def test_divmod_numeric(self): + # GH#19365 + td = Timedelta(days=2, hours=6) + + result = divmod(td, 53 * 3600 * 1e9) + assert result[0] == Timedelta(1, unit='ns') + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=1) + + assert result + result = divmod(td, np.nan) + assert result[0] is NaT + assert result[1] is NaT + + def test_divmod(self): + # GH#19365 + td = Timedelta(days=2, hours=6) + + result = divmod(td, timedelta(days=1)) + assert result[0] == 2 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=6) + + result = divmod(td, 54) + assert result[0] == Timedelta(hours=1) + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(0) + + result = divmod(td, NaT) + assert np.isnan(result[0]) + assert result[1] is NaT + + def test_divmod_offset(self): + # GH#19365 + td = Timedelta(days=2, hours=6) + + result = divmod(td, pd.offsets.Hour(-4)) + assert result[0] == -14 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=-2) + + def test_divmod_invalid(self): + # GH#19365 + td = Timedelta(days=2, hours=6) + + with pytest.raises(TypeError): + divmod(td, Timestamp('2018-01-22')) + + def test_rdivmod_pytimedelta(self): + # GH#19365 + result = divmod(timedelta(days=2, hours=6), Timedelta(days=1)) + assert result[0] == 2 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=6) + + def test_rdivmod_offset(self): + result = divmod(pd.offsets.Hour(54), Timedelta(hours=-4)) + assert result[0] == -14 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=-2) + + def test_rdivmod_invalid(self): + # GH#19365 + td = Timedelta(minutes=3) + + with pytest.raises(TypeError): + divmod(Timestamp('2018-01-22'), td) + + with pytest.raises(TypeError): + divmod(15, td) + + with pytest.raises(TypeError): + divmod(16.0, td) + + with pytest.raises(TypeError): + divmod(np.array([22, 24]), td) + + # ---------------------------------------------------------------- + + @pytest.mark.parametrize('op', [ + operator.mul, + ops.rmul, + operator.truediv, + ops.rdiv, + ops.rsub]) + @pytest.mark.parametrize('arr', [ + np.array([Timestamp('20130101 9:01'), Timestamp('20121230 9:02')]), + np.array([Timestamp.now(), Timedelta('1D')]) + ]) + def test_td_op_timedelta_timedeltalike_array(self, op, arr): + with pytest.raises(TypeError): + op(arr, Timedelta('1D')) diff --git a/pandas/tests/scalar/timedelta/test_construction.py b/pandas/tests/scalar/timedelta/test_construction.py new file mode 100644 index 0000000000000..880eca914749b --- /dev/null +++ b/pandas/tests/scalar/timedelta/test_construction.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta + +import numpy as np +import pytest + +from pandas import Timedelta, offsets, to_timedelta + + +def test_construction(): + expected = np.timedelta64(10, 'D').astype('m8[ns]').view('i8') + assert Timedelta(10, unit='d').value == expected + assert Timedelta(10.0, unit='d').value == expected + assert Timedelta('10 days').value == expected + assert Timedelta(days=10).value == expected + assert Timedelta(days=10.0).value == expected + + expected += np.timedelta64(10, 's').astype('m8[ns]').view('i8') + assert Timedelta('10 days 00:00:10').value == expected + assert Timedelta(days=10, seconds=10).value == expected + assert Timedelta(days=10, milliseconds=10 * 1000).value == expected + assert Timedelta(days=10, + microseconds=10 * 1000 * 1000).value == expected + + # rounding cases + assert Timedelta(82739999850000).value == 82739999850000 + assert ('0 days 22:58:59.999850' in str(Timedelta(82739999850000))) + assert Timedelta(123072001000000).value == 123072001000000 + assert ('1 days 10:11:12.001' in str(Timedelta(123072001000000))) + + # string conversion with/without leading zero + # GH#9570 + assert Timedelta('0:00:00') == timedelta(hours=0) + assert Timedelta('00:00:00') == timedelta(hours=0) + assert Timedelta('-1:00:00') == -timedelta(hours=1) + assert Timedelta('-01:00:00') == -timedelta(hours=1) + + # more strings & abbrevs + # GH#8190 + assert Timedelta('1 h') == timedelta(hours=1) + assert Timedelta('1 hour') == timedelta(hours=1) + assert Timedelta('1 hr') == timedelta(hours=1) + assert Timedelta('1 hours') == timedelta(hours=1) + assert Timedelta('-1 hours') == -timedelta(hours=1) + assert Timedelta('1 m') == timedelta(minutes=1) + assert Timedelta('1.5 m') == timedelta(seconds=90) + assert Timedelta('1 minute') == timedelta(minutes=1) + assert Timedelta('1 minutes') == timedelta(minutes=1) + assert Timedelta('1 s') == timedelta(seconds=1) + assert Timedelta('1 second') == timedelta(seconds=1) + assert Timedelta('1 seconds') == timedelta(seconds=1) + assert Timedelta('1 ms') == timedelta(milliseconds=1) + assert Timedelta('1 milli') == timedelta(milliseconds=1) + assert Timedelta('1 millisecond') == timedelta(milliseconds=1) + assert Timedelta('1 us') == timedelta(microseconds=1) + assert Timedelta('1 micros') == timedelta(microseconds=1) + assert Timedelta('1 microsecond') == timedelta(microseconds=1) + assert Timedelta('1.5 microsecond') == Timedelta('00:00:00.000001500') + assert Timedelta('1 ns') == Timedelta('00:00:00.000000001') + assert Timedelta('1 nano') == Timedelta('00:00:00.000000001') + assert Timedelta('1 nanosecond') == Timedelta('00:00:00.000000001') + + # combos + assert Timedelta('10 days 1 hour') == timedelta(days=10, hours=1) + assert Timedelta('10 days 1 h') == timedelta(days=10, hours=1) + assert Timedelta('10 days 1 h 1m 1s') == timedelta( + days=10, hours=1, minutes=1, seconds=1) + assert Timedelta('-10 days 1 h 1m 1s') == -timedelta( + days=10, hours=1, minutes=1, seconds=1) + assert Timedelta('-10 days 1 h 1m 1s') == -timedelta( + days=10, hours=1, minutes=1, seconds=1) + assert Timedelta('-10 days 1 h 1m 1s 3us') == -timedelta( + days=10, hours=1, minutes=1, seconds=1, microseconds=3) + assert Timedelta('-10 days 1 h 1.5m 1s 3us') == -timedelta( + days=10, hours=1, minutes=1, seconds=31, microseconds=3) + + # Currently invalid as it has a - on the hh:mm:dd part + # (only allowed on the days) + with pytest.raises(ValueError): + Timedelta('-10 days -1 h 1.5m 1s 3us') + + # only leading neg signs are allowed + with pytest.raises(ValueError): + Timedelta('10 days -1 h 1.5m 1s 3us') + + # no units specified + with pytest.raises(ValueError): + Timedelta('3.1415') + + # invalid construction + with pytest.raises(ValueError, match="cannot construct a Timedelta"): + Timedelta() + + with pytest.raises(ValueError, match="unit abbreviation w/o a number"): + Timedelta('foo') + + msg = ("cannot construct a Timedelta from " + "the passed arguments, allowed keywords are ") + with pytest.raises(ValueError, match=msg): + Timedelta(day=10) + + # floats + expected = np.timedelta64( + 10, 's').astype('m8[ns]').view('i8') + np.timedelta64( + 500, 'ms').astype('m8[ns]').view('i8') + assert Timedelta(10.5, unit='s').value == expected + + # offset + assert to_timedelta(offsets.Hour(2)) == Timedelta(hours=2) + assert Timedelta(offsets.Hour(2)) == Timedelta(hours=2) + assert Timedelta(offsets.Second(2)) == Timedelta(seconds=2) + + # GH#11995: unicode + expected = Timedelta('1H') + result = Timedelta(u'1H') + assert result == expected + assert to_timedelta(offsets.Hour(2)) == Timedelta(u'0 days, 02:00:00') + + with pytest.raises(ValueError): + Timedelta(u'foo bar') + + +@pytest.mark.parametrize('item', list({'days': 'D', + 'seconds': 's', + 'microseconds': 'us', + 'milliseconds': 'ms', + 'minutes': 'm', + 'hours': 'h', + 'weeks': 'W'}.items())) +@pytest.mark.parametrize('npdtype', [np.int64, np.int32, np.int16, + np.float64, np.float32, np.float16]) +def test_td_construction_with_np_dtypes(npdtype, item): + # GH#8757: test construction with np dtypes + pykwarg, npkwarg = item + expected = np.timedelta64(1, npkwarg).astype('m8[ns]').view('i8') + assert Timedelta(**{pykwarg: npdtype(1)}).value == expected + + +@pytest.mark.parametrize('val', [ + '1s', '-1s', '1us', '-1us', '1 day', '-1 day', + '-23:59:59.999999', '-1 days +23:59:59.999999', '-1ns', + '1ns', '-23:59:59.999999999']) +def test_td_from_repr_roundtrip(val): + # round-trip both for string and value + td = Timedelta(val) + assert Timedelta(td.value) == td + + # str does not normally display nanos + if not td.nanoseconds: + assert Timedelta(str(td)) == td + assert Timedelta(td._repr_base(format='all')) == td + + +def test_overflow_on_construction(): + # GH#3374 + value = Timedelta('1day').value * 20169940 + with pytest.raises(OverflowError): + Timedelta(value) + + # xref GH#17637 + with pytest.raises(OverflowError): + Timedelta(7 * 19999, unit='D') + + with pytest.raises(OverflowError): + Timedelta(timedelta(days=13 * 19999)) + + +@pytest.mark.parametrize('fmt,exp', [ + ('P6DT0H50M3.010010012S', Timedelta(days=6, minutes=50, seconds=3, + milliseconds=10, microseconds=10, + nanoseconds=12)), + ('P-6DT0H50M3.010010012S', Timedelta(days=-6, minutes=50, seconds=3, + milliseconds=10, microseconds=10, + nanoseconds=12)), + ('P4DT12H30M5S', Timedelta(days=4, hours=12, minutes=30, seconds=5)), + ('P0DT0H0M0.000000123S', Timedelta(nanoseconds=123)), + ('P0DT0H0M0.00001S', Timedelta(microseconds=10)), + ('P0DT0H0M0.001S', Timedelta(milliseconds=1)), + ('P0DT0H1M0S', Timedelta(minutes=1)), + ('P1DT25H61M61S', Timedelta(days=1, hours=25, minutes=61, seconds=61)) +]) +def test_iso_constructor(fmt, exp): + assert Timedelta(fmt) == exp + + +@pytest.mark.parametrize('fmt', [ + 'PPPPPPPPPPPP', 'PDTHMS', 'P0DT999H999M999S', + 'P1DT0H0M0.0000000000000S', 'P1DT0H0M00000000000S', + 'P1DT0H0M0.S']) +def test_iso_constructor_raises(fmt): + with pytest.raises(ValueError, match=('Invalid ISO 8601 Duration ' + 'format - {}'.format(fmt))): + Timedelta(fmt) + + +@pytest.mark.parametrize('constructed_td, conversion', [ + (Timedelta(nanoseconds=100), '100ns'), + (Timedelta(days=1, hours=1, minutes=1, weeks=1, seconds=1, milliseconds=1, + microseconds=1, nanoseconds=1), 694861001001001), + (Timedelta(microseconds=1) + Timedelta(nanoseconds=1), '1us1ns'), + (Timedelta(microseconds=1) - Timedelta(nanoseconds=1), '999ns'), + (Timedelta(microseconds=1) + 5 * Timedelta(nanoseconds=-2), '990ns')]) +def test_td_constructor_on_nanoseconds(constructed_td, conversion): + # GH#9273 + assert constructed_td == Timedelta(conversion) + + +def test_td_constructor_value_error(): + with pytest.raises(TypeError): + Timedelta(nanoseconds='abc') diff --git a/pandas/tests/scalar/timedelta/test_formats.py b/pandas/tests/scalar/timedelta/test_formats.py new file mode 100644 index 0000000000000..0d0b24f192f96 --- /dev/null +++ b/pandas/tests/scalar/timedelta/test_formats.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import pytest + +from pandas import Timedelta + + +@pytest.mark.parametrize('td, expected_repr', [ + (Timedelta(10, unit='d'), "Timedelta('10 days 00:00:00')"), + (Timedelta(10, unit='s'), "Timedelta('0 days 00:00:10')"), + (Timedelta(10, unit='ms'), "Timedelta('0 days 00:00:00.010000')"), + (Timedelta(-10, unit='ms'), "Timedelta('-1 days +23:59:59.990000')")]) +def test_repr(td, expected_repr): + assert repr(td) == expected_repr + + +@pytest.mark.parametrize('td, expected_iso', [ + (Timedelta(days=6, minutes=50, seconds=3, milliseconds=10, microseconds=10, + nanoseconds=12), 'P6DT0H50M3.010010012S'), + (Timedelta(days=4, hours=12, minutes=30, seconds=5), 'P4DT12H30M5S'), + (Timedelta(nanoseconds=123), 'P0DT0H0M0.000000123S'), + # trim nano + (Timedelta(microseconds=10), 'P0DT0H0M0.00001S'), + # trim micro + (Timedelta(milliseconds=1), 'P0DT0H0M0.001S'), + # don't strip every 0 + (Timedelta(minutes=1), 'P0DT0H1M0S')]) +def test_isoformat(td, expected_iso): + assert td.isoformat() == expected_iso diff --git a/pandas/tests/scalar/timedelta/test_timedelta.py b/pandas/tests/scalar/timedelta/test_timedelta.py new file mode 100644 index 0000000000000..ee2c2e9e1959c --- /dev/null +++ b/pandas/tests/scalar/timedelta/test_timedelta.py @@ -0,0 +1,759 @@ +""" test the scalar Timedelta """ +from datetime import timedelta +import re + +import numpy as np +import pytest + +from pandas._libs.tslibs import NaT, iNaT +import pandas.compat as compat + +import pandas as pd +from pandas import ( + Series, Timedelta, TimedeltaIndex, timedelta_range, to_timedelta) +import pandas.util.testing as tm + + +class TestTimedeltaArithmetic(object): + + def test_arithmetic_overflow(self): + with pytest.raises(OverflowError): + pd.Timestamp('1700-01-01') + pd.Timedelta(13 * 19999, unit='D') + + with pytest.raises(OverflowError): + pd.Timestamp('1700-01-01') + timedelta(days=13 * 19999) + + def test_array_timedelta_floordiv(self): + # https://github.com/pandas-dev/pandas/issues/19761 + ints = pd.date_range('2012-10-08', periods=4, freq='D').view('i8') + msg = r"Use 'array // timedelta.value'" + with tm.assert_produces_warning(FutureWarning) as m: + result = ints // pd.Timedelta(1, unit='s') + + assert msg in str(m[0].message) + expected = np.array([1349654400, 1349740800, 1349827200, 1349913600], + dtype='i8') + tm.assert_numpy_array_equal(result, expected) + + def test_ops_error_str(self): + # GH 13624 + td = Timedelta('1 day') + + for left, right in [(td, 'a'), ('a', td)]: + + with pytest.raises(TypeError): + left + right + + with pytest.raises(TypeError): + left > right + + assert not left == right + assert left != right + + def test_ops_notimplemented(self): + class Other(object): + pass + + other = Other() + + td = Timedelta('1 day') + assert td.__add__(other) is NotImplemented + assert td.__sub__(other) is NotImplemented + assert td.__truediv__(other) is NotImplemented + assert td.__mul__(other) is NotImplemented + assert td.__floordiv__(other) is NotImplemented + + def test_unary_ops(self): + td = Timedelta(10, unit='d') + + # __neg__, __pos__ + assert -td == Timedelta(-10, unit='d') + assert -td == Timedelta('-10d') + assert +td == Timedelta(10, unit='d') + + # __abs__, __abs__(__neg__) + assert abs(td) == td + assert abs(-td) == td + assert abs(-td) == Timedelta('10d') + + +class TestTimedeltaComparison(object): + def test_compare_tick(self, tick_classes): + cls = tick_classes + + off = cls(4) + td = off.delta + assert isinstance(td, Timedelta) + + assert td == off + assert not td != off + assert td <= off + assert td >= off + assert not td < off + assert not td > off + + assert not td == 2 * off + assert td != 2 * off + assert td <= 2 * off + assert td < 2 * off + assert not td >= 2 * off + assert not td > 2 * off + + def test_comparison_object_array(self): + # analogous to GH#15183 + td = Timedelta('2 days') + other = Timedelta('3 hours') + + arr = np.array([other, td], dtype=object) + res = arr == td + expected = np.array([False, True], dtype=bool) + assert (res == expected).all() + + # 2D case + arr = np.array([[other, td], + [td, other]], + dtype=object) + res = arr != td + expected = np.array([[True, False], [False, True]], dtype=bool) + assert res.shape == expected.shape + assert (res == expected).all() + + def test_compare_timedelta_ndarray(self): + # GH11835 + periods = [Timedelta('0 days 01:00:00'), Timedelta('0 days 01:00:00')] + arr = np.array(periods) + result = arr[0] > arr + expected = np.array([False, False]) + tm.assert_numpy_array_equal(result, expected) + + @pytest.mark.skip(reason="GH#20829 is reverted until after 0.24.0") + def test_compare_custom_object(self): + """ + Make sure non supported operations on Timedelta returns NonImplemented + and yields to other operand (GH#20829). + """ + class CustomClass(object): + + def __init__(self, cmp_result=None): + self.cmp_result = cmp_result + + def generic_result(self): + if self.cmp_result is None: + return NotImplemented + else: + return self.cmp_result + + def __eq__(self, other): + return self.generic_result() + + def __gt__(self, other): + return self.generic_result() + + t = Timedelta('1s') + + assert not (t == "string") + assert not (t == 1) + assert not (t == CustomClass()) + assert not (t == CustomClass(cmp_result=False)) + + assert t < CustomClass(cmp_result=True) + assert not (t < CustomClass(cmp_result=False)) + + assert t == CustomClass(cmp_result=True) + + @pytest.mark.parametrize("val", ["string", 1]) + def test_compare_unknown_type(self, val): + # GH20829 + t = Timedelta('1s') + with pytest.raises(TypeError): + t >= val + with pytest.raises(TypeError): + t > val + with pytest.raises(TypeError): + t <= val + with pytest.raises(TypeError): + t < val + + +class TestTimedeltas(object): + + @pytest.mark.parametrize("unit, value, expected", [ + ('us', 9.999, 9999), ('ms', 9.999999, 9999999), + ('s', 9.999999999, 9999999999)]) + def test_rounding_on_int_unit_construction(self, unit, value, expected): + # GH 12690 + result = Timedelta(value, unit=unit) + assert result.value == expected + result = Timedelta(str(value) + unit) + assert result.value == expected + + def test_total_seconds_scalar(self): + # see gh-10939 + rng = Timedelta('1 days, 10:11:12.100123456') + expt = 1 * 86400 + 10 * 3600 + 11 * 60 + 12 + 100123456. / 1e9 + tm.assert_almost_equal(rng.total_seconds(), expt) + + rng = Timedelta(np.nan) + assert np.isnan(rng.total_seconds()) + + def test_conversion(self): + + for td in [Timedelta(10, unit='d'), + Timedelta('1 days, 10:11:12.012345')]: + pydt = td.to_pytimedelta() + assert td == Timedelta(pydt) + assert td == pydt + assert (isinstance(pydt, timedelta) and not isinstance( + pydt, Timedelta)) + + assert td == np.timedelta64(td.value, 'ns') + td64 = td.to_timedelta64() + + assert td64 == np.timedelta64(td.value, 'ns') + assert td == td64 + + assert isinstance(td64, np.timedelta64) + + # this is NOT equal and cannot be roundtriped (because of the nanos) + td = Timedelta('1 days, 10:11:12.012345678') + assert td != td.to_pytimedelta() + + def test_freq_conversion(self): + + # truediv + td = Timedelta('1 days 2 hours 3 ns') + result = td / np.timedelta64(1, 'D') + assert result == td.value / float(86400 * 1e9) + result = td / np.timedelta64(1, 's') + assert result == td.value / float(1e9) + result = td / np.timedelta64(1, 'ns') + assert result == td.value + + # floordiv + td = Timedelta('1 days 2 hours 3 ns') + result = td // np.timedelta64(1, 'D') + assert result == 1 + result = td // np.timedelta64(1, 's') + assert result == 93600 + result = td // np.timedelta64(1, 'ns') + assert result == td.value + + def test_fields(self): + def check(value): + # that we are int/long like + assert isinstance(value, (int, compat.long)) + + # compat to datetime.timedelta + rng = to_timedelta('1 days, 10:11:12') + assert rng.days == 1 + assert rng.seconds == 10 * 3600 + 11 * 60 + 12 + assert rng.microseconds == 0 + assert rng.nanoseconds == 0 + + msg = "'Timedelta' object has no attribute '{}'" + with pytest.raises(AttributeError, match=msg.format('hours')): + rng.hours + with pytest.raises(AttributeError, match=msg.format('minutes')): + rng.minutes + with pytest.raises(AttributeError, match=msg.format('milliseconds')): + rng.milliseconds + + # GH 10050 + check(rng.days) + check(rng.seconds) + check(rng.microseconds) + check(rng.nanoseconds) + + td = Timedelta('-1 days, 10:11:12') + assert abs(td) == Timedelta('13:48:48') + assert str(td) == "-1 days +10:11:12" + assert -td == Timedelta('0 days 13:48:48') + assert -Timedelta('-1 days, 10:11:12').value == 49728000000000 + assert Timedelta('-1 days, 10:11:12').value == -49728000000000 + + rng = to_timedelta('-1 days, 10:11:12.100123456') + assert rng.days == -1 + assert rng.seconds == 10 * 3600 + 11 * 60 + 12 + assert rng.microseconds == 100 * 1000 + 123 + assert rng.nanoseconds == 456 + msg = "'Timedelta' object has no attribute '{}'" + with pytest.raises(AttributeError, match=msg.format('hours')): + rng.hours + with pytest.raises(AttributeError, match=msg.format('minutes')): + rng.minutes + with pytest.raises(AttributeError, match=msg.format('milliseconds')): + rng.milliseconds + + # components + tup = pd.to_timedelta(-1, 'us').components + assert tup.days == -1 + assert tup.hours == 23 + assert tup.minutes == 59 + assert tup.seconds == 59 + assert tup.milliseconds == 999 + assert tup.microseconds == 999 + assert tup.nanoseconds == 0 + + # GH 10050 + check(tup.days) + check(tup.hours) + check(tup.minutes) + check(tup.seconds) + check(tup.milliseconds) + check(tup.microseconds) + check(tup.nanoseconds) + + tup = Timedelta('-1 days 1 us').components + assert tup.days == -2 + assert tup.hours == 23 + assert tup.minutes == 59 + assert tup.seconds == 59 + assert tup.milliseconds == 999 + assert tup.microseconds == 999 + assert tup.nanoseconds == 0 + + def test_iso_conversion(self): + # GH #21877 + expected = Timedelta(1, unit='s') + assert to_timedelta('P0DT0H0M1S') == expected + + def test_nat_converters(self): + result = to_timedelta('nat', box=False) + assert result.dtype.kind == 'm' + assert result.astype('int64') == iNaT + + result = to_timedelta('nan', box=False) + assert result.dtype.kind == 'm' + assert result.astype('int64') == iNaT + + @pytest.mark.filterwarnings("ignore:M and Y units are deprecated") + @pytest.mark.parametrize('units, np_unit', + [(['Y', 'y'], 'Y'), + (['M'], 'M'), + (['W', 'w'], 'W'), + (['D', 'd', 'days', 'day', 'Days', 'Day'], 'D'), + (['m', 'minute', 'min', 'minutes', 't', + 'Minute', 'Min', 'Minutes', 'T'], 'm'), + (['s', 'seconds', 'sec', 'second', + 'S', 'Seconds', 'Sec', 'Second'], 's'), + (['ms', 'milliseconds', 'millisecond', 'milli', + 'millis', 'l', 'MS', 'Milliseconds', + 'Millisecond', 'Milli', 'Millis', 'L'], 'ms'), + (['us', 'microseconds', 'microsecond', 'micro', + 'micros', 'u', 'US', 'Microseconds', + 'Microsecond', 'Micro', 'Micros', 'U'], 'us'), + (['ns', 'nanoseconds', 'nanosecond', 'nano', + 'nanos', 'n', 'NS', 'Nanoseconds', + 'Nanosecond', 'Nano', 'Nanos', 'N'], 'ns')]) + @pytest.mark.parametrize('wrapper', [np.array, list, pd.Index]) + def test_unit_parser(self, units, np_unit, wrapper): + # validate all units, GH 6855, GH 21762 + for unit in units: + # array-likes + expected = TimedeltaIndex([np.timedelta64(i, np_unit) + for i in np.arange(5).tolist()]) + result = to_timedelta(wrapper(range(5)), unit=unit) + tm.assert_index_equal(result, expected) + result = TimedeltaIndex(wrapper(range(5)), unit=unit) + tm.assert_index_equal(result, expected) + + if unit == 'M': + # M is treated as minutes in string repr + expected = TimedeltaIndex([np.timedelta64(i, 'm') + for i in np.arange(5).tolist()]) + + str_repr = ['{}{}'.format(x, unit) for x in np.arange(5)] + result = to_timedelta(wrapper(str_repr)) + tm.assert_index_equal(result, expected) + result = TimedeltaIndex(wrapper(str_repr)) + tm.assert_index_equal(result, expected) + + # scalar + expected = Timedelta(np.timedelta64(2, np_unit).astype( + 'timedelta64[ns]')) + + result = to_timedelta(2, unit=unit) + assert result == expected + result = Timedelta(2, unit=unit) + assert result == expected + + if unit == 'M': + expected = Timedelta(np.timedelta64(2, 'm').astype( + 'timedelta64[ns]')) + + result = to_timedelta('2{}'.format(unit)) + assert result == expected + result = Timedelta('2{}'.format(unit)) + assert result == expected + + @pytest.mark.skipif(compat.PY2, reason="requires python3.5 or higher") + @pytest.mark.parametrize('unit', ['Y', 'y', 'M']) + def test_unit_m_y_deprecated(self, unit): + with tm.assert_produces_warning(FutureWarning) as w1: + Timedelta(10, unit) + msg = r'.* units are deprecated .*' + assert re.match(msg, str(w1[0].message)) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False) as w2: + to_timedelta(10, unit) + msg = r'.* units are deprecated .*' + assert re.match(msg, str(w2[0].message)) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False) as w3: + to_timedelta([1, 2], unit) + msg = r'.* units are deprecated .*' + assert re.match(msg, str(w3[0].message)) + + def test_numeric_conversions(self): + assert Timedelta(0) == np.timedelta64(0, 'ns') + assert Timedelta(10) == np.timedelta64(10, 'ns') + assert Timedelta(10, unit='ns') == np.timedelta64(10, 'ns') + + assert Timedelta(10, unit='us') == np.timedelta64(10, 'us') + assert Timedelta(10, unit='ms') == np.timedelta64(10, 'ms') + assert Timedelta(10, unit='s') == np.timedelta64(10, 's') + assert Timedelta(10, unit='d') == np.timedelta64(10, 'D') + + def test_timedelta_conversions(self): + assert (Timedelta(timedelta(seconds=1)) == + np.timedelta64(1, 's').astype('m8[ns]')) + assert (Timedelta(timedelta(microseconds=1)) == + np.timedelta64(1, 'us').astype('m8[ns]')) + assert (Timedelta(timedelta(days=1)) == + np.timedelta64(1, 'D').astype('m8[ns]')) + + def test_to_numpy_alias(self): + # GH 24653: alias .to_numpy() for scalars + td = Timedelta('10m7s') + assert td.to_timedelta64() == td.to_numpy() + + def test_round(self): + + t1 = Timedelta('1 days 02:34:56.789123456') + t2 = Timedelta('-1 days 02:34:56.789123456') + + for (freq, s1, s2) in [('N', t1, t2), + ('U', Timedelta('1 days 02:34:56.789123000'), + Timedelta('-1 days 02:34:56.789123000')), + ('L', Timedelta('1 days 02:34:56.789000000'), + Timedelta('-1 days 02:34:56.789000000')), + ('S', Timedelta('1 days 02:34:57'), + Timedelta('-1 days 02:34:57')), + ('2S', Timedelta('1 days 02:34:56'), + Timedelta('-1 days 02:34:56')), + ('5S', Timedelta('1 days 02:34:55'), + Timedelta('-1 days 02:34:55')), + ('T', Timedelta('1 days 02:35:00'), + Timedelta('-1 days 02:35:00')), + ('12T', Timedelta('1 days 02:36:00'), + Timedelta('-1 days 02:36:00')), + ('H', Timedelta('1 days 03:00:00'), + Timedelta('-1 days 03:00:00')), + ('d', Timedelta('1 days'), + Timedelta('-1 days'))]: + r1 = t1.round(freq) + assert r1 == s1 + r2 = t2.round(freq) + assert r2 == s2 + + # invalid + for freq, msg in [ + ('Y', ' is a non-fixed frequency'), + ('M', ' is a non-fixed frequency'), + ('foobar', 'Invalid frequency: foobar')]: + with pytest.raises(ValueError, match=msg): + t1.round(freq) + + t1 = timedelta_range('1 days', periods=3, freq='1 min 2 s 3 us') + t2 = -1 * t1 + t1a = timedelta_range('1 days', periods=3, freq='1 min 2 s') + t1c = pd.TimedeltaIndex([1, 1, 1], unit='D') + + # note that negative times round DOWN! so don't give whole numbers + for (freq, s1, s2) in [('N', t1, t2), + ('U', t1, t2), + ('L', t1a, + TimedeltaIndex(['-1 days +00:00:00', + '-2 days +23:58:58', + '-2 days +23:57:56'], + dtype='timedelta64[ns]', + freq=None) + ), + ('S', t1a, + TimedeltaIndex(['-1 days +00:00:00', + '-2 days +23:58:58', + '-2 days +23:57:56'], + dtype='timedelta64[ns]', + freq=None) + ), + ('12T', t1c, + TimedeltaIndex(['-1 days', + '-1 days', + '-1 days'], + dtype='timedelta64[ns]', + freq=None) + ), + ('H', t1c, + TimedeltaIndex(['-1 days', + '-1 days', + '-1 days'], + dtype='timedelta64[ns]', + freq=None) + ), + ('d', t1c, + pd.TimedeltaIndex([-1, -1, -1], unit='D') + )]: + + r1 = t1.round(freq) + tm.assert_index_equal(r1, s1) + r2 = t2.round(freq) + tm.assert_index_equal(r2, s2) + + # invalid + for freq, msg in [ + ('Y', ' is a non-fixed frequency'), + ('M', ' is a non-fixed frequency'), + ('foobar', 'Invalid frequency: foobar')]: + with pytest.raises(ValueError, match=msg): + t1.round(freq) + + def test_contains(self): + # Checking for any NaT-like objects + # GH 13603 + td = to_timedelta(range(5), unit='d') + pd.offsets.Hour(1) + for v in [pd.NaT, None, float('nan'), np.nan]: + assert not (v in td) + + td = to_timedelta([pd.NaT]) + for v in [pd.NaT, None, float('nan'), np.nan]: + assert (v in td) + + def test_identity(self): + + td = Timedelta(10, unit='d') + assert isinstance(td, Timedelta) + assert isinstance(td, timedelta) + + def test_short_format_converters(self): + def conv(v): + return v.astype('m8[ns]') + + assert Timedelta('10') == np.timedelta64(10, 'ns') + assert Timedelta('10ns') == np.timedelta64(10, 'ns') + assert Timedelta('100') == np.timedelta64(100, 'ns') + assert Timedelta('100ns') == np.timedelta64(100, 'ns') + + assert Timedelta('1000') == np.timedelta64(1000, 'ns') + assert Timedelta('1000ns') == np.timedelta64(1000, 'ns') + assert Timedelta('1000NS') == np.timedelta64(1000, 'ns') + + assert Timedelta('10us') == np.timedelta64(10000, 'ns') + assert Timedelta('100us') == np.timedelta64(100000, 'ns') + assert Timedelta('1000us') == np.timedelta64(1000000, 'ns') + assert Timedelta('1000Us') == np.timedelta64(1000000, 'ns') + assert Timedelta('1000uS') == np.timedelta64(1000000, 'ns') + + assert Timedelta('1ms') == np.timedelta64(1000000, 'ns') + assert Timedelta('10ms') == np.timedelta64(10000000, 'ns') + assert Timedelta('100ms') == np.timedelta64(100000000, 'ns') + assert Timedelta('1000ms') == np.timedelta64(1000000000, 'ns') + + assert Timedelta('-1s') == -np.timedelta64(1000000000, 'ns') + assert Timedelta('1s') == np.timedelta64(1000000000, 'ns') + assert Timedelta('10s') == np.timedelta64(10000000000, 'ns') + assert Timedelta('100s') == np.timedelta64(100000000000, 'ns') + assert Timedelta('1000s') == np.timedelta64(1000000000000, 'ns') + + assert Timedelta('1d') == conv(np.timedelta64(1, 'D')) + assert Timedelta('-1d') == -conv(np.timedelta64(1, 'D')) + assert Timedelta('1D') == conv(np.timedelta64(1, 'D')) + assert Timedelta('10D') == conv(np.timedelta64(10, 'D')) + assert Timedelta('100D') == conv(np.timedelta64(100, 'D')) + assert Timedelta('1000D') == conv(np.timedelta64(1000, 'D')) + assert Timedelta('10000D') == conv(np.timedelta64(10000, 'D')) + + # space + assert Timedelta(' 10000D ') == conv(np.timedelta64(10000, 'D')) + assert Timedelta(' - 10000D ') == -conv(np.timedelta64(10000, 'D')) + + # invalid + with pytest.raises(ValueError): + Timedelta('1foo') + with pytest.raises(ValueError): + Timedelta('foo') + + def test_full_format_converters(self): + def conv(v): + return v.astype('m8[ns]') + + d1 = np.timedelta64(1, 'D') + + assert Timedelta('1days') == conv(d1) + assert Timedelta('1days,') == conv(d1) + assert Timedelta('- 1days,') == -conv(d1) + + assert Timedelta('00:00:01') == conv(np.timedelta64(1, 's')) + assert Timedelta('06:00:01') == conv(np.timedelta64(6 * 3600 + 1, 's')) + assert Timedelta('06:00:01.0') == conv( + np.timedelta64(6 * 3600 + 1, 's')) + assert Timedelta('06:00:01.01') == conv(np.timedelta64( + 1000 * (6 * 3600 + 1) + 10, 'ms')) + + assert (Timedelta('- 1days, 00:00:01') == + conv(-d1 + np.timedelta64(1, 's'))) + assert (Timedelta('1days, 06:00:01') == + conv(d1 + np.timedelta64(6 * 3600 + 1, 's'))) + assert (Timedelta('1days, 06:00:01.01') == + conv(d1 + np.timedelta64(1000 * (6 * 3600 + 1) + 10, 'ms'))) + + # invalid + with pytest.raises(ValueError): + Timedelta('- 1days, 00') + + def test_overflow(self): + # GH 9442 + s = Series(pd.date_range('20130101', periods=100000, freq='H')) + s[0] += pd.Timedelta('1s 1ms') + + # mean + result = (s - s.min()).mean() + expected = pd.Timedelta((pd.TimedeltaIndex((s - s.min())).asi8 / len(s) + ).sum()) + + # the computation is converted to float so + # might be some loss of precision + assert np.allclose(result.value / 1000, expected.value / 1000) + + # sum + msg = "overflow in timedelta operation" + with pytest.raises(ValueError, match=msg): + (s - s.min()).sum() + s1 = s[0:10000] + with pytest.raises(ValueError, match=msg): + (s1 - s1.min()).sum() + s2 = s[0:1000] + result = (s2 - s2.min()).sum() + + def test_pickle(self): + + v = Timedelta('1 days 10:11:12.0123456') + v_p = tm.round_trip_pickle(v) + assert v == v_p + + def test_timedelta_hash_equality(self): + # GH 11129 + v = Timedelta(1, 'D') + td = timedelta(days=1) + assert hash(v) == hash(td) + + d = {td: 2} + assert d[v] == 2 + + tds = timedelta_range('1 second', periods=20) + assert all(hash(td) == hash(td.to_pytimedelta()) for td in tds) + + # python timedeltas drop ns resolution + ns_td = Timedelta(1, 'ns') + assert hash(ns_td) != hash(ns_td.to_pytimedelta()) + + def test_implementation_limits(self): + min_td = Timedelta(Timedelta.min) + max_td = Timedelta(Timedelta.max) + + # GH 12727 + # timedelta limits correspond to int64 boundaries + assert min_td.value == np.iinfo(np.int64).min + 1 + assert max_td.value == np.iinfo(np.int64).max + + # Beyond lower limit, a NAT before the Overflow + assert (min_td - Timedelta(1, 'ns')) is NaT + + with pytest.raises(OverflowError): + min_td - Timedelta(2, 'ns') + + with pytest.raises(OverflowError): + max_td + Timedelta(1, 'ns') + + # Same tests using the internal nanosecond values + td = Timedelta(min_td.value - 1, 'ns') + assert td is NaT + + with pytest.raises(OverflowError): + Timedelta(min_td.value - 2, 'ns') + + with pytest.raises(OverflowError): + Timedelta(max_td.value + 1, 'ns') + + def test_total_seconds_precision(self): + # GH 19458 + assert Timedelta('30S').total_seconds() == 30.0 + assert Timedelta('0').total_seconds() == 0.0 + assert Timedelta('-2S').total_seconds() == -2.0 + assert Timedelta('5.324S').total_seconds() == 5.324 + assert (Timedelta('30S').total_seconds() - 30.0) < 1e-20 + assert (30.0 - Timedelta('30S').total_seconds()) < 1e-20 + + def test_timedelta_arithmetic(self): + data = pd.Series(['nat', '32 days'], dtype='timedelta64[ns]') + deltas = [timedelta(days=1), Timedelta(1, unit='D')] + for delta in deltas: + result_method = data.add(delta) + result_operator = data + delta + expected = pd.Series(['nat', '33 days'], dtype='timedelta64[ns]') + tm.assert_series_equal(result_operator, expected) + tm.assert_series_equal(result_method, expected) + + result_method = data.sub(delta) + result_operator = data - delta + expected = pd.Series(['nat', '31 days'], dtype='timedelta64[ns]') + tm.assert_series_equal(result_operator, expected) + tm.assert_series_equal(result_method, expected) + # GH 9396 + result_method = data.div(delta) + result_operator = data / delta + expected = pd.Series([np.nan, 32.], dtype='float64') + tm.assert_series_equal(result_operator, expected) + tm.assert_series_equal(result_method, expected) + + def test_apply_to_timedelta(self): + timedelta_NaT = pd.to_timedelta('NaT') + + list_of_valid_strings = ['00:00:01', '00:00:02'] + a = pd.to_timedelta(list_of_valid_strings) + b = Series(list_of_valid_strings).apply(pd.to_timedelta) + # Can't compare until apply on a Series gives the correct dtype + # assert_series_equal(a, b) + + list_of_strings = ['00:00:01', np.nan, pd.NaT, timedelta_NaT] + + # TODO: unused? + a = pd.to_timedelta(list_of_strings) # noqa + b = Series(list_of_strings).apply(pd.to_timedelta) # noqa + # Can't compare until apply on a Series gives the correct dtype + # assert_series_equal(a, b) + + def test_components(self): + rng = timedelta_range('1 days, 10:11:12', periods=2, freq='s') + rng.components + + # with nat + s = Series(rng) + s[1] = np.nan + + result = s.dt.components + assert not result.iloc[0].isna().all() + assert result.iloc[1].isna().all() + + +@pytest.mark.parametrize('value, expected', [ + (Timedelta('10S'), True), + (Timedelta('-10S'), True), + (Timedelta(10, unit='ns'), True), + (Timedelta(0, unit='ns'), False), + (Timedelta(-10, unit='ns'), True), + (Timedelta(None), True), + (pd.NaT, True), +]) +def test_truthiness(value, expected): + # https://github.com/pandas-dev/pandas/issues/21484 + assert bool(value) is expected diff --git a/pandas/tests/scalar/timestamp/__init__.py b/pandas/tests/scalar/timestamp/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py new file mode 100644 index 0000000000000..331d66589802d --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +from datetime import datetime, timedelta + +import numpy as np +import pytest + +from pandas.compat import long + +from pandas import Timedelta, Timestamp +import pandas.util.testing as tm + +from pandas.tseries import offsets +from pandas.tseries.frequencies import to_offset + + +class TestTimestampArithmetic(object): + def test_overflow_offset(self): + # no overflow expected + + stamp = Timestamp("2000/1/1") + offset_no_overflow = to_offset("D") * 100 + + expected = Timestamp("2000/04/10") + assert stamp + offset_no_overflow == expected + + assert offset_no_overflow + stamp == expected + + expected = Timestamp("1999/09/23") + assert stamp - offset_no_overflow == expected + + def test_overflow_offset_raises(self): + # xref https://github.com/statsmodels/statsmodels/issues/3374 + # ends up multiplying really large numbers which overflow + + stamp = Timestamp('2017-01-13 00:00:00', freq='D') + offset_overflow = 20169940 * offsets.Day(1) + msg = ("the add operation between " + r"\<-?\d+ \* Days\> and \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} " + "will overflow") + + with pytest.raises(OverflowError, match=msg): + stamp + offset_overflow + + with pytest.raises(OverflowError, match=msg): + offset_overflow + stamp + + with pytest.raises(OverflowError, match=msg): + stamp - offset_overflow + + # xref https://github.com/pandas-dev/pandas/issues/14080 + # used to crash, so check for proper overflow exception + + stamp = Timestamp("2000/1/1") + offset_overflow = to_offset("D") * 100 ** 25 + + with pytest.raises(OverflowError, match=msg): + stamp + offset_overflow + + with pytest.raises(OverflowError, match=msg): + offset_overflow + stamp + + with pytest.raises(OverflowError, match=msg): + stamp - offset_overflow + + def test_delta_preserve_nanos(self): + val = Timestamp(long(1337299200000000123)) + result = val + timedelta(1) + assert result.nanosecond == val.nanosecond + + def test_timestamp_sub_datetime(self): + dt = datetime(2013, 10, 12) + ts = Timestamp(datetime(2013, 10, 13)) + assert (ts - dt).days == 1 + assert (dt - ts).days == -1 + + def test_addition_subtraction_types(self): + # Assert on the types resulting from Timestamp +/- various date/time + # objects + dt = datetime(2014, 3, 4) + td = timedelta(seconds=1) + # build a timestamp with a frequency, since then it supports + # addition/subtraction of integers + ts = Timestamp(dt, freq='D') + + with tm.assert_produces_warning(FutureWarning): + # GH#22535 add/sub with integers is deprecated + assert type(ts + 1) == Timestamp + assert type(ts - 1) == Timestamp + + # Timestamp + datetime not supported, though subtraction is supported + # and yields timedelta more tests in tseries/base/tests/test_base.py + assert type(ts - dt) == Timedelta + assert type(ts + td) == Timestamp + assert type(ts - td) == Timestamp + + # Timestamp +/- datetime64 not supported, so not tested (could possibly + # assert error raised?) + td64 = np.timedelta64(1, 'D') + assert type(ts + td64) == Timestamp + assert type(ts - td64) == Timestamp + + def test_addition_subtraction_preserve_frequency(self): + ts = Timestamp('2014-03-05', freq='D') + td = timedelta(days=1) + original_freq = ts.freq + + with tm.assert_produces_warning(FutureWarning): + # GH#22535 add/sub with integers is deprecated + assert (ts + 1).freq == original_freq + assert (ts - 1).freq == original_freq + + assert (ts + td).freq == original_freq + assert (ts - td).freq == original_freq + + td64 = np.timedelta64(1, 'D') + assert (ts + td64).freq == original_freq + assert (ts - td64).freq == original_freq diff --git a/pandas/tests/scalar/timestamp/test_comparisons.py b/pandas/tests/scalar/timestamp/test_comparisons.py new file mode 100644 index 0000000000000..74dd52c48153f --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_comparisons.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +import operator + +import numpy as np +import pytest + +from pandas.compat import PY2, long + +from pandas import Timestamp + + +class TestTimestampComparison(object): + def test_comparison_object_array(self): + # GH#15183 + ts = Timestamp('2011-01-03 00:00:00-0500', tz='US/Eastern') + other = Timestamp('2011-01-01 00:00:00-0500', tz='US/Eastern') + naive = Timestamp('2011-01-01 00:00:00') + + arr = np.array([other, ts], dtype=object) + res = arr == ts + expected = np.array([False, True], dtype=bool) + assert (res == expected).all() + + # 2D case + arr = np.array([[other, ts], + [ts, other]], + dtype=object) + res = arr != ts + expected = np.array([[True, False], [False, True]], dtype=bool) + assert res.shape == expected.shape + assert (res == expected).all() + + # tzaware mismatch + arr = np.array([naive], dtype=object) + with pytest.raises(TypeError): + arr < ts + + def test_comparison(self): + # 5-18-2012 00:00:00.000 + stamp = long(1337299200000000000) + + val = Timestamp(stamp) + + assert val == val + assert not val != val + assert not val < val + assert val <= val + assert not val > val + assert val >= val + + other = datetime(2012, 5, 18) + assert val == other + assert not val != other + assert not val < other + assert val <= other + assert not val > other + assert val >= other + + other = Timestamp(stamp + 100) + + assert val != other + assert val != other + assert val < other + assert val <= other + assert other > val + assert other >= val + + def test_compare_invalid(self): + # GH#8058 + val = Timestamp('20130101 12:01:02') + assert not val == 'foo' + assert not val == 10.0 + assert not val == 1 + assert not val == long(1) + assert not val == [] + assert not val == {'foo': 1} + assert not val == np.float64(1) + assert not val == np.int64(1) + + assert val != 'foo' + assert val != 10.0 + assert val != 1 + assert val != long(1) + assert val != [] + assert val != {'foo': 1} + assert val != np.float64(1) + assert val != np.int64(1) + + def test_cant_compare_tz_naive_w_aware(self, utc_fixture): + # see GH#1404 + a = Timestamp('3/12/2012') + b = Timestamp('3/12/2012', tz=utc_fixture) + + with pytest.raises(TypeError): + a == b + with pytest.raises(TypeError): + a != b + with pytest.raises(TypeError): + a < b + with pytest.raises(TypeError): + a <= b + with pytest.raises(TypeError): + a > b + with pytest.raises(TypeError): + a >= b + + with pytest.raises(TypeError): + b == a + with pytest.raises(TypeError): + b != a + with pytest.raises(TypeError): + b < a + with pytest.raises(TypeError): + b <= a + with pytest.raises(TypeError): + b > a + with pytest.raises(TypeError): + b >= a + + if PY2: + with pytest.raises(TypeError): + a == b.to_pydatetime() + with pytest.raises(TypeError): + a.to_pydatetime() == b + else: + assert not a == b.to_pydatetime() + assert not a.to_pydatetime() == b + + def test_timestamp_compare_scalars(self): + # case where ndim == 0 + lhs = np.datetime64(datetime(2013, 12, 6)) + rhs = Timestamp('now') + nat = Timestamp('nat') + + ops = {'gt': 'lt', + 'lt': 'gt', + 'ge': 'le', + 'le': 'ge', + 'eq': 'eq', + 'ne': 'ne'} + + for left, right in ops.items(): + left_f = getattr(operator, left) + right_f = getattr(operator, right) + expected = left_f(lhs, rhs) + + result = right_f(rhs, lhs) + assert result == expected + + expected = left_f(rhs, nat) + result = right_f(nat, rhs) + assert result == expected + + def test_timestamp_compare_with_early_datetime(self): + # e.g. datetime.min + stamp = Timestamp('2012-01-01') + + assert not stamp == datetime.min + assert not stamp == datetime(1600, 1, 1) + assert not stamp == datetime(2700, 1, 1) + assert stamp != datetime.min + assert stamp != datetime(1600, 1, 1) + assert stamp != datetime(2700, 1, 1) + assert stamp > datetime(1600, 1, 1) + assert stamp >= datetime(1600, 1, 1) + assert stamp < datetime(2700, 1, 1) + assert stamp <= datetime(2700, 1, 1) diff --git a/pandas/tests/scalar/timestamp/test_rendering.py b/pandas/tests/scalar/timestamp/test_rendering.py new file mode 100644 index 0000000000000..29b65ee4df745 --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_rendering.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +from distutils.version import LooseVersion +import pprint + +import dateutil +import pytest +import pytz # noqa # a test below uses pytz but only inside a `eval` call + +from pandas import Timestamp + + +class TestTimestampRendering(object): + + # dateutil zone change (only matters for repr) + if LooseVersion(dateutil.__version__) >= LooseVersion('2.6.0'): + timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern', + 'dateutil/US/Pacific'] + else: + timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern', + 'dateutil/America/Los_Angeles'] + + @pytest.mark.parametrize('tz', timezones) + @pytest.mark.parametrize('freq', ['D', 'M', 'S', 'N']) + @pytest.mark.parametrize('date', ['2014-03-07', '2014-01-01 09:00', + '2014-01-01 00:00:00.000000001']) + def test_repr(self, date, freq, tz): + # avoid to match with timezone name + freq_repr = "'{0}'".format(freq) + if tz.startswith('dateutil'): + tz_repr = tz.replace('dateutil', '') + else: + tz_repr = tz + + date_only = Timestamp(date) + assert date in repr(date_only) + assert tz_repr not in repr(date_only) + assert freq_repr not in repr(date_only) + assert date_only == eval(repr(date_only)) + + date_tz = Timestamp(date, tz=tz) + assert date in repr(date_tz) + assert tz_repr in repr(date_tz) + assert freq_repr not in repr(date_tz) + assert date_tz == eval(repr(date_tz)) + + date_freq = Timestamp(date, freq=freq) + assert date in repr(date_freq) + assert tz_repr not in repr(date_freq) + assert freq_repr in repr(date_freq) + assert date_freq == eval(repr(date_freq)) + + date_tz_freq = Timestamp(date, tz=tz, freq=freq) + assert date in repr(date_tz_freq) + assert tz_repr in repr(date_tz_freq) + assert freq_repr in repr(date_tz_freq) + assert date_tz_freq == eval(repr(date_tz_freq)) + + def test_repr_utcoffset(self): + # This can cause the tz field to be populated, but it's redundant to + # include this information in the date-string. + date_with_utc_offset = Timestamp('2014-03-13 00:00:00-0400', tz=None) + assert '2014-03-13 00:00:00-0400' in repr(date_with_utc_offset) + assert 'tzoffset' not in repr(date_with_utc_offset) + assert 'pytz.FixedOffset(-240)' in repr(date_with_utc_offset) + expr = repr(date_with_utc_offset).replace("'pytz.FixedOffset(-240)'", + 'pytz.FixedOffset(-240)') + assert date_with_utc_offset == eval(expr) + + def test_timestamp_repr_pre1900(self): + # pre-1900 + stamp = Timestamp('1850-01-01', tz='US/Eastern') + repr(stamp) + + iso8601 = '1850-01-01 01:23:45.012345' + stamp = Timestamp(iso8601, tz='US/Eastern') + result = repr(stamp) + assert iso8601 in result + + def test_pprint(self): + # GH#12622 + nested_obj = {'foo': 1, + 'bar': [{'w': {'a': Timestamp('2011-01-01')}}] * 10} + result = pprint.pformat(nested_obj, width=50) + expected = r"""{'bar': [{'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}, + {'w': {'a': Timestamp('2011-01-01 00:00:00')}}], + 'foo': 1}""" + assert result == expected diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py new file mode 100644 index 0000000000000..b55d00b44fd67 --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -0,0 +1,988 @@ +""" test the scalar Timestamp """ + +import calendar +from datetime import datetime, timedelta +import locale +import unicodedata + +import dateutil +from dateutil.tz import tzutc +import numpy as np +import pytest +import pytz +from pytz import timezone, utc + +from pandas._libs.tslibs import conversion +from pandas._libs.tslibs.timezones import dateutil_gettz as gettz, get_timezone +from pandas.compat import PY2, PY3, long +from pandas.compat.numpy import np_datetime64_compat +from pandas.errors import OutOfBoundsDatetime +import pandas.util._test_decorators as td + +from pandas import NaT, Period, Timedelta, Timestamp +import pandas.util.testing as tm + +from pandas.tseries import offsets + + +class TestTimestampProperties(object): + + def test_properties_business(self): + ts = Timestamp('2017-10-01', freq='B') + control = Timestamp('2017-10-01') + assert ts.dayofweek == 6 + assert not ts.is_month_start # not a weekday + assert not ts.is_quarter_start # not a weekday + # Control case: non-business is month/qtr start + assert control.is_month_start + assert control.is_quarter_start + + ts = Timestamp('2017-09-30', freq='B') + control = Timestamp('2017-09-30') + assert ts.dayofweek == 5 + assert not ts.is_month_end # not a weekday + assert not ts.is_quarter_end # not a weekday + # Control case: non-business is month/qtr start + assert control.is_month_end + assert control.is_quarter_end + + def test_fields(self): + def check(value, equal): + # that we are int/long like + assert isinstance(value, (int, long)) + assert value == equal + + # GH 10050 + ts = Timestamp('2015-05-10 09:06:03.000100001') + check(ts.year, 2015) + check(ts.month, 5) + check(ts.day, 10) + check(ts.hour, 9) + check(ts.minute, 6) + check(ts.second, 3) + msg = "'Timestamp' object has no attribute 'millisecond'" + with pytest.raises(AttributeError, match=msg): + ts.millisecond + check(ts.microsecond, 100) + check(ts.nanosecond, 1) + check(ts.dayofweek, 6) + check(ts.quarter, 2) + check(ts.dayofyear, 130) + check(ts.week, 19) + check(ts.daysinmonth, 31) + check(ts.daysinmonth, 31) + + # GH 13303 + ts = Timestamp('2014-12-31 23:59:00-05:00', tz='US/Eastern') + check(ts.year, 2014) + check(ts.month, 12) + check(ts.day, 31) + check(ts.hour, 23) + check(ts.minute, 59) + check(ts.second, 0) + msg = "'Timestamp' object has no attribute 'millisecond'" + with pytest.raises(AttributeError, match=msg): + ts.millisecond + check(ts.microsecond, 0) + check(ts.nanosecond, 0) + check(ts.dayofweek, 2) + check(ts.quarter, 4) + check(ts.dayofyear, 365) + check(ts.week, 1) + check(ts.daysinmonth, 31) + + ts = Timestamp('2014-01-01 00:00:00+01:00') + starts = ['is_month_start', 'is_quarter_start', 'is_year_start'] + for start in starts: + assert getattr(ts, start) + ts = Timestamp('2014-12-31 23:59:59+01:00') + ends = ['is_month_end', 'is_year_end', 'is_quarter_end'] + for end in ends: + assert getattr(ts, end) + + # GH 12806 + @pytest.mark.parametrize('data', + [Timestamp('2017-08-28 23:00:00'), + Timestamp('2017-08-28 23:00:00', tz='EST')]) + @pytest.mark.parametrize('time_locale', [ + None] if tm.get_locales() is None else [None] + tm.get_locales()) + def test_names(self, data, time_locale): + # GH 17354 + # Test .weekday_name, .day_name(), .month_name + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + assert data.weekday_name == 'Monday' + if time_locale is None: + expected_day = 'Monday' + expected_month = 'August' + else: + with tm.set_locale(time_locale, locale.LC_TIME): + expected_day = calendar.day_name[0].capitalize() + expected_month = calendar.month_name[8].capitalize() + + result_day = data.day_name(time_locale) + result_month = data.month_name(time_locale) + + # Work around https://github.com/pandas-dev/pandas/issues/22342 + # different normalizations + + if not PY2: + expected_day = unicodedata.normalize("NFD", expected_day) + expected_month = unicodedata.normalize("NFD", expected_month) + + result_day = unicodedata.normalize("NFD", result_day,) + result_month = unicodedata.normalize("NFD", result_month) + + assert result_day == expected_day + assert result_month == expected_month + + # Test NaT + nan_ts = Timestamp(NaT) + assert np.isnan(nan_ts.day_name(time_locale)) + assert np.isnan(nan_ts.month_name(time_locale)) + + def test_is_leap_year(self, tz_naive_fixture): + tz = tz_naive_fixture + # GH 13727 + dt = Timestamp('2000-01-01 00:00:00', tz=tz) + assert dt.is_leap_year + assert isinstance(dt.is_leap_year, bool) + + dt = Timestamp('1999-01-01 00:00:00', tz=tz) + assert not dt.is_leap_year + + dt = Timestamp('2004-01-01 00:00:00', tz=tz) + assert dt.is_leap_year + + dt = Timestamp('2100-01-01 00:00:00', tz=tz) + assert not dt.is_leap_year + + def test_woy_boundary(self): + # make sure weeks at year boundaries are correct + d = datetime(2013, 12, 31) + result = Timestamp(d).week + expected = 1 # ISO standard + assert result == expected + + d = datetime(2008, 12, 28) + result = Timestamp(d).week + expected = 52 # ISO standard + assert result == expected + + d = datetime(2009, 12, 31) + result = Timestamp(d).week + expected = 53 # ISO standard + assert result == expected + + d = datetime(2010, 1, 1) + result = Timestamp(d).week + expected = 53 # ISO standard + assert result == expected + + d = datetime(2010, 1, 3) + result = Timestamp(d).week + expected = 53 # ISO standard + assert result == expected + + result = np.array([Timestamp(datetime(*args)).week + for args in [(2000, 1, 1), (2000, 1, 2), ( + 2005, 1, 1), (2005, 1, 2)]]) + assert (result == [52, 52, 53, 53]).all() + + def test_resolution(self): + # GH#21336, GH#21365 + dt = Timestamp('2100-01-01 00:00:00') + assert dt.resolution == Timedelta(nanoseconds=1) + + +class TestTimestampConstructors(object): + + def test_constructor(self): + base_str = '2014-07-01 09:00' + base_dt = datetime(2014, 7, 1, 9) + base_expected = 1404205200000000000 + + # confirm base representation is correct + import calendar + assert (calendar.timegm(base_dt.timetuple()) * 1000000000 == + base_expected) + + tests = [(base_str, base_dt, base_expected), + ('2014-07-01 10:00', datetime(2014, 7, 1, 10), + base_expected + 3600 * 1000000000), + ('2014-07-01 09:00:00.000008000', + datetime(2014, 7, 1, 9, 0, 0, 8), + base_expected + 8000), + ('2014-07-01 09:00:00.000000005', + Timestamp('2014-07-01 09:00:00.000000005'), + base_expected + 5)] + + timezones = [(None, 0), ('UTC', 0), (pytz.utc, 0), ('Asia/Tokyo', 9), + ('US/Eastern', -4), ('dateutil/US/Pacific', -7), + (pytz.FixedOffset(-180), -3), + (dateutil.tz.tzoffset(None, 18000), 5)] + + for date_str, date, expected in tests: + for result in [Timestamp(date_str), Timestamp(date)]: + # only with timestring + assert result.value == expected + assert conversion.pydt_to_i8(result) == expected + + # re-creation shouldn't affect to internal value + result = Timestamp(result) + assert result.value == expected + assert conversion.pydt_to_i8(result) == expected + + # with timezone + for tz, offset in timezones: + for result in [Timestamp(date_str, tz=tz), Timestamp(date, + tz=tz)]: + expected_tz = expected - offset * 3600 * 1000000000 + assert result.value == expected_tz + assert conversion.pydt_to_i8(result) == expected_tz + + # should preserve tz + result = Timestamp(result) + assert result.value == expected_tz + assert conversion.pydt_to_i8(result) == expected_tz + + # should convert to UTC + if tz is not None: + result = Timestamp(result).tz_convert('UTC') + else: + result = Timestamp(result, tz='UTC') + expected_utc = expected - offset * 3600 * 1000000000 + assert result.value == expected_utc + assert conversion.pydt_to_i8(result) == expected_utc + + def test_constructor_with_stringoffset(self): + # GH 7833 + base_str = '2014-07-01 11:00:00+02:00' + base_dt = datetime(2014, 7, 1, 9) + base_expected = 1404205200000000000 + + # confirm base representation is correct + import calendar + assert (calendar.timegm(base_dt.timetuple()) * 1000000000 == + base_expected) + + tests = [(base_str, base_expected), + ('2014-07-01 12:00:00+02:00', + base_expected + 3600 * 1000000000), + ('2014-07-01 11:00:00.000008000+02:00', base_expected + 8000), + ('2014-07-01 11:00:00.000000005+02:00', base_expected + 5)] + + timezones = [(None, 0), ('UTC', 0), (pytz.utc, 0), ('Asia/Tokyo', 9), + ('US/Eastern', -4), ('dateutil/US/Pacific', -7), + (pytz.FixedOffset(-180), -3), + (dateutil.tz.tzoffset(None, 18000), 5)] + + for date_str, expected in tests: + for result in [Timestamp(date_str)]: + # only with timestring + assert result.value == expected + assert conversion.pydt_to_i8(result) == expected + + # re-creation shouldn't affect to internal value + result = Timestamp(result) + assert result.value == expected + assert conversion.pydt_to_i8(result) == expected + + # with timezone + for tz, offset in timezones: + result = Timestamp(date_str, tz=tz) + expected_tz = expected + assert result.value == expected_tz + assert conversion.pydt_to_i8(result) == expected_tz + + # should preserve tz + result = Timestamp(result) + assert result.value == expected_tz + assert conversion.pydt_to_i8(result) == expected_tz + + # should convert to UTC + result = Timestamp(result).tz_convert('UTC') + expected_utc = expected + assert result.value == expected_utc + assert conversion.pydt_to_i8(result) == expected_utc + + # This should be 2013-11-01 05:00 in UTC + # converted to Chicago tz + result = Timestamp('2013-11-01 00:00:00-0500', tz='America/Chicago') + assert result.value == Timestamp('2013-11-01 05:00').value + expected = "Timestamp('2013-11-01 00:00:00-0500', tz='America/Chicago')" # noqa + assert repr(result) == expected + assert result == eval(repr(result)) + + # This should be 2013-11-01 05:00 in UTC + # converted to Tokyo tz (+09:00) + result = Timestamp('2013-11-01 00:00:00-0500', tz='Asia/Tokyo') + assert result.value == Timestamp('2013-11-01 05:00').value + expected = "Timestamp('2013-11-01 14:00:00+0900', tz='Asia/Tokyo')" + assert repr(result) == expected + assert result == eval(repr(result)) + + # GH11708 + # This should be 2015-11-18 10:00 in UTC + # converted to Asia/Katmandu + result = Timestamp("2015-11-18 15:45:00+05:45", tz="Asia/Katmandu") + assert result.value == Timestamp("2015-11-18 10:00").value + expected = "Timestamp('2015-11-18 15:45:00+0545', tz='Asia/Katmandu')" + assert repr(result) == expected + assert result == eval(repr(result)) + + # This should be 2015-11-18 10:00 in UTC + # converted to Asia/Kolkata + result = Timestamp("2015-11-18 15:30:00+05:30", tz="Asia/Kolkata") + assert result.value == Timestamp("2015-11-18 10:00").value + expected = "Timestamp('2015-11-18 15:30:00+0530', tz='Asia/Kolkata')" + assert repr(result) == expected + assert result == eval(repr(result)) + + def test_constructor_invalid(self): + with pytest.raises(TypeError, match='Cannot convert input'): + Timestamp(slice(2)) + with pytest.raises(ValueError, match='Cannot convert Period'): + Timestamp(Period('1000-01-01')) + + def test_constructor_invalid_tz(self): + # GH#17690 + with pytest.raises(TypeError, match='must be a datetime.tzinfo'): + Timestamp('2017-10-22', tzinfo='US/Eastern') + + with pytest.raises(ValueError, match='at most one of'): + Timestamp('2017-10-22', tzinfo=utc, tz='UTC') + + with pytest.raises(ValueError, match="Invalid frequency:"): + # GH#5168 + # case where user tries to pass tz as an arg, not kwarg, gets + # interpreted as a `freq` + Timestamp('2012-01-01', 'US/Pacific') + + def test_constructor_strptime(self): + # GH25016 + # Test support for Timestamp.strptime + fmt = '%Y%m%d-%H%M%S-%f%z' + ts = '20190129-235348-000001+0000' + with pytest.raises(NotImplementedError): + Timestamp.strptime(ts, fmt) + + def test_constructor_tz_or_tzinfo(self): + # GH#17943, GH#17690, GH#5168 + stamps = [Timestamp(year=2017, month=10, day=22, tz='UTC'), + Timestamp(year=2017, month=10, day=22, tzinfo=utc), + Timestamp(year=2017, month=10, day=22, tz=utc), + Timestamp(datetime(2017, 10, 22), tzinfo=utc), + Timestamp(datetime(2017, 10, 22), tz='UTC'), + Timestamp(datetime(2017, 10, 22), tz=utc)] + assert all(ts == stamps[0] for ts in stamps) + + def test_constructor_positional(self): + # see gh-10758 + with pytest.raises(TypeError): + Timestamp(2000, 1) + with pytest.raises(ValueError): + Timestamp(2000, 0, 1) + with pytest.raises(ValueError): + Timestamp(2000, 13, 1) + with pytest.raises(ValueError): + Timestamp(2000, 1, 0) + with pytest.raises(ValueError): + Timestamp(2000, 1, 32) + + # see gh-11630 + assert (repr(Timestamp(2015, 11, 12)) == + repr(Timestamp('20151112'))) + assert (repr(Timestamp(2015, 11, 12, 1, 2, 3, 999999)) == + repr(Timestamp('2015-11-12 01:02:03.999999'))) + + def test_constructor_keyword(self): + # GH 10758 + with pytest.raises(TypeError): + Timestamp(year=2000, month=1) + with pytest.raises(ValueError): + Timestamp(year=2000, month=0, day=1) + with pytest.raises(ValueError): + Timestamp(year=2000, month=13, day=1) + with pytest.raises(ValueError): + Timestamp(year=2000, month=1, day=0) + with pytest.raises(ValueError): + Timestamp(year=2000, month=1, day=32) + + assert (repr(Timestamp(year=2015, month=11, day=12)) == + repr(Timestamp('20151112'))) + + assert (repr(Timestamp(year=2015, month=11, day=12, hour=1, minute=2, + second=3, microsecond=999999)) == + repr(Timestamp('2015-11-12 01:02:03.999999'))) + + def test_constructor_fromordinal(self): + base = datetime(2000, 1, 1) + + ts = Timestamp.fromordinal(base.toordinal(), freq='D') + assert base == ts + assert ts.freq == 'D' + assert base.toordinal() == ts.toordinal() + + ts = Timestamp.fromordinal(base.toordinal(), tz='US/Eastern') + assert Timestamp('2000-01-01', tz='US/Eastern') == ts + assert base.toordinal() == ts.toordinal() + + # GH#3042 + dt = datetime(2011, 4, 16, 0, 0) + ts = Timestamp.fromordinal(dt.toordinal()) + assert ts.to_pydatetime() == dt + + # with a tzinfo + stamp = Timestamp('2011-4-16', tz='US/Eastern') + dt_tz = stamp.to_pydatetime() + ts = Timestamp.fromordinal(dt_tz.toordinal(), tz='US/Eastern') + assert ts.to_pydatetime() == dt_tz + + @pytest.mark.parametrize('result', [ + Timestamp(datetime(2000, 1, 2, 3, 4, 5, 6), nanosecond=1), + Timestamp(year=2000, month=1, day=2, hour=3, minute=4, second=5, + microsecond=6, nanosecond=1), + Timestamp(year=2000, month=1, day=2, hour=3, minute=4, second=5, + microsecond=6, nanosecond=1, tz='UTC'), + Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, None), + Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, pytz.UTC)]) + def test_constructor_nanosecond(self, result): + # GH 18898 + expected = Timestamp(datetime(2000, 1, 2, 3, 4, 5, 6), tz=result.tz) + expected = expected + Timedelta(nanoseconds=1) + assert result == expected + + @pytest.mark.parametrize('z', ['Z0', 'Z00']) + def test_constructor_invalid_Z0_isostring(self, z): + # GH 8910 + with pytest.raises(ValueError): + Timestamp('2014-11-02 01:00{}'.format(z)) + + @pytest.mark.parametrize('arg', ['year', 'month', 'day', 'hour', 'minute', + 'second', 'microsecond', 'nanosecond']) + def test_invalid_date_kwarg_with_string_input(self, arg): + kwarg = {arg: 1} + with pytest.raises(ValueError): + Timestamp('2010-10-10 12:59:59.999999999', **kwarg) + + def test_out_of_bounds_value(self): + one_us = np.timedelta64(1).astype('timedelta64[us]') + + # By definition we can't go out of bounds in [ns], so we + # convert the datetime64s to [us] so we can go out of bounds + min_ts_us = np.datetime64(Timestamp.min).astype('M8[us]') + max_ts_us = np.datetime64(Timestamp.max).astype('M8[us]') + + # No error for the min/max datetimes + Timestamp(min_ts_us) + Timestamp(max_ts_us) + + # One us less than the minimum is an error + with pytest.raises(ValueError): + Timestamp(min_ts_us - one_us) + + # One us more than the maximum is an error + with pytest.raises(ValueError): + Timestamp(max_ts_us + one_us) + + def test_out_of_bounds_string(self): + with pytest.raises(ValueError): + Timestamp('1676-01-01') + with pytest.raises(ValueError): + Timestamp('2263-01-01') + + def test_barely_out_of_bounds(self): + # GH#19529 + # GH#19382 close enough to bounds that dropping nanos would result + # in an in-bounds datetime + with pytest.raises(OutOfBoundsDatetime): + Timestamp('2262-04-11 23:47:16.854775808') + + def test_bounds_with_different_units(self): + out_of_bounds_dates = ('1677-09-21', '2262-04-12') + + time_units = ('D', 'h', 'm', 's', 'ms', 'us') + + for date_string in out_of_bounds_dates: + for unit in time_units: + dt64 = np.datetime64(date_string, dtype='M8[%s]' % unit) + with pytest.raises(ValueError): + Timestamp(dt64) + + in_bounds_dates = ('1677-09-23', '2262-04-11') + + for date_string in in_bounds_dates: + for unit in time_units: + dt64 = np.datetime64(date_string, dtype='M8[%s]' % unit) + Timestamp(dt64) + + def test_min_valid(self): + # Ensure that Timestamp.min is a valid Timestamp + Timestamp(Timestamp.min) + + def test_max_valid(self): + # Ensure that Timestamp.max is a valid Timestamp + Timestamp(Timestamp.max) + + def test_now(self): + # GH#9000 + ts_from_string = Timestamp('now') + ts_from_method = Timestamp.now() + ts_datetime = datetime.now() + + ts_from_string_tz = Timestamp('now', tz='US/Eastern') + ts_from_method_tz = Timestamp.now(tz='US/Eastern') + + # Check that the delta between the times is less than 1s (arbitrarily + # small) + delta = Timedelta(seconds=1) + assert abs(ts_from_method - ts_from_string) < delta + assert abs(ts_datetime - ts_from_method) < delta + assert abs(ts_from_method_tz - ts_from_string_tz) < delta + assert (abs(ts_from_string_tz.tz_localize(None) - + ts_from_method_tz.tz_localize(None)) < delta) + + def test_today(self): + ts_from_string = Timestamp('today') + ts_from_method = Timestamp.today() + ts_datetime = datetime.today() + + ts_from_string_tz = Timestamp('today', tz='US/Eastern') + ts_from_method_tz = Timestamp.today(tz='US/Eastern') + + # Check that the delta between the times is less than 1s (arbitrarily + # small) + delta = Timedelta(seconds=1) + assert abs(ts_from_method - ts_from_string) < delta + assert abs(ts_datetime - ts_from_method) < delta + assert abs(ts_from_method_tz - ts_from_string_tz) < delta + assert (abs(ts_from_string_tz.tz_localize(None) - + ts_from_method_tz.tz_localize(None)) < delta) + + @pytest.mark.parametrize('tz', [None, pytz.timezone('US/Pacific')]) + def test_disallow_setting_tz(self, tz): + # GH 3746 + ts = Timestamp('2010') + with pytest.raises(AttributeError): + ts.tz = tz + + @pytest.mark.parametrize('offset', ['+0300', '+0200']) + def test_construct_timestamp_near_dst(self, offset): + # GH 20854 + expected = Timestamp('2016-10-30 03:00:00{}'.format(offset), + tz='Europe/Helsinki') + result = Timestamp(expected).tz_convert('Europe/Helsinki') + assert result == expected + + @pytest.mark.parametrize('arg', [ + '2013/01/01 00:00:00+09:00', '2013-01-01 00:00:00+09:00']) + def test_construct_with_different_string_format(self, arg): + # GH 12064 + result = Timestamp(arg) + expected = Timestamp(datetime(2013, 1, 1), tz=pytz.FixedOffset(540)) + assert result == expected + + def test_construct_timestamp_preserve_original_frequency(self): + # GH 22311 + result = Timestamp(Timestamp('2010-08-08', freq='D')).freq + expected = offsets.Day() + assert result == expected + + def test_constructor_invalid_frequency(self): + # GH 22311 + with pytest.raises(ValueError, match="Invalid frequency:"): + Timestamp('2012-01-01', freq=[]) + + @pytest.mark.parametrize('box', [datetime, Timestamp]) + def test_depreciate_tz_and_tzinfo_in_datetime_input(self, box): + # GH 23579 + kwargs = {'year': 2018, 'month': 1, 'day': 1, 'tzinfo': utc} + with tm.assert_produces_warning(FutureWarning): + Timestamp(box(**kwargs), tz='US/Pacific') + + def test_dont_convert_dateutil_utc_to_pytz_utc(self): + result = Timestamp(datetime(2018, 1, 1), tz=tzutc()) + expected = Timestamp(datetime(2018, 1, 1)).tz_localize(tzutc()) + assert result == expected + + +class TestTimestamp(object): + + def test_tz(self): + tstr = '2014-02-01 09:00' + ts = Timestamp(tstr) + local = ts.tz_localize('Asia/Tokyo') + assert local.hour == 9 + assert local == Timestamp(tstr, tz='Asia/Tokyo') + conv = local.tz_convert('US/Eastern') + assert conv == Timestamp('2014-01-31 19:00', tz='US/Eastern') + assert conv.hour == 19 + + # preserves nanosecond + ts = Timestamp(tstr) + offsets.Nano(5) + local = ts.tz_localize('Asia/Tokyo') + assert local.hour == 9 + assert local.nanosecond == 5 + conv = local.tz_convert('US/Eastern') + assert conv.nanosecond == 5 + assert conv.hour == 19 + + def test_utc_z_designator(self): + assert get_timezone(Timestamp('2014-11-02 01:00Z').tzinfo) is utc + + def test_asm8(self): + np.random.seed(7960929) + ns = [Timestamp.min.value, Timestamp.max.value, 1000] + + for n in ns: + assert (Timestamp(n).asm8.view('i8') == + np.datetime64(n, 'ns').view('i8') == n) + + assert (Timestamp('nat').asm8.view('i8') == + np.datetime64('nat', 'ns').view('i8')) + + def test_class_ops_pytz(self): + def compare(x, y): + assert (int(Timestamp(x).value / 1e9) == + int(Timestamp(y).value / 1e9)) + + compare(Timestamp.now(), datetime.now()) + compare(Timestamp.now('UTC'), datetime.now(timezone('UTC'))) + compare(Timestamp.utcnow(), datetime.utcnow()) + compare(Timestamp.today(), datetime.today()) + current_time = calendar.timegm(datetime.now().utctimetuple()) + compare(Timestamp.utcfromtimestamp(current_time), + datetime.utcfromtimestamp(current_time)) + compare(Timestamp.fromtimestamp(current_time), + datetime.fromtimestamp(current_time)) + + date_component = datetime.utcnow() + time_component = (date_component + timedelta(minutes=10)).time() + compare(Timestamp.combine(date_component, time_component), + datetime.combine(date_component, time_component)) + + def test_class_ops_dateutil(self): + def compare(x, y): + assert (int(np.round(Timestamp(x).value / 1e9)) == + int(np.round(Timestamp(y).value / 1e9))) + + compare(Timestamp.now(), datetime.now()) + compare(Timestamp.now('UTC'), datetime.now(tzutc())) + compare(Timestamp.utcnow(), datetime.utcnow()) + compare(Timestamp.today(), datetime.today()) + current_time = calendar.timegm(datetime.now().utctimetuple()) + compare(Timestamp.utcfromtimestamp(current_time), + datetime.utcfromtimestamp(current_time)) + compare(Timestamp.fromtimestamp(current_time), + datetime.fromtimestamp(current_time)) + + date_component = datetime.utcnow() + time_component = (date_component + timedelta(minutes=10)).time() + compare(Timestamp.combine(date_component, time_component), + datetime.combine(date_component, time_component)) + + def test_basics_nanos(self): + val = np.int64(946684800000000000).view('M8[ns]') + stamp = Timestamp(val.view('i8') + 500) + assert stamp.year == 2000 + assert stamp.month == 1 + assert stamp.microsecond == 0 + assert stamp.nanosecond == 500 + + # GH 14415 + val = np.iinfo(np.int64).min + 80000000000000 + stamp = Timestamp(val) + assert stamp.year == 1677 + assert stamp.month == 9 + assert stamp.day == 21 + assert stamp.microsecond == 145224 + assert stamp.nanosecond == 192 + + @pytest.mark.parametrize('value, check_kwargs', [ + [946688461000000000, {}], + [946688461000000000 / long(1000), dict(unit='us')], + [946688461000000000 / long(1000000), dict(unit='ms')], + [946688461000000000 / long(1000000000), dict(unit='s')], + [10957, dict(unit='D', h=0)], + pytest.param((946688461000000000 + 500000) / long(1000000000), + dict(unit='s', us=499, ns=964), + marks=pytest.mark.skipif(not PY3, + reason='using truediv, so these' + ' are like floats')), + pytest.param((946688461000000000 + 500000000) / long(1000000000), + dict(unit='s', us=500000), + marks=pytest.mark.skipif(not PY3, + reason='using truediv, so these' + ' are like floats')), + pytest.param((946688461000000000 + 500000) / long(1000000), + dict(unit='ms', us=500), + marks=pytest.mark.skipif(not PY3, + reason='using truediv, so these' + ' are like floats')), + pytest.param((946688461000000000 + 500000) / long(1000000000), + dict(unit='s'), + marks=pytest.mark.skipif(PY3, + reason='get chopped in py2')), + pytest.param((946688461000000000 + 500000000) / long(1000000000), + dict(unit='s'), + marks=pytest.mark.skipif(PY3, + reason='get chopped in py2')), + pytest.param((946688461000000000 + 500000) / long(1000000), + dict(unit='ms'), + marks=pytest.mark.skipif(PY3, + reason='get chopped in py2')), + [(946688461000000000 + 500000) / long(1000), dict(unit='us', us=500)], + [(946688461000000000 + 500000000) / long(1000000), + dict(unit='ms', us=500000)], + [946688461000000000 / 1000.0 + 5, dict(unit='us', us=5)], + [946688461000000000 / 1000.0 + 5000, dict(unit='us', us=5000)], + [946688461000000000 / 1000000.0 + 0.5, dict(unit='ms', us=500)], + [946688461000000000 / 1000000.0 + 0.005, dict(unit='ms', us=5, ns=5)], + [946688461000000000 / 1000000000.0 + 0.5, dict(unit='s', us=500000)], + [10957 + 0.5, dict(unit='D', h=12)]]) + def test_unit(self, value, check_kwargs): + def check(value, unit=None, h=1, s=1, us=0, ns=0): + stamp = Timestamp(value, unit=unit) + assert stamp.year == 2000 + assert stamp.month == 1 + assert stamp.day == 1 + assert stamp.hour == h + if unit != 'D': + assert stamp.minute == 1 + assert stamp.second == s + assert stamp.microsecond == us + else: + assert stamp.minute == 0 + assert stamp.second == 0 + assert stamp.microsecond == 0 + assert stamp.nanosecond == ns + + check(value, **check_kwargs) + + def test_roundtrip(self): + + # test value to string and back conversions + # further test accessors + base = Timestamp('20140101 00:00:00') + + result = Timestamp(base.value + Timedelta('5ms').value) + assert result == Timestamp(str(base) + ".005000") + assert result.microsecond == 5000 + + result = Timestamp(base.value + Timedelta('5us').value) + assert result == Timestamp(str(base) + ".000005") + assert result.microsecond == 5 + + result = Timestamp(base.value + Timedelta('5ns').value) + assert result == Timestamp(str(base) + ".000000005") + assert result.nanosecond == 5 + assert result.microsecond == 0 + + result = Timestamp(base.value + Timedelta('6ms 5us').value) + assert result == Timestamp(str(base) + ".006005") + assert result.microsecond == 5 + 6 * 1000 + + result = Timestamp(base.value + Timedelta('200ms 5us').value) + assert result == Timestamp(str(base) + ".200005") + assert result.microsecond == 5 + 200 * 1000 + + def test_hash_equivalent(self): + d = {datetime(2011, 1, 1): 5} + stamp = Timestamp(datetime(2011, 1, 1)) + assert d[stamp] == 5 + + def test_tz_conversion_freq(self, tz_naive_fixture): + # GH25241 + t1 = Timestamp('2019-01-01 10:00', freq='H') + assert t1.tz_localize(tz=tz_naive_fixture).freq == t1.freq + t2 = Timestamp('2019-01-02 12:00', tz='UTC', freq='T') + assert t2.tz_convert(tz='UTC').freq == t2.freq + + +class TestTimestampNsOperations(object): + + def setup_method(self, method): + self.timestamp = Timestamp(datetime.utcnow()) + + def assert_ns_timedelta(self, modified_timestamp, expected_value): + value = self.timestamp.value + modified_value = modified_timestamp.value + + assert modified_value - value == expected_value + + def test_timedelta_ns_arithmetic(self): + self.assert_ns_timedelta(self.timestamp + np.timedelta64(-123, 'ns'), + -123) + + def test_timedelta_ns_based_arithmetic(self): + self.assert_ns_timedelta(self.timestamp + np.timedelta64( + 1234567898, 'ns'), 1234567898) + + def test_timedelta_us_arithmetic(self): + self.assert_ns_timedelta(self.timestamp + np.timedelta64(-123, 'us'), + -123000) + + def test_timedelta_ms_arithmetic(self): + time = self.timestamp + np.timedelta64(-123, 'ms') + self.assert_ns_timedelta(time, -123000000) + + def test_nanosecond_string_parsing(self): + ts = Timestamp('2013-05-01 07:15:45.123456789') + # GH 7878 + expected_repr = '2013-05-01 07:15:45.123456789' + expected_value = 1367392545123456789 + assert ts.value == expected_value + assert expected_repr in repr(ts) + + ts = Timestamp('2013-05-01 07:15:45.123456789+09:00', tz='Asia/Tokyo') + assert ts.value == expected_value - 9 * 3600 * 1000000000 + assert expected_repr in repr(ts) + + ts = Timestamp('2013-05-01 07:15:45.123456789', tz='UTC') + assert ts.value == expected_value + assert expected_repr in repr(ts) + + ts = Timestamp('2013-05-01 07:15:45.123456789', tz='US/Eastern') + assert ts.value == expected_value + 4 * 3600 * 1000000000 + assert expected_repr in repr(ts) + + # GH 10041 + ts = Timestamp('20130501T071545.123456789') + assert ts.value == expected_value + assert expected_repr in repr(ts) + + def test_nanosecond_timestamp(self): + # GH 7610 + expected = 1293840000000000005 + t = Timestamp('2011-01-01') + offsets.Nano(5) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000005')" + assert t.value == expected + assert t.nanosecond == 5 + + t = Timestamp(t) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000005')" + assert t.value == expected + assert t.nanosecond == 5 + + t = Timestamp(np_datetime64_compat('2011-01-01 00:00:00.000000005Z')) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000005')" + assert t.value == expected + assert t.nanosecond == 5 + + expected = 1293840000000000010 + t = t + offsets.Nano(5) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000010')" + assert t.value == expected + assert t.nanosecond == 10 + + t = Timestamp(t) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000010')" + assert t.value == expected + assert t.nanosecond == 10 + + t = Timestamp(np_datetime64_compat('2011-01-01 00:00:00.000000010Z')) + assert repr(t) == "Timestamp('2011-01-01 00:00:00.000000010')" + assert t.value == expected + assert t.nanosecond == 10 + + +class TestTimestampToJulianDate(object): + + def test_compare_1700(self): + r = Timestamp('1700-06-23').to_julian_date() + assert r == 2342145.5 + + def test_compare_2000(self): + r = Timestamp('2000-04-12').to_julian_date() + assert r == 2451646.5 + + def test_compare_2100(self): + r = Timestamp('2100-08-12').to_julian_date() + assert r == 2488292.5 + + def test_compare_hour01(self): + r = Timestamp('2000-08-12T01:00:00').to_julian_date() + assert r == 2451768.5416666666666666 + + def test_compare_hour13(self): + r = Timestamp('2000-08-12T13:00:00').to_julian_date() + assert r == 2451769.0416666666666666 + + +class TestTimestampConversion(object): + def test_conversion(self): + # GH#9255 + ts = Timestamp('2000-01-01') + + result = ts.to_pydatetime() + expected = datetime(2000, 1, 1) + assert result == expected + assert type(result) == type(expected) + + result = ts.to_datetime64() + expected = np.datetime64(ts.value, 'ns') + assert result == expected + assert type(result) == type(expected) + assert result.dtype == expected.dtype + + def test_to_pydatetime_nonzero_nano(self): + ts = Timestamp('2011-01-01 9:00:00.123456789') + + # Warn the user of data loss (nanoseconds). + with tm.assert_produces_warning(UserWarning, + check_stacklevel=False): + expected = datetime(2011, 1, 1, 9, 0, 0, 123456) + result = ts.to_pydatetime() + assert result == expected + + def test_timestamp_to_datetime(self): + stamp = Timestamp('20090415', tz='US/Eastern', freq='D') + dtval = stamp.to_pydatetime() + assert stamp == dtval + assert stamp.tzinfo == dtval.tzinfo + + def test_timestamp_to_datetime_dateutil(self): + stamp = Timestamp('20090415', tz='dateutil/US/Eastern', freq='D') + dtval = stamp.to_pydatetime() + assert stamp == dtval + assert stamp.tzinfo == dtval.tzinfo + + def test_timestamp_to_datetime_explicit_pytz(self): + stamp = Timestamp('20090415', tz=pytz.timezone('US/Eastern'), freq='D') + dtval = stamp.to_pydatetime() + assert stamp == dtval + assert stamp.tzinfo == dtval.tzinfo + + @td.skip_if_windows_python_3 + def test_timestamp_to_datetime_explicit_dateutil(self): + stamp = Timestamp('20090415', tz=gettz('US/Eastern'), freq='D') + dtval = stamp.to_pydatetime() + assert stamp == dtval + assert stamp.tzinfo == dtval.tzinfo + + def test_to_datetime_bijective(self): + # Ensure that converting to datetime and back only loses precision + # by going from nanoseconds to microseconds. + exp_warning = None if Timestamp.max.nanosecond == 0 else UserWarning + with tm.assert_produces_warning(exp_warning, check_stacklevel=False): + assert (Timestamp(Timestamp.max.to_pydatetime()).value / 1000 == + Timestamp.max.value / 1000) + + exp_warning = None if Timestamp.min.nanosecond == 0 else UserWarning + with tm.assert_produces_warning(exp_warning, check_stacklevel=False): + assert (Timestamp(Timestamp.min.to_pydatetime()).value / 1000 == + Timestamp.min.value / 1000) + + def test_to_period_tz_warning(self): + # GH#21333 make sure a warning is issued when timezone + # info is lost + ts = Timestamp('2009-04-15 16:17:18', tz='US/Eastern') + with tm.assert_produces_warning(UserWarning): + # warning that timezone info will be lost + ts.to_period('D') + + def test_to_numpy_alias(self): + # GH 24653: alias .to_numpy() for scalars + ts = Timestamp(datetime.now()) + assert ts.to_datetime64() == ts.to_numpy() diff --git a/pandas/tests/scalar/timestamp/test_timezones.py b/pandas/tests/scalar/timestamp/test_timezones.py new file mode 100644 index 0000000000000..bc67a3e72f8d0 --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_timezones.py @@ -0,0 +1,389 @@ +# -*- coding: utf-8 -*- +""" +Tests for Timestamp timezone-related methods +""" +from datetime import date, datetime, timedelta +from distutils.version import LooseVersion + +import dateutil +from dateutil.tz import gettz, tzoffset +import pytest +import pytz +from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError + +from pandas._libs.tslibs import timezones +from pandas.errors import OutOfBoundsDatetime +import pandas.util._test_decorators as td + +from pandas import NaT, Timestamp +import pandas.util.testing as tm + + +class TestTimestampTZOperations(object): + # -------------------------------------------------------------- + # Timestamp.tz_localize + + def test_tz_localize_pushes_out_of_bounds(self): + # GH#12677 + # tz_localize that pushes away from the boundary is OK + pac = Timestamp.min.tz_localize('US/Pacific') + assert pac.value > Timestamp.min.value + pac.tz_convert('Asia/Tokyo') # tz_convert doesn't change value + with pytest.raises(OutOfBoundsDatetime): + Timestamp.min.tz_localize('Asia/Tokyo') + + # tz_localize that pushes away from the boundary is OK + tokyo = Timestamp.max.tz_localize('Asia/Tokyo') + assert tokyo.value < Timestamp.max.value + tokyo.tz_convert('US/Pacific') # tz_convert doesn't change value + with pytest.raises(OutOfBoundsDatetime): + Timestamp.max.tz_localize('US/Pacific') + + def test_tz_localize_ambiguous_bool(self): + # make sure that we are correctly accepting bool values as ambiguous + # GH#14402 + ts = Timestamp('2015-11-01 01:00:03') + expected0 = Timestamp('2015-11-01 01:00:03-0500', tz='US/Central') + expected1 = Timestamp('2015-11-01 01:00:03-0600', tz='US/Central') + + with pytest.raises(pytz.AmbiguousTimeError): + ts.tz_localize('US/Central') + + result = ts.tz_localize('US/Central', ambiguous=True) + assert result == expected0 + + result = ts.tz_localize('US/Central', ambiguous=False) + assert result == expected1 + + def test_tz_localize_ambiguous(self): + ts = Timestamp('2014-11-02 01:00') + ts_dst = ts.tz_localize('US/Eastern', ambiguous=True) + ts_no_dst = ts.tz_localize('US/Eastern', ambiguous=False) + + assert (ts_no_dst.value - ts_dst.value) / 1e9 == 3600 + with pytest.raises(ValueError): + ts.tz_localize('US/Eastern', ambiguous='infer') + + # GH#8025 + msg = ('Cannot localize tz-aware Timestamp, ' + 'use tz_convert for conversions') + with pytest.raises(TypeError, match=msg): + Timestamp('2011-01-01', tz='US/Eastern').tz_localize('Asia/Tokyo') + + msg = ('Cannot convert tz-naive Timestamp, ' + 'use tz_localize to localize') + with pytest.raises(TypeError, match=msg): + Timestamp('2011-01-01').tz_convert('Asia/Tokyo') + + @pytest.mark.parametrize('stamp, tz', [ + ('2015-03-08 02:00', 'US/Eastern'), + ('2015-03-08 02:30', 'US/Pacific'), + ('2015-03-29 02:00', 'Europe/Paris'), + ('2015-03-29 02:30', 'Europe/Belgrade')]) + @pytest.mark.filterwarnings('ignore::FutureWarning') + def test_tz_localize_nonexistent(self, stamp, tz): + # GH#13057 + ts = Timestamp(stamp) + with pytest.raises(NonExistentTimeError): + ts.tz_localize(tz) + # GH 22644 + with pytest.raises(NonExistentTimeError): + with tm.assert_produces_warning(FutureWarning): + ts.tz_localize(tz, errors='raise') + with tm.assert_produces_warning(FutureWarning): + assert ts.tz_localize(tz, errors='coerce') is NaT + + def test_tz_localize_errors_ambiguous(self): + # GH#13057 + ts = Timestamp('2015-11-1 01:00') + with pytest.raises(AmbiguousTimeError): + with tm.assert_produces_warning(FutureWarning): + ts.tz_localize('US/Pacific', errors='coerce') + + @pytest.mark.filterwarnings('ignore::FutureWarning') + def test_tz_localize_errors_invalid_arg(self): + # GH 22644 + tz = 'Europe/Warsaw' + ts = Timestamp('2015-03-29 02:00:00') + with pytest.raises(ValueError): + with tm.assert_produces_warning(FutureWarning): + ts.tz_localize(tz, errors='foo') + + def test_tz_localize_errors_coerce(self): + # GH 22644 + # make sure errors='coerce' gets mapped correctly to nonexistent + tz = 'Europe/Warsaw' + ts = Timestamp('2015-03-29 02:00:00') + with tm.assert_produces_warning(FutureWarning): + result = ts.tz_localize(tz, errors='coerce') + expected = ts.tz_localize(tz, nonexistent='NaT') + assert result is expected + + @pytest.mark.parametrize('stamp', ['2014-02-01 09:00', '2014-07-08 09:00', + '2014-11-01 17:00', '2014-11-05 00:00']) + def test_tz_localize_roundtrip(self, stamp, tz_aware_fixture): + tz = tz_aware_fixture + ts = Timestamp(stamp) + localized = ts.tz_localize(tz) + assert localized == Timestamp(stamp, tz=tz) + + with pytest.raises(TypeError): + localized.tz_localize(tz) + + reset = localized.tz_localize(None) + assert reset == ts + assert reset.tzinfo is None + + def test_tz_localize_ambiguous_compat(self): + # validate that pytz and dateutil are compat for dst + # when the transition happens + naive = Timestamp('2013-10-27 01:00:00') + + pytz_zone = 'Europe/London' + dateutil_zone = 'dateutil/Europe/London' + result_pytz = naive.tz_localize(pytz_zone, ambiguous=0) + result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=0) + assert result_pytz.value == result_dateutil.value + assert result_pytz.value == 1382835600000000000 + + if LooseVersion(dateutil.__version__) < LooseVersion('2.6.0'): + # dateutil 2.6 buggy w.r.t. ambiguous=0 + # see gh-14621 + # see https://github.com/dateutil/dateutil/issues/321 + assert (result_pytz.to_pydatetime().tzname() == + result_dateutil.to_pydatetime().tzname()) + assert str(result_pytz) == str(result_dateutil) + elif LooseVersion(dateutil.__version__) > LooseVersion('2.6.0'): + # fixed ambiguous behavior + assert result_pytz.to_pydatetime().tzname() == 'GMT' + assert result_dateutil.to_pydatetime().tzname() == 'BST' + assert str(result_pytz) != str(result_dateutil) + + # 1 hour difference + result_pytz = naive.tz_localize(pytz_zone, ambiguous=1) + result_dateutil = naive.tz_localize(dateutil_zone, ambiguous=1) + assert result_pytz.value == result_dateutil.value + assert result_pytz.value == 1382832000000000000 + + # dateutil < 2.6 is buggy w.r.t. ambiguous timezones + if LooseVersion(dateutil.__version__) > LooseVersion('2.5.3'): + # see gh-14621 + assert str(result_pytz) == str(result_dateutil) + assert (result_pytz.to_pydatetime().tzname() == + result_dateutil.to_pydatetime().tzname()) + + @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'), + gettz('US/Eastern'), + 'US/Eastern', 'dateutil/US/Eastern']) + def test_timestamp_tz_localize(self, tz): + stamp = Timestamp('3/11/2012 04:00') + + result = stamp.tz_localize(tz) + expected = Timestamp('3/11/2012 04:00', tz=tz) + assert result.hour == expected.hour + assert result == expected + + @pytest.mark.parametrize('start_ts, tz, end_ts, shift', [ + ['2015-03-29 02:20:00', 'Europe/Warsaw', '2015-03-29 03:00:00', + 'forward'], + ['2015-03-29 02:20:00', 'Europe/Warsaw', + '2015-03-29 01:59:59.999999999', 'backward'], + ['2015-03-29 02:20:00', 'Europe/Warsaw', + '2015-03-29 03:20:00', timedelta(hours=1)], + ['2015-03-29 02:20:00', 'Europe/Warsaw', + '2015-03-29 01:20:00', timedelta(hours=-1)], + ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 03:00:00', + 'forward'], + ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 01:59:59.999999999', + 'backward'], + ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 03:33:00', + timedelta(hours=1)], + ['2018-03-11 02:33:00', 'US/Pacific', '2018-03-11 01:33:00', + timedelta(hours=-1)] + ]) + @pytest.mark.parametrize('tz_type', ['', 'dateutil/']) + def test_timestamp_tz_localize_nonexistent_shift(self, start_ts, tz, + end_ts, shift, + tz_type): + # GH 8917, 24466 + tz = tz_type + tz + if isinstance(shift, str): + shift = 'shift_' + shift + ts = Timestamp(start_ts) + result = ts.tz_localize(tz, nonexistent=shift) + expected = Timestamp(end_ts).tz_localize(tz) + assert result == expected + + @pytest.mark.parametrize('offset', [-1, 1]) + @pytest.mark.parametrize('tz_type', ['', 'dateutil/']) + def test_timestamp_tz_localize_nonexistent_shift_invalid(self, offset, + tz_type): + # GH 8917, 24466 + tz = tz_type + 'Europe/Warsaw' + ts = Timestamp('2015-03-29 02:20:00') + msg = "The provided timedelta will relocalize on a nonexistent time" + with pytest.raises(ValueError, match=msg): + ts.tz_localize(tz, nonexistent=timedelta(seconds=offset)) + + @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw']) + def test_timestamp_tz_localize_nonexistent_NaT(self, tz): + # GH 8917 + ts = Timestamp('2015-03-29 02:20:00') + result = ts.tz_localize(tz, nonexistent='NaT') + assert result is NaT + + @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw']) + def test_timestamp_tz_localize_nonexistent_raise(self, tz): + # GH 8917 + ts = Timestamp('2015-03-29 02:20:00') + with pytest.raises(pytz.NonExistentTimeError): + ts.tz_localize(tz, nonexistent='raise') + with pytest.raises(ValueError): + ts.tz_localize(tz, nonexistent='foo') + + # ------------------------------------------------------------------ + # Timestamp.tz_convert + + @pytest.mark.parametrize('stamp', ['2014-02-01 09:00', '2014-07-08 09:00', + '2014-11-01 17:00', '2014-11-05 00:00']) + def test_tz_convert_roundtrip(self, stamp, tz_aware_fixture): + tz = tz_aware_fixture + + ts = Timestamp(stamp, tz='UTC') + converted = ts.tz_convert(tz) + + reset = converted.tz_convert(None) + assert reset == Timestamp(stamp) + assert reset.tzinfo is None + assert reset == converted.tz_convert('UTC').tz_localize(None) + + @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern']) + def test_astimezone(self, tzstr): + # astimezone is an alias for tz_convert, so keep it with + # the tz_convert tests + utcdate = Timestamp('3/11/2012 22:00', tz='UTC') + expected = utcdate.tz_convert(tzstr) + result = utcdate.astimezone(tzstr) + assert expected == result + assert isinstance(result, Timestamp) + + @td.skip_if_windows + def test_tz_convert_utc_with_system_utc(self): + from pandas._libs.tslibs.timezones import maybe_get_tz + + # from system utc to real utc + ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC')) + # check that the time hasn't changed. + assert ts == ts.tz_convert(dateutil.tz.tzutc()) + + # from system utc to real utc + ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC')) + # check that the time hasn't changed. + assert ts == ts.tz_convert(dateutil.tz.tzutc()) + + # ------------------------------------------------------------------ + # Timestamp.__init__ with tz str or tzinfo + + def test_timestamp_constructor_tz_utc(self): + utc_stamp = Timestamp('3/11/2012 05:00', tz='utc') + assert utc_stamp.tzinfo is pytz.utc + assert utc_stamp.hour == 5 + + utc_stamp = Timestamp('3/11/2012 05:00').tz_localize('utc') + assert utc_stamp.hour == 5 + + def test_timestamp_to_datetime_tzoffset(self): + tzinfo = tzoffset(None, 7200) + expected = Timestamp('3/11/2012 04:00', tz=tzinfo) + result = Timestamp(expected.to_pydatetime()) + assert expected == result + + def test_timestamp_constructor_near_dst_boundary(self): + # GH#11481 & GH#15777 + # Naive string timestamps were being localized incorrectly + # with tz_convert_single instead of tz_localize_to_utc + + for tz in ['Europe/Brussels', 'Europe/Prague']: + result = Timestamp('2015-10-25 01:00', tz=tz) + expected = Timestamp('2015-10-25 01:00').tz_localize(tz) + assert result == expected + + with pytest.raises(pytz.AmbiguousTimeError): + Timestamp('2015-10-25 02:00', tz=tz) + + result = Timestamp('2017-03-26 01:00', tz='Europe/Paris') + expected = Timestamp('2017-03-26 01:00').tz_localize('Europe/Paris') + assert result == expected + + with pytest.raises(pytz.NonExistentTimeError): + Timestamp('2017-03-26 02:00', tz='Europe/Paris') + + # GH#11708 + naive = Timestamp('2015-11-18 10:00:00') + result = naive.tz_localize('UTC').tz_convert('Asia/Kolkata') + expected = Timestamp('2015-11-18 15:30:00+0530', tz='Asia/Kolkata') + assert result == expected + + # GH#15823 + result = Timestamp('2017-03-26 00:00', tz='Europe/Paris') + expected = Timestamp('2017-03-26 00:00:00+0100', tz='Europe/Paris') + assert result == expected + + result = Timestamp('2017-03-26 01:00', tz='Europe/Paris') + expected = Timestamp('2017-03-26 01:00:00+0100', tz='Europe/Paris') + assert result == expected + + with pytest.raises(pytz.NonExistentTimeError): + Timestamp('2017-03-26 02:00', tz='Europe/Paris') + + result = Timestamp('2017-03-26 02:00:00+0100', tz='Europe/Paris') + naive = Timestamp(result.value) + expected = naive.tz_localize('UTC').tz_convert('Europe/Paris') + assert result == expected + + result = Timestamp('2017-03-26 03:00', tz='Europe/Paris') + expected = Timestamp('2017-03-26 03:00:00+0200', tz='Europe/Paris') + assert result == expected + + @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'), + gettz('US/Eastern'), + 'US/Eastern', 'dateutil/US/Eastern']) + def test_timestamp_constructed_by_date_and_tz(self, tz): + # GH#2993, Timestamp cannot be constructed by datetime.date + # and tz correctly + + result = Timestamp(date(2012, 3, 11), tz=tz) + + expected = Timestamp('3/11/2012', tz=tz) + assert result.hour == expected.hour + assert result == expected + + @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'), + gettz('US/Eastern'), + 'US/Eastern', 'dateutil/US/Eastern']) + def test_timestamp_add_timedelta_push_over_dst_boundary(self, tz): + # GH#1389 + + # 4 hours before DST transition + stamp = Timestamp('3/10/2012 22:00', tz=tz) + + result = stamp + timedelta(hours=6) + + # spring forward, + "7" hours + expected = Timestamp('3/11/2012 05:00', tz=tz) + + assert result == expected + + def test_timestamp_timetz_equivalent_with_datetime_tz(self, + tz_naive_fixture): + # GH21358 + tz = timezones.maybe_get_tz(tz_naive_fixture) + + stamp = Timestamp('2018-06-04 10:20:30', tz=tz) + _datetime = datetime(2018, 6, 4, hour=10, + minute=20, second=30, tzinfo=tz) + + result = stamp.timetz() + expected = _datetime.timetz() + + assert result == expected diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py new file mode 100644 index 0000000000000..adcf66200a672 --- /dev/null +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -0,0 +1,377 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +from dateutil.tz import gettz +import pytest +import pytz +from pytz import utc + +from pandas._libs.tslibs import conversion +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG +from pandas.compat import PY3, PY36 +import pandas.util._test_decorators as td + +from pandas import NaT, Timestamp +import pandas.util.testing as tm + +from pandas.tseries.frequencies import to_offset + + +class TestTimestampUnaryOps(object): + + # -------------------------------------------------------------- + # Timestamp.round + @pytest.mark.parametrize('timestamp, freq, expected', [ + ('20130101 09:10:11', 'D', '20130101'), + ('20130101 19:10:11', 'D', '20130102'), + ('20130201 12:00:00', 'D', '20130202'), + ('20130104 12:00:00', 'D', '20130105'), + ('2000-01-05 05:09:15.13', 'D', '2000-01-05 00:00:00'), + ('2000-01-05 05:09:15.13', 'H', '2000-01-05 05:00:00'), + ('2000-01-05 05:09:15.13', 'S', '2000-01-05 05:09:15') + ]) + def test_round_frequencies(self, timestamp, freq, expected): + dt = Timestamp(timestamp) + result = dt.round(freq) + expected = Timestamp(expected) + assert result == expected + + def test_round_tzaware(self): + dt = Timestamp('20130101 09:10:11', tz='US/Eastern') + result = dt.round('D') + expected = Timestamp('20130101', tz='US/Eastern') + assert result == expected + + dt = Timestamp('20130101 09:10:11', tz='US/Eastern') + result = dt.round('s') + assert result == dt + + def test_round_30min(self): + # round + dt = Timestamp('20130104 12:32:00') + result = dt.round('30Min') + expected = Timestamp('20130104 12:30:00') + assert result == expected + + def test_round_subsecond(self): + # GH#14440 & GH#15578 + result = Timestamp('2016-10-17 12:00:00.0015').round('ms') + expected = Timestamp('2016-10-17 12:00:00.002000') + assert result == expected + + result = Timestamp('2016-10-17 12:00:00.00149').round('ms') + expected = Timestamp('2016-10-17 12:00:00.001000') + assert result == expected + + ts = Timestamp('2016-10-17 12:00:00.0015') + for freq in ['us', 'ns']: + assert ts == ts.round(freq) + + result = Timestamp('2016-10-17 12:00:00.001501031').round('10ns') + expected = Timestamp('2016-10-17 12:00:00.001501030') + assert result == expected + + def test_round_nonstandard_freq(self): + with tm.assert_produces_warning(False): + Timestamp('2016-10-17 12:00:00.001501031').round('1010ns') + + def test_round_invalid_arg(self): + stamp = Timestamp('2000-01-05 05:09:15.13') + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + stamp.round('foo') + + @pytest.mark.parametrize('test_input, rounder, freq, expected', [ + ('2117-01-01 00:00:45', 'floor', '15s', '2117-01-01 00:00:45'), + ('2117-01-01 00:00:45', 'ceil', '15s', '2117-01-01 00:00:45'), + ('2117-01-01 00:00:45.000000012', 'floor', '10ns', + '2117-01-01 00:00:45.000000010'), + ('1823-01-01 00:00:01.000000012', 'ceil', '10ns', + '1823-01-01 00:00:01.000000020'), + ('1823-01-01 00:00:01', 'floor', '1s', '1823-01-01 00:00:01'), + ('1823-01-01 00:00:01', 'ceil', '1s', '1823-01-01 00:00:01'), + ('NaT', 'floor', '1s', 'NaT'), + ('NaT', 'ceil', '1s', 'NaT') + ]) + def test_ceil_floor_edge(self, test_input, rounder, freq, expected): + dt = Timestamp(test_input) + func = getattr(dt, rounder) + result = func(freq) + + if dt is NaT: + assert result is NaT + else: + expected = Timestamp(expected) + assert result == expected + + @pytest.mark.parametrize('test_input, freq, expected', [ + ('2018-01-01 00:02:06', '2s', '2018-01-01 00:02:06'), + ('2018-01-01 00:02:00', '2T', '2018-01-01 00:02:00'), + ('2018-01-01 00:04:00', '4T', '2018-01-01 00:04:00'), + ('2018-01-01 00:15:00', '15T', '2018-01-01 00:15:00'), + ('2018-01-01 00:20:00', '20T', '2018-01-01 00:20:00'), + ('2018-01-01 03:00:00', '3H', '2018-01-01 03:00:00'), + ]) + @pytest.mark.parametrize('rounder', ['ceil', 'floor', 'round']) + def test_round_minute_freq(self, test_input, freq, expected, rounder): + # Ensure timestamps that shouldnt round dont! + # GH#21262 + + dt = Timestamp(test_input) + expected = Timestamp(expected) + func = getattr(dt, rounder) + result = func(freq) + assert result == expected + + def test_ceil(self): + dt = Timestamp('20130101 09:10:11') + result = dt.ceil('D') + expected = Timestamp('20130102') + assert result == expected + + def test_floor(self): + dt = Timestamp('20130101 09:10:11') + result = dt.floor('D') + expected = Timestamp('20130101') + assert result == expected + + @pytest.mark.parametrize('method', ['ceil', 'round', 'floor']) + def test_round_dst_border_ambiguous(self, method): + # GH 18946 round near "fall back" DST + ts = Timestamp('2017-10-29 00:00:00', tz='UTC').tz_convert( + 'Europe/Madrid' + ) + # + result = getattr(ts, method)('H', ambiguous=True) + assert result == ts + + result = getattr(ts, method)('H', ambiguous=False) + expected = Timestamp('2017-10-29 01:00:00', tz='UTC').tz_convert( + 'Europe/Madrid' + ) + assert result == expected + + result = getattr(ts, method)('H', ambiguous='NaT') + assert result is NaT + + with pytest.raises(pytz.AmbiguousTimeError): + getattr(ts, method)('H', ambiguous='raise') + + @pytest.mark.parametrize('method, ts_str, freq', [ + ['ceil', '2018-03-11 01:59:00-0600', '5min'], + ['round', '2018-03-11 01:59:00-0600', '5min'], + ['floor', '2018-03-11 03:01:00-0500', '2H']]) + def test_round_dst_border_nonexistent(self, method, ts_str, freq): + # GH 23324 round near "spring forward" DST + ts = Timestamp(ts_str, tz='America/Chicago') + result = getattr(ts, method)(freq, nonexistent='shift_forward') + expected = Timestamp('2018-03-11 03:00:00', tz='America/Chicago') + assert result == expected + + result = getattr(ts, method)(freq, nonexistent='NaT') + assert result is NaT + + with pytest.raises(pytz.NonExistentTimeError, + match='2018-03-11 02:00:00'): + getattr(ts, method)(freq, nonexistent='raise') + + @pytest.mark.parametrize('timestamp', [ + '2018-01-01 0:0:0.124999360', + '2018-01-01 0:0:0.125000367', + '2018-01-01 0:0:0.125500', + '2018-01-01 0:0:0.126500', + '2018-01-01 12:00:00', + '2019-01-01 12:00:00', + ]) + @pytest.mark.parametrize('freq', [ + '2ns', '3ns', '4ns', '5ns', '6ns', '7ns', + '250ns', '500ns', '750ns', + '1us', '19us', '250us', '500us', '750us', + '1s', '2s', '3s', + '1D', + ]) + def test_round_int64(self, timestamp, freq): + """check that all rounding modes are accurate to int64 precision + see GH#22591 + """ + dt = Timestamp(timestamp) + unit = to_offset(freq).nanos + + # test floor + result = dt.floor(freq) + assert result.value % unit == 0, "floor not a {} multiple".format(freq) + assert 0 <= dt.value - result.value < unit, "floor error" + + # test ceil + result = dt.ceil(freq) + assert result.value % unit == 0, "ceil not a {} multiple".format(freq) + assert 0 <= result.value - dt.value < unit, "ceil error" + + # test round + result = dt.round(freq) + assert result.value % unit == 0, "round not a {} multiple".format(freq) + assert abs(result.value - dt.value) <= unit // 2, "round error" + if unit % 2 == 0 and abs(result.value - dt.value) == unit // 2: + # round half to even + assert result.value // unit % 2 == 0, "round half to even error" + + # -------------------------------------------------------------- + # Timestamp.replace + + def test_replace_naive(self): + # GH#14621, GH#7825 + ts = Timestamp('2016-01-01 09:00:00') + result = ts.replace(hour=0) + expected = Timestamp('2016-01-01 00:00:00') + assert result == expected + + def test_replace_aware(self, tz_aware_fixture): + tz = tz_aware_fixture + # GH#14621, GH#7825 + # replacing datetime components with and w/o presence of a timezone + ts = Timestamp('2016-01-01 09:00:00', tz=tz) + result = ts.replace(hour=0) + expected = Timestamp('2016-01-01 00:00:00', tz=tz) + assert result == expected + + def test_replace_preserves_nanos(self, tz_aware_fixture): + tz = tz_aware_fixture + # GH#14621, GH#7825 + ts = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) + result = ts.replace(hour=0) + expected = Timestamp('2016-01-01 00:00:00.000000123', tz=tz) + assert result == expected + + def test_replace_multiple(self, tz_aware_fixture): + tz = tz_aware_fixture + # GH#14621, GH#7825 + # replacing datetime components with and w/o presence of a timezone + # test all + ts = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) + result = ts.replace(year=2015, month=2, day=2, hour=0, minute=5, + second=5, microsecond=5, nanosecond=5) + expected = Timestamp('2015-02-02 00:05:05.000005005', tz=tz) + assert result == expected + + def test_replace_invalid_kwarg(self, tz_aware_fixture): + tz = tz_aware_fixture + # GH#14621, GH#7825 + ts = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) + with pytest.raises(TypeError): + ts.replace(foo=5) + + def test_replace_integer_args(self, tz_aware_fixture): + tz = tz_aware_fixture + # GH#14621, GH#7825 + ts = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) + with pytest.raises(ValueError): + ts.replace(hour=0.1) + + def test_replace_tzinfo_equiv_tz_localize_none(self): + # GH#14621, GH#7825 + # assert conversion to naive is the same as replacing tzinfo with None + ts = Timestamp('2013-11-03 01:59:59.999999-0400', tz='US/Eastern') + assert ts.tz_localize(None) == ts.replace(tzinfo=None) + + @td.skip_if_windows + def test_replace_tzinfo(self): + # GH#15683 + dt = datetime(2016, 3, 27, 1) + tzinfo = pytz.timezone('CET').localize(dt, is_dst=False).tzinfo + + result_dt = dt.replace(tzinfo=tzinfo) + result_pd = Timestamp(dt).replace(tzinfo=tzinfo) + + if PY3: + # datetime.timestamp() converts in the local timezone + with tm.set_timezone('UTC'): + assert result_dt.timestamp() == result_pd.timestamp() + + assert result_dt == result_pd + assert result_dt == result_pd.to_pydatetime() + + result_dt = dt.replace(tzinfo=tzinfo).replace(tzinfo=None) + result_pd = Timestamp(dt).replace(tzinfo=tzinfo).replace(tzinfo=None) + + if PY3: + # datetime.timestamp() converts in the local timezone + with tm.set_timezone('UTC'): + assert result_dt.timestamp() == result_pd.timestamp() + + assert result_dt == result_pd + assert result_dt == result_pd.to_pydatetime() + + @pytest.mark.parametrize('tz, normalize', [ + (pytz.timezone('US/Eastern'), lambda x: x.tzinfo.normalize(x)), + (gettz('US/Eastern'), lambda x: x)]) + def test_replace_across_dst(self, tz, normalize): + # GH#18319 check that 1) timezone is correctly normalized and + # 2) that hour is not incorrectly changed by this normalization + ts_naive = Timestamp('2017-12-03 16:03:30') + ts_aware = conversion.localize_pydatetime(ts_naive, tz) + + # Preliminary sanity-check + assert ts_aware == normalize(ts_aware) + + # Replace across DST boundary + ts2 = ts_aware.replace(month=6) + + # Check that `replace` preserves hour literal + assert (ts2.hour, ts2.minute) == (ts_aware.hour, ts_aware.minute) + + # Check that post-replace object is appropriately normalized + ts2b = normalize(ts2) + assert ts2 == ts2b + + def test_replace_dst_border(self): + # Gh 7825 + t = Timestamp('2013-11-3', tz='America/Chicago') + result = t.replace(hour=3) + expected = Timestamp('2013-11-3 03:00:00', tz='America/Chicago') + assert result == expected + + @pytest.mark.skipif(not PY36, reason='Fold not available until PY3.6') + @pytest.mark.parametrize('fold', [0, 1]) + @pytest.mark.parametrize('tz', ['dateutil/Europe/London', 'Europe/London']) + def test_replace_dst_fold(self, fold, tz): + # GH 25017 + d = datetime(2019, 10, 27, 2, 30) + ts = Timestamp(d, tz=tz) + result = ts.replace(hour=1, fold=fold) + expected = Timestamp(datetime(2019, 10, 27, 1, 30)).tz_localize( + tz, ambiguous=not fold + ) + assert result == expected + + # -------------------------------------------------------------- + # Timestamp.normalize + + @pytest.mark.parametrize('arg', ['2013-11-30', '2013-11-30 12:00:00']) + def test_normalize(self, tz_naive_fixture, arg): + tz = tz_naive_fixture + ts = Timestamp(arg, tz=tz) + result = ts.normalize() + expected = Timestamp('2013-11-30', tz=tz) + assert result == expected + + # -------------------------------------------------------------- + + @td.skip_if_windows + def test_timestamp(self): + # GH#17329 + # tz-naive --> treat it as if it were UTC for purposes of timestamp() + ts = Timestamp.now() + uts = ts.replace(tzinfo=utc) + assert ts.timestamp() == uts.timestamp() + + tsc = Timestamp('2014-10-11 11:00:01.12345678', tz='US/Central') + utsc = tsc.tz_convert('UTC') + + # utsc is a different representation of the same time + assert tsc.timestamp() == utsc.timestamp() + + if PY3: + # datetime.timestamp() converts in the local timezone + with tm.set_timezone('UTC'): + # should agree with datetime.timestamp method + dt = ts.to_pydatetime() + assert dt.timestamp() == ts.timestamp() diff --git a/pandas/tests/series/common.py b/pandas/tests/series/common.py index 613961e1c670f..cacca38b2d608 100644 --- a/pandas/tests/series/common.py +++ b/pandas/tests/series/common.py @@ -1,6 +1,7 @@ -from pandas.util.decorators import cache_readonly -import pandas.util.testing as tm +from pandas.util._decorators import cache_readonly + import pandas as pd +import pandas.util.testing as tm _ts = tm.makeTimeSeries() diff --git a/pandas/tests/series/conftest.py b/pandas/tests/series/conftest.py new file mode 100644 index 0000000000000..367e7a1baa7f3 --- /dev/null +++ b/pandas/tests/series/conftest.py @@ -0,0 +1,33 @@ +import pytest + +import pandas.util.testing as tm + + +@pytest.fixture +def datetime_series(): + """ + Fixture for Series of floats with DatetimeIndex + """ + s = tm.makeTimeSeries() + s.name = 'ts' + return s + + +@pytest.fixture +def string_series(): + """ + Fixture for Series of floats with Index of unique strings + """ + s = tm.makeStringSeries() + s.name = 'series' + return s + + +@pytest.fixture +def object_series(): + """ + Fixture for Series of dtype datetime64[ns] with Index of unique strings + """ + s = tm.makeObjectSeries() + s.name = 'objects' + return s diff --git a/pandas/tests/series/indexing/__init__.py b/pandas/tests/series/indexing/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/series/indexing/conftest.py b/pandas/tests/series/indexing/conftest.py new file mode 100644 index 0000000000000..0e06f6b8e4640 --- /dev/null +++ b/pandas/tests/series/indexing/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from pandas.tests.series.common import TestData + + +@pytest.fixture(scope='module') +def test_data(): + return TestData() diff --git a/pandas/tests/series/indexing/test_alter_index.py b/pandas/tests/series/indexing/test_alter_index.py new file mode 100644 index 0000000000000..a826a0644fa78 --- /dev/null +++ b/pandas/tests/series/indexing/test_alter_index.py @@ -0,0 +1,564 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +from datetime import datetime + +import numpy as np +from numpy import nan +import pytest + +import pandas.compat as compat +from pandas.compat import lrange, range + +import pandas as pd +from pandas import Categorical, Series, date_range, isna +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal + + +@pytest.mark.parametrize( + 'first_slice,second_slice', [ + [[2, None], [None, -5]], + [[None, 0], [None, -5]], + [[None, -5], [None, 0]], + [[None, 0], [None, 0]] + ]) +@pytest.mark.parametrize('fill', [None, -1]) +def test_align(test_data, first_slice, second_slice, join_type, fill): + a = test_data.ts[slice(*first_slice)] + b = test_data.ts[slice(*second_slice)] + + aa, ab = a.align(b, join=join_type, fill_value=fill) + + join_index = a.index.join(b.index, how=join_type) + if fill is not None: + diff_a = aa.index.difference(join_index) + diff_b = ab.index.difference(join_index) + if len(diff_a) > 0: + assert (aa.reindex(diff_a) == fill).all() + if len(diff_b) > 0: + assert (ab.reindex(diff_b) == fill).all() + + ea = a.reindex(join_index) + eb = b.reindex(join_index) + + if fill is not None: + ea = ea.fillna(fill) + eb = eb.fillna(fill) + + assert_series_equal(aa, ea) + assert_series_equal(ab, eb) + assert aa.name == 'ts' + assert ea.name == 'ts' + assert ab.name == 'ts' + assert eb.name == 'ts' + + +@pytest.mark.parametrize( + 'first_slice,second_slice', [ + [[2, None], [None, -5]], + [[None, 0], [None, -5]], + [[None, -5], [None, 0]], + [[None, 0], [None, 0]] + ]) +@pytest.mark.parametrize('method', ['pad', 'bfill']) +@pytest.mark.parametrize('limit', [None, 1]) +def test_align_fill_method(test_data, + first_slice, second_slice, + join_type, method, limit): + a = test_data.ts[slice(*first_slice)] + b = test_data.ts[slice(*second_slice)] + + aa, ab = a.align(b, join=join_type, method=method, limit=limit) + + join_index = a.index.join(b.index, how=join_type) + ea = a.reindex(join_index) + eb = b.reindex(join_index) + + ea = ea.fillna(method=method, limit=limit) + eb = eb.fillna(method=method, limit=limit) + + assert_series_equal(aa, ea) + assert_series_equal(ab, eb) + + +def test_align_nocopy(test_data): + b = test_data.ts[:5].copy() + + # do copy + a = test_data.ts.copy() + ra, _ = a.align(b, join='left') + ra[:5] = 5 + assert not (a[:5] == 5).any() + + # do not copy + a = test_data.ts.copy() + ra, _ = a.align(b, join='left', copy=False) + ra[:5] = 5 + assert (a[:5] == 5).all() + + # do copy + a = test_data.ts.copy() + b = test_data.ts[:5].copy() + _, rb = a.align(b, join='right') + rb[:3] = 5 + assert not (b[:3] == 5).any() + + # do not copy + a = test_data.ts.copy() + b = test_data.ts[:5].copy() + _, rb = a.align(b, join='right', copy=False) + rb[:2] = 5 + assert (b[:2] == 5).all() + + +def test_align_same_index(test_data): + a, b = test_data.ts.align(test_data.ts, copy=False) + assert a.index is test_data.ts.index + assert b.index is test_data.ts.index + + a, b = test_data.ts.align(test_data.ts, copy=True) + assert a.index is not test_data.ts.index + assert b.index is not test_data.ts.index + + +def test_align_multiindex(): + # GH 10665 + + midx = pd.MultiIndex.from_product([range(2), range(3), range(2)], + names=('a', 'b', 'c')) + idx = pd.Index(range(2), name='b') + s1 = pd.Series(np.arange(12, dtype='int64'), index=midx) + s2 = pd.Series(np.arange(2, dtype='int64'), index=idx) + + # these must be the same results (but flipped) + res1l, res1r = s1.align(s2, join='left') + res2l, res2r = s2.align(s1, join='right') + + expl = s1 + tm.assert_series_equal(expl, res1l) + tm.assert_series_equal(expl, res2r) + expr = pd.Series([0, 0, 1, 1, np.nan, np.nan] * 2, index=midx) + tm.assert_series_equal(expr, res1r) + tm.assert_series_equal(expr, res2l) + + res1l, res1r = s1.align(s2, join='right') + res2l, res2r = s2.align(s1, join='left') + + exp_idx = pd.MultiIndex.from_product([range(2), range(2), range(2)], + names=('a', 'b', 'c')) + expl = pd.Series([0, 1, 2, 3, 6, 7, 8, 9], index=exp_idx) + tm.assert_series_equal(expl, res1l) + tm.assert_series_equal(expl, res2r) + expr = pd.Series([0, 0, 1, 1] * 2, index=exp_idx) + tm.assert_series_equal(expr, res1r) + tm.assert_series_equal(expr, res2l) + + +def test_reindex(test_data): + identity = test_data.series.reindex(test_data.series.index) + + # __array_interface__ is not defined for older numpies + # and on some pythons + try: + assert np.may_share_memory(test_data.series.index, identity.index) + except AttributeError: + pass + + assert identity.index.is_(test_data.series.index) + assert identity.index.identical(test_data.series.index) + + subIndex = test_data.series.index[10:20] + subSeries = test_data.series.reindex(subIndex) + + for idx, val in compat.iteritems(subSeries): + assert val == test_data.series[idx] + + subIndex2 = test_data.ts.index[10:20] + subTS = test_data.ts.reindex(subIndex2) + + for idx, val in compat.iteritems(subTS): + assert val == test_data.ts[idx] + stuffSeries = test_data.ts.reindex(subIndex) + + assert np.isnan(stuffSeries).all() + + # This is extremely important for the Cython code to not screw up + nonContigIndex = test_data.ts.index[::2] + subNonContig = test_data.ts.reindex(nonContigIndex) + for idx, val in compat.iteritems(subNonContig): + assert val == test_data.ts[idx] + + # return a copy the same index here + result = test_data.ts.reindex() + assert not (result is test_data.ts) + + +def test_reindex_nan(): + ts = Series([2, 3, 5, 7], index=[1, 4, nan, 8]) + + i, j = [nan, 1, nan, 8, 4, nan], [2, 0, 2, 3, 1, 2] + assert_series_equal(ts.reindex(i), ts.iloc[j]) + + ts.index = ts.index.astype('object') + + # reindex coerces index.dtype to float, loc/iloc doesn't + assert_series_equal(ts.reindex(i), ts.iloc[j], check_index_type=False) + + +def test_reindex_series_add_nat(): + rng = date_range('1/1/2000 00:00:00', periods=10, freq='10s') + series = Series(rng) + + result = series.reindex(lrange(15)) + assert np.issubdtype(result.dtype, np.dtype('M8[ns]')) + + mask = result.isna() + assert mask[-5:].all() + assert not mask[:-5].any() + + +def test_reindex_with_datetimes(): + rng = date_range('1/1/2000', periods=20) + ts = Series(np.random.randn(20), index=rng) + + result = ts.reindex(list(ts.index[5:10])) + expected = ts[5:10] + tm.assert_series_equal(result, expected) + + result = ts[list(ts.index[5:10])] + tm.assert_series_equal(result, expected) + + +def test_reindex_corner(test_data): + # (don't forget to fix this) I think it's fixed + test_data.empty.reindex(test_data.ts.index, method='pad') # it works + + # corner case: pad empty series + reindexed = test_data.empty.reindex(test_data.ts.index, method='pad') + + # pass non-Index + reindexed = test_data.ts.reindex(list(test_data.ts.index)) + assert_series_equal(test_data.ts, reindexed) + + # bad fill method + ts = test_data.ts[::2] + msg = (r"Invalid fill method\. Expecting pad \(ffill\), backfill" + r" \(bfill\) or nearest\. Got foo") + with pytest.raises(ValueError, match=msg): + ts.reindex(test_data.ts.index, method='foo') + + +def test_reindex_pad(): + s = Series(np.arange(10), dtype='int64') + s2 = s[::2] + + reindexed = s2.reindex(s.index, method='pad') + reindexed2 = s2.reindex(s.index, method='ffill') + assert_series_equal(reindexed, reindexed2) + + expected = Series([0, 0, 2, 2, 4, 4, 6, 6, 8, 8], index=np.arange(10)) + assert_series_equal(reindexed, expected) + + # GH4604 + s = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e']) + new_index = ['a', 'g', 'c', 'f'] + expected = Series([1, 1, 3, 3], index=new_index) + + # this changes dtype because the ffill happens after + result = s.reindex(new_index).ffill() + assert_series_equal(result, expected.astype('float64')) + + result = s.reindex(new_index).ffill(downcast='infer') + assert_series_equal(result, expected) + + expected = Series([1, 5, 3, 5], index=new_index) + result = s.reindex(new_index, method='ffill') + assert_series_equal(result, expected) + + # inference of new dtype + s = Series([True, False, False, True], index=list('abcd')) + new_index = 'agc' + result = s.reindex(list(new_index)).ffill() + expected = Series([True, True, False], index=list(new_index)) + assert_series_equal(result, expected) + + # GH4618 shifted series downcasting + s = Series(False, index=lrange(0, 5)) + result = s.shift(1).fillna(method='bfill') + expected = Series(False, index=lrange(0, 5)) + assert_series_equal(result, expected) + + +def test_reindex_nearest(): + s = Series(np.arange(10, dtype='int64')) + target = [0.1, 0.9, 1.5, 2.0] + actual = s.reindex(target, method='nearest') + expected = Series(np.around(target).astype('int64'), target) + assert_series_equal(expected, actual) + + actual = s.reindex_like(actual, method='nearest') + assert_series_equal(expected, actual) + + actual = s.reindex_like(actual, method='nearest', tolerance=1) + assert_series_equal(expected, actual) + actual = s.reindex_like(actual, method='nearest', + tolerance=[1, 2, 3, 4]) + assert_series_equal(expected, actual) + + actual = s.reindex(target, method='nearest', tolerance=0.2) + expected = Series([0, 1, np.nan, 2], target) + assert_series_equal(expected, actual) + + actual = s.reindex(target, method='nearest', + tolerance=[0.3, 0.01, 0.4, 3]) + expected = Series([0, np.nan, np.nan, 2], target) + assert_series_equal(expected, actual) + + +def test_reindex_backfill(): + pass + + +def test_reindex_int(test_data): + ts = test_data.ts[::2] + int_ts = Series(np.zeros(len(ts), dtype=int), index=ts.index) + + # this should work fine + reindexed_int = int_ts.reindex(test_data.ts.index) + + # if NaNs introduced + assert reindexed_int.dtype == np.float_ + + # NO NaNs introduced + reindexed_int = int_ts.reindex(int_ts.index[::2]) + assert reindexed_int.dtype == np.int_ + + +def test_reindex_bool(test_data): + # A series other than float, int, string, or object + ts = test_data.ts[::2] + bool_ts = Series(np.zeros(len(ts), dtype=bool), index=ts.index) + + # this should work fine + reindexed_bool = bool_ts.reindex(test_data.ts.index) + + # if NaNs introduced + assert reindexed_bool.dtype == np.object_ + + # NO NaNs introduced + reindexed_bool = bool_ts.reindex(bool_ts.index[::2]) + assert reindexed_bool.dtype == np.bool_ + + +def test_reindex_bool_pad(test_data): + # fail + ts = test_data.ts[5:] + bool_ts = Series(np.zeros(len(ts), dtype=bool), index=ts.index) + filled_bool = bool_ts.reindex(test_data.ts.index, method='pad') + assert isna(filled_bool[:5]).all() + + +def test_reindex_categorical(): + index = date_range('20000101', periods=3) + + # reindexing to an invalid Categorical + s = Series(['a', 'b', 'c'], dtype='category') + result = s.reindex(index) + expected = Series(Categorical(values=[np.nan, np.nan, np.nan], + categories=['a', 'b', 'c'])) + expected.index = index + tm.assert_series_equal(result, expected) + + # partial reindexing + expected = Series(Categorical(values=['b', 'c'], categories=['a', 'b', + 'c'])) + expected.index = [1, 2] + result = s.reindex([1, 2]) + tm.assert_series_equal(result, expected) + + expected = Series(Categorical( + values=['c', np.nan], categories=['a', 'b', 'c'])) + expected.index = [2, 3] + result = s.reindex([2, 3]) + tm.assert_series_equal(result, expected) + + +def test_reindex_like(test_data): + other = test_data.ts[::2] + assert_series_equal(test_data.ts.reindex(other.index), + test_data.ts.reindex_like(other)) + + # GH 7179 + day1 = datetime(2013, 3, 5) + day2 = datetime(2013, 5, 5) + day3 = datetime(2014, 3, 5) + + series1 = Series([5, None, None], [day1, day2, day3]) + series2 = Series([None, None], [day1, day3]) + + result = series1.reindex_like(series2, method='pad') + expected = Series([5, np.nan], index=[day1, day3]) + assert_series_equal(result, expected) + + +def test_reindex_fill_value(): + # ----------------------------------------------------------- + # floats + floats = Series([1., 2., 3.]) + result = floats.reindex([1, 2, 3]) + expected = Series([2., 3., np.nan], index=[1, 2, 3]) + assert_series_equal(result, expected) + + result = floats.reindex([1, 2, 3], fill_value=0) + expected = Series([2., 3., 0], index=[1, 2, 3]) + assert_series_equal(result, expected) + + # ----------------------------------------------------------- + # ints + ints = Series([1, 2, 3]) + + result = ints.reindex([1, 2, 3]) + expected = Series([2., 3., np.nan], index=[1, 2, 3]) + assert_series_equal(result, expected) + + # don't upcast + result = ints.reindex([1, 2, 3], fill_value=0) + expected = Series([2, 3, 0], index=[1, 2, 3]) + assert issubclass(result.dtype.type, np.integer) + assert_series_equal(result, expected) + + # ----------------------------------------------------------- + # objects + objects = Series([1, 2, 3], dtype=object) + + result = objects.reindex([1, 2, 3]) + expected = Series([2, 3, np.nan], index=[1, 2, 3], dtype=object) + assert_series_equal(result, expected) + + result = objects.reindex([1, 2, 3], fill_value='foo') + expected = Series([2, 3, 'foo'], index=[1, 2, 3], dtype=object) + assert_series_equal(result, expected) + + # ------------------------------------------------------------ + # bools + bools = Series([True, False, True]) + + result = bools.reindex([1, 2, 3]) + expected = Series([False, True, np.nan], index=[1, 2, 3], dtype=object) + assert_series_equal(result, expected) + + result = bools.reindex([1, 2, 3], fill_value=False) + expected = Series([False, True, False], index=[1, 2, 3]) + assert_series_equal(result, expected) + + +def test_reindex_datetimeindexes_tz_naive_and_aware(): + # GH 8306 + idx = date_range('20131101', tz='America/Chicago', periods=7) + newidx = date_range('20131103', periods=10, freq='H') + s = Series(range(7), index=idx) + with pytest.raises(TypeError): + s.reindex(newidx, method='ffill') + + +def test_reindex_empty_series_tz_dtype(): + # GH 20869 + result = Series(dtype='datetime64[ns, UTC]').reindex([0, 1]) + expected = Series([pd.NaT] * 2, dtype='datetime64[ns, UTC]') + tm.assert_equal(result, expected) + + +def test_rename(): + # GH 17407 + s = Series(range(1, 6), index=pd.Index(range(2, 7), name='IntIndex')) + result = s.rename(str) + expected = s.rename(lambda i: str(i)) + assert_series_equal(result, expected) + + assert result.name == expected.name + + +@pytest.mark.parametrize( + 'data, index, drop_labels,' + ' axis, expected_data, expected_index', + [ + # Unique Index + ([1, 2], ['one', 'two'], ['two'], + 0, [1], ['one']), + ([1, 2], ['one', 'two'], ['two'], + 'rows', [1], ['one']), + ([1, 1, 2], ['one', 'two', 'one'], ['two'], + 0, [1, 2], ['one', 'one']), + + # GH 5248 Non-Unique Index + ([1, 1, 2], ['one', 'two', 'one'], 'two', + 0, [1, 2], ['one', 'one']), + ([1, 1, 2], ['one', 'two', 'one'], ['one'], + 0, [1], ['two']), + ([1, 1, 2], ['one', 'two', 'one'], 'one', + 0, [1], ['two'])]) +def test_drop_unique_and_non_unique_index(data, index, axis, drop_labels, + expected_data, expected_index): + + s = Series(data=data, index=index) + result = s.drop(drop_labels, axis=axis) + expected = Series(data=expected_data, index=expected_index) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize( + 'data, index, drop_labels,' + ' axis, error_type, error_desc', + [ + # single string/tuple-like + (range(3), list('abc'), 'bc', + 0, KeyError, 'not found in axis'), + + # bad axis + (range(3), list('abc'), ('a',), + 0, KeyError, 'not found in axis'), + (range(3), list('abc'), 'one', + 'columns', ValueError, 'No axis named columns')]) +def test_drop_exception_raised(data, index, drop_labels, + axis, error_type, error_desc): + + with pytest.raises(error_type, match=error_desc): + Series(data, index=index).drop(drop_labels, axis=axis) + + +def test_drop_with_ignore_errors(): + # errors='ignore' + s = Series(range(3), index=list('abc')) + result = s.drop('bc', errors='ignore') + tm.assert_series_equal(result, s) + result = s.drop(['a', 'd'], errors='ignore') + expected = s.iloc[1:] + tm.assert_series_equal(result, expected) + + # GH 8522 + s = Series([2, 3], index=[True, False]) + assert s.index.is_object() + result = s.drop(True) + expected = Series([3], index=[False]) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize('index', [[1, 2, 3], [1, 1, 3]]) +@pytest.mark.parametrize('drop_labels', [[], [1], [3]]) +def test_drop_empty_list(index, drop_labels): + # GH 21494 + expected_index = [i for i in index if i not in drop_labels] + series = pd.Series(index=index).drop(drop_labels) + tm.assert_series_equal(series, pd.Series(index=expected_index)) + + +@pytest.mark.parametrize('data, index, drop_labels', [ + (None, [1, 2, 3], [1, 4]), + (None, [1, 2, 2], [1, 4]), + ([2, 3], [0, 1], [False, True]) +]) +def test_drop_non_empty_list(data, index, drop_labels): + # GH 21494 and GH 16877 + with pytest.raises(KeyError, match='not found in axis'): + pd.Series(data=data, index=index).drop(drop_labels) diff --git a/pandas/tests/series/indexing/test_boolean.py b/pandas/tests/series/indexing/test_boolean.py new file mode 100644 index 0000000000000..9017d13051b88 --- /dev/null +++ b/pandas/tests/series/indexing/test_boolean.py @@ -0,0 +1,634 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +import numpy as np +import pytest + +from pandas.compat import lrange, range + +from pandas.core.dtypes.common import is_integer + +import pandas as pd +from pandas import Index, Series, Timestamp, date_range, isna +from pandas.core.indexing import IndexingError +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal + +from pandas.tseries.offsets import BDay + + +def test_getitem_boolean(test_data): + s = test_data.series + mask = s > s.median() + + # passing list is OK + result = s[list(mask)] + expected = s[mask] + assert_series_equal(result, expected) + tm.assert_index_equal(result.index, s.index[mask]) + + +def test_getitem_boolean_empty(): + s = Series([], dtype=np.int64) + s.index.name = 'index_name' + s = s[s.isna()] + assert s.index.name == 'index_name' + assert s.dtype == np.int64 + + # GH5877 + # indexing with empty series + s = Series(['A', 'B']) + expected = Series(np.nan, index=['C'], dtype=object) + result = s[Series(['C'], dtype=object)] + assert_series_equal(result, expected) + + s = Series(['A', 'B']) + expected = Series(dtype=object, index=Index([], dtype='int64')) + result = s[Series([], dtype=object)] + assert_series_equal(result, expected) + + # invalid because of the boolean indexer + # that's empty or not-aligned + msg = (r"Unalignable boolean Series provided as indexer \(index of" + r" the boolean Series and of the indexed object do not match") + with pytest.raises(IndexingError, match=msg): + s[Series([], dtype=bool)] + + with pytest.raises(IndexingError, match=msg): + s[Series([True], dtype=bool)] + + +def test_getitem_boolean_object(test_data): + # using column from DataFrame + + s = test_data.series + mask = s > s.median() + omask = mask.astype(object) + + # getitem + result = s[omask] + expected = s[mask] + assert_series_equal(result, expected) + + # setitem + s2 = s.copy() + cop = s.copy() + cop[omask] = 5 + s2[mask] = 5 + assert_series_equal(cop, s2) + + # nans raise exception + omask[5:10] = np.nan + msg = "cannot index with vector containing NA / NaN values" + with pytest.raises(ValueError, match=msg): + s[omask] + with pytest.raises(ValueError, match=msg): + s[omask] = 5 + + +def test_getitem_setitem_boolean_corner(test_data): + ts = test_data.ts + mask_shifted = ts.shift(1, freq=BDay()) > ts.median() + + # these used to raise...?? + + msg = (r"Unalignable boolean Series provided as indexer \(index of" + r" the boolean Series and of the indexed object do not match") + with pytest.raises(IndexingError, match=msg): + ts[mask_shifted] + with pytest.raises(IndexingError, match=msg): + ts[mask_shifted] = 1 + + with pytest.raises(IndexingError, match=msg): + ts.loc[mask_shifted] + with pytest.raises(IndexingError, match=msg): + ts.loc[mask_shifted] = 1 + + +def test_setitem_boolean(test_data): + mask = test_data.series > test_data.series.median() + + # similar indexed series + result = test_data.series.copy() + result[mask] = test_data.series * 2 + expected = test_data.series * 2 + assert_series_equal(result[mask], expected[mask]) + + # needs alignment + result = test_data.series.copy() + result[mask] = (test_data.series * 2)[0:5] + expected = (test_data.series * 2)[0:5].reindex_like(test_data.series) + expected[-mask] = test_data.series[mask] + assert_series_equal(result[mask], expected[mask]) + + +def test_get_set_boolean_different_order(test_data): + ordered = test_data.series.sort_values() + + # setting + copy = test_data.series.copy() + copy[ordered > 0] = 0 + + expected = test_data.series.copy() + expected[expected > 0] = 0 + + assert_series_equal(copy, expected) + + # getting + sel = test_data.series[ordered > 0] + exp = test_data.series[test_data.series > 0] + assert_series_equal(sel, exp) + + +def test_where_unsafe_int(sint_dtype): + s = Series(np.arange(10), dtype=sint_dtype) + mask = s < 5 + + s[mask] = lrange(2, 7) + expected = Series(lrange(2, 7) + lrange(5, 10), dtype=sint_dtype) + + assert_series_equal(s, expected) + + +def test_where_unsafe_float(float_dtype): + s = Series(np.arange(10), dtype=float_dtype) + mask = s < 5 + + s[mask] = lrange(2, 7) + expected = Series(lrange(2, 7) + lrange(5, 10), dtype=float_dtype) + + assert_series_equal(s, expected) + + +@pytest.mark.parametrize("dtype,expected_dtype", [ + (np.int8, np.float64), + (np.int16, np.float64), + (np.int32, np.float64), + (np.int64, np.float64), + (np.float32, np.float32), + (np.float64, np.float64) +]) +def test_where_unsafe_upcast(dtype, expected_dtype): + # see gh-9743 + s = Series(np.arange(10), dtype=dtype) + values = [2.5, 3.5, 4.5, 5.5, 6.5] + mask = s < 5 + expected = Series(values + lrange(5, 10), dtype=expected_dtype) + s[mask] = values + assert_series_equal(s, expected) + + +def test_where_unsafe(): + # see gh-9731 + s = Series(np.arange(10), dtype="int64") + values = [2.5, 3.5, 4.5, 5.5] + + mask = s > 5 + expected = Series(lrange(6) + values, dtype="float64") + + s[mask] = values + assert_series_equal(s, expected) + + # see gh-3235 + s = Series(np.arange(10), dtype='int64') + mask = s < 5 + s[mask] = lrange(2, 7) + expected = Series(lrange(2, 7) + lrange(5, 10), dtype='int64') + assert_series_equal(s, expected) + assert s.dtype == expected.dtype + + s = Series(np.arange(10), dtype='int64') + mask = s > 5 + s[mask] = [0] * 4 + expected = Series([0, 1, 2, 3, 4, 5] + [0] * 4, dtype='int64') + assert_series_equal(s, expected) + + s = Series(np.arange(10)) + mask = s > 5 + + msg = "cannot assign mismatch length to masked array" + with pytest.raises(ValueError, match=msg): + s[mask] = [5, 4, 3, 2, 1] + + with pytest.raises(ValueError, match=msg): + s[mask] = [0] * 5 + + # dtype changes + s = Series([1, 2, 3, 4]) + result = s.where(s > 2, np.nan) + expected = Series([np.nan, np.nan, 3, 4]) + assert_series_equal(result, expected) + + # GH 4667 + # setting with None changes dtype + s = Series(range(10)).astype(float) + s[8] = None + result = s[8] + assert isna(result) + + s = Series(range(10)).astype(float) + s[s > 8] = None + result = s[isna(s)] + expected = Series(np.nan, index=[9]) + assert_series_equal(result, expected) + + +def test_where_raise_on_error_deprecation(): + # gh-14968 + # deprecation of raise_on_error + s = Series(np.random.randn(5)) + cond = s > 0 + with tm.assert_produces_warning(FutureWarning): + s.where(cond, raise_on_error=True) + with tm.assert_produces_warning(FutureWarning): + s.mask(cond, raise_on_error=True) + + +def test_where(): + s = Series(np.random.randn(5)) + cond = s > 0 + + rs = s.where(cond).dropna() + rs2 = s[cond] + assert_series_equal(rs, rs2) + + rs = s.where(cond, -s) + assert_series_equal(rs, s.abs()) + + rs = s.where(cond) + assert (s.shape == rs.shape) + assert (rs is not s) + + # test alignment + cond = Series([True, False, False, True, False], index=s.index) + s2 = -(s.abs()) + + expected = s2[cond].reindex(s2.index[:3]).reindex(s2.index) + rs = s2.where(cond[:3]) + assert_series_equal(rs, expected) + + expected = s2.abs() + expected.iloc[0] = s2[0] + rs = s2.where(cond[:3], -s2) + assert_series_equal(rs, expected) + + +def test_where_error(): + s = Series(np.random.randn(5)) + cond = s > 0 + + msg = "Array conditional must be same shape as self" + with pytest.raises(ValueError, match=msg): + s.where(1) + with pytest.raises(ValueError, match=msg): + s.where(cond[:3].values, -s) + + # GH 2745 + s = Series([1, 2]) + s[[True, False]] = [0, 1] + expected = Series([0, 2]) + assert_series_equal(s, expected) + + # failures + msg = "cannot assign mismatch length to masked array" + with pytest.raises(ValueError, match=msg): + s[[True, False]] = [0, 2, 3] + msg = ("NumPy boolean array indexing assignment cannot assign 0 input" + " values to the 1 output values where the mask is true") + with pytest.raises(ValueError, match=msg): + s[[True, False]] = [] + + +@pytest.mark.parametrize('klass', [list, tuple, np.array, Series]) +def test_where_array_like(klass): + # see gh-15414 + s = Series([1, 2, 3]) + cond = [False, True, True] + expected = Series([np.nan, 2, 3]) + + result = s.where(klass(cond)) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize('cond', [ + [1, 0, 1], + Series([2, 5, 7]), + ["True", "False", "True"], + [Timestamp("2017-01-01"), pd.NaT, Timestamp("2017-01-02")] +]) +def test_where_invalid_input(cond): + # see gh-15414: only boolean arrays accepted + s = Series([1, 2, 3]) + msg = "Boolean array expected for the condition" + + with pytest.raises(ValueError, match=msg): + s.where(cond) + + msg = "Array conditional must be same shape as self" + with pytest.raises(ValueError, match=msg): + s.where([True]) + + +def test_where_ndframe_align(): + msg = "Array conditional must be same shape as self" + s = Series([1, 2, 3]) + + cond = [True] + with pytest.raises(ValueError, match=msg): + s.where(cond) + + expected = Series([1, np.nan, np.nan]) + + out = s.where(Series(cond)) + tm.assert_series_equal(out, expected) + + cond = np.array([False, True, False, True]) + with pytest.raises(ValueError, match=msg): + s.where(cond) + + expected = Series([np.nan, 2, np.nan]) + + out = s.where(Series(cond)) + tm.assert_series_equal(out, expected) + + +def test_where_setitem_invalid(): + # GH 2702 + # make sure correct exceptions are raised on invalid list assignment + + msg = ("cannot set using a {} indexer with a different length than" + " the value") + + # slice + s = Series(list('abc')) + + with pytest.raises(ValueError, match=msg.format('slice')): + s[0:3] = list(range(27)) + + s[0:3] = list(range(3)) + expected = Series([0, 1, 2]) + assert_series_equal(s.astype(np.int64), expected, ) + + # slice with step + s = Series(list('abcdef')) + + with pytest.raises(ValueError, match=msg.format('slice')): + s[0:4:2] = list(range(27)) + + s = Series(list('abcdef')) + s[0:4:2] = list(range(2)) + expected = Series([0, 'b', 1, 'd', 'e', 'f']) + assert_series_equal(s, expected) + + # neg slices + s = Series(list('abcdef')) + + with pytest.raises(ValueError, match=msg.format('slice')): + s[:-1] = list(range(27)) + + s[-3:-1] = list(range(2)) + expected = Series(['a', 'b', 'c', 0, 1, 'f']) + assert_series_equal(s, expected) + + # list + s = Series(list('abc')) + + with pytest.raises(ValueError, match=msg.format('list-like')): + s[[0, 1, 2]] = list(range(27)) + + s = Series(list('abc')) + + with pytest.raises(ValueError, match=msg.format('list-like')): + s[[0, 1, 2]] = list(range(2)) + + # scalar + s = Series(list('abc')) + s[0] = list(range(10)) + expected = Series([list(range(10)), 'b', 'c']) + assert_series_equal(s, expected) + + +@pytest.mark.parametrize('size', range(2, 6)) +@pytest.mark.parametrize('mask', [ + [True, False, False, False, False], + [True, False], + [False] +]) +@pytest.mark.parametrize('item', [ + 2.0, np.nan, np.finfo(np.float).max, np.finfo(np.float).min +]) +# Test numpy arrays, lists and tuples as the input to be +# broadcast +@pytest.mark.parametrize('box', [ + lambda x: np.array([x]), + lambda x: [x], + lambda x: (x,) +]) +def test_broadcast(size, mask, item, box): + selection = np.resize(mask, size) + + data = np.arange(size, dtype=float) + + # Construct the expected series by taking the source + # data or item based on the selection + expected = Series([item if use_item else data[ + i] for i, use_item in enumerate(selection)]) + + s = Series(data) + s[selection] = box(item) + assert_series_equal(s, expected) + + s = Series(data) + result = s.where(~selection, box(item)) + assert_series_equal(result, expected) + + s = Series(data) + result = s.mask(selection, box(item)) + assert_series_equal(result, expected) + + +def test_where_inplace(): + s = Series(np.random.randn(5)) + cond = s > 0 + + rs = s.copy() + + rs.where(cond, inplace=True) + assert_series_equal(rs.dropna(), s[cond]) + assert_series_equal(rs, s.where(cond)) + + rs = s.copy() + rs.where(cond, -s, inplace=True) + assert_series_equal(rs, s.where(cond, -s)) + + +def test_where_dups(): + # GH 4550 + # where crashes with dups in index + s1 = Series(list(range(3))) + s2 = Series(list(range(3))) + comb = pd.concat([s1, s2]) + result = comb.where(comb < 2) + expected = Series([0, 1, np.nan, 0, 1, np.nan], + index=[0, 1, 2, 0, 1, 2]) + assert_series_equal(result, expected) + + # GH 4548 + # inplace updating not working with dups + comb[comb < 1] = 5 + expected = Series([5, 1, 2, 5, 1, 2], index=[0, 1, 2, 0, 1, 2]) + assert_series_equal(comb, expected) + + comb[comb < 2] += 10 + expected = Series([5, 11, 2, 5, 11, 2], index=[0, 1, 2, 0, 1, 2]) + assert_series_equal(comb, expected) + + +def test_where_numeric_with_string(): + # GH 9280 + s = pd.Series([1, 2, 3]) + w = s.where(s > 1, 'X') + + assert not is_integer(w[0]) + assert is_integer(w[1]) + assert is_integer(w[2]) + assert isinstance(w[0], str) + assert w.dtype == 'object' + + w = s.where(s > 1, ['X', 'Y', 'Z']) + assert not is_integer(w[0]) + assert is_integer(w[1]) + assert is_integer(w[2]) + assert isinstance(w[0], str) + assert w.dtype == 'object' + + w = s.where(s > 1, np.array(['X', 'Y', 'Z'])) + assert not is_integer(w[0]) + assert is_integer(w[1]) + assert is_integer(w[2]) + assert isinstance(w[0], str) + assert w.dtype == 'object' + + +def test_where_timedelta_coerce(): + s = Series([1, 2], dtype='timedelta64[ns]') + expected = Series([10, 10]) + mask = np.array([False, False]) + + rs = s.where(mask, [10, 10]) + assert_series_equal(rs, expected) + + rs = s.where(mask, 10) + assert_series_equal(rs, expected) + + rs = s.where(mask, 10.0) + assert_series_equal(rs, expected) + + rs = s.where(mask, [10.0, 10.0]) + assert_series_equal(rs, expected) + + rs = s.where(mask, [10.0, np.nan]) + expected = Series([10, None], dtype='object') + assert_series_equal(rs, expected) + + +def test_where_datetime_conversion(): + s = Series(date_range('20130102', periods=2)) + expected = Series([10, 10]) + mask = np.array([False, False]) + + rs = s.where(mask, [10, 10]) + assert_series_equal(rs, expected) + + rs = s.where(mask, 10) + assert_series_equal(rs, expected) + + rs = s.where(mask, 10.0) + assert_series_equal(rs, expected) + + rs = s.where(mask, [10.0, 10.0]) + assert_series_equal(rs, expected) + + rs = s.where(mask, [10.0, np.nan]) + expected = Series([10, None], dtype='object') + assert_series_equal(rs, expected) + + # GH 15701 + timestamps = ['2016-12-31 12:00:04+00:00', + '2016-12-31 12:00:04.010000+00:00'] + s = Series([pd.Timestamp(t) for t in timestamps]) + rs = s.where(Series([False, True])) + expected = Series([pd.NaT, s[1]]) + assert_series_equal(rs, expected) + + +def test_where_dt_tz_values(tz_naive_fixture): + ser1 = pd.Series(pd.DatetimeIndex(['20150101', '20150102', '20150103'], + tz=tz_naive_fixture)) + ser2 = pd.Series(pd.DatetimeIndex(['20160514', '20160515', '20160516'], + tz=tz_naive_fixture)) + mask = pd.Series([True, True, False]) + result = ser1.where(mask, ser2) + exp = pd.Series(pd.DatetimeIndex(['20150101', '20150102', '20160516'], + tz=tz_naive_fixture)) + assert_series_equal(exp, result) + + +def test_mask(): + # compare with tested results in test_where + s = Series(np.random.randn(5)) + cond = s > 0 + + rs = s.where(~cond, np.nan) + assert_series_equal(rs, s.mask(cond)) + + rs = s.where(~cond) + rs2 = s.mask(cond) + assert_series_equal(rs, rs2) + + rs = s.where(~cond, -s) + rs2 = s.mask(cond, -s) + assert_series_equal(rs, rs2) + + cond = Series([True, False, False, True, False], index=s.index) + s2 = -(s.abs()) + rs = s2.where(~cond[:3]) + rs2 = s2.mask(cond[:3]) + assert_series_equal(rs, rs2) + + rs = s2.where(~cond[:3], -s2) + rs2 = s2.mask(cond[:3], -s2) + assert_series_equal(rs, rs2) + + msg = "Array conditional must be same shape as self" + with pytest.raises(ValueError, match=msg): + s.mask(1) + with pytest.raises(ValueError, match=msg): + s.mask(cond[:3].values, -s) + + # dtype changes + s = Series([1, 2, 3, 4]) + result = s.mask(s > 2, np.nan) + expected = Series([1, 2, np.nan, np.nan]) + assert_series_equal(result, expected) + + # see gh-21891 + s = Series([1, 2]) + res = s.mask([True, False]) + + exp = Series([np.nan, 2]) + tm.assert_series_equal(res, exp) + + +def test_mask_inplace(): + s = Series(np.random.randn(5)) + cond = s > 0 + + rs = s.copy() + rs.mask(cond, inplace=True) + assert_series_equal(rs.dropna(), s[~cond]) + assert_series_equal(rs, s.mask(cond)) + + rs = s.copy() + rs.mask(cond, -s, inplace=True) + assert_series_equal(rs, s.mask(cond, -s)) diff --git a/pandas/tests/series/indexing/test_callable.py b/pandas/tests/series/indexing/test_callable.py new file mode 100644 index 0000000000000..b656137545903 --- /dev/null +++ b/pandas/tests/series/indexing/test_callable.py @@ -0,0 +1,33 @@ +import pandas as pd +import pandas.util.testing as tm + + +def test_getitem_callable(): + # GH 12533 + s = pd.Series(4, index=list('ABCD')) + result = s[lambda x: 'A'] + assert result == s.loc['A'] + + result = s[lambda x: ['A', 'B']] + tm.assert_series_equal(result, s.loc[['A', 'B']]) + + result = s[lambda x: [True, False, True, True]] + tm.assert_series_equal(result, s.iloc[[0, 2, 3]]) + + +def test_setitem_callable(): + # GH 12533 + s = pd.Series([1, 2, 3, 4], index=list('ABCD')) + s[lambda x: 'A'] = -1 + tm.assert_series_equal(s, pd.Series([-1, 2, 3, 4], index=list('ABCD'))) + + +def test_setitem_other_callable(): + # GH 13299 + inc = lambda x: x + 1 + + s = pd.Series([1, 2, -1, 4]) + s[s < 0] = inc + + expected = pd.Series([1, 2, inc, 4]) + tm.assert_series_equal(s, expected) diff --git a/pandas/tests/series/indexing/test_datetime.py b/pandas/tests/series/indexing/test_datetime.py new file mode 100644 index 0000000000000..0efc9feb0dbd4 --- /dev/null +++ b/pandas/tests/series/indexing/test_datetime.py @@ -0,0 +1,714 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +from datetime import datetime, timedelta + +import numpy as np +import pytest + +from pandas._libs import iNaT +import pandas._libs.index as _index +from pandas.compat import lrange, range + +import pandas as pd +from pandas import DataFrame, DatetimeIndex, NaT, Series, Timestamp, date_range +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) + + +""" +Also test support for datetime64[ns] in Series / DataFrame +""" + + +def test_fancy_getitem(): + dti = date_range(freq='WOM-1FRI', start=datetime(2005, 1, 1), + end=datetime(2010, 1, 1)) + + s = Series(np.arange(len(dti)), index=dti) + + assert s[48] == 48 + assert s['1/2/2009'] == 48 + assert s['2009-1-2'] == 48 + assert s[datetime(2009, 1, 2)] == 48 + assert s[Timestamp(datetime(2009, 1, 2))] == 48 + with pytest.raises(KeyError, match=r"^'2009-1-3'$"): + s['2009-1-3'] + assert_series_equal(s['3/6/2009':'2009-06-05'], + s[datetime(2009, 3, 6):datetime(2009, 6, 5)]) + + +def test_fancy_setitem(): + dti = date_range(freq='WOM-1FRI', start=datetime(2005, 1, 1), + end=datetime(2010, 1, 1)) + + s = Series(np.arange(len(dti)), index=dti) + s[48] = -1 + assert s[48] == -1 + s['1/2/2009'] = -2 + assert s[48] == -2 + s['1/2/2009':'2009-06-05'] = -3 + assert (s[48:54] == -3).all() + + +def test_dti_snap(): + dti = DatetimeIndex(['1/1/2002', '1/2/2002', '1/3/2002', '1/4/2002', + '1/5/2002', '1/6/2002', '1/7/2002'], freq='D') + + res = dti.snap(freq='W-MON') + exp = date_range('12/31/2001', '1/7/2002', freq='w-mon') + exp = exp.repeat([3, 4]) + assert (res == exp).all() + + res = dti.snap(freq='B') + + exp = date_range('1/1/2002', '1/7/2002', freq='b') + exp = exp.repeat([1, 1, 1, 2, 2]) + assert (res == exp).all() + + +def test_dti_reset_index_round_trip(): + dti = date_range(start='1/1/2001', end='6/1/2001', freq='D') + d1 = DataFrame({'v': np.random.rand(len(dti))}, index=dti) + d2 = d1.reset_index() + assert d2.dtypes[0] == np.dtype('M8[ns]') + d3 = d2.set_index('index') + assert_frame_equal(d1, d3, check_names=False) + + # #2329 + stamp = datetime(2012, 11, 22) + df = DataFrame([[stamp, 12.1]], columns=['Date', 'Value']) + df = df.set_index('Date') + + assert df.index[0] == stamp + assert df.reset_index()['Date'][0] == stamp + + +def test_series_set_value(): + # #1561 + + dates = [datetime(2001, 1, 1), datetime(2001, 1, 2)] + index = DatetimeIndex(dates) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + s = Series().set_value(dates[0], 1.) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + s2 = s.set_value(dates[1], np.nan) + + exp = Series([1., np.nan], index=index) + + assert_series_equal(s2, exp) + + # s = Series(index[:1], index[:1]) + # s2 = s.set_value(dates[1], index[1]) + # assert s2.values.dtype == 'M8[ns]' + + +@pytest.mark.slow +def test_slice_locs_indexerror(): + times = [datetime(2000, 1, 1) + timedelta(minutes=i * 10) + for i in range(100000)] + s = Series(lrange(100000), times) + s.loc[datetime(1900, 1, 1):datetime(2100, 1, 1)] + + +def test_slicing_datetimes(): + # GH 7523 + + # unique + df = DataFrame(np.arange(4., dtype='float64'), + index=[datetime(2001, 1, i, 10, 00) + for i in [1, 2, 3, 4]]) + result = df.loc[datetime(2001, 1, 1, 10):] + assert_frame_equal(result, df) + result = df.loc[:datetime(2001, 1, 4, 10)] + assert_frame_equal(result, df) + result = df.loc[datetime(2001, 1, 1, 10):datetime(2001, 1, 4, 10)] + assert_frame_equal(result, df) + + result = df.loc[datetime(2001, 1, 1, 11):] + expected = df.iloc[1:] + assert_frame_equal(result, expected) + result = df.loc['20010101 11':] + assert_frame_equal(result, expected) + + # duplicates + df = pd.DataFrame(np.arange(5., dtype='float64'), + index=[datetime(2001, 1, i, 10, 00) + for i in [1, 2, 2, 3, 4]]) + + result = df.loc[datetime(2001, 1, 1, 10):] + assert_frame_equal(result, df) + result = df.loc[:datetime(2001, 1, 4, 10)] + assert_frame_equal(result, df) + result = df.loc[datetime(2001, 1, 1, 10):datetime(2001, 1, 4, 10)] + assert_frame_equal(result, df) + + result = df.loc[datetime(2001, 1, 1, 11):] + expected = df.iloc[1:] + assert_frame_equal(result, expected) + result = df.loc['20010101 11':] + assert_frame_equal(result, expected) + + +def test_frame_datetime64_duplicated(): + dates = date_range('2010-07-01', end='2010-08-05') + + tst = DataFrame({'symbol': 'AAA', 'date': dates}) + result = tst.duplicated(['date', 'symbol']) + assert (-result).all() + + tst = DataFrame({'date': dates}) + result = tst.duplicated() + assert (-result).all() + + +def test_getitem_setitem_datetime_tz_pytz(): + from pytz import timezone as tz + from pandas import date_range + + N = 50 + # testing with timezone, GH #2785 + rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern') + ts = Series(np.random.randn(N), index=rng) + + # also test Timestamp tz handling, GH #2789 + result = ts.copy() + result["1990-01-01 09:00:00+00:00"] = 0 + result["1990-01-01 09:00:00+00:00"] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + result["1990-01-01 03:00:00-06:00"] = 0 + result["1990-01-01 03:00:00-06:00"] = ts[4] + assert_series_equal(result, ts) + + # repeat with datetimes + result = ts.copy() + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0 + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + + # comparison dates with datetime MUST be localized! + date = tz('US/Central').localize(datetime(1990, 1, 1, 3)) + result[date] = 0 + result[date] = ts[4] + assert_series_equal(result, ts) + + +def test_getitem_setitem_datetime_tz_dateutil(): + from dateutil.tz import tzutc + from pandas._libs.tslibs.timezones import dateutil_gettz as gettz + + tz = lambda x: tzutc() if x == 'UTC' else gettz( + x) # handle special case for utc in dateutil + + from pandas import date_range + + N = 50 + + # testing with timezone, GH #2785 + rng = date_range('1/1/1990', periods=N, freq='H', + tz='America/New_York') + ts = Series(np.random.randn(N), index=rng) + + # also test Timestamp tz handling, GH #2789 + result = ts.copy() + result["1990-01-01 09:00:00+00:00"] = 0 + result["1990-01-01 09:00:00+00:00"] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + result["1990-01-01 03:00:00-06:00"] = 0 + result["1990-01-01 03:00:00-06:00"] = ts[4] + assert_series_equal(result, ts) + + # repeat with datetimes + result = ts.copy() + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0 + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + result[datetime(1990, 1, 1, 3, tzinfo=tz('America/Chicago'))] = 0 + result[datetime(1990, 1, 1, 3, tzinfo=tz('America/Chicago'))] = ts[4] + assert_series_equal(result, ts) + + +def test_getitem_setitem_datetimeindex(): + N = 50 + # testing with timezone, GH #2785 + rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern') + ts = Series(np.random.randn(N), index=rng) + + result = ts["1990-01-01 04:00:00"] + expected = ts[4] + assert result == expected + + result = ts.copy() + result["1990-01-01 04:00:00"] = 0 + result["1990-01-01 04:00:00"] = ts[4] + assert_series_equal(result, ts) + + result = ts["1990-01-01 04:00:00":"1990-01-01 07:00:00"] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts.copy() + result["1990-01-01 04:00:00":"1990-01-01 07:00:00"] = 0 + result["1990-01-01 04:00:00":"1990-01-01 07:00:00"] = ts[4:8] + assert_series_equal(result, ts) + + lb = "1990-01-01 04:00:00" + rb = "1990-01-01 07:00:00" + # GH#18435 strings get a pass from tzawareness compat + result = ts[(ts.index >= lb) & (ts.index <= rb)] + expected = ts[4:8] + assert_series_equal(result, expected) + + lb = "1990-01-01 04:00:00-0500" + rb = "1990-01-01 07:00:00-0500" + result = ts[(ts.index >= lb) & (ts.index <= rb)] + expected = ts[4:8] + assert_series_equal(result, expected) + + # repeat all the above with naive datetimes + result = ts[datetime(1990, 1, 1, 4)] + expected = ts[4] + assert result == expected + + result = ts.copy() + result[datetime(1990, 1, 1, 4)] = 0 + result[datetime(1990, 1, 1, 4)] = ts[4] + assert_series_equal(result, ts) + + result = ts[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts.copy() + result[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] = 0 + result[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] = ts[4:8] + assert_series_equal(result, ts) + + lb = datetime(1990, 1, 1, 4) + rb = datetime(1990, 1, 1, 7) + msg = "Cannot compare tz-naive and tz-aware datetime-like objects" + with pytest.raises(TypeError, match=msg): + # tznaive vs tzaware comparison is invalid + # see GH#18376, GH#18162 + ts[(ts.index >= lb) & (ts.index <= rb)] + + lb = pd.Timestamp(datetime(1990, 1, 1, 4)).tz_localize(rng.tzinfo) + rb = pd.Timestamp(datetime(1990, 1, 1, 7)).tz_localize(rng.tzinfo) + result = ts[(ts.index >= lb) & (ts.index <= rb)] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts[ts.index[4]] + expected = ts[4] + assert result == expected + + result = ts[ts.index[4:8]] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts.copy() + result[ts.index[4:8]] = 0 + result[4:8] = ts[4:8] + assert_series_equal(result, ts) + + # also test partial date slicing + result = ts["1990-01-02"] + expected = ts[24:48] + assert_series_equal(result, expected) + + result = ts.copy() + result["1990-01-02"] = 0 + result["1990-01-02"] = ts[24:48] + assert_series_equal(result, ts) + + +def test_getitem_setitem_periodindex(): + from pandas import period_range + + N = 50 + rng = period_range('1/1/1990', periods=N, freq='H') + ts = Series(np.random.randn(N), index=rng) + + result = ts["1990-01-01 04"] + expected = ts[4] + assert result == expected + + result = ts.copy() + result["1990-01-01 04"] = 0 + result["1990-01-01 04"] = ts[4] + assert_series_equal(result, ts) + + result = ts["1990-01-01 04":"1990-01-01 07"] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts.copy() + result["1990-01-01 04":"1990-01-01 07"] = 0 + result["1990-01-01 04":"1990-01-01 07"] = ts[4:8] + assert_series_equal(result, ts) + + lb = "1990-01-01 04" + rb = "1990-01-01 07" + result = ts[(ts.index >= lb) & (ts.index <= rb)] + expected = ts[4:8] + assert_series_equal(result, expected) + + # GH 2782 + result = ts[ts.index[4]] + expected = ts[4] + assert result == expected + + result = ts[ts.index[4:8]] + expected = ts[4:8] + assert_series_equal(result, expected) + + result = ts.copy() + result[ts.index[4:8]] = 0 + result[4:8] = ts[4:8] + assert_series_equal(result, ts) + + +# FutureWarning from NumPy. +@pytest.mark.filterwarnings("ignore:Using a non-tuple:FutureWarning") +def test_getitem_median_slice_bug(): + index = date_range('20090415', '20090519', freq='2B') + s = Series(np.random.randn(13), index=index) + + indexer = [slice(6, 7, None)] + result = s[indexer] + expected = s[indexer[0]] + assert_series_equal(result, expected) + + +def test_datetime_indexing(): + from pandas import date_range + + index = date_range('1/1/2000', '1/7/2000') + index = index.repeat(3) + + s = Series(len(index), index=index) + stamp = Timestamp('1/8/2000') + + with pytest.raises(KeyError, match=r"^947289600000000000L?$"): + s[stamp] + s[stamp] = 0 + assert s[stamp] == 0 + + # not monotonic + s = Series(len(index), index=index) + s = s[::-1] + + with pytest.raises(KeyError, match=r"^947289600000000000L?$"): + s[stamp] + s[stamp] = 0 + assert s[stamp] == 0 + + +""" +test duplicates in time series +""" + + +@pytest.fixture(scope='module') +def dups(): + dates = [datetime(2000, 1, 2), datetime(2000, 1, 2), + datetime(2000, 1, 2), datetime(2000, 1, 3), + datetime(2000, 1, 3), datetime(2000, 1, 3), + datetime(2000, 1, 4), datetime(2000, 1, 4), + datetime(2000, 1, 4), datetime(2000, 1, 5)] + + return Series(np.random.randn(len(dates)), index=dates) + + +def test_constructor(dups): + assert isinstance(dups, Series) + assert isinstance(dups.index, DatetimeIndex) + + +def test_is_unique_monotonic(dups): + assert not dups.index.is_unique + + +def test_index_unique(dups): + uniques = dups.index.unique() + expected = DatetimeIndex([datetime(2000, 1, 2), datetime(2000, 1, 3), + datetime(2000, 1, 4), datetime(2000, 1, 5)]) + assert uniques.dtype == 'M8[ns]' # sanity + tm.assert_index_equal(uniques, expected) + assert dups.index.nunique() == 4 + + # #2563 + assert isinstance(uniques, DatetimeIndex) + + dups_local = dups.index.tz_localize('US/Eastern') + dups_local.name = 'foo' + result = dups_local.unique() + expected = DatetimeIndex(expected, name='foo') + expected = expected.tz_localize('US/Eastern') + assert result.tz is not None + assert result.name == 'foo' + tm.assert_index_equal(result, expected) + + # NaT, note this is excluded + arr = [1370745748 + t for t in range(20)] + [iNaT] + idx = DatetimeIndex(arr * 3) + tm.assert_index_equal(idx.unique(), DatetimeIndex(arr)) + assert idx.nunique() == 20 + assert idx.nunique(dropna=False) == 21 + + arr = [Timestamp('2013-06-09 02:42:28') + timedelta(seconds=t) + for t in range(20)] + [NaT] + idx = DatetimeIndex(arr * 3) + tm.assert_index_equal(idx.unique(), DatetimeIndex(arr)) + assert idx.nunique() == 20 + assert idx.nunique(dropna=False) == 21 + + +def test_index_dupes_contains(): + d = datetime(2011, 12, 5, 20, 30) + ix = DatetimeIndex([d, d]) + assert d in ix + + +def test_duplicate_dates_indexing(dups): + ts = dups + + uniques = ts.index.unique() + for date in uniques: + result = ts[date] + + mask = ts.index == date + total = (ts.index == date).sum() + expected = ts[mask] + if total > 1: + assert_series_equal(result, expected) + else: + assert_almost_equal(result, expected[0]) + + cp = ts.copy() + cp[date] = 0 + expected = Series(np.where(mask, 0, ts), index=ts.index) + assert_series_equal(cp, expected) + + with pytest.raises(KeyError, match=r"^947116800000000000L?$"): + ts[datetime(2000, 1, 6)] + + # new index + ts[datetime(2000, 1, 6)] = 0 + assert ts[datetime(2000, 1, 6)] == 0 + + +def test_range_slice(): + idx = DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000', '1/3/2000', + '1/4/2000']) + + ts = Series(np.random.randn(len(idx)), index=idx) + + result = ts['1/2/2000':] + expected = ts[1:] + assert_series_equal(result, expected) + + result = ts['1/2/2000':'1/3/2000'] + expected = ts[1:4] + assert_series_equal(result, expected) + + +def test_groupby_average_dup_values(dups): + result = dups.groupby(level=0).mean() + expected = dups.groupby(dups.index).mean() + assert_series_equal(result, expected) + + +def test_indexing_over_size_cutoff(): + import datetime + # #1821 + + old_cutoff = _index._SIZE_CUTOFF + try: + _index._SIZE_CUTOFF = 1000 + + # create large list of non periodic datetime + dates = [] + sec = datetime.timedelta(seconds=1) + half_sec = datetime.timedelta(microseconds=500000) + d = datetime.datetime(2011, 12, 5, 20, 30) + n = 1100 + for i in range(n): + dates.append(d) + dates.append(d + sec) + dates.append(d + sec + half_sec) + dates.append(d + sec + sec + half_sec) + d += 3 * sec + + # duplicate some values in the list + duplicate_positions = np.random.randint(0, len(dates) - 1, 20) + for p in duplicate_positions: + dates[p + 1] = dates[p] + + df = DataFrame(np.random.randn(len(dates), 4), + index=dates, + columns=list('ABCD')) + + pos = n * 3 + timestamp = df.index[pos] + assert timestamp in df.index + + # it works! + df.loc[timestamp] + assert len(df.loc[[timestamp]]) > 0 + finally: + _index._SIZE_CUTOFF = old_cutoff + + +def test_indexing_unordered(): + # GH 2437 + rng = date_range(start='2011-01-01', end='2011-01-15') + ts = Series(np.random.rand(len(rng)), index=rng) + ts2 = pd.concat([ts[0:4], ts[-4:], ts[4:-4]]) + + for t in ts.index: + # TODO: unused? + s = str(t) # noqa + + expected = ts[t] + result = ts2[t] + assert expected == result + + # GH 3448 (ranges) + def compare(slobj): + result = ts2[slobj].copy() + result = result.sort_index() + expected = ts[slobj] + assert_series_equal(result, expected) + + compare(slice('2011-01-01', '2011-01-15')) + compare(slice('2010-12-30', '2011-01-15')) + compare(slice('2011-01-01', '2011-01-16')) + + # partial ranges + compare(slice('2011-01-01', '2011-01-6')) + compare(slice('2011-01-06', '2011-01-8')) + compare(slice('2011-01-06', '2011-01-12')) + + # single values + result = ts2['2011'].sort_index() + expected = ts['2011'] + assert_series_equal(result, expected) + + # diff freq + rng = date_range(datetime(2005, 1, 1), periods=20, freq='M') + ts = Series(np.arange(len(rng)), index=rng) + ts = ts.take(np.random.permutation(20)) + + result = ts['2005'] + for t in result.index: + assert t.year == 2005 + + +def test_indexing(): + idx = date_range("2001-1-1", periods=20, freq='M') + ts = Series(np.random.rand(len(idx)), index=idx) + + # getting + + # GH 3070, make sure semantics work on Series/Frame + expected = ts['2001'] + expected.name = 'A' + + df = DataFrame(dict(A=ts)) + result = df['2001']['A'] + assert_series_equal(expected, result) + + # setting + ts['2001'] = 1 + expected = ts['2001'] + expected.name = 'A' + + df.loc['2001', 'A'] = 1 + + result = df['2001']['A'] + assert_series_equal(expected, result) + + # GH3546 (not including times on the last day) + idx = date_range(start='2013-05-31 00:00', end='2013-05-31 23:00', + freq='H') + ts = Series(lrange(len(idx)), index=idx) + expected = ts['2013-05'] + assert_series_equal(expected, ts) + + idx = date_range(start='2013-05-31 00:00', end='2013-05-31 23:59', + freq='S') + ts = Series(lrange(len(idx)), index=idx) + expected = ts['2013-05'] + assert_series_equal(expected, ts) + + idx = [Timestamp('2013-05-31 00:00'), + Timestamp(datetime(2013, 5, 31, 23, 59, 59, 999999))] + ts = Series(lrange(len(idx)), index=idx) + expected = ts['2013'] + assert_series_equal(expected, ts) + + # GH14826, indexing with a seconds resolution string / datetime object + df = DataFrame(np.random.rand(5, 5), + columns=['open', 'high', 'low', 'close', 'volume'], + index=date_range('2012-01-02 18:01:00', + periods=5, tz='US/Central', freq='s')) + expected = df.loc[[df.index[2]]] + + # this is a single date, so will raise + with pytest.raises(KeyError, match=r"^'2012-01-02 18:01:02'$"): + df['2012-01-02 18:01:02'] + msg = r"Timestamp\('2012-01-02 18:01:02-0600', tz='US/Central', freq='S'\)" + with pytest.raises(KeyError, match=msg): + df[df.index[2]] + + +""" +test NaT support +""" + + +def test_set_none_nan(): + series = Series(date_range('1/1/2000', periods=10)) + series[3] = None + assert series[3] is NaT + + series[3:5] = None + assert series[4] is NaT + + series[5] = np.nan + assert series[5] is NaT + + series[5:7] = np.nan + assert series[6] is NaT + + +def test_nat_operations(): + # GH 8617 + s = Series([0, pd.NaT], dtype='m8[ns]') + exp = s[0] + assert s.median() == exp + assert s.min() == exp + assert s.max() == exp + + +@pytest.mark.parametrize('method', ["round", "floor", "ceil"]) +@pytest.mark.parametrize('freq', ["s", "5s", "min", "5min", "h", "5h"]) +def test_round_nat(method, freq): + # GH14940 + s = Series([pd.NaT]) + expected = Series(pd.NaT) + round_method = getattr(s.dt, method) + assert_series_equal(round_method(freq), expected) diff --git a/pandas/tests/series/indexing/test_iloc.py b/pandas/tests/series/indexing/test_iloc.py new file mode 100644 index 0000000000000..fa85da6a70d62 --- /dev/null +++ b/pandas/tests/series/indexing/test_iloc.py @@ -0,0 +1,37 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +import numpy as np + +from pandas.compat import lrange, range + +from pandas import Series +from pandas.util.testing import assert_almost_equal, assert_series_equal + + +def test_iloc(): + s = Series(np.random.randn(10), index=lrange(0, 20, 2)) + + for i in range(len(s)): + result = s.iloc[i] + exp = s[s.index[i]] + assert_almost_equal(result, exp) + + # pass a slice + result = s.iloc[slice(1, 3)] + expected = s.loc[2:4] + assert_series_equal(result, expected) + + # test slice is a view + result[:] = 0 + assert (s[1:3] == 0).all() + + # list of integers + result = s.iloc[[0, 2, 3, 4, 5]] + expected = s.reindex(s.index[[0, 2, 3, 4, 5]]) + assert_series_equal(result, expected) + + +def test_iloc_nonunique(): + s = Series([0, 1, 2], index=[0, 1, 0]) + assert s.iloc[2] == 2 diff --git a/pandas/tests/series/indexing/test_indexing.py b/pandas/tests/series/indexing/test_indexing.py new file mode 100644 index 0000000000000..dbe667a166d0a --- /dev/null +++ b/pandas/tests/series/indexing/test_indexing.py @@ -0,0 +1,841 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +""" test get/set & misc """ + +from datetime import timedelta + +import numpy as np +import pytest + +from pandas.compat import lrange, range + +from pandas.core.dtypes.common import is_scalar + +import pandas as pd +from pandas import ( + Categorical, DataFrame, MultiIndex, Series, Timedelta, Timestamp) +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal + +from pandas.tseries.offsets import BDay + + +def test_basic_indexing(): + s = Series(np.random.randn(5), index=['a', 'b', 'a', 'a', 'b']) + + msg = "index out of bounds" + with pytest.raises(IndexError, match=msg): + s[5] + msg = "index 5 is out of bounds for axis 0 with size 5" + with pytest.raises(IndexError, match=msg): + s[5] = 0 + + with pytest.raises(KeyError, match=r"^'c'$"): + s['c'] + + s = s.sort_index() + + msg = r"index out of bounds|^5$" + with pytest.raises(IndexError, match=msg): + s[5] + msg = r"index 5 is out of bounds for axis (0|1) with size 5|^5$" + with pytest.raises(IndexError, match=msg): + s[5] = 0 + + +def test_basic_getitem_with_labels(test_data): + indices = test_data.ts.index[[5, 10, 15]] + + result = test_data.ts[indices] + expected = test_data.ts.reindex(indices) + assert_series_equal(result, expected) + + result = test_data.ts[indices[0]:indices[2]] + expected = test_data.ts.loc[indices[0]:indices[2]] + assert_series_equal(result, expected) + + # integer indexes, be careful + s = Series(np.random.randn(10), index=lrange(0, 20, 2)) + inds = [0, 2, 5, 7, 8] + arr_inds = np.array([0, 2, 5, 7, 8]) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = s[inds] + expected = s.reindex(inds) + assert_series_equal(result, expected) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = s[arr_inds] + expected = s.reindex(arr_inds) + assert_series_equal(result, expected) + + # GH12089 + # with tz for values + s = Series(pd.date_range("2011-01-01", periods=3, tz="US/Eastern"), + index=['a', 'b', 'c']) + expected = Timestamp('2011-01-01', tz='US/Eastern') + result = s.loc['a'] + assert result == expected + result = s.iloc[0] + assert result == expected + result = s['a'] + assert result == expected + + +def test_getitem_setitem_ellipsis(): + s = Series(np.random.randn(10)) + + np.fix(s) + + result = s[...] + assert_series_equal(result, s) + + s[...] = 5 + assert (result == 5).all() + + +def test_getitem_get(test_data): + test_series = test_data.series + test_obj_series = test_data.objSeries + + idx1 = test_series.index[5] + idx2 = test_obj_series.index[5] + + assert test_series[idx1] == test_series.get(idx1) + assert test_obj_series[idx2] == test_obj_series.get(idx2) + + assert test_series[idx1] == test_series[5] + assert test_obj_series[idx2] == test_obj_series[5] + + assert test_series.get(-1) == test_series.get(test_series.index[-1]) + assert test_series[5] == test_series.get(test_series.index[5]) + + # missing + d = test_data.ts.index[0] - BDay() + msg = r"Timestamp\('1999-12-31 00:00:00', freq='B'\)" + with pytest.raises(KeyError, match=msg): + test_data.ts[d] + + # None + # GH 5652 + for s in [Series(), Series(index=list('abc'))]: + result = s.get(None) + assert result is None + + +def test_getitem_fancy(test_data): + slice1 = test_data.series[[1, 2, 3]] + slice2 = test_data.objSeries[[1, 2, 3]] + assert test_data.series.index[2] == slice1.index[1] + assert test_data.objSeries.index[2] == slice2.index[1] + assert test_data.series[2] == slice1[1] + assert test_data.objSeries[2] == slice2[1] + + +def test_getitem_generator(test_data): + gen = (x > 0 for x in test_data.series) + result = test_data.series[gen] + result2 = test_data.series[iter(test_data.series > 0)] + expected = test_data.series[test_data.series > 0] + assert_series_equal(result, expected) + assert_series_equal(result2, expected) + + +def test_type_promotion(): + # GH12599 + s = pd.Series() + s["a"] = pd.Timestamp("2016-01-01") + s["b"] = 3.0 + s["c"] = "foo" + expected = Series([pd.Timestamp("2016-01-01"), 3.0, "foo"], + index=["a", "b", "c"]) + assert_series_equal(s, expected) + + +@pytest.mark.parametrize( + 'result_1, duplicate_item, expected_1', + [ + [ + pd.Series({1: 12, 2: [1, 2, 2, 3]}), pd.Series({1: 313}), + pd.Series({1: 12, }, dtype=object), + ], + [ + pd.Series({1: [1, 2, 3], 2: [1, 2, 2, 3]}), + pd.Series({1: [1, 2, 3]}), pd.Series({1: [1, 2, 3], }), + ], + ]) +def test_getitem_with_duplicates_indices( + result_1, duplicate_item, expected_1): + # GH 17610 + result = result_1.append(duplicate_item) + expected = expected_1.append(duplicate_item) + assert_series_equal(result[1], expected) + assert result[2] == result_1[2] + + +def test_getitem_out_of_bounds(test_data): + # don't segfault, GH #495 + msg = "index out of bounds" + with pytest.raises(IndexError, match=msg): + test_data.ts[len(test_data.ts)] + + # GH #917 + s = Series([]) + with pytest.raises(IndexError, match=msg): + s[-1] + + +def test_getitem_setitem_integers(): + # caused bug without test + s = Series([1, 2, 3], ['a', 'b', 'c']) + + assert s.iloc[0] == s['a'] + s.iloc[0] = 5 + tm.assert_almost_equal(s['a'], 5) + + +def test_getitem_box_float64(test_data): + value = test_data.ts[5] + assert isinstance(value, np.float64) + + +@pytest.mark.parametrize( + 'arr', + [ + np.random.randn(10), + tm.makeDateIndex(10, name='a').tz_localize( + tz='US/Eastern'), + ]) +def test_get(arr): + # GH 21260 + s = Series(arr, index=[2 * i for i in range(len(arr))]) + assert s.get(4) == s.iloc[2] + + result = s.get([4, 6]) + expected = s.iloc[[2, 3]] + tm.assert_series_equal(result, expected) + + result = s.get(slice(2)) + expected = s.iloc[[0, 1]] + tm.assert_series_equal(result, expected) + + assert s.get(-1) is None + assert s.get(s.index.max() + 1) is None + + s = Series(arr[:6], index=list('abcdef')) + assert s.get('c') == s.iloc[2] + + result = s.get(slice('b', 'd')) + expected = s.iloc[[1, 2, 3]] + tm.assert_series_equal(result, expected) + + result = s.get('Z') + assert result is None + + assert s.get(4) == s.iloc[4] + assert s.get(-1) == s.iloc[-1] + assert s.get(len(s)) is None + + # GH 21257 + s = pd.Series(arr) + s2 = s[::2] + assert s2.get(1) is None + + +def test_series_box_timestamp(): + rng = pd.date_range('20090415', '20090519', freq='B') + ser = Series(rng) + + assert isinstance(ser[5], pd.Timestamp) + + rng = pd.date_range('20090415', '20090519', freq='B') + ser = Series(rng, index=rng) + assert isinstance(ser[5], pd.Timestamp) + + assert isinstance(ser.iat[5], pd.Timestamp) + + +def test_getitem_ambiguous_keyerror(): + s = Series(lrange(10), index=lrange(0, 20, 2)) + with pytest.raises(KeyError, match=r"^1L?$"): + s[1] + with pytest.raises(KeyError, match=r"^1L?$"): + s.loc[1] + + +def test_getitem_unordered_dup(): + obj = Series(lrange(5), index=['c', 'a', 'a', 'b', 'b']) + assert is_scalar(obj['c']) + assert obj['c'] == 0 + + +def test_getitem_dups_with_missing(): + # breaks reindex, so need to use .loc internally + # GH 4246 + s = Series([1, 2, 3, 4], ['foo', 'bar', 'foo', 'bah']) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + expected = s.loc[['foo', 'bar', 'bah', 'bam']] + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = s[['foo', 'bar', 'bah', 'bam']] + assert_series_equal(result, expected) + + +def test_getitem_dups(): + s = Series(range(5), index=['A', 'A', 'B', 'C', 'C'], dtype=np.int64) + expected = Series([3, 4], index=['C', 'C'], dtype=np.int64) + result = s['C'] + assert_series_equal(result, expected) + + +def test_setitem_ambiguous_keyerror(): + s = Series(lrange(10), index=lrange(0, 20, 2)) + + # equivalent of an append + s2 = s.copy() + s2[1] = 5 + expected = s.append(Series([5], index=[1])) + assert_series_equal(s2, expected) + + s2 = s.copy() + s2.loc[1] = 5 + expected = s.append(Series([5], index=[1])) + assert_series_equal(s2, expected) + + +def test_getitem_dataframe(): + rng = list(range(10)) + s = pd.Series(10, index=rng) + df = pd.DataFrame(rng, index=rng) + msg = ("Indexing a Series with DataFrame is not supported," + " use the appropriate DataFrame column") + with pytest.raises(TypeError, match=msg): + s[df > 5] + + +def test_setitem(test_data): + test_data.ts[test_data.ts.index[5]] = np.NaN + test_data.ts[[1, 2, 17]] = np.NaN + test_data.ts[6] = np.NaN + assert np.isnan(test_data.ts[6]) + assert np.isnan(test_data.ts[2]) + test_data.ts[np.isnan(test_data.ts)] = 5 + assert not np.isnan(test_data.ts[2]) + + # caught this bug when writing tests + series = Series(tm.makeIntIndex(20).astype(float), + index=tm.makeIntIndex(20)) + + series[::2] = 0 + assert (series[::2] == 0).all() + + # set item that's not contained + s = test_data.series.copy() + s['foobar'] = 1 + + app = Series([1], index=['foobar'], name='series') + expected = test_data.series.append(app) + assert_series_equal(s, expected) + + # Test for issue #10193 + key = pd.Timestamp('2012-01-01') + series = pd.Series() + series[key] = 47 + expected = pd.Series(47, [key]) + assert_series_equal(series, expected) + + series = pd.Series([], pd.DatetimeIndex([], freq='D')) + series[key] = 47 + expected = pd.Series(47, pd.DatetimeIndex([key], freq='D')) + assert_series_equal(series, expected) + + +def test_setitem_dtypes(): + # change dtypes + # GH 4463 + expected = Series([np.nan, 2, 3]) + + s = Series([1, 2, 3]) + s.iloc[0] = np.nan + assert_series_equal(s, expected) + + s = Series([1, 2, 3]) + s.loc[0] = np.nan + assert_series_equal(s, expected) + + s = Series([1, 2, 3]) + s[0] = np.nan + assert_series_equal(s, expected) + + s = Series([False]) + s.loc[0] = np.nan + assert_series_equal(s, Series([np.nan])) + + s = Series([False, True]) + s.loc[0] = np.nan + assert_series_equal(s, Series([np.nan, 1.0])) + + +def test_set_value(test_data): + idx = test_data.ts.index[10] + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + res = test_data.ts.set_value(idx, 0) + assert res is test_data.ts + assert test_data.ts[idx] == 0 + + # equiv + s = test_data.series.copy() + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + res = s.set_value('foobar', 0) + assert res is s + assert res.index[-1] == 'foobar' + assert res['foobar'] == 0 + + s = test_data.series.copy() + s.loc['foobar'] = 0 + assert s.index[-1] == 'foobar' + assert s['foobar'] == 0 + + +def test_setslice(test_data): + sl = test_data.ts[5:20] + assert len(sl) == len(sl.index) + assert sl.index.is_unique is True + + +# FutureWarning from NumPy about [slice(None, 5). +@pytest.mark.filterwarnings("ignore:Using a non-tuple:FutureWarning") +def test_basic_getitem_setitem_corner(test_data): + # invalid tuples, e.g. td.ts[:, None] vs. td.ts[:, 2] + msg = "Can only tuple-index with a MultiIndex" + with pytest.raises(ValueError, match=msg): + test_data.ts[:, 2] + with pytest.raises(ValueError, match=msg): + test_data.ts[:, 2] = 2 + + # weird lists. [slice(0, 5)] will work but not two slices + result = test_data.ts[[slice(None, 5)]] + expected = test_data.ts[:5] + assert_series_equal(result, expected) + + # OK + msg = r"unhashable type(: 'slice')?" + with pytest.raises(TypeError, match=msg): + test_data.ts[[5, slice(None, None)]] + with pytest.raises(TypeError, match=msg): + test_data.ts[[5, slice(None, None)]] = 2 + + +@pytest.mark.parametrize('tz', ['US/Eastern', 'UTC', 'Asia/Tokyo']) +def test_setitem_with_tz(tz): + orig = pd.Series(pd.date_range('2016-01-01', freq='H', periods=3, + tz=tz)) + assert orig.dtype == 'datetime64[ns, {0}]'.format(tz) + + # scalar + s = orig.copy() + s[1] = pd.Timestamp('2011-01-01', tz=tz) + exp = pd.Series([pd.Timestamp('2016-01-01 00:00', tz=tz), + pd.Timestamp('2011-01-01 00:00', tz=tz), + pd.Timestamp('2016-01-01 02:00', tz=tz)]) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.loc[1] = pd.Timestamp('2011-01-01', tz=tz) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.iloc[1] = pd.Timestamp('2011-01-01', tz=tz) + tm.assert_series_equal(s, exp) + + # vector + vals = pd.Series([pd.Timestamp('2011-01-01', tz=tz), + pd.Timestamp('2012-01-01', tz=tz)], index=[1, 2]) + assert vals.dtype == 'datetime64[ns, {0}]'.format(tz) + + s[[1, 2]] = vals + exp = pd.Series([pd.Timestamp('2016-01-01 00:00', tz=tz), + pd.Timestamp('2011-01-01 00:00', tz=tz), + pd.Timestamp('2012-01-01 00:00', tz=tz)]) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.loc[[1, 2]] = vals + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.iloc[[1, 2]] = vals + tm.assert_series_equal(s, exp) + + +def test_setitem_with_tz_dst(): + # GH XXX + tz = 'US/Eastern' + orig = pd.Series(pd.date_range('2016-11-06', freq='H', periods=3, + tz=tz)) + assert orig.dtype == 'datetime64[ns, {0}]'.format(tz) + + # scalar + s = orig.copy() + s[1] = pd.Timestamp('2011-01-01', tz=tz) + exp = pd.Series([pd.Timestamp('2016-11-06 00:00-04:00', tz=tz), + pd.Timestamp('2011-01-01 00:00-05:00', tz=tz), + pd.Timestamp('2016-11-06 01:00-05:00', tz=tz)]) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.loc[1] = pd.Timestamp('2011-01-01', tz=tz) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.iloc[1] = pd.Timestamp('2011-01-01', tz=tz) + tm.assert_series_equal(s, exp) + + # vector + vals = pd.Series([pd.Timestamp('2011-01-01', tz=tz), + pd.Timestamp('2012-01-01', tz=tz)], index=[1, 2]) + assert vals.dtype == 'datetime64[ns, {0}]'.format(tz) + + s[[1, 2]] = vals + exp = pd.Series([pd.Timestamp('2016-11-06 00:00', tz=tz), + pd.Timestamp('2011-01-01 00:00', tz=tz), + pd.Timestamp('2012-01-01 00:00', tz=tz)]) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.loc[[1, 2]] = vals + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.iloc[[1, 2]] = vals + tm.assert_series_equal(s, exp) + + +def test_categorial_assigning_ops(): + orig = Series(Categorical(["b", "b"], categories=["a", "b"])) + s = orig.copy() + s[:] = "a" + exp = Series(Categorical(["a", "a"], categories=["a", "b"])) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s[1] = "a" + exp = Series(Categorical(["b", "a"], categories=["a", "b"])) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s[s.index > 0] = "a" + exp = Series(Categorical(["b", "a"], categories=["a", "b"])) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s[[False, True]] = "a" + exp = Series(Categorical(["b", "a"], categories=["a", "b"])) + tm.assert_series_equal(s, exp) + + s = orig.copy() + s.index = ["x", "y"] + s["y"] = "a" + exp = Series(Categorical(["b", "a"], categories=["a", "b"]), + index=["x", "y"]) + tm.assert_series_equal(s, exp) + + # ensure that one can set something to np.nan + s = Series(Categorical([1, 2, 3])) + exp = Series(Categorical([1, np.nan, 3], categories=[1, 2, 3])) + s[1] = np.nan + tm.assert_series_equal(s, exp) + + +def test_slice(test_data): + numSlice = test_data.series[10:20] + numSliceEnd = test_data.series[-10:] + objSlice = test_data.objSeries[10:20] + + assert test_data.series.index[9] not in numSlice.index + assert test_data.objSeries.index[9] not in objSlice.index + + assert len(numSlice) == len(numSlice.index) + assert test_data.series[numSlice.index[0]] == numSlice[numSlice.index[0]] + + assert numSlice.index[1] == test_data.series.index[11] + assert tm.equalContents(numSliceEnd, np.array(test_data.series)[-10:]) + + # Test return view. + sl = test_data.series[10:20] + sl[:] = 0 + + assert (test_data.series[10:20] == 0).all() + + +def test_slice_can_reorder_not_uniquely_indexed(): + s = Series(1, index=['a', 'a', 'b', 'b', 'c']) + s[::-1] # it works! + + +def test_ix_setitem(test_data): + inds = test_data.series.index[[3, 4, 7]] + + result = test_data.series.copy() + result.loc[inds] = 5 + + expected = test_data.series.copy() + expected[[3, 4, 7]] = 5 + assert_series_equal(result, expected) + + result.iloc[5:10] = 10 + expected[5:10] = 10 + assert_series_equal(result, expected) + + # set slice with indices + d1, d2 = test_data.series.index[[5, 15]] + result.loc[d1:d2] = 6 + expected[5:16] = 6 # because it's inclusive + assert_series_equal(result, expected) + + # set index value + test_data.series.loc[d1] = 4 + test_data.series.loc[d2] = 6 + assert test_data.series[d1] == 4 + assert test_data.series[d2] == 6 + + +def test_setitem_na(): + # these induce dtype changes + expected = Series([np.nan, 3, np.nan, 5, np.nan, 7, np.nan, 9, np.nan]) + s = Series([2, 3, 4, 5, 6, 7, 8, 9, 10]) + s[::2] = np.nan + assert_series_equal(s, expected) + + # gets coerced to float, right? + expected = Series([np.nan, 1, np.nan, 0]) + s = Series([True, True, False, False]) + s[::2] = np.nan + assert_series_equal(s, expected) + + expected = Series([np.nan, np.nan, np.nan, np.nan, np.nan, 5, 6, 7, 8, + 9]) + s = Series(np.arange(10)) + s[:5] = np.nan + assert_series_equal(s, expected) + + +def test_timedelta_assignment(): + # GH 8209 + s = Series([]) + s.loc['B'] = timedelta(1) + tm.assert_series_equal(s, Series(Timedelta('1 days'), index=['B'])) + + s = s.reindex(s.index.insert(0, 'A')) + tm.assert_series_equal(s, Series( + [np.nan, Timedelta('1 days')], index=['A', 'B'])) + + result = s.fillna(timedelta(1)) + expected = Series(Timedelta('1 days'), index=['A', 'B']) + tm.assert_series_equal(result, expected) + + s.loc['A'] = timedelta(1) + tm.assert_series_equal(s, expected) + + # GH 14155 + s = Series(10 * [np.timedelta64(10, 'm')]) + s.loc[[1, 2, 3]] = np.timedelta64(20, 'm') + expected = pd.Series(10 * [np.timedelta64(10, 'm')]) + expected.loc[[1, 2, 3]] = pd.Timedelta(np.timedelta64(20, 'm')) + tm.assert_series_equal(s, expected) + + +def test_underlying_data_conversion(): + # GH 4080 + df = DataFrame({c: [1, 2, 3] for c in ['a', 'b', 'c']}) + df.set_index(['a', 'b', 'c'], inplace=True) + s = Series([1], index=[(2, 2, 2)]) + df['val'] = 0 + df + df['val'].update(s) + + expected = DataFrame( + dict(a=[1, 2, 3], b=[1, 2, 3], c=[1, 2, 3], val=[0, 1, 0])) + expected.set_index(['a', 'b', 'c'], inplace=True) + tm.assert_frame_equal(df, expected) + + # GH 3970 + # these are chained assignments as well + pd.set_option('chained_assignment', None) + df = DataFrame({"aa": range(5), "bb": [2.2] * 5}) + df["cc"] = 0.0 + + ck = [True] * len(df) + + df["bb"].iloc[0] = .13 + + # TODO: unused + df_tmp = df.iloc[ck] # noqa + + df["bb"].iloc[0] = .15 + assert df['bb'].iloc[0] == 0.15 + pd.set_option('chained_assignment', 'raise') + + # GH 3217 + df = DataFrame(dict(a=[1, 3], b=[np.nan, 2])) + df['c'] = np.nan + df['c'].update(pd.Series(['foo'], index=[0])) + + expected = DataFrame(dict(a=[1, 3], b=[np.nan, 2], c=['foo', np.nan])) + tm.assert_frame_equal(df, expected) + + +def test_preserve_refs(test_data): + seq = test_data.ts[[5, 10, 15]] + seq[1] = np.NaN + assert not np.isnan(test_data.ts[10]) + + +def test_cast_on_putmask(): + # GH 2746 + + # need to upcast + s = Series([1, 2], index=[1, 2], dtype='int64') + s[[True, False]] = Series([0], index=[1], dtype='int64') + expected = Series([0, 2], index=[1, 2], dtype='int64') + + assert_series_equal(s, expected) + + +def test_type_promote_putmask(): + # GH8387: test that changing types does not break alignment + ts = Series(np.random.randn(100), index=np.arange(100, 0, -1)).round(5) + left, mask = ts.copy(), ts > 0 + right = ts[mask].copy().map(str) + left[mask] = right + assert_series_equal(left, ts.map(lambda t: str(t) if t > 0 else t)) + + s = Series([0, 1, 2, 0]) + mask = s > 0 + s2 = s[mask].map(str) + s[mask] = s2 + assert_series_equal(s, Series([0, '1', '2', 0])) + + s = Series([0, 'foo', 'bar', 0]) + mask = Series([False, True, True, False]) + s2 = s[mask] + s[mask] = s2 + assert_series_equal(s, Series([0, 'foo', 'bar', 0])) + + +def test_multilevel_preserve_name(): + index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', + 'three']], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + names=['first', 'second']) + s = Series(np.random.randn(len(index)), index=index, name='sth') + + result = s['foo'] + result2 = s.loc['foo'] + assert result.name == s.name + assert result2.name == s.name + + +def test_setitem_scalar_into_readonly_backing_data(): + # GH14359: test that you cannot mutate a read only buffer + + array = np.zeros(5) + array.flags.writeable = False # make the array immutable + series = Series(array) + + for n in range(len(series)): + msg = "assignment destination is read-only" + with pytest.raises(ValueError, match=msg): + series[n] = 1 + + assert array[n] == 0 + + +def test_setitem_slice_into_readonly_backing_data(): + # GH14359: test that you cannot mutate a read only buffer + + array = np.zeros(5) + array.flags.writeable = False # make the array immutable + series = Series(array) + + msg = "assignment destination is read-only" + with pytest.raises(ValueError, match=msg): + series[1:3] = 1 + + assert not array.any() + + +""" +miscellaneous methods +""" + + +def test_select(test_data): + # deprecated: gh-12410 + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + n = len(test_data.ts) + result = test_data.ts.select(lambda x: x >= test_data.ts.index[n // 2]) + expected = test_data.ts.reindex(test_data.ts.index[n // 2:]) + assert_series_equal(result, expected) + + result = test_data.ts.select(lambda x: x.weekday() == 2) + expected = test_data.ts[test_data.ts.index.weekday == 2] + assert_series_equal(result, expected) + + +def test_pop(): + # GH 6600 + df = DataFrame({'A': 0, 'B': np.arange(5, dtype='int64'), 'C': 0, }) + k = df.iloc[4] + + result = k.pop('B') + assert result == 4 + + expected = Series([0, 0], index=['A', 'C'], name=4) + assert_series_equal(k, expected) + + +def test_take(): + s = Series([-1, 5, 6, 2, 4]) + + actual = s.take([1, 3, 4]) + expected = Series([5, 2, 4], index=[1, 3, 4]) + tm.assert_series_equal(actual, expected) + + actual = s.take([-1, 3, 4]) + expected = Series([4, 2, 4], index=[4, 3, 4]) + tm.assert_series_equal(actual, expected) + + msg = "index {} is out of bounds for size 5" + with pytest.raises(IndexError, match=msg.format(10)): + s.take([1, 10]) + with pytest.raises(IndexError, match=msg.format(5)): + s.take([2, 5]) + + with tm.assert_produces_warning(FutureWarning): + s.take([-1, 3, 4], convert=False) + + +def test_take_categorical(): + # https://github.com/pandas-dev/pandas/issues/20664 + s = Series(pd.Categorical(['a', 'b', 'c'])) + result = s.take([-2, -2, 0]) + expected = Series(pd.Categorical(['b', 'b', 'a'], + categories=['a', 'b', 'c']), + index=[1, 1, 0]) + assert_series_equal(result, expected) + + +def test_head_tail(test_data): + assert_series_equal(test_data.series.head(), test_data.series[:5]) + assert_series_equal(test_data.series.head(0), test_data.series[0:0]) + assert_series_equal(test_data.series.tail(), test_data.series[-5:]) + assert_series_equal(test_data.series.tail(0), test_data.series[0:0]) diff --git a/pandas/tests/series/indexing/test_loc.py b/pandas/tests/series/indexing/test_loc.py new file mode 100644 index 0000000000000..8c1709ff016b3 --- /dev/null +++ b/pandas/tests/series/indexing/test_loc.py @@ -0,0 +1,168 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +import numpy as np +import pytest + +from pandas.compat import lrange + +import pandas as pd +from pandas import Series, Timestamp +from pandas.util.testing import assert_series_equal + + +@pytest.mark.parametrize("val,expected", [ + (2**63 - 1, 3), + (2**63, 4), +]) +def test_loc_uint64(val, expected): + # see gh-19399 + s = Series({2**63 - 1: 3, 2**63: 4}) + assert s.loc[val] == expected + + +def test_loc_getitem(test_data): + inds = test_data.series.index[[3, 4, 7]] + assert_series_equal( + test_data.series.loc[inds], + test_data.series.reindex(inds)) + assert_series_equal(test_data.series.iloc[5::2], test_data.series[5::2]) + + # slice with indices + d1, d2 = test_data.ts.index[[5, 15]] + result = test_data.ts.loc[d1:d2] + expected = test_data.ts.truncate(d1, d2) + assert_series_equal(result, expected) + + # boolean + mask = test_data.series > test_data.series.median() + assert_series_equal(test_data.series.loc[mask], test_data.series[mask]) + + # ask for index value + assert test_data.ts.loc[d1] == test_data.ts[d1] + assert test_data.ts.loc[d2] == test_data.ts[d2] + + +def test_loc_getitem_not_monotonic(test_data): + d1, d2 = test_data.ts.index[[5, 15]] + + ts2 = test_data.ts[::2][[1, 2, 0]] + + msg = r"Timestamp\('2000-01-10 00:00:00'\)" + with pytest.raises(KeyError, match=msg): + ts2.loc[d1:d2] + with pytest.raises(KeyError, match=msg): + ts2.loc[d1:d2] = 0 + + +def test_loc_getitem_setitem_integer_slice_keyerrors(): + s = Series(np.random.randn(10), index=lrange(0, 20, 2)) + + # this is OK + cp = s.copy() + cp.iloc[4:10] = 0 + assert (cp.iloc[4:10] == 0).all() + + # so is this + cp = s.copy() + cp.iloc[3:11] = 0 + assert (cp.iloc[3:11] == 0).values.all() + + result = s.iloc[2:6] + result2 = s.loc[3:11] + expected = s.reindex([4, 6, 8, 10]) + + assert_series_equal(result, expected) + assert_series_equal(result2, expected) + + # non-monotonic, raise KeyError + s2 = s.iloc[lrange(5) + lrange(5, 10)[::-1]] + with pytest.raises(KeyError, match=r"^3L?$"): + s2.loc[3:11] + with pytest.raises(KeyError, match=r"^3L?$"): + s2.loc[3:11] = 0 + + +def test_loc_getitem_iterator(test_data): + idx = iter(test_data.series.index[:10]) + result = test_data.series.loc[idx] + assert_series_equal(result, test_data.series[:10]) + + +def test_loc_setitem_boolean(test_data): + mask = test_data.series > test_data.series.median() + + result = test_data.series.copy() + result.loc[mask] = 0 + expected = test_data.series + expected[mask] = 0 + assert_series_equal(result, expected) + + +def test_loc_setitem_corner(test_data): + inds = list(test_data.series.index[[5, 8, 12]]) + test_data.series.loc[inds] = 5 + msg = r"\['foo'\] not in index" + with pytest.raises(KeyError, match=msg): + test_data.series.loc[inds + ['foo']] = 5 + + +def test_basic_setitem_with_labels(test_data): + indices = test_data.ts.index[[5, 10, 15]] + + cp = test_data.ts.copy() + exp = test_data.ts.copy() + cp[indices] = 0 + exp.loc[indices] = 0 + assert_series_equal(cp, exp) + + cp = test_data.ts.copy() + exp = test_data.ts.copy() + cp[indices[0]:indices[2]] = 0 + exp.loc[indices[0]:indices[2]] = 0 + assert_series_equal(cp, exp) + + # integer indexes, be careful + s = Series(np.random.randn(10), index=lrange(0, 20, 2)) + inds = [0, 4, 6] + arr_inds = np.array([0, 4, 6]) + + cp = s.copy() + exp = s.copy() + s[inds] = 0 + s.loc[inds] = 0 + assert_series_equal(cp, exp) + + cp = s.copy() + exp = s.copy() + s[arr_inds] = 0 + s.loc[arr_inds] = 0 + assert_series_equal(cp, exp) + + inds_notfound = [0, 4, 5, 6] + arr_inds_notfound = np.array([0, 4, 5, 6]) + msg = r"\[5\] not contained in the index" + with pytest.raises(ValueError, match=msg): + s[inds_notfound] = 0 + with pytest.raises(Exception, match=msg): + s[arr_inds_notfound] = 0 + + # GH12089 + # with tz for values + s = Series(pd.date_range("2011-01-01", periods=3, tz="US/Eastern"), + index=['a', 'b', 'c']) + s2 = s.copy() + expected = Timestamp('2011-01-03', tz='US/Eastern') + s2.loc['a'] = expected + result = s2.loc['a'] + assert result == expected + + s2 = s.copy() + s2.iloc[0] = expected + result = s2.iloc[0] + assert result == expected + + s2 = s.copy() + s2['a'] = expected + result = s2['a'] + assert result == expected diff --git a/pandas/tests/series/indexing/test_numeric.py b/pandas/tests/series/indexing/test_numeric.py new file mode 100644 index 0000000000000..e4afb0e456706 --- /dev/null +++ b/pandas/tests/series/indexing/test_numeric.py @@ -0,0 +1,259 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 + +import numpy as np +import pytest + +from pandas.compat import lrange, range + +import pandas as pd +from pandas import DataFrame, Index, Series +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal + + +def test_get(): + # GH 6383 + s = Series(np.array([43, 48, 60, 48, 50, 51, 50, 45, 57, 48, 56, 45, + 51, 39, 55, 43, 54, 52, 51, 54])) + + result = s.get(25, 0) + expected = 0 + assert result == expected + + s = Series(np.array([43, 48, 60, 48, 50, 51, 50, 45, 57, 48, 56, + 45, 51, 39, 55, 43, 54, 52, 51, 54]), + index=pd.Float64Index( + [25.0, 36.0, 49.0, 64.0, 81.0, 100.0, + 121.0, 144.0, 169.0, 196.0, 1225.0, + 1296.0, 1369.0, 1444.0, 1521.0, 1600.0, + 1681.0, 1764.0, 1849.0, 1936.0], + dtype='object')) + + result = s.get(25, 0) + expected = 43 + assert result == expected + + # GH 7407 + # with a boolean accessor + df = pd.DataFrame({'i': [0] * 3, 'b': [False] * 3}) + vc = df.i.value_counts() + result = vc.get(99, default='Missing') + assert result == 'Missing' + + vc = df.b.value_counts() + result = vc.get(False, default='Missing') + assert result == 3 + + result = vc.get(True, default='Missing') + assert result == 'Missing' + + +def test_get_nan(): + # GH 8569 + s = pd.Float64Index(range(10)).to_series() + assert s.get(np.nan) is None + assert s.get(np.nan, default='Missing') == 'Missing' + + +def test_get_nan_multiple(): + # GH 8569 + # ensure that fixing "test_get_nan" above hasn't broken get + # with multiple elements + s = pd.Float64Index(range(10)).to_series() + + idx = [2, 30] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + assert_series_equal(s.get(idx), + Series([2, np.nan], index=idx)) + + idx = [2, np.nan] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + assert_series_equal(s.get(idx), + Series([2, np.nan], index=idx)) + + # GH 17295 - all missing keys + idx = [20, 30] + assert(s.get(idx) is None) + + idx = [np.nan, np.nan] + assert(s.get(idx) is None) + + +def test_delitem(): + # GH 5542 + # should delete the item inplace + s = Series(lrange(5)) + del s[0] + + expected = Series(lrange(1, 5), index=lrange(1, 5)) + assert_series_equal(s, expected) + + del s[1] + expected = Series(lrange(2, 5), index=lrange(2, 5)) + assert_series_equal(s, expected) + + # empty + s = Series() + + with pytest.raises(KeyError, match=r"^0$"): + del s[0] + + # only 1 left, del, add, del + s = Series(1) + del s[0] + assert_series_equal(s, Series(dtype='int64', index=Index( + [], dtype='int64'))) + s[0] = 1 + assert_series_equal(s, Series(1)) + del s[0] + assert_series_equal(s, Series(dtype='int64', index=Index( + [], dtype='int64'))) + + # Index(dtype=object) + s = Series(1, index=['a']) + del s['a'] + assert_series_equal(s, Series(dtype='int64', index=Index( + [], dtype='object'))) + s['a'] = 1 + assert_series_equal(s, Series(1, index=['a'])) + del s['a'] + assert_series_equal(s, Series(dtype='int64', index=Index( + [], dtype='object'))) + + +def test_slice_float64(): + values = np.arange(10., 50., 2) + index = Index(values) + + start, end = values[[5, 15]] + + s = Series(np.random.randn(20), index=index) + + result = s[start:end] + expected = s.iloc[5:16] + assert_series_equal(result, expected) + + result = s.loc[start:end] + assert_series_equal(result, expected) + + df = DataFrame(np.random.randn(20, 3), index=index) + + result = df[start:end] + expected = df.iloc[5:16] + tm.assert_frame_equal(result, expected) + + result = df.loc[start:end] + tm.assert_frame_equal(result, expected) + + +def test_getitem_negative_out_of_bounds(): + s = Series(tm.rands_array(5, 10), index=tm.rands_array(10, 10)) + + msg = "index out of bounds" + with pytest.raises(IndexError, match=msg): + s[-11] + msg = "index -11 is out of bounds for axis 0 with size 10" + with pytest.raises(IndexError, match=msg): + s[-11] = 'foo' + + +def test_getitem_regression(): + s = Series(lrange(5), index=lrange(5)) + result = s[lrange(5)] + assert_series_equal(result, s) + + +def test_getitem_setitem_slice_bug(): + s = Series(lrange(10), lrange(10)) + result = s[-12:] + assert_series_equal(result, s) + + result = s[-7:] + assert_series_equal(result, s[3:]) + + result = s[:-12] + assert_series_equal(result, s[:0]) + + s = Series(lrange(10), lrange(10)) + s[-12:] = 0 + assert (s == 0).all() + + s[:-12] = 5 + assert (s == 0).all() + + +def test_getitem_setitem_slice_integers(): + s = Series(np.random.randn(8), index=[2, 4, 6, 8, 10, 12, 14, 16]) + + result = s[:4] + expected = s.reindex([2, 4, 6, 8]) + assert_series_equal(result, expected) + + s[:4] = 0 + assert (s[:4] == 0).all() + assert not (s[4:] == 0).any() + + +def test_setitem_float_labels(): + # note labels are floats + s = Series(['a', 'b', 'c'], index=[0, 0.5, 1]) + tmp = s.copy() + + s.loc[1] = 'zoo' + tmp.iloc[2] = 'zoo' + + assert_series_equal(s, tmp) + + +def test_slice_float_get_set(test_data): + msg = (r"cannot do slice indexing on with these indexers \[{key}\]" + r" of <(class|type) 'float'>") + with pytest.raises(TypeError, match=msg.format(key=r"4\.0")): + test_data.ts[4.0:10.0] + + with pytest.raises(TypeError, match=msg.format(key=r"4\.0")): + test_data.ts[4.0:10.0] = 0 + + with pytest.raises(TypeError, match=msg.format(key=r"4\.5")): + test_data.ts[4.5:10.0] + with pytest.raises(TypeError, match=msg.format(key=r"4\.5")): + test_data.ts[4.5:10.0] = 0 + + +def test_slice_floats2(): + s = Series(np.random.rand(10), index=np.arange(10, 20, dtype=float)) + + assert len(s.loc[12.0:]) == 8 + assert len(s.loc[12.5:]) == 7 + + i = np.arange(10, 20, dtype=float) + i[2] = 12.2 + s.index = i + assert len(s.loc[12.0:]) == 8 + assert len(s.loc[12.5:]) == 7 + + +def test_int_indexing(): + s = Series(np.random.randn(6), index=[0, 0, 1, 1, 2, 2]) + + with pytest.raises(KeyError, match=r"^5$"): + s[5] + + with pytest.raises(KeyError, match=r"^'c'$"): + s['c'] + + # not monotonic + s = Series(np.random.randn(6), index=[2, 2, 0, 0, 1, 1]) + + with pytest.raises(KeyError, match=r"^5$"): + s[5] + + with pytest.raises(KeyError, match=r"^'c'$"): + s['c'] + + +def test_getitem_int64(test_data): + idx = np.int64(5) + assert test_data.ts[idx] == test_data.ts[5] diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py index 5997b91097cbc..73adc7d4bf82f 100644 --- a/pandas/tests/series/test_alter_axes.py +++ b/pandas/tests/series/test_alter_axes.py @@ -4,56 +4,57 @@ from datetime import datetime import numpy as np -import pandas as pd - -from pandas import Index, Series -from pandas.core.index import MultiIndex, RangeIndex +import pytest from pandas.compat import lrange, range, zip -from pandas.util.testing import assert_series_equal, assert_frame_equal -import pandas.util.testing as tm -from .common import TestData +from pandas import DataFrame, Index, MultiIndex, RangeIndex, Series +import pandas.util.testing as tm -class TestSeriesAlterAxes(TestData, tm.TestCase): +class TestSeriesAlterAxes(object): - def test_setindex(self): + def test_setindex(self, string_series): # wrong type - series = self.series.copy() - self.assertRaises(TypeError, setattr, series, 'index', None) + msg = (r"Index\(\.\.\.\) must be called with a collection of some" + r" kind, None was passed") + with pytest.raises(TypeError, match=msg): + string_series.index = None # wrong length - series = self.series.copy() - self.assertRaises(Exception, setattr, series, 'index', - np.arange(len(series) - 1)) + msg = ("Length mismatch: Expected axis has 30 elements, new" + " values have 29 elements") + with pytest.raises(ValueError, match=msg): + string_series.index = np.arange(len(string_series) - 1) # works - series = self.series.copy() - series.index = np.arange(len(series)) - tm.assertIsInstance(series.index, Index) + string_series.index = np.arange(len(string_series)) + assert isinstance(string_series.index, Index) - def test_rename(self): + # Renaming + + def test_rename(self, datetime_series): + ts = datetime_series renamer = lambda x: x.strftime('%Y%m%d') - renamed = self.ts.rename(renamer) - self.assertEqual(renamed.index[0], renamer(self.ts.index[0])) + renamed = ts.rename(renamer) + assert renamed.index[0] == renamer(ts.index[0]) # dict - rename_dict = dict(zip(self.ts.index, renamed.index)) - renamed2 = self.ts.rename(rename_dict) - assert_series_equal(renamed, renamed2) + rename_dict = dict(zip(ts.index, renamed.index)) + renamed2 = ts.rename(rename_dict) + tm.assert_series_equal(renamed, renamed2) # partial dict s = Series(np.arange(4), index=['a', 'b', 'c', 'd'], dtype='int64') renamed = s.rename({'b': 'foo', 'd': 'bar'}) - self.assert_index_equal(renamed.index, Index(['a', 'foo', 'c', 'bar'])) + tm.assert_index_equal(renamed.index, Index(['a', 'foo', 'c', 'bar'])) # index with name renamer = Series(np.arange(4), index=Index(['a', 'b', 'c', 'd'], name='name'), dtype='int64') renamed = renamer.rename({}) - self.assertEqual(renamed.index.name, renamer.index.name) + assert renamed.index.name == renamer.index.name def test_rename_by_series(self): s = Series(range(5), name='foo') @@ -66,48 +67,56 @@ def test_rename_set_name(self): s = Series(range(4), index=list('abcd')) for name in ['foo', 123, 123., datetime(2001, 11, 11), ('foo',)]: result = s.rename(name) - self.assertEqual(result.name, name) - self.assert_numpy_array_equal(result.index.values, s.index.values) - self.assertTrue(s.name is None) + assert result.name == name + tm.assert_numpy_array_equal(result.index.values, s.index.values) + assert s.name is None def test_rename_set_name_inplace(self): s = Series(range(3), index=list('abc')) for name in ['foo', 123, 123., datetime(2001, 11, 11), ('foo',)]: s.rename(name, inplace=True) - self.assertEqual(s.name, name) + assert s.name == name exp = np.array(['a', 'b', 'c'], dtype=np.object_) - self.assert_numpy_array_equal(s.index.values, exp) + tm.assert_numpy_array_equal(s.index.values, exp) + + def test_rename_axis_supported(self): + # Supporting axis for compatibility, detailed in GH-18589 + s = Series(range(5)) + s.rename({}, axis=0) + s.rename({}, axis='index') + with pytest.raises(ValueError, match='No axis named 5'): + s.rename({}, axis=5) def test_set_name_attribute(self): s = Series([1, 2, 3]) s2 = Series([1, 2, 3], name='bar') for name in [7, 7., 'name', datetime(2001, 1, 1), (1,), u"\u05D0"]: s.name = name - self.assertEqual(s.name, name) + assert s.name == name s2.name = name - self.assertEqual(s2.name, name) + assert s2.name == name def test_set_name(self): s = Series([1, 2, 3]) s2 = s._set_name('foo') - self.assertEqual(s2.name, 'foo') - self.assertTrue(s.name is None) - self.assertTrue(s is not s2) + assert s2.name == 'foo' + assert s.name is None + assert s is not s2 - def test_rename_inplace(self): + def test_rename_inplace(self, datetime_series): renamer = lambda x: x.strftime('%Y%m%d') - expected = renamer(self.ts.index[0]) + expected = renamer(datetime_series.index[0]) - self.ts.rename(renamer, inplace=True) - self.assertEqual(self.ts.index[0], expected) + datetime_series.rename(renamer, inplace=True) + assert datetime_series.index[0] == expected def test_set_index_makes_timeseries(self): idx = tm.makeDateIndex(10) s = Series(lrange(10)) s.index = idx - self.assertTrue(s.index.is_all_dates) + assert s.index.is_all_dates def test_reset_index(self): df = tm.makeDataFrame()[:5] @@ -116,70 +125,223 @@ def test_reset_index(self): ser.name = 'value' df = ser.reset_index() - self.assertIn('value', df) + assert 'value' in df df = ser.reset_index(name='value2') - self.assertIn('value2', df) + assert 'value2' in df # check inplace s = ser.reset_index(drop=True) s2 = ser s2.reset_index(drop=True, inplace=True) - assert_series_equal(s, s2) + tm.assert_series_equal(s, s2) # level index = MultiIndex(levels=[['bar'], ['one', 'two', 'three'], [0, 1]], - labels=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], - [0, 1, 0, 1, 0, 1]]) + codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], + [0, 1, 0, 1, 0, 1]]) s = Series(np.random.randn(6), index=index) rs = s.reset_index(level=1) - self.assertEqual(len(rs.columns), 2) + assert len(rs.columns) == 2 rs = s.reset_index(level=[0, 2], drop=True) - self.assert_index_equal(rs.index, Index(index.get_level_values(1))) - tm.assertIsInstance(rs, Series) + tm.assert_index_equal(rs.index, Index(index.get_level_values(1))) + assert isinstance(rs, Series) + + def test_reset_index_name(self): + s = Series([1, 2, 3], index=Index(range(3), name='x')) + assert s.reset_index().index.name is None + assert s.reset_index(drop=True).index.name is None + + def test_reset_index_level(self): + df = DataFrame([[1, 2, 3], [4, 5, 6]], + columns=['A', 'B', 'C']) + + for levels in ['A', 'B'], [0, 1]: + # With MultiIndex + s = df.set_index(['A', 'B'])['C'] + + result = s.reset_index(level=levels[0]) + tm.assert_frame_equal(result, df.set_index('B')) + + result = s.reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df.set_index('B')) + + result = s.reset_index(level=levels) + tm.assert_frame_equal(result, df) + + result = df.set_index(['A', 'B']).reset_index(level=levels, + drop=True) + tm.assert_frame_equal(result, df[['C']]) + + with pytest.raises(KeyError, match='Level E '): + s.reset_index(level=['A', 'E']) + + # With single-level Index + s = df.set_index('A')['B'] + + result = s.reset_index(level=levels[0]) + tm.assert_frame_equal(result, df[['A', 'B']]) + + result = s.reset_index(level=levels[:1]) + tm.assert_frame_equal(result, df[['A', 'B']]) + + result = s.reset_index(level=levels[0], drop=True) + tm.assert_series_equal(result, df['B']) + + with pytest.raises(IndexError, match='Too many levels'): + s.reset_index(level=[0, 1, 2]) + + # Check that .reset_index([],drop=True) doesn't fail + result = Series(range(4)).reset_index([], drop=True) + expected = Series(range(4)) + tm.assert_series_equal(result, expected) def test_reset_index_range(self): # GH 12071 - s = pd.Series(range(2), name='A', dtype='int64') + s = Series(range(2), name='A', dtype='int64') series_result = s.reset_index() - tm.assertIsInstance(series_result.index, RangeIndex) - series_expected = pd.DataFrame([[0, 0], [1, 1]], - columns=['index', 'A'], - index=RangeIndex(stop=2)) - assert_frame_equal(series_result, series_expected) + assert isinstance(series_result.index, RangeIndex) + series_expected = DataFrame([[0, 0], [1, 1]], + columns=['index', 'A'], + index=RangeIndex(stop=2)) + tm.assert_frame_equal(series_result, series_expected) def test_reorder_levels(self): index = MultiIndex(levels=[['bar'], ['one', 'two', 'three'], [0, 1]], - labels=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], - [0, 1, 0, 1, 0, 1]], + codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], + [0, 1, 0, 1, 0, 1]], names=['L0', 'L1', 'L2']) s = Series(np.arange(6), index=index) # no change, position result = s.reorder_levels([0, 1, 2]) - assert_series_equal(s, result) + tm.assert_series_equal(s, result) # no change, labels result = s.reorder_levels(['L0', 'L1', 'L2']) - assert_series_equal(s, result) + tm.assert_series_equal(s, result) # rotate, position result = s.reorder_levels([1, 2, 0]) e_idx = MultiIndex(levels=[['one', 'two', 'three'], [0, 1], ['bar']], - labels=[[0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1], - [0, 0, 0, 0, 0, 0]], + codes=[[0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1], + [0, 0, 0, 0, 0, 0]], names=['L1', 'L2', 'L0']) expected = Series(np.arange(6), index=e_idx) - assert_series_equal(result, expected) - - result = s.reorder_levels([0, 0, 0]) - e_idx = MultiIndex(levels=[['bar'], ['bar'], ['bar']], - labels=[[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0]], - names=['L0', 'L0', 'L0']) - expected = Series(range(6), index=e_idx) - assert_series_equal(result, expected) - - result = s.reorder_levels(['L0', 'L0', 'L0']) - assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) + + def test_rename_axis_mapper(self): + # GH 19978 + mi = MultiIndex.from_product([['a', 'b', 'c'], [1, 2]], + names=['ll', 'nn']) + s = Series([i for i in range(len(mi))], index=mi) + + result = s.rename_axis(index={'ll': 'foo'}) + assert result.index.names == ['foo', 'nn'] + + result = s.rename_axis(index=str.upper, axis=0) + assert result.index.names == ['LL', 'NN'] + + result = s.rename_axis(index=['foo', 'goo']) + assert result.index.names == ['foo', 'goo'] + + with pytest.raises(TypeError, match='unexpected'): + s.rename_axis(columns='wrong') + + def test_rename_axis_inplace(self, datetime_series): + # GH 15704 + expected = datetime_series.rename_axis('foo') + result = datetime_series + no_return = result.rename_axis('foo', inplace=True) + + assert no_return is None + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('kwargs', [{'mapper': None}, {'index': None}, {}]) + def test_rename_axis_none(self, kwargs): + # GH 25034 + index = Index(list('abc'), name='foo') + df = Series([1, 2, 3], index=index) + + result = df.rename_axis(**kwargs) + expected_index = index.rename(None) if kwargs else index + expected = Series([1, 2, 3], index=expected_index) + tm.assert_series_equal(result, expected) + + def test_set_axis_inplace_axes(self, axis_series): + # GH14636 + ser = Series(np.arange(4), index=[1, 3, 5, 7], dtype='int64') + + expected = ser.copy() + expected.index = list('abcd') + + # inplace=True + # The FutureWarning comes from the fact that we would like to have + # inplace default to False some day + for inplace, warn in [(None, FutureWarning), (True, None)]: + result = ser.copy() + kwargs = {'inplace': inplace} + with tm.assert_produces_warning(warn): + result.set_axis(list('abcd'), axis=axis_series, **kwargs) + tm.assert_series_equal(result, expected) + + def test_set_axis_inplace(self): + # GH14636 + + s = Series(np.arange(4), index=[1, 3, 5, 7], dtype='int64') + + expected = s.copy() + expected.index = list('abcd') + + # inplace=False + result = s.set_axis(list('abcd'), axis=0, inplace=False) + tm.assert_series_equal(expected, result) + + # omitting the "axis" parameter + with tm.assert_produces_warning(None): + result = s.set_axis(list('abcd'), inplace=False) + tm.assert_series_equal(result, expected) + + # wrong values for the "axis" parameter + for axis in [2, 'foo']: + with pytest.raises(ValueError, match='No axis named'): + s.set_axis(list('abcd'), axis=axis, inplace=False) + + def test_set_axis_prior_to_deprecation_signature(self): + s = Series(np.arange(4), index=[1, 3, 5, 7], dtype='int64') + + expected = s.copy() + expected.index = list('abcd') + + for axis in [0, 'index']: + with tm.assert_produces_warning(FutureWarning): + result = s.set_axis(0, list('abcd'), inplace=False) + tm.assert_series_equal(result, expected) + + def test_reset_index_drop_errors(self): + # GH 20925 + + # KeyError raised for series index when passed level name is missing + s = Series(range(4)) + with pytest.raises(KeyError, match='must be same as name'): + s.reset_index('wrong', drop=True) + with pytest.raises(KeyError, match='must be same as name'): + s.reset_index('wrong') + + # KeyError raised for series when level to be dropped is missing + s = Series(range(4), index=MultiIndex.from_product([[1, 2]] * 2)) + with pytest.raises(KeyError, match='not found'): + s.reset_index('wrong', drop=True) + + def test_droplevel(self): + # GH20342 + ser = Series([1, 2, 3, 4]) + ser.index = MultiIndex.from_arrays([(1, 2, 3, 4), (5, 6, 7, 8)], + names=['a', 'b']) + expected = ser.reset_index('b', drop=True) + result = ser.droplevel('b', axis='index') + tm.assert_series_equal(result, expected) + # test that droplevel raises ValueError on axis != 0 + with pytest.raises(ValueError): + ser.droplevel(1, axis='columns') diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index b747a680c17dd..d7d9c526503cb 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -1,317 +1,31 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -from itertools import product from distutils.version import LooseVersion +from itertools import product +import operator +import numpy as np +from numpy import nan import pytest -from numpy import nan -import numpy as np -import pandas as pd +from pandas.compat import PY2, PY35, is_platform_windows, lrange, range +import pandas.util._test_decorators as td -from pandas import (Series, Categorical, DataFrame, isnull, notnull, - bdate_range, date_range, _np_version_under1p10) +import pandas as pd +from pandas import ( + Categorical, CategoricalIndex, DataFrame, Series, compat, date_range, isna, + notna) +from pandas.api.types import is_scalar from pandas.core.index import MultiIndex -from pandas.tseries.index import Timestamp -from pandas.tseries.tdi import Timedelta -import pandas.core.config as cf - -import pandas.core.nanops as nanops - -from pandas.compat import lrange, range -from pandas import compat -from pandas.util.testing import (assert_series_equal, assert_almost_equal, - assert_frame_equal, assert_index_equal) +from pandas.core.indexes.datetimes import Timestamp import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_index_equal, + assert_series_equal) -from .common import TestData - - -class TestSeriesAnalytics(TestData, tm.TestCase): - - def test_sum_zero(self): - arr = np.array([]) - self.assertEqual(nanops.nansum(arr), 0) - - arr = np.empty((10, 0)) - self.assertTrue((nanops.nansum(arr, axis=1) == 0).all()) - - # GH #844 - s = Series([], index=[]) - self.assertEqual(s.sum(), 0) - - df = DataFrame(np.empty((10, 0))) - self.assertTrue((df.sum(1) == 0).all()) - - def test_nansum_buglet(self): - s = Series([1.0, np.nan], index=[0, 1]) - result = np.nansum(s) - assert_almost_equal(result, 1) - - def test_overflow(self): - # GH 6915 - # overflowing on the smaller int dtypes - for dtype in ['int32', 'int64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # no bottleneck - result = s.sum(skipna=False) - self.assertEqual(int(result), v.sum(dtype='int64')) - result = s.min(skipna=False) - self.assertEqual(int(result), 0) - result = s.max(skipna=False) - self.assertEqual(int(result), v[-1]) - - # use bottleneck if available - result = s.sum() - self.assertEqual(int(result), v.sum(dtype='int64')) - result = s.min() - self.assertEqual(int(result), 0) - result = s.max() - self.assertEqual(int(result), v[-1]) - - for dtype in ['float32', 'float64']: - v = np.arange(5000000, dtype=dtype) - s = Series(v) - - # no bottleneck - result = s.sum(skipna=False) - self.assertEqual(result, v.sum(dtype=dtype)) - result = s.min(skipna=False) - self.assertTrue(np.allclose(float(result), 0.0)) - result = s.max(skipna=False) - self.assertTrue(np.allclose(float(result), v[-1])) - - # use bottleneck if available - result = s.sum() - self.assertEqual(result, v.sum(dtype=dtype)) - result = s.min() - self.assertTrue(np.allclose(float(result), 0.0)) - result = s.max() - self.assertTrue(np.allclose(float(result), v[-1])) - - def test_sum(self): - self._check_stat_op('sum', np.sum, check_allna=True) - - def test_sum_inf(self): - import pandas.core.nanops as nanops - - s = Series(np.random.randn(10)) - s2 = s.copy() - - s[5:8] = np.inf - s2[5:8] = np.nan - - self.assertTrue(np.isinf(s.sum())) - - arr = np.random.randn(100, 100).astype('f4') - arr[:, 2] = np.inf - - with cf.option_context("mode.use_inf_as_null", True): - assert_almost_equal(s.sum(), s2.sum()) - - res = nanops.nansum(arr, axis=1) - self.assertTrue(np.isinf(res).all()) - - def test_mean(self): - self._check_stat_op('mean', np.mean) - - def test_median(self): - self._check_stat_op('median', np.median) - - # test with integers, test failure - int_ts = Series(np.ones(10, dtype=int), index=lrange(10)) - self.assertAlmostEqual(np.median(int_ts), int_ts.median()) - - def test_mode(self): - # No mode should be found. - exp = Series([], dtype=np.float64) - tm.assert_series_equal(Series([]).mode(), exp) - - exp = Series([1], dtype=np.int64) - tm.assert_series_equal(Series([1]).mode(), exp) - - exp = Series(['a', 'b', 'c'], dtype=np.object) - tm.assert_series_equal(Series(['a', 'b', 'c']).mode(), exp) - - # Test numerical data types. - exp_single = [1] - data_single = [1] * 5 + [2] * 3 - - exp_multi = [1, 3] - data_multi = [1] * 5 + [2] * 3 + [3] * 5 - - for dt in np.typecodes['AllInteger'] + np.typecodes['Float']: - s = Series(data_single, dtype=dt) - exp = Series(exp_single, dtype=dt) - tm.assert_series_equal(s.mode(), exp) - - s = Series(data_multi, dtype=dt) - exp = Series(exp_multi, dtype=dt) - tm.assert_series_equal(s.mode(), exp) - - # Test string and object types. - exp = ['b'] - data = ['a'] * 2 + ['b'] * 3 - - s = Series(data, dtype='c') - exp = Series(exp, dtype='c') - tm.assert_series_equal(s.mode(), exp) - - exp = ['bar'] - data = ['foo'] * 2 + ['bar'] * 3 - - for dt in [str, object]: - s = Series(data, dtype=dt) - exp = Series(exp, dtype=dt) - tm.assert_series_equal(s.mode(), exp) - - # Test datetime types. - exp = Series(['1900-05-03', '2011-01-03', - '2013-01-02'], dtype='M8[ns]') - s = Series(['2011-01-03', '2013-01-02', - '1900-05-03'], dtype='M8[ns]') - tm.assert_series_equal(s.mode(), exp) - - exp = Series(['2011-01-03', '2013-01-02'], dtype='M8[ns]') - s = Series(['2011-01-03', '2013-01-02', '1900-05-03', - '2011-01-03', '2013-01-02'], dtype='M8[ns]') - tm.assert_series_equal(s.mode(), exp) - - # gh-5986: Test timedelta types. - exp = Series(['-1 days', '0 days', '1 days'], dtype='timedelta64[ns]') - s = Series(['1 days', '-1 days', '0 days'], - dtype='timedelta64[ns]') - tm.assert_series_equal(s.mode(), exp) - - exp = Series(['2 min', '1 day'], dtype='timedelta64[ns]') - s = Series(['1 day', '1 day', '-1 day', '-1 day 2 min', - '2 min', '2 min'], dtype='timedelta64[ns]') - tm.assert_series_equal(s.mode(), exp) - - # Test mixed dtype. - exp = Series(['foo']) - s = Series([1, 'foo', 'foo']) - tm.assert_series_equal(s.mode(), exp) - - # Test for uint64 overflow. - exp = Series([2**63], dtype=np.uint64) - s = Series([1, 2**63, 2**63], dtype=np.uint64) - tm.assert_series_equal(s.mode(), exp) - - exp = Series([1, 2**63], dtype=np.uint64) - s = Series([1, 2**63], dtype=np.uint64) - tm.assert_series_equal(s.mode(), exp) - - # Test category dtype. - c = Categorical([1, 2]) - exp = Categorical([1, 2], categories=[1, 2]) - exp = Series(exp, dtype='category') - tm.assert_series_equal(Series(c).mode(), exp) - - c = Categorical([1, 'a', 'a']) - exp = Categorical(['a'], categories=[1, 'a']) - exp = Series(exp, dtype='category') - tm.assert_series_equal(Series(c).mode(), exp) - - c = Categorical([1, 1, 2, 3, 3]) - exp = Categorical([1, 3], categories=[1, 2, 3]) - exp = Series(exp, dtype='category') - tm.assert_series_equal(Series(c).mode(), exp) - - def test_prod(self): - self._check_stat_op('prod', np.prod) - - def test_min(self): - self._check_stat_op('min', np.min, check_objects=True) - - def test_max(self): - self._check_stat_op('max', np.max, check_objects=True) - - def test_var_std(self): - alt = lambda x: np.std(x, ddof=1) - self._check_stat_op('std', alt) - - alt = lambda x: np.var(x, ddof=1) - self._check_stat_op('var', alt) - - result = self.ts.std(ddof=4) - expected = np.std(self.ts.values, ddof=4) - assert_almost_equal(result, expected) - - result = self.ts.var(ddof=4) - expected = np.var(self.ts.values, ddof=4) - assert_almost_equal(result, expected) - - # 1 - element series with ddof=1 - s = self.ts.iloc[[0]] - result = s.var(ddof=1) - self.assertTrue(isnull(result)) - - result = s.std(ddof=1) - self.assertTrue(isnull(result)) - def test_sem(self): - alt = lambda x: np.std(x, ddof=1) / np.sqrt(len(x)) - self._check_stat_op('sem', alt) - - result = self.ts.sem(ddof=4) - expected = np.std(self.ts.values, - ddof=4) / np.sqrt(len(self.ts.values)) - assert_almost_equal(result, expected) - - # 1 - element series with ddof=1 - s = self.ts.iloc[[0]] - result = s.sem(ddof=1) - self.assertTrue(isnull(result)) - - def test_skew(self): - tm._skip_if_no_scipy() - - from scipy.stats import skew - alt = lambda x: skew(x, bias=False) - self._check_stat_op('skew', alt) - - # test corner cases, skew() returns NaN unless there's at least 3 - # values - min_N = 3 - for i in range(1, min_N + 1): - s = Series(np.ones(i)) - df = DataFrame(np.ones((i, i))) - if i < min_N: - self.assertTrue(np.isnan(s.skew())) - self.assertTrue(np.isnan(df.skew()).all()) - else: - self.assertEqual(0, s.skew()) - self.assertTrue((df.skew() == 0).all()) - - def test_kurt(self): - tm._skip_if_no_scipy() - - from scipy.stats import kurtosis - alt = lambda x: kurtosis(x, bias=False) - self._check_stat_op('kurt', alt) - - index = MultiIndex(levels=[['bar'], ['one', 'two', 'three'], [0, 1]], - labels=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], - [0, 1, 0, 1, 0, 1]]) - s = Series(np.random.randn(6), index=index) - self.assertAlmostEqual(s.kurt(), s.kurt(level=0)['bar']) - - # test corner cases, kurt() returns NaN unless there's at least 4 - # values - min_N = 4 - for i in range(1, min_N + 1): - s = Series(np.ones(i)) - df = DataFrame(np.ones((i, i))) - if i < min_N: - self.assertTrue(np.isnan(s.kurt())) - self.assertTrue(np.isnan(df.kurt()).all()) - else: - self.assertEqual(0, s.kurt()) - self.assertTrue((df.kurt() == 0).all()) +class TestSeriesAnalytics(object): def test_describe(self): s = Series([0, 1, 2, 3, 4], name='int_data') @@ -320,31 +34,48 @@ def test_describe(self): name='int_data', index=['count', 'mean', 'std', 'min', '25%', '50%', '75%', 'max']) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) s = Series([True, True, False, False, False], name='bool_data') result = s.describe() expected = Series([5, 2, False, 3], name='bool_data', index=['count', 'unique', 'top', 'freq']) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) s = Series(['a', 'a', 'b', 'c', 'd'], name='str_data') result = s.describe() expected = Series([5, 4, 'a', 2], name='str_data', index=['count', 'unique', 'top', 'freq']) - self.assert_series_equal(result, expected) - - def test_argsort(self): - self._check_accum_op('argsort', check_dtype=False) - argsorted = self.ts.argsort() - self.assertTrue(issubclass(argsorted.dtype.type, np.integer)) + tm.assert_series_equal(result, expected) + + def test_describe_with_tz(self, tz_naive_fixture): + # GH 21332 + tz = tz_naive_fixture + name = str(tz_naive_fixture) + start = Timestamp(2018, 1, 1) + end = Timestamp(2018, 1, 5) + s = Series(date_range(start, end, tz=tz), name=name) + result = s.describe() + expected = Series( + [5, 5, s.value_counts().index[0], 1, start.tz_localize(tz), + end.tz_localize(tz) + ], + name=name, + index=['count', 'unique', 'top', 'freq', 'first', 'last'] + ) + tm.assert_series_equal(result, expected) + + def test_argsort(self, datetime_series): + self._check_accum_op('argsort', datetime_series, check_dtype=False) + argsorted = datetime_series.argsort() + assert issubclass(argsorted.dtype.type, np.integer) # GH 2967 (introduced bug in 0.11-dev I think) s = Series([Timestamp('201301%02d' % (i + 1)) for i in range(5)]) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' shifted = s.shift(-1) - self.assertEqual(shifted.dtype, 'datetime64[ns]') - self.assertTrue(isnull(shifted[4])) + assert shifted.dtype == 'datetime64[ns]' + assert isna(shifted[4]) result = s.argsort() expected = Series(lrange(5), dtype='int64') @@ -362,37 +93,42 @@ def test_argsort_stable(self): mexpected = np.argsort(s.values, kind='mergesort') qexpected = np.argsort(s.values, kind='quicksort') - self.assert_series_equal(mindexer, Series(mexpected), - check_dtype=False) - self.assert_series_equal(qindexer, Series(qexpected), - check_dtype=False) - self.assertFalse(np.array_equal(qindexer, mindexer)) - - def test_cumsum(self): - self._check_accum_op('cumsum') - - def test_cumprod(self): - self._check_accum_op('cumprod') - - def test_cummin(self): - self.assert_numpy_array_equal(self.ts.cummin().values, - np.minimum.accumulate(np.array(self.ts))) - ts = self.ts.copy() + tm.assert_series_equal(mindexer, Series(mexpected), + check_dtype=False) + tm.assert_series_equal(qindexer, Series(qexpected), + check_dtype=False) + msg = (r"ndarray Expected type <(class|type) 'numpy\.ndarray'>," + r" found instead") + with pytest.raises(AssertionError, match=msg): + tm.assert_numpy_array_equal(qindexer, mindexer) + + def test_cumsum(self, datetime_series): + self._check_accum_op('cumsum', datetime_series) + + def test_cumprod(self, datetime_series): + self._check_accum_op('cumprod', datetime_series) + + def test_cummin(self, datetime_series): + tm.assert_numpy_array_equal(datetime_series.cummin().values, + np.minimum + .accumulate(np.array(datetime_series))) + ts = datetime_series.copy() ts[::2] = np.NaN result = ts.cummin()[1::2] - expected = np.minimum.accumulate(ts.valid()) + expected = np.minimum.accumulate(ts.dropna()) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) - def test_cummax(self): - self.assert_numpy_array_equal(self.ts.cummax().values, - np.maximum.accumulate(np.array(self.ts))) - ts = self.ts.copy() + def test_cummax(self, datetime_series): + tm.assert_numpy_array_equal(datetime_series.cummax().values, + np.maximum + .accumulate(np.array(datetime_series))) + ts = datetime_series.copy() ts[::2] = np.NaN result = ts.cummax()[1::2] - expected = np.maximum.accumulate(ts.valid()) + expected = np.maximum.accumulate(ts.dropna()) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) def test_cummin_datetime64(self): s = pd.Series(pd.to_datetime(['NaT', '2000-1-2', 'NaT', '2000-1-1', @@ -401,13 +137,13 @@ def test_cummin_datetime64(self): expected = pd.Series(pd.to_datetime(['NaT', '2000-1-2', 'NaT', '2000-1-1', 'NaT', '2000-1-1'])) result = s.cummin(skipna=True) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) expected = pd.Series(pd.to_datetime( ['NaT', '2000-1-2', '2000-1-2', '2000-1-1', '2000-1-1', '2000-1-1' ])) result = s.cummin(skipna=False) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) def test_cummax_datetime64(self): s = pd.Series(pd.to_datetime(['NaT', '2000-1-2', 'NaT', '2000-1-1', @@ -416,13 +152,13 @@ def test_cummax_datetime64(self): expected = pd.Series(pd.to_datetime(['NaT', '2000-1-2', 'NaT', '2000-1-2', 'NaT', '2000-1-3'])) result = s.cummax(skipna=True) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) expected = pd.Series(pd.to_datetime( ['NaT', '2000-1-2', '2000-1-2', '2000-1-2', '2000-1-2', '2000-1-3' ])) result = s.cummax(skipna=False) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) def test_cummin_timedelta64(self): s = pd.Series(pd.to_timedelta(['NaT', @@ -439,7 +175,7 @@ def test_cummin_timedelta64(self): 'NaT', '1 min', ])) result = s.cummin(skipna=True) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) expected = pd.Series(pd.to_timedelta(['NaT', '2 min', @@ -448,7 +184,7 @@ def test_cummin_timedelta64(self): '1 min', '1 min', ])) result = s.cummin(skipna=False) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) def test_cummax_timedelta64(self): s = pd.Series(pd.to_timedelta(['NaT', @@ -465,7 +201,7 @@ def test_cummax_timedelta64(self): 'NaT', '3 min', ])) result = s.cummax(skipna=True) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) expected = pd.Series(pd.to_timedelta(['NaT', '2 min', @@ -474,7 +210,7 @@ def test_cummax_timedelta64(self): '2 min', '3 min', ])) result = s.cummax(skipna=False) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) def test_npdiff(self): pytest.skip("skipping due to Series no longer being an " @@ -486,97 +222,21 @@ def test_npdiff(self): r = np.diff(s) assert_series_equal(Series([nan, 0, 0, 0, nan]), r) - def _check_stat_op(self, name, alternate, check_objects=False, - check_allna=False): - import pandas.core.nanops as nanops - - def testit(): - f = getattr(Series, name) - - # add some NaNs - self.series[5:15] = np.NaN - - # idxmax, idxmin, min, and max are valid for dates - if name not in ['max', 'min']: - ds = Series(date_range('1/1/2001', periods=10)) - self.assertRaises(TypeError, f, ds) - - # skipna or no - self.assertTrue(notnull(f(self.series))) - self.assertTrue(isnull(f(self.series, skipna=False))) - - # check the result is correct - nona = self.series.dropna() - assert_almost_equal(f(nona), alternate(nona.values)) - assert_almost_equal(f(self.series), alternate(nona.values)) - - allna = self.series * nan - - if check_allna: - # xref 9422 - # bottleneck >= 1.0 give 0.0 for an allna Series sum - try: - self.assertTrue(nanops._USE_BOTTLENECK) - import bottleneck as bn # noqa - self.assertTrue(bn.__version__ >= LooseVersion('1.0')) - self.assertEqual(f(allna), 0.0) - except: - self.assertTrue(np.isnan(f(allna))) - - # dtype=object with None, it works! - s = Series([1, 2, 3, None, 5]) - f(s) - - # 2888 - l = [0] - l.extend(lrange(2 ** 40, 2 ** 40 + 1000)) - s = Series(l, dtype='int64') - assert_almost_equal(float(f(s)), float(alternate(s.values))) - - # check date range - if check_objects: - s = Series(bdate_range('1/1/2000', periods=10)) - res = f(s) - exp = alternate(s) - self.assertEqual(res, exp) - - # check on string data - if name not in ['sum', 'min', 'max']: - self.assertRaises(TypeError, f, Series(list('abc'))) - - # Invalid axis. - self.assertRaises(ValueError, f, self.series, axis=1) - - # Unimplemented numeric_only parameter. - if 'numeric_only' in compat.signature(f).args: - self.assertRaisesRegexp(NotImplementedError, name, f, - self.series, numeric_only=True) - - testit() - - try: - import bottleneck as bn # noqa - nanops._USE_BOTTLENECK = False - testit() - nanops._USE_BOTTLENECK = True - except ImportError: - pass - - def _check_accum_op(self, name, check_dtype=True): + def _check_accum_op(self, name, datetime_series_, check_dtype=True): func = getattr(np, name) - self.assert_numpy_array_equal(func(self.ts).values, - func(np.array(self.ts)), - check_dtype=check_dtype) + tm.assert_numpy_array_equal(func(datetime_series_).values, + func(np.array(datetime_series_)), + check_dtype=check_dtype) # with missing values - ts = self.ts.copy() + ts = datetime_series_.copy() ts[::2] = np.NaN result = func(ts)[1::2] - expected = func(np.array(ts.valid())) + expected = func(np.array(ts.dropna())) - self.assert_numpy_array_equal(result.values, expected, - check_dtype=False) + tm.assert_numpy_array_equal(result.values, expected, + check_dtype=False) def test_compress(self): cond = [True, False, True, False, False] @@ -584,7 +244,9 @@ def test_compress(self): index=list('abcde'), name='foo') expected = Series(s.values.compress(cond), index=list('ac'), name='foo') - tm.assert_series_equal(s.compress(cond), expected) + with tm.assert_produces_warning(FutureWarning): + result = s.compress(cond) + tm.assert_series_equal(result, expected) def test_numpy_compress(self): cond = [True, False, True, False, False] @@ -592,23 +254,25 @@ def test_numpy_compress(self): index=list('abcde'), name='foo') expected = Series(s.values.compress(cond), index=list('ac'), name='foo') - tm.assert_series_equal(np.compress(cond, s), expected) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + tm.assert_series_equal(np.compress(cond, s), expected) - msg = "the 'axis' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.compress, - cond, s, axis=1) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + msg = "the 'axis' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.compress(cond, s, axis=1) - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.compress, - cond, s, out=s) - - def test_round(self): - self.ts.index.name = "index_name" - result = self.ts.round(2) - expected = Series(np.round(self.ts.values, 2), - index=self.ts.index, name='ts') + msg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.compress(cond, s, out=s) + + def test_round(self, datetime_series): + datetime_series.index.name = "index_name" + result = datetime_series.round(2) + expected = Series(np.round(datetime_series.values, 2), + index=datetime_series.index, name='ts') assert_series_equal(result, expected) - self.assertEqual(result.name, self.ts.name) + assert result.name == datetime_series.name def test_numpy_round(self): # See gh-12600 @@ -618,157 +282,75 @@ def test_numpy_round(self): assert_series_equal(out, expected) msg = "the 'out' parameter is not supported" - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): np.round(s, decimals=0, out=s) + @pytest.mark.xfail( + PY2 and is_platform_windows(), reason="numpy/numpy#7882", + raises=AssertionError, strict=True) + def test_numpy_round_nan(self): + # See gh-14197 + s = Series([1.53, np.nan, 0.06]) + with tm.assert_produces_warning(None): + result = s.round() + expected = Series([2., np.nan, 0.]) + assert_series_equal(result, expected) + def test_built_in_round(self): if not compat.PY3: pytest.skip( - 'build in round cannot be overriden prior to Python 3') + 'build in round cannot be overridden prior to Python 3') s = Series([1.123, 2.123, 3.123], index=lrange(3)) result = round(s) expected_rounded0 = Series([1., 2., 3.], index=lrange(3)) - self.assert_series_equal(result, expected_rounded0) + tm.assert_series_equal(result, expected_rounded0) decimals = 2 expected_rounded = Series([1.12, 2.12, 3.12], index=lrange(3)) result = round(s, decimals) - self.assert_series_equal(result, expected_rounded) + tm.assert_series_equal(result, expected_rounded) def test_prod_numpy16_bug(self): s = Series([1., 1., 1.], index=lrange(3)) result = s.prod() - self.assertNotIsInstance(result, Series) - - def test_all_any(self): - ts = tm.makeTimeSeries() - bool_series = ts > 0 - self.assertFalse(bool_series.all()) - self.assertTrue(bool_series.any()) - - # Alternative types, with implicit 'object' dtype. - s = Series(['abc', True]) - self.assertEqual('abc', s.any()) # 'abc' || True => 'abc' - - def test_all_any_params(self): - # Check skipna, with implicit 'object' dtype. - s1 = Series([np.nan, True]) - s2 = Series([np.nan, False]) - self.assertTrue(s1.all(skipna=False)) # nan && True => True - self.assertTrue(s1.all(skipna=True)) - self.assertTrue(np.isnan(s2.any(skipna=False))) # nan || False => nan - self.assertFalse(s2.any(skipna=True)) - - # Check level. - s = pd.Series([False, False, True, True, False, True], - index=[0, 0, 1, 1, 2, 2]) - assert_series_equal(s.all(level=0), Series([False, True, False])) - assert_series_equal(s.any(level=0), Series([False, True, True])) - - # bool_only is not implemented with level option. - self.assertRaises(NotImplementedError, s.any, bool_only=True, level=0) - self.assertRaises(NotImplementedError, s.all, bool_only=True, level=0) - - # bool_only is not implemented alone. - self.assertRaises(NotImplementedError, s.any, bool_only=True) - self.assertRaises(NotImplementedError, s.all, bool_only=True) - - def test_modulo(self): - with np.errstate(all='ignore'): - - # GH3590, modulo as ints - p = DataFrame({'first': [3, 4, 5, 8], 'second': [0, 0, 0, 3]}) - result = p['first'] % p['second'] - expected = Series(p['first'].values % p['second'].values, - dtype='float64') - expected.iloc[0:3] = np.nan - assert_series_equal(result, expected) - - result = p['first'] % 0 - expected = Series(np.nan, index=p.index, name='first') - assert_series_equal(result, expected) - - p = p.astype('float64') - result = p['first'] % p['second'] - expected = Series(p['first'].values % p['second'].values) - assert_series_equal(result, expected) - - p = p.astype('float64') - result = p['first'] % p['second'] - result2 = p['second'] % p['first'] - self.assertFalse(np.array_equal(result, result2)) - - # GH 9144 - s = Series([0, 1]) - - result = s % 0 - expected = Series([nan, nan]) - assert_series_equal(result, expected) - - result = 0 % s - expected = Series([nan, 0.0]) - assert_series_equal(result, expected) - - def test_ops_consistency_on_empty(self): - - # GH 7869 - # consistency on empty - # float - result = Series(dtype=float).sum() - self.assertEqual(result, 0) - - result = Series(dtype=float).mean() - self.assertTrue(isnull(result)) - - result = Series(dtype=float).median() - self.assertTrue(isnull(result)) - - # timedelta64[ns] - result = Series(dtype='m8[ns]').sum() - self.assertEqual(result, Timedelta(0)) - - result = Series(dtype='m8[ns]').mean() - self.assertTrue(result is pd.NaT) - - result = Series(dtype='m8[ns]').median() - self.assertTrue(result is pd.NaT) - - def test_corr(self): - tm._skip_if_no_scipy() + assert not isinstance(result, Series) + @td.skip_if_no_scipy + def test_corr(self, datetime_series): import scipy.stats as stats # full overlap - self.assertAlmostEqual(self.ts.corr(self.ts), 1) + tm.assert_almost_equal(datetime_series.corr(datetime_series), 1) # partial overlap - self.assertAlmostEqual(self.ts[:15].corr(self.ts[5:]), 1) + tm.assert_almost_equal(datetime_series[:15].corr(datetime_series[5:]), + 1) - self.assertTrue(isnull(self.ts[:15].corr(self.ts[5:], min_periods=12))) + assert isna(datetime_series[:15].corr(datetime_series[5:], + min_periods=12)) - ts1 = self.ts[:15].reindex(self.ts.index) - ts2 = self.ts[5:].reindex(self.ts.index) - self.assertTrue(isnull(ts1.corr(ts2, min_periods=12))) + ts1 = datetime_series[:15].reindex(datetime_series.index) + ts2 = datetime_series[5:].reindex(datetime_series.index) + assert isna(ts1.corr(ts2, min_periods=12)) # No overlap - self.assertTrue(np.isnan(self.ts[::2].corr(self.ts[1::2]))) + assert np.isnan(datetime_series[::2].corr(datetime_series[1::2])) # all NA - cp = self.ts[:10].copy() + cp = datetime_series[:10].copy() cp[:] = np.nan - self.assertTrue(isnull(cp.corr(cp))) + assert isna(cp.corr(cp)) A = tm.makeTimeSeries() B = tm.makeTimeSeries() result = A.corr(B) expected, _ = stats.pearsonr(A, B) - self.assertAlmostEqual(result, expected) + tm.assert_almost_equal(result, expected) + @td.skip_if_no_scipy def test_corr_rank(self): - tm._skip_if_no_scipy() - import scipy import scipy.stats as stats @@ -778,14 +360,14 @@ def test_corr_rank(self): A[-5:] = A[:5] result = A.corr(B, method='kendall') expected = stats.kendalltau(A, B)[0] - self.assertAlmostEqual(result, expected) + tm.assert_almost_equal(result, expected) result = A.corr(B, method='spearman') expected = stats.spearmanr(A, B)[0] - self.assertAlmostEqual(result, expected) + tm.assert_almost_equal(result, expected) # these methods got rewritten in 0.8 - if scipy.__version__ < LooseVersion('0.9'): + if LooseVersion(scipy.__version__) < LooseVersion('0.9'): pytest.skip("skipping corr rank because of scipy version " "{0}".format(scipy.__version__)) @@ -798,38 +380,81 @@ def test_corr_rank(self): 1.17258718, -1.06009347, -0.10222060, -0.89076239, 0.89372375]) kexp = 0.4319297 sexp = 0.5853767 - self.assertAlmostEqual(A.corr(B, method='kendall'), kexp) - self.assertAlmostEqual(A.corr(B, method='spearman'), sexp) + tm.assert_almost_equal(A.corr(B, method='kendall'), kexp) + tm.assert_almost_equal(A.corr(B, method='spearman'), sexp) + + def test_corr_invalid_method(self): + # GH PR #22298 + s1 = pd.Series(np.random.randn(10)) + s2 = pd.Series(np.random.randn(10)) + msg = ("method must be either 'pearson', 'spearman', " + "or 'kendall'") + with pytest.raises(ValueError, match=msg): + s1.corr(s2, method="____") + + def test_corr_callable_method(self, datetime_series): + # simple correlation example + # returns 1 if exact equality, 0 otherwise + my_corr = lambda a, b: 1. if (a == b).all() else 0. + + # simple example + s1 = Series([1, 2, 3, 4, 5]) + s2 = Series([5, 4, 3, 2, 1]) + expected = 0 + tm.assert_almost_equal( + s1.corr(s2, method=my_corr), + expected) - def test_cov(self): # full overlap - self.assertAlmostEqual(self.ts.cov(self.ts), self.ts.std() ** 2) + tm.assert_almost_equal(datetime_series.corr( + datetime_series, method=my_corr), 1.) # partial overlap - self.assertAlmostEqual(self.ts[:15].cov(self.ts[5:]), - self.ts[5:15].std() ** 2) + tm.assert_almost_equal(datetime_series[:15].corr( + datetime_series[5:], method=my_corr), 1.) # No overlap - self.assertTrue(np.isnan(self.ts[::2].cov(self.ts[1::2]))) + assert np.isnan(datetime_series[::2].corr( + datetime_series[1::2], method=my_corr)) + + # dataframe example + df = pd.DataFrame([s1, s2]) + expected = pd.DataFrame([ + {0: 1., 1: 0}, {0: 0, 1: 1.}]) + tm.assert_almost_equal( + df.transpose().corr(method=my_corr), expected) + + def test_cov(self, datetime_series): + # full overlap + tm.assert_almost_equal(datetime_series.cov(datetime_series), + datetime_series.std() ** 2) + + # partial overlap + tm.assert_almost_equal(datetime_series[:15].cov(datetime_series[5:]), + datetime_series[5:15].std() ** 2) + + # No overlap + assert np.isnan(datetime_series[::2].cov(datetime_series[1::2])) # all NA - cp = self.ts[:10].copy() + cp = datetime_series[:10].copy() cp[:] = np.nan - self.assertTrue(isnull(cp.cov(cp))) + assert isna(cp.cov(cp)) # min_periods - self.assertTrue(isnull(self.ts[:15].cov(self.ts[5:], min_periods=12))) + assert isna(datetime_series[:15].cov(datetime_series[5:], + min_periods=12)) - ts1 = self.ts[:15].reindex(self.ts.index) - ts2 = self.ts[5:].reindex(self.ts.index) - self.assertTrue(isnull(ts1.cov(ts2, min_periods=12))) + ts1 = datetime_series[:15].reindex(datetime_series.index) + ts2 = datetime_series[5:].reindex(datetime_series.index) + assert isna(ts1.cov(ts2, min_periods=12)) - def test_count(self): - self.assertEqual(self.ts.count(), len(self.ts)) + def test_count(self, datetime_series): + assert datetime_series.count() == len(datetime_series) - self.ts[::2] = np.NaN + datetime_series[::2] = np.NaN - self.assertEqual(self.ts.count(), np.isfinite(self.ts).sum()) + assert datetime_series.count() == np.isfinite(datetime_series).sum() mi = MultiIndex.from_arrays([list('aabbcc'), [1, 2, 2, nan, 1, 2]]) ts = Series(np.arange(len(mi)), index=mi) @@ -857,110 +482,106 @@ def test_dot(self): # Check ndarray argument result = a.dot(b.values) - self.assertTrue(np.all(result == expected.values)) + assert np.all(result == expected.values) assert_almost_equal(a.dot(b['2'].values), expected['2']) # Check series argument assert_almost_equal(a.dot(b['1']), expected['1']) assert_almost_equal(a.dot(b2['1']), expected['1']) - self.assertRaises(Exception, a.dot, a.values[:3]) - self.assertRaises(ValueError, a.dot, b.T) - - def test_value_counts_nunique(self): - - # basics.rst doc example - series = Series(np.random.randn(500)) - series[20:500] = np.nan - series[10:20] = 5000 - result = series.nunique() - self.assertEqual(result, 11) - - def test_unique(self): - - # 714 also, dtype=float - s = Series([1.2345] * 100) - s[::2] = np.nan - result = s.unique() - self.assertEqual(len(result), 2) - - s = Series([1.2345] * 100, dtype='f4') - s[::2] = np.nan - result = s.unique() - self.assertEqual(len(result), 2) - - # NAs in object arrays #714 - s = Series(['foo'] * 100, dtype='O') - s[::2] = np.nan - result = s.unique() - self.assertEqual(len(result), 2) - - # decision about None - s = Series([1, 2, 3, None, None, None], dtype=object) - result = s.unique() - expected = np.array([1, 2, 3, None], dtype=object) - self.assert_numpy_array_equal(result, expected) - - def test_drop_duplicates(self): - # check both int and object - for s in [Series([1, 2, 3, 3]), Series(['1', '2', '3', '3'])]: - expected = Series([False, False, False, True]) - assert_series_equal(s.duplicated(), expected) - assert_series_equal(s.drop_duplicates(), s[~expected]) - sc = s.copy() - sc.drop_duplicates(inplace=True) - assert_series_equal(sc, s[~expected]) - - expected = Series([False, False, True, False]) - assert_series_equal(s.duplicated(keep='last'), expected) - assert_series_equal(s.drop_duplicates(keep='last'), s[~expected]) - sc = s.copy() - sc.drop_duplicates(keep='last', inplace=True) - assert_series_equal(sc, s[~expected]) - - expected = Series([False, False, True, True]) - assert_series_equal(s.duplicated(keep=False), expected) - assert_series_equal(s.drop_duplicates(keep=False), s[~expected]) - sc = s.copy() - sc.drop_duplicates(keep=False, inplace=True) - assert_series_equal(sc, s[~expected]) - - for s in [Series([1, 2, 3, 5, 3, 2, 4]), - Series(['1', '2', '3', '5', '3', '2', '4'])]: - expected = Series([False, False, False, False, True, True, False]) - assert_series_equal(s.duplicated(), expected) - assert_series_equal(s.drop_duplicates(), s[~expected]) - sc = s.copy() - sc.drop_duplicates(inplace=True) - assert_series_equal(sc, s[~expected]) - - expected = Series([False, True, True, False, False, False, False]) - assert_series_equal(s.duplicated(keep='last'), expected) - assert_series_equal(s.drop_duplicates(keep='last'), s[~expected]) - sc = s.copy() - sc.drop_duplicates(keep='last', inplace=True) - assert_series_equal(sc, s[~expected]) - - expected = Series([False, True, True, False, True, True, False]) - assert_series_equal(s.duplicated(keep=False), expected) - assert_series_equal(s.drop_duplicates(keep=False), s[~expected]) - sc = s.copy() - sc.drop_duplicates(keep=False, inplace=True) - assert_series_equal(sc, s[~expected]) - - def test_clip(self): - val = self.ts.median() - - self.assertEqual(self.ts.clip_lower(val).min(), val) - self.assertEqual(self.ts.clip_upper(val).max(), val) - - self.assertEqual(self.ts.clip(lower=val).min(), val) - self.assertEqual(self.ts.clip(upper=val).max(), val) - - result = self.ts.clip(-0.5, 0.5) - expected = np.clip(self.ts, -0.5, 0.5) + msg = r"Dot product shape mismatch, \(4L?,\) vs \(3L?,\)" + # exception raised is of type Exception + with pytest.raises(Exception, match=msg): + a.dot(a.values[:3]) + msg = "matrices are not aligned" + with pytest.raises(ValueError, match=msg): + a.dot(b.T) + + @pytest.mark.skipif(not PY35, + reason='matmul supported for Python>=3.5') + def test_matmul(self): + # matmul test is for GH #10259 + a = Series(np.random.randn(4), index=['p', 'q', 'r', 's']) + b = DataFrame(np.random.randn(3, 4), index=['1', '2', '3'], + columns=['p', 'q', 'r', 's']).T + + # Series @ DataFrame + result = operator.matmul(a, b) + expected = Series(np.dot(a.values, b.values), index=['1', '2', '3']) + assert_series_equal(result, expected) + + # DataFrame @ Series + result = operator.matmul(b.T, a) + expected = Series(np.dot(b.T.values, a.T.values), + index=['1', '2', '3']) assert_series_equal(result, expected) - tm.assertIsInstance(expected, Series) + + # Series @ Series + result = operator.matmul(a, a) + expected = np.dot(a.values, a.values) + assert_almost_equal(result, expected) + + # GH 21530 + # vector (1D np.array) @ Series (__rmatmul__) + result = operator.matmul(a.values, a) + expected = np.dot(a.values, a.values) + assert_almost_equal(result, expected) + + # GH 21530 + # vector (1D list) @ Series (__rmatmul__) + result = operator.matmul(a.values.tolist(), a) + expected = np.dot(a.values, a.values) + assert_almost_equal(result, expected) + + # GH 21530 + # matrix (2D np.array) @ Series (__rmatmul__) + result = operator.matmul(b.T.values, a) + expected = np.dot(b.T.values, a.values) + assert_almost_equal(result, expected) + + # GH 21530 + # matrix (2D nested lists) @ Series (__rmatmul__) + result = operator.matmul(b.T.values.tolist(), a) + expected = np.dot(b.T.values, a.values) + assert_almost_equal(result, expected) + + # mixed dtype DataFrame @ Series + a['p'] = int(a.p) + result = operator.matmul(b.T, a) + expected = Series(np.dot(b.T.values, a.T.values), + index=['1', '2', '3']) + assert_series_equal(result, expected) + + # different dtypes DataFrame @ Series + a = a.astype(int) + result = operator.matmul(b.T, a) + expected = Series(np.dot(b.T.values, a.T.values), + index=['1', '2', '3']) + assert_series_equal(result, expected) + + msg = r"Dot product shape mismatch, \(4,\) vs \(3,\)" + # exception raised is of type Exception + with pytest.raises(Exception, match=msg): + a.dot(a.values[:3]) + msg = "matrices are not aligned" + with pytest.raises(ValueError, match=msg): + a.dot(b.T) + + def test_clip(self, datetime_series): + val = datetime_series.median() + + with tm.assert_produces_warning(FutureWarning): + assert datetime_series.clip_lower(val).min() == val + with tm.assert_produces_warning(FutureWarning): + assert datetime_series.clip_upper(val).max() == val + + assert datetime_series.clip(lower=val).min() == val + assert datetime_series.clip(upper=val).max() == val + + result = datetime_series.clip(-0.5, 0.5) + expected = np.clip(datetime_series, -0.5, 0.5) + assert_series_equal(result, expected) + assert isinstance(expected, Series) def test_clip_types_and_nulls(self): @@ -970,12 +591,29 @@ def test_clip_types_and_nulls(self): for s in sers: thresh = s[2] - l = s.clip_lower(thresh) - u = s.clip_upper(thresh) - self.assertEqual(l[notnull(l)].min(), thresh) - self.assertEqual(u[notnull(u)].max(), thresh) - self.assertEqual(list(isnull(s)), list(isnull(l))) - self.assertEqual(list(isnull(s)), list(isnull(u))) + with tm.assert_produces_warning(FutureWarning): + lower = s.clip_lower(thresh) + with tm.assert_produces_warning(FutureWarning): + upper = s.clip_upper(thresh) + assert lower[notna(lower)].min() == thresh + assert upper[notna(upper)].max() == thresh + assert list(isna(s)) == list(isna(lower)) + assert list(isna(s)) == list(isna(upper)) + + def test_clip_with_na_args(self): + """Should process np.nan argument as None """ + # GH # 17276 + s = Series([1, 2, 3]) + + assert_series_equal(s.clip(np.nan), Series([1, 2, 3])) + assert_series_equal(s.clip(upper=np.nan, lower=np.nan), + Series([1, 2, 3])) + + # GH #19992 + assert_series_equal(s.clip(lower=[0, 4, np.nan]), + Series([1, 4, np.nan])) + assert_series_equal(s.clip(upper=[1, np.nan, 1]), + Series([1, np.nan, 1])) def test_clip_against_series(self): # GH #6966 @@ -983,25 +621,42 @@ def test_clip_against_series(self): s = Series([1.0, 1.0, 4.0]) threshold = Series([1.0, 2.0, 3.0]) - assert_series_equal(s.clip_lower(threshold), Series([1.0, 2.0, 4.0])) - assert_series_equal(s.clip_upper(threshold), Series([1.0, 1.0, 3.0])) + with tm.assert_produces_warning(FutureWarning): + assert_series_equal(s.clip_lower(threshold), + Series([1.0, 2.0, 4.0])) + with tm.assert_produces_warning(FutureWarning): + assert_series_equal(s.clip_upper(threshold), + Series([1.0, 1.0, 3.0])) lower = Series([1.0, 2.0, 3.0]) upper = Series([1.5, 2.5, 3.5]) + assert_series_equal(s.clip(lower, upper), Series([1.0, 2.0, 3.5])) assert_series_equal(s.clip(1.5, upper), Series([1.5, 1.5, 3.5])) + @pytest.mark.parametrize("inplace", [True, False]) + @pytest.mark.parametrize("upper", [[1, 2, 3], np.asarray([1, 2, 3])]) + def test_clip_against_list_like(self, inplace, upper): + # GH #15390 + original = pd.Series([5, 6, 7]) + result = original.clip(upper=upper, inplace=inplace) + expected = pd.Series([1, 2, 3]) + + if inplace: + result = original + tm.assert_series_equal(result, expected, check_exact=True) + def test_clip_with_datetimes(self): # GH 11838 # naive and tz-aware datetimes t = Timestamp('2015-12-01 09:30:30') - s = Series([Timestamp('2015-12-01 09:30:00'), Timestamp( - '2015-12-01 09:31:00')]) + s = Series([Timestamp('2015-12-01 09:30:00'), + Timestamp('2015-12-01 09:31:00')]) result = s.clip(upper=t) - expected = Series([Timestamp('2015-12-01 09:30:00'), Timestamp( - '2015-12-01 09:30:30')]) + expected = Series([Timestamp('2015-12-01 09:30:00'), + Timestamp('2015-12-01 09:30:30')]) assert_series_equal(result, expected) t = Timestamp('2015-12-01 09:30:30', tz='US/Eastern') @@ -1014,12 +669,6 @@ def test_clip_with_datetimes(self): def test_cummethods_bool(self): # GH 6270 - # looks like a buggy np.maximum.accumulate for numpy 1.6.1, py 3.2 - def cummin(x): - return np.minimum.accumulate(x) - - def cummax(x): - return np.maximum.accumulate(x) a = pd.Series([False, False, False, True, True, False, False]) b = ~a @@ -1027,8 +676,8 @@ def cummax(x): d = ~c methods = {'cumsum': np.cumsum, 'cumprod': np.cumprod, - 'cummin': cummin, - 'cummax': cummax} + 'cummin': np.minimum.accumulate, + 'cummax': np.maximum.accumulate} args = product((a, b, c, d), methods) for s, method in args: expected = Series(methods[method](s.values)) @@ -1056,14 +705,28 @@ def test_isin(self): expected = Series([True, False, True, False, False, False, True, True]) assert_series_equal(result, expected) + # GH: 16012 + # This specific issue has to have a series over 1e6 in len, but the + # comparison array (in_list) must be large enough so that numpy doesn't + # do a manual masking trick that will avoid this issue altogether + s = Series(list('abcdefghijk' * 10 ** 5)) + # If numpy doesn't do the manual comparison/mask, these + # unorderable mixed types are what cause the exception in numpy + in_list = [-1, 'a', 'b', 'G', 'Y', 'Z', 'E', + 'K', 'E', 'S', 'I', 'R', 'R'] * 6 + + assert s.isin(in_list).sum() == 200000 + def test_isin_with_string_scalar(self): # GH4763 s = Series(['A', 'B', 'C', 'a', 'B', 'B', 'A', 'C']) - with tm.assertRaises(TypeError): + msg = (r"only list-like objects are allowed to be passed to isin\(\)," + r" you passed a \[str\]") + with pytest.raises(TypeError, match=msg): s.isin('a') - with tm.assertRaises(TypeError): - s = Series(['aaa', 'b', 'c']) + s = Series(['aaa', 'b', 'c']) + with pytest.raises(TypeError, match=msg): s.isin('aaa') def test_isin_with_i8(self): @@ -1099,187 +762,60 @@ def test_isin_with_i8(self): result = s.isin(s[0:2]) assert_series_equal(result, expected) - def test_timedelta64_analytics(self): - from pandas import date_range - - # index min/max - td = Series(date_range('2012-1-1', periods=3, freq='D')) - \ - Timestamp('20120101') - - result = td.idxmin() - self.assertEqual(result, 0) - - result = td.idxmax() - self.assertEqual(result, 2) - - # GH 2982 - # with NaT - td[0] = np.nan - - result = td.idxmin() - self.assertEqual(result, 1) - - result = td.idxmax() - self.assertEqual(result, 2) - - # abs - s1 = Series(date_range('20120101', periods=3)) - s2 = Series(date_range('20120102', periods=3)) - expected = Series(s2 - s1) + @pytest.mark.parametrize("empty", [[], Series(), np.array([])]) + def test_isin_empty(self, empty): + # see gh-16991 + s = Series(["a", "b"]) + expected = Series([False, False]) - # this fails as numpy returns timedelta64[us] - # result = np.abs(s1-s2) - # assert_frame_equal(result,expected) - - result = (s1 - s2).abs() - assert_series_equal(result, expected) - - # max/min - result = td.max() - expected = Timedelta('2 days') - self.assertEqual(result, expected) - - result = td.min() - expected = Timedelta('1 days') - self.assertEqual(result, expected) - - def test_idxmin(self): - # test idxmin - # _check_stat_op approach can not be used here because of isnull check. - - # add some NaNs - self.series[5:15] = np.NaN - - # skipna or no - self.assertEqual(self.series[self.series.idxmin()], self.series.min()) - self.assertTrue(isnull(self.series.idxmin(skipna=False))) - - # no NaNs - nona = self.series.dropna() - self.assertEqual(nona[nona.idxmin()], nona.min()) - self.assertEqual(nona.index.values.tolist().index(nona.idxmin()), - nona.values.argmin()) - - # all NaNs - allna = self.series * nan - self.assertTrue(isnull(allna.idxmin())) - - # datetime64[ns] - from pandas import date_range - s = Series(date_range('20130102', periods=6)) - result = s.idxmin() - self.assertEqual(result, 0) - - s[0] = np.nan - result = s.idxmin() - self.assertEqual(result, 1) - - def test_numpy_argmin(self): - # argmin is aliased to idxmin - data = np.random.randint(0, 11, size=10) - result = np.argmin(Series(data)) - self.assertEqual(result, np.argmin(data)) - - if not _np_version_under1p10: - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.argmin, - Series(data), out=data) - - def test_idxmax(self): - # test idxmax - # _check_stat_op approach can not be used here because of isnull check. - - # add some NaNs - self.series[5:15] = np.NaN - - # skipna or no - self.assertEqual(self.series[self.series.idxmax()], self.series.max()) - self.assertTrue(isnull(self.series.idxmax(skipna=False))) - - # no NaNs - nona = self.series.dropna() - self.assertEqual(nona[nona.idxmax()], nona.max()) - self.assertEqual(nona.index.values.tolist().index(nona.idxmax()), - nona.values.argmax()) - - # all NaNs - allna = self.series * nan - self.assertTrue(isnull(allna.idxmax())) - - from pandas import date_range - s = Series(date_range('20130102', periods=6)) - result = s.idxmax() - self.assertEqual(result, 5) - - s[5] = np.nan - result = s.idxmax() - self.assertEqual(result, 4) - - # Float64Index - # GH 5914 - s = pd.Series([1, 2, 3], [1.1, 2.1, 3.1]) - result = s.idxmax() - self.assertEqual(result, 3.1) - result = s.idxmin() - self.assertEqual(result, 1.1) - - s = pd.Series(s.index, s.index) - result = s.idxmax() - self.assertEqual(result, 3.1) - result = s.idxmin() - self.assertEqual(result, 1.1) - - def test_numpy_argmax(self): - - # argmax is aliased to idxmax - data = np.random.randint(0, 11, size=10) - result = np.argmax(Series(data)) - self.assertEqual(result, np.argmax(data)) - - if not _np_version_under1p10: - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.argmax, - Series(data), out=data) + result = s.isin(empty) + tm.assert_series_equal(expected, result) + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_ptp(self): + # GH21614 N = 1000 arr = np.random.randn(N) ser = Series(arr) - self.assertEqual(np.ptp(ser), np.ptp(arr)) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + assert np.ptp(ser) == np.ptp(arr) # GH11163 s = Series([3, 5, np.nan, -3, 10]) - self.assertEqual(s.ptp(), 13) - self.assertTrue(pd.isnull(s.ptp(skipna=False))) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + assert s.ptp() == 13 + assert pd.isna(s.ptp(skipna=False)) mi = pd.MultiIndex.from_product([['a', 'b'], [1, 2, 3]]) s = pd.Series([1, np.nan, 7, 3, 5, np.nan], index=mi) expected = pd.Series([6, 2], index=['a', 'b'], dtype=np.float64) - self.assert_series_equal(s.ptp(level=0), expected) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + tm.assert_series_equal(s.ptp(level=0), expected) expected = pd.Series([np.nan, np.nan], index=['a', 'b']) - self.assert_series_equal(s.ptp(level=0, skipna=False), expected) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + tm.assert_series_equal(s.ptp(level=0, skipna=False), expected) - with self.assertRaises(ValueError): - s.ptp(axis=1) + msg = ("No axis named 1 for object type" + " ") + with pytest.raises(ValueError, match=msg): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + s.ptp(axis=1) s = pd.Series(['a', 'b', 'c', 'd', 'e']) - with self.assertRaises(TypeError): - s.ptp() - - with self.assertRaises(NotImplementedError): - s.ptp(numeric_only=True) - - def test_empty_timeseries_redections_return_nat(self): - # covers #11245 - for dtype in ('m8[ns]', 'm8[ns]', 'M8[ns]', 'M8[ns, UTC]'): - self.assertIs(Series([], dtype=dtype).min(), pd.NaT) - self.assertIs(Series([], dtype=dtype).max(), pd.NaT) - - def test_unique_data_ownership(self): - # it works! #1807 - Series(Series(["a", "c", "b"]).unique()).sort_values() + msg = r"unsupported operand type\(s\) for -: 'str' and 'str'" + with pytest.raises(TypeError, match=msg): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + s.ptp() + + msg = r"Series\.ptp does not implement numeric_only\." + with pytest.raises(NotImplementedError, match=msg): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + s.ptp(numeric_only=True) def test_repeat(self): s = Series(np.random.randn(3), index=['a', 'b', 'c']) @@ -1288,10 +824,6 @@ def test_repeat(self): exp = Series(s.values.repeat(5), index=s.index.values.repeat(5)) assert_series_equal(reps, exp) - with tm.assert_produces_warning(FutureWarning): - result = s.repeat(reps=5) - assert_series_equal(result, exp) - to_rep = [2, 3, 4] reps = s.repeat(to_rep) exp = Series(s.values.repeat(to_rep), @@ -1305,26 +837,25 @@ def test_numpy_repeat(self): assert_series_equal(np.repeat(s, 2), expected) msg = "the 'axis' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.repeat, s, 2, axis=0) + with pytest.raises(ValueError, match=msg): + np.repeat(s, 2, axis=0) def test_searchsorted(self): s = Series([1, 2, 3]) - idx = s.searchsorted(1, side='left') - tm.assert_numpy_array_equal(idx, np.array([0], dtype=np.intp)) + result = s.searchsorted(1, side='left') + assert is_scalar(result) + assert result == 0 - idx = s.searchsorted(1, side='right') - tm.assert_numpy_array_equal(idx, np.array([1], dtype=np.intp)) - - with tm.assert_produces_warning(FutureWarning): - idx = s.searchsorted(v=1, side='left') - tm.assert_numpy_array_equal(idx, np.array([0], dtype=np.intp)) + result = s.searchsorted(1, side='right') + assert is_scalar(result) + assert result == 1 def test_searchsorted_numeric_dtypes_scalar(self): s = Series([1, 2, 90, 1000, 3e9]) r = s.searchsorted(30) - e = 2 - self.assertEqual(r, e) + assert is_scalar(r) + assert r == 2 r = s.searchsorted([30]) e = np.array([2], dtype=np.intp) @@ -1340,8 +871,8 @@ def test_search_sorted_datetime64_scalar(self): s = Series(pd.date_range('20120101', periods=10, freq='2D')) v = pd.Timestamp('20120102') r = s.searchsorted(v) - e = 1 - self.assertEqual(r, e) + assert is_scalar(r) + assert r == 1 def test_search_sorted_datetime64_list(self): s = Series(pd.date_range('20120101', periods=10, freq='2D')) @@ -1357,103 +888,22 @@ def test_searchsorted_sorter(self): e = np.array([0, 2], dtype=np.intp) tm.assert_numpy_array_equal(r, e) - def test_is_unique(self): - # GH11946 - s = Series(np.random.randint(0, 10, size=1000)) - self.assertFalse(s.is_unique) - s = Series(np.arange(1000)) - self.assertTrue(s.is_unique) - def test_is_monotonic(self): s = Series(np.random.randint(0, 10, size=1000)) - self.assertFalse(s.is_monotonic) + assert not s.is_monotonic s = Series(np.arange(1000)) - self.assertTrue(s.is_monotonic) - self.assertTrue(s.is_monotonic_increasing) + assert s.is_monotonic is True + assert s.is_monotonic_increasing is True s = Series(np.arange(1000, 0, -1)) - self.assertTrue(s.is_monotonic_decreasing) + assert s.is_monotonic_decreasing is True s = Series(pd.date_range('20130101', periods=10)) - self.assertTrue(s.is_monotonic) - self.assertTrue(s.is_monotonic_increasing) + assert s.is_monotonic is True + assert s.is_monotonic_increasing is True s = Series(list(reversed(s.tolist()))) - self.assertFalse(s.is_monotonic) - self.assertTrue(s.is_monotonic_decreasing) - - def test_nsmallest_nlargest(self): - # float, int, datetime64 (use i8), timedelts64 (same), - # object that are numbers, object that are strings - - base = [3, 2, 1, 2, 5] - - s_list = [ - Series(base, dtype='int8'), - Series(base, dtype='int16'), - Series(base, dtype='int32'), - Series(base, dtype='int64'), - Series(base, dtype='float32'), - Series(base, dtype='float64'), - Series(base, dtype='uint8'), - Series(base, dtype='uint16'), - Series(base, dtype='uint32'), - Series(base, dtype='uint64'), - Series(base).astype('timedelta64[ns]'), - Series(pd.to_datetime(['2003', '2002', '2001', '2002', '2005'])), - ] - - raising = [ - Series([3., 2, 1, 2, '5'], dtype='object'), - Series([3., 2, 1, 2, 5], dtype='object'), - # not supported on some archs - # Series([3., 2, 1, 2, 5], dtype='complex256'), - Series([3., 2, 1, 2, 5], dtype='complex128'), - ] - - for r in raising: - dt = r.dtype - msg = "Cannot use method 'n(larg|small)est' with dtype %s" % dt - args = 2, len(r), 0, -1 - methods = r.nlargest, r.nsmallest - for method, arg in product(methods, args): - with tm.assertRaisesRegexp(TypeError, msg): - method(arg) - - for s in s_list: - - assert_series_equal(s.nsmallest(2), s.iloc[[2, 1]]) - assert_series_equal(s.nsmallest(2, keep='last'), s.iloc[[2, 3]]) - - empty = s.iloc[0:0] - assert_series_equal(s.nsmallest(0), empty) - assert_series_equal(s.nsmallest(-1), empty) - assert_series_equal(s.nlargest(0), empty) - assert_series_equal(s.nlargest(-1), empty) - - assert_series_equal(s.nsmallest(len(s)), s.sort_values()) - assert_series_equal(s.nsmallest(len(s) + 1), s.sort_values()) - assert_series_equal(s.nlargest(len(s)), s.iloc[[4, 0, 1, 3, 2]]) - assert_series_equal(s.nlargest(len(s) + 1), - s.iloc[[4, 0, 1, 3, 2]]) - - s = Series([3., np.nan, 1, 2, 5]) - assert_series_equal(s.nlargest(), s.iloc[[4, 0, 3, 2]]) - assert_series_equal(s.nsmallest(), s.iloc[[2, 3, 0, 4]]) - - msg = 'keep must be either "first", "last"' - with tm.assertRaisesRegexp(ValueError, msg): - s.nsmallest(keep='invalid') - with tm.assertRaisesRegexp(ValueError, msg): - s.nlargest(keep='invalid') - - # GH 13412 - s = Series([1, 4, 3, 2], index=[0, 0, 1, 1]) - result = s.nlargest(3) - expected = s.sort_values(ascending=False).head(3) - assert_series_equal(result, expected) - result = s.nsmallest(3) - expected = s.sort_values().head(3) - assert_series_equal(result, expected) + assert s.is_monotonic is False + assert s.is_monotonic_decreasing is True def test_sort_index_level(self): mi = MultiIndex.from_tuples([[1, 1, 3], [1, 1, 1]], names=list('ABC')) @@ -1489,10 +939,10 @@ def test_apply_categorical(self): result = s.apply(lambda x: 'A') exp = pd.Series(['A'] * 7, name='XX', index=list('abcdefg')) tm.assert_series_equal(result, exp) - self.assertEqual(result.dtype, np.object) + assert result.dtype == np.object - def test_shift_int(self): - ts = self.ts.astype(int) + def test_shift_int(self, datetime_series): + ts = datetime_series.astype(int) shifted = ts.shift(1) expected = ts.astype(float).shift(1) assert_series_equal(shifted, expected) @@ -1501,84 +951,26 @@ def test_shift_categorical(self): # GH 9416 s = pd.Series(['a', 'b', 'c', 'd'], dtype='category') - assert_series_equal(s.iloc[:-1], s.shift(1).shift(-1).valid()) + assert_series_equal(s.iloc[:-1], s.shift(1).shift(-1).dropna()) sp1 = s.shift(1) assert_index_equal(s.index, sp1.index) - self.assertTrue(np.all(sp1.values.codes[:1] == -1)) - self.assertTrue(np.all(s.values.codes[:-1] == sp1.values.codes[1:])) + assert np.all(sp1.values.codes[:1] == -1) + assert np.all(s.values.codes[:-1] == sp1.values.codes[1:]) sn2 = s.shift(-2) assert_index_equal(s.index, sn2.index) - self.assertTrue(np.all(sn2.values.codes[-2:] == -1)) - self.assertTrue(np.all(s.values.codes[2:] == sn2.values.codes[:-2])) + assert np.all(sn2.values.codes[-2:] == -1) + assert np.all(s.values.codes[2:] == sn2.values.codes[:-2]) assert_index_equal(s.values.categories, sp1.values.categories) assert_index_equal(s.values.categories, sn2.values.categories) - def test_reshape_deprecate(self): - x = Series(np.random.random(10), name='x') - tm.assert_produces_warning(FutureWarning, x.reshape, x.shape) - - def test_reshape_non_2d(self): - # see gh-4554 - with tm.assert_produces_warning(FutureWarning): - x = Series(np.random.random(201), name='x') - self.assertTrue(x.reshape(x.shape, ) is x) - - # see gh-2719 - with tm.assert_produces_warning(FutureWarning): - a = Series([1, 2, 3, 4]) - result = a.reshape(2, 2) - expected = a.values.reshape(2, 2) - tm.assert_numpy_array_equal(result, expected) - self.assertIsInstance(result, type(expected)) - - def test_reshape_2d_return_array(self): - x = Series(np.random.random(201), name='x') - - with tm.assert_produces_warning(FutureWarning): - result = x.reshape((-1, 1)) - self.assertNotIsInstance(result, Series) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result2 = np.reshape(x, (-1, 1)) - self.assertNotIsInstance(result2, Series) - - with tm.assert_produces_warning(FutureWarning): - result = x[:, None] - expected = x.reshape((-1, 1)) - assert_almost_equal(result, expected) - - def test_reshape_bad_kwarg(self): - a = Series([1, 2, 3, 4]) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - msg = "'foo' is an invalid keyword argument for this function" - tm.assertRaisesRegexp(TypeError, msg, a.reshape, (2, 2), foo=2) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - msg = r"reshape\(\) got an unexpected keyword argument 'foo'" - tm.assertRaisesRegexp(TypeError, msg, a.reshape, a.shape, foo=2) - - def test_numpy_reshape(self): - a = Series([1, 2, 3, 4]) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = np.reshape(a, (2, 2)) - expected = a.values.reshape(2, 2) - tm.assert_numpy_array_equal(result, expected) - self.assertIsInstance(result, type(expected)) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = np.reshape(a, a.shape) - tm.assert_series_equal(result, a) - def test_unstack(self): from numpy import nan index = MultiIndex(levels=[['bar', 'foo'], ['one', 'three', 'two']], - labels=[[1, 1, 0, 0], [0, 1, 0, 2]]) + codes=[[1, 1, 0, 0], [0, 1, 0, 2]]) s = Series(np.arange(4.), index=index) unstacked = s.unstack() @@ -1593,14 +985,14 @@ def test_unstack(self): assert_frame_equal(unstacked, expected.T) index = MultiIndex(levels=[['bar'], ['one', 'two', 'three'], [0, 1]], - labels=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], - [0, 1, 0, 1, 0, 1]]) + codes=[[0, 0, 0, 0, 0, 0], [0, 1, 2, 0, 1, 2], + [0, 1, 0, 1, 0, 1]]) s = Series(np.random.randn(6), index=index) exp_index = MultiIndex(levels=[['one', 'two', 'three'], [0, 1]], - labels=[[0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1]]) + codes=[[0, 1, 2, 0, 1, 2], [0, 1, 0, 1, 0, 1]]) expected = DataFrame({'bar': s.values}, index=exp_index).sort_index(level=0) - unstacked = s.unstack(0) + unstacked = s.unstack(0).sort_index() assert_frame_equal(unstacked, expected) # GH5873 @@ -1729,3 +1121,392 @@ def test_value_counts_categorical_not_ordered(self): index=exp_idx, name='xxx') tm.assert_series_equal(s.value_counts(normalize=True), exp) tm.assert_series_equal(idx.value_counts(normalize=True), exp) + + @pytest.mark.parametrize("func", [np.any, np.all]) + @pytest.mark.parametrize("kwargs", [ + dict(keepdims=True), + dict(out=object()), + ]) + @td.skip_if_np_lt_115 + def test_validate_any_all_out_keepdims_raises(self, kwargs, func): + s = pd.Series([1, 2]) + param = list(kwargs)[0] + name = func.__name__ + + msg = (r"the '{arg}' parameter is not " + r"supported in the pandas " + r"implementation of {fname}\(\)").format(arg=param, fname=name) + with pytest.raises(ValueError, match=msg): + func(s, **kwargs) + + @td.skip_if_np_lt_115 + def test_validate_sum_initial(self): + s = pd.Series([1, 2]) + msg = (r"the 'initial' parameter is not " + r"supported in the pandas " + r"implementation of sum\(\)") + with pytest.raises(ValueError, match=msg): + np.sum(s, initial=10) + + def test_validate_median_initial(self): + s = pd.Series([1, 2]) + msg = (r"the 'overwrite_input' parameter is not " + r"supported in the pandas " + r"implementation of median\(\)") + with pytest.raises(ValueError, match=msg): + # It seems like np.median doesn't dispatch, so we use the + # method instead of the ufunc. + s.median(overwrite_input=True) + + @td.skip_if_np_lt_115 + def test_validate_stat_keepdims(self): + s = pd.Series([1, 2]) + msg = (r"the 'keepdims' parameter is not " + r"supported in the pandas " + r"implementation of sum\(\)") + with pytest.raises(ValueError, match=msg): + np.sum(s, keepdims=True) + + +main_dtypes = [ + 'datetime', + 'datetimetz', + 'timedelta', + 'int8', + 'int16', + 'int32', + 'int64', + 'float32', + 'float64', + 'uint8', + 'uint16', + 'uint32', + 'uint64' +] + + +@pytest.fixture +def s_main_dtypes(): + """A DataFrame with many dtypes + + * datetime + * datetimetz + * timedelta + * [u]int{8,16,32,64} + * float{32,64} + + The columns are the name of the dtype. + """ + df = pd.DataFrame( + {'datetime': pd.to_datetime(['2003', '2002', + '2001', '2002', + '2005']), + 'datetimetz': pd.to_datetime( + ['2003', '2002', + '2001', '2002', + '2005']).tz_localize('US/Eastern'), + 'timedelta': pd.to_timedelta(['3d', '2d', '1d', + '2d', '5d'])}) + + for dtype in ['int8', 'int16', 'int32', 'int64', + 'float32', 'float64', + 'uint8', 'uint16', 'uint32', 'uint64']: + df[dtype] = Series([3, 2, 1, 2, 5], dtype=dtype) + + return df + + +@pytest.fixture(params=main_dtypes) +def s_main_dtypes_split(request, s_main_dtypes): + """Each series in s_main_dtypes.""" + return s_main_dtypes[request.param] + + +def assert_check_nselect_boundary(vals, dtype, method): + # helper function for 'test_boundary_{dtype}' tests + s = Series(vals, dtype=dtype) + result = getattr(s, method)(3) + expected_idxr = [0, 1, 2] if method == 'nsmallest' else [3, 2, 1] + expected = s.loc[expected_idxr] + tm.assert_series_equal(result, expected) + + +class TestNLargestNSmallest(object): + + @pytest.mark.parametrize( + "r", [Series([3., 2, 1, 2, '5'], dtype='object'), + Series([3., 2, 1, 2, 5], dtype='object'), + # not supported on some archs + # Series([3., 2, 1, 2, 5], dtype='complex256'), + Series([3., 2, 1, 2, 5], dtype='complex128'), + Series(list('abcde')), + Series(list('abcde'), dtype='category')]) + def test_error(self, r): + dt = r.dtype + msg = ("Cannot use method 'n(larg|small)est' with " + "dtype {dt}".format(dt=dt)) + args = 2, len(r), 0, -1 + methods = r.nlargest, r.nsmallest + for method, arg in product(methods, args): + with pytest.raises(TypeError, match=msg): + method(arg) + + def test_nsmallest_nlargest(self, s_main_dtypes_split): + # float, int, datetime64 (use i8), timedelts64 (same), + # object that are numbers, object that are strings + s = s_main_dtypes_split + + assert_series_equal(s.nsmallest(2), s.iloc[[2, 1]]) + assert_series_equal(s.nsmallest(2, keep='last'), s.iloc[[2, 3]]) + + empty = s.iloc[0:0] + assert_series_equal(s.nsmallest(0), empty) + assert_series_equal(s.nsmallest(-1), empty) + assert_series_equal(s.nlargest(0), empty) + assert_series_equal(s.nlargest(-1), empty) + + assert_series_equal(s.nsmallest(len(s)), s.sort_values()) + assert_series_equal(s.nsmallest(len(s) + 1), s.sort_values()) + assert_series_equal(s.nlargest(len(s)), s.iloc[[4, 0, 1, 3, 2]]) + assert_series_equal(s.nlargest(len(s) + 1), + s.iloc[[4, 0, 1, 3, 2]]) + + def test_misc(self): + + s = Series([3., np.nan, 1, 2, 5]) + assert_series_equal(s.nlargest(), s.iloc[[4, 0, 3, 2]]) + assert_series_equal(s.nsmallest(), s.iloc[[2, 3, 0, 4]]) + + msg = 'keep must be either "first", "last"' + with pytest.raises(ValueError, match=msg): + s.nsmallest(keep='invalid') + with pytest.raises(ValueError, match=msg): + s.nlargest(keep='invalid') + + # GH 15297 + s = Series([1] * 5, index=[1, 2, 3, 4, 5]) + expected_first = Series([1] * 3, index=[1, 2, 3]) + expected_last = Series([1] * 3, index=[5, 4, 3]) + + result = s.nsmallest(3) + assert_series_equal(result, expected_first) + + result = s.nsmallest(3, keep='last') + assert_series_equal(result, expected_last) + + result = s.nlargest(3) + assert_series_equal(result, expected_first) + + result = s.nlargest(3, keep='last') + assert_series_equal(result, expected_last) + + @pytest.mark.parametrize('n', range(1, 5)) + def test_n(self, n): + + # GH 13412 + s = Series([1, 4, 3, 2], index=[0, 0, 1, 1]) + result = s.nlargest(n) + expected = s.sort_values(ascending=False).head(n) + assert_series_equal(result, expected) + + result = s.nsmallest(n) + expected = s.sort_values().head(n) + assert_series_equal(result, expected) + + def test_boundary_integer(self, nselect_method, any_int_dtype): + # GH 21426 + dtype_info = np.iinfo(any_int_dtype) + min_val, max_val = dtype_info.min, dtype_info.max + vals = [min_val, min_val + 1, max_val - 1, max_val] + assert_check_nselect_boundary(vals, any_int_dtype, nselect_method) + + def test_boundary_float(self, nselect_method, float_dtype): + # GH 21426 + dtype_info = np.finfo(float_dtype) + min_val, max_val = dtype_info.min, dtype_info.max + min_2nd, max_2nd = np.nextafter( + [min_val, max_val], 0, dtype=float_dtype) + vals = [min_val, min_2nd, max_2nd, max_val] + assert_check_nselect_boundary(vals, float_dtype, nselect_method) + + @pytest.mark.parametrize('dtype', ['datetime64[ns]', 'timedelta64[ns]']) + def test_boundary_datetimelike(self, nselect_method, dtype): + # GH 21426 + # use int64 bounds and +1 to min_val since true minimum is NaT + # (include min_val/NaT at end to maintain same expected_idxr) + dtype_info = np.iinfo('int64') + min_val, max_val = dtype_info.min, dtype_info.max + vals = [min_val + 1, min_val + 2, max_val - 1, max_val, min_val] + assert_check_nselect_boundary(vals, dtype, nselect_method) + + def test_duplicate_keep_all_ties(self): + # see gh-16818 + s = Series([10, 9, 8, 7, 7, 7, 7, 6]) + result = s.nlargest(4, keep='all') + expected = Series([10, 9, 8, 7, 7, 7, 7]) + assert_series_equal(result, expected) + + result = s.nsmallest(2, keep='all') + expected = Series([6, 7, 7, 7, 7], index=[7, 3, 4, 5, 6]) + assert_series_equal(result, expected) + + +class TestCategoricalSeriesAnalytics(object): + + def test_count(self): + + s = Series(Categorical([np.nan, 1, 2, np.nan], + categories=[5, 4, 3, 2, 1], ordered=True)) + result = s.count() + assert result == 2 + + def test_value_counts(self): + # GH 12835 + cats = Categorical(list('abcccb'), categories=list('cabd')) + s = Series(cats, name='xxx') + res = s.value_counts(sort=False) + + exp_index = CategoricalIndex(list('cabd'), categories=cats.categories) + exp = Series([3, 1, 2, 0], name='xxx', index=exp_index) + tm.assert_series_equal(res, exp) + + res = s.value_counts(sort=True) + + exp_index = CategoricalIndex(list('cbad'), categories=cats.categories) + exp = Series([3, 2, 1, 0], name='xxx', index=exp_index) + tm.assert_series_equal(res, exp) + + # check object dtype handles the Series.name as the same + # (tested in test_base.py) + s = Series(["a", "b", "c", "c", "c", "b"], name='xxx') + res = s.value_counts() + exp = Series([3, 2, 1], name='xxx', index=["c", "b", "a"]) + tm.assert_series_equal(res, exp) + + def test_value_counts_with_nan(self): + # see gh-9443 + + # sanity check + s = Series(["a", "b", "a"], dtype="category") + exp = Series([2, 1], index=CategoricalIndex(["a", "b"])) + + res = s.value_counts(dropna=True) + tm.assert_series_equal(res, exp) + + res = s.value_counts(dropna=True) + tm.assert_series_equal(res, exp) + + # same Series via two different constructions --> same behaviour + series = [ + Series(["a", "b", None, "a", None, None], dtype="category"), + Series(Categorical(["a", "b", None, "a", None, None], + categories=["a", "b"])) + ] + + for s in series: + # None is a NaN value, so we exclude its count here + exp = Series([2, 1], index=CategoricalIndex(["a", "b"])) + res = s.value_counts(dropna=True) + tm.assert_series_equal(res, exp) + + # we don't exclude the count of None and sort by counts + exp = Series([3, 2, 1], index=CategoricalIndex([np.nan, "a", "b"])) + res = s.value_counts(dropna=False) + tm.assert_series_equal(res, exp) + + # When we aren't sorting by counts, and np.nan isn't a + # category, it should be last. + exp = Series([2, 1, 3], index=CategoricalIndex(["a", "b", np.nan])) + res = s.value_counts(dropna=False, sort=False) + tm.assert_series_equal(res, exp) + + @pytest.mark.parametrize( + "dtype", + ["int_", "uint", "float_", "unicode_", "timedelta64[h]", + pytest.param("datetime64[D]", + marks=pytest.mark.xfail(reason="GH#7996"))] + ) + @pytest.mark.parametrize("is_ordered", [True, False]) + def test_drop_duplicates_categorical_non_bool(self, dtype, is_ordered): + cat_array = np.array([1, 2, 3, 4, 5], dtype=np.dtype(dtype)) + + # Test case 1 + input1 = np.array([1, 2, 3, 3], dtype=np.dtype(dtype)) + tc1 = Series(Categorical(input1, categories=cat_array, + ordered=is_ordered)) + + expected = Series([False, False, False, True]) + tm.assert_series_equal(tc1.duplicated(), expected) + tm.assert_series_equal(tc1.drop_duplicates(), tc1[~expected]) + sc = tc1.copy() + sc.drop_duplicates(inplace=True) + tm.assert_series_equal(sc, tc1[~expected]) + + expected = Series([False, False, True, False]) + tm.assert_series_equal(tc1.duplicated(keep='last'), expected) + tm.assert_series_equal(tc1.drop_duplicates(keep='last'), + tc1[~expected]) + sc = tc1.copy() + sc.drop_duplicates(keep='last', inplace=True) + tm.assert_series_equal(sc, tc1[~expected]) + + expected = Series([False, False, True, True]) + tm.assert_series_equal(tc1.duplicated(keep=False), expected) + tm.assert_series_equal(tc1.drop_duplicates(keep=False), tc1[~expected]) + sc = tc1.copy() + sc.drop_duplicates(keep=False, inplace=True) + tm.assert_series_equal(sc, tc1[~expected]) + + # Test case 2 + input2 = np.array([1, 2, 3, 5, 3, 2, 4], dtype=np.dtype(dtype)) + tc2 = Series(Categorical( + input2, categories=cat_array, ordered=is_ordered) + ) + + expected = Series([False, False, False, False, True, True, False]) + tm.assert_series_equal(tc2.duplicated(), expected) + tm.assert_series_equal(tc2.drop_duplicates(), tc2[~expected]) + sc = tc2.copy() + sc.drop_duplicates(inplace=True) + tm.assert_series_equal(sc, tc2[~expected]) + + expected = Series([False, True, True, False, False, False, False]) + tm.assert_series_equal(tc2.duplicated(keep='last'), expected) + tm.assert_series_equal(tc2.drop_duplicates(keep='last'), + tc2[~expected]) + sc = tc2.copy() + sc.drop_duplicates(keep='last', inplace=True) + tm.assert_series_equal(sc, tc2[~expected]) + + expected = Series([False, True, True, False, True, True, False]) + tm.assert_series_equal(tc2.duplicated(keep=False), expected) + tm.assert_series_equal(tc2.drop_duplicates(keep=False), tc2[~expected]) + sc = tc2.copy() + sc.drop_duplicates(keep=False, inplace=True) + tm.assert_series_equal(sc, tc2[~expected]) + + @pytest.mark.parametrize("is_ordered", [True, False]) + def test_drop_duplicates_categorical_bool(self, is_ordered): + tc = Series(Categorical([True, False, True, False], + categories=[True, False], ordered=is_ordered)) + + expected = Series([False, False, True, True]) + tm.assert_series_equal(tc.duplicated(), expected) + tm.assert_series_equal(tc.drop_duplicates(), tc[~expected]) + sc = tc.copy() + sc.drop_duplicates(inplace=True) + tm.assert_series_equal(sc, tc[~expected]) + + expected = Series([True, True, False, False]) + tm.assert_series_equal(tc.duplicated(keep='last'), expected) + tm.assert_series_equal(tc.drop_duplicates(keep='last'), tc[~expected]) + sc = tc.copy() + sc.drop_duplicates(keep='last', inplace=True) + tm.assert_series_equal(sc, tc[~expected]) + + expected = Series([True, True, True, True]) + tm.assert_series_equal(tc.duplicated(keep=False), expected) + tm.assert_series_equal(tc.drop_duplicates(keep=False), tc[~expected]) + sc = tc.copy() + sc.drop_duplicates(keep=False, inplace=True) + tm.assert_series_equal(sc, tc[~expected]) diff --git a/pandas/tests/series/test_api.py b/pandas/tests/series/test_api.py new file mode 100644 index 0000000000000..1f2e2b179c687 --- /dev/null +++ b/pandas/tests/series/test_api.py @@ -0,0 +1,712 @@ +# coding=utf-8 +# pylint: disable-msg=E1101,W0612 +from collections import OrderedDict +import pydoc +import warnings + +import numpy as np +import pytest + +import pandas.compat as compat +from pandas.compat import isidentifier, lzip, range, string_types + +import pandas as pd +from pandas import ( + Categorical, DataFrame, DatetimeIndex, Index, Series, TimedeltaIndex, + date_range, period_range, timedelta_range) +from pandas.core.arrays import PeriodArray +from pandas.core.indexes.datetimes import Timestamp +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal, ensure_clean + +import pandas.io.formats.printing as printing + +from .common import TestData + + +class SharedWithSparse(object): + """ + A collection of tests Series and SparseSeries can share. + + In generic tests on this class, use ``self._assert_series_equal()`` + which is implemented in sub-classes. + """ + def _assert_series_equal(self, left, right): + """Dispatch to series class dependent assertion""" + raise NotImplementedError + + def test_scalarop_preserve_name(self): + result = self.ts * 2 + assert result.name == self.ts.name + + def test_copy_name(self): + result = self.ts.copy() + assert result.name == self.ts.name + + def test_copy_index_name_checking(self): + # don't want to be able to modify the index stored elsewhere after + # making a copy + + self.ts.index.name = None + assert self.ts.index.name is None + assert self.ts is self.ts + + cp = self.ts.copy() + cp.index.name = 'foo' + printing.pprint_thing(self.ts.index.name) + assert self.ts.index.name is None + + def test_append_preserve_name(self): + result = self.ts[:5].append(self.ts[5:]) + assert result.name == self.ts.name + + def test_binop_maybe_preserve_name(self): + # names match, preserve + result = self.ts * self.ts + assert result.name == self.ts.name + result = self.ts.mul(self.ts) + assert result.name == self.ts.name + + result = self.ts * self.ts[:-2] + assert result.name == self.ts.name + + # names don't match, don't preserve + cp = self.ts.copy() + cp.name = 'something else' + result = self.ts + cp + assert result.name is None + result = self.ts.add(cp) + assert result.name is None + + ops = ['add', 'sub', 'mul', 'div', 'truediv', 'floordiv', 'mod', 'pow'] + ops = ops + ['r' + op for op in ops] + for op in ops: + # names match, preserve + s = self.ts.copy() + result = getattr(s, op)(s) + assert result.name == self.ts.name + + # names don't match, don't preserve + cp = self.ts.copy() + cp.name = 'changed' + result = getattr(s, op)(cp) + assert result.name is None + + def test_combine_first_name(self): + result = self.ts.combine_first(self.ts[:5]) + assert result.name == self.ts.name + + def test_getitem_preserve_name(self): + result = self.ts[self.ts > 0] + assert result.name == self.ts.name + + result = self.ts[[0, 2, 4]] + assert result.name == self.ts.name + + result = self.ts[5:10] + assert result.name == self.ts.name + + def test_pickle(self): + unp_series = self._pickle_roundtrip(self.series) + unp_ts = self._pickle_roundtrip(self.ts) + assert_series_equal(unp_series, self.series) + assert_series_equal(unp_ts, self.ts) + + def _pickle_roundtrip(self, obj): + + with ensure_clean() as path: + obj.to_pickle(path) + unpickled = pd.read_pickle(path) + return unpickled + + def test_argsort_preserve_name(self): + result = self.ts.argsort() + assert result.name == self.ts.name + + def test_sort_index_name(self): + result = self.ts.sort_index(ascending=False) + assert result.name == self.ts.name + + def test_to_sparse_pass_name(self): + result = self.ts.to_sparse() + assert result.name == self.ts.name + + def test_constructor_dict(self): + d = {'a': 0., 'b': 1., 'c': 2.} + result = self.series_klass(d) + expected = self.series_klass(d, index=sorted(d.keys())) + self._assert_series_equal(result, expected) + + result = self.series_klass(d, index=['b', 'c', 'd', 'a']) + expected = self.series_klass([1, 2, np.nan, 0], + index=['b', 'c', 'd', 'a']) + self._assert_series_equal(result, expected) + + def test_constructor_subclass_dict(self): + data = tm.TestSubDict((x, 10.0 * x) for x in range(10)) + series = self.series_klass(data) + expected = self.series_klass(dict(compat.iteritems(data))) + self._assert_series_equal(series, expected) + + def test_constructor_ordereddict(self): + # GH3283 + data = OrderedDict( + ('col%s' % i, np.random.random()) for i in range(12)) + + series = self.series_klass(data) + expected = self.series_klass(list(data.values()), list(data.keys())) + self._assert_series_equal(series, expected) + + # Test with subclass + class A(OrderedDict): + pass + + series = self.series_klass(A(data)) + self._assert_series_equal(series, expected) + + def test_constructor_dict_multiindex(self): + d = {('a', 'a'): 0., ('b', 'a'): 1., ('b', 'c'): 2.} + _d = sorted(d.items()) + result = self.series_klass(d) + expected = self.series_klass( + [x[1] for x in _d], + index=pd.MultiIndex.from_tuples([x[0] for x in _d])) + self._assert_series_equal(result, expected) + + d['z'] = 111. + _d.insert(0, ('z', d['z'])) + result = self.series_klass(d) + expected = self.series_klass([x[1] for x in _d], + index=pd.Index([x[0] for x in _d], + tupleize_cols=False)) + result = result.reindex(index=expected.index) + self._assert_series_equal(result, expected) + + def test_constructor_dict_timedelta_index(self): + # GH #12169 : Resample category data with timedelta index + # construct Series from dict as data and TimedeltaIndex as index + # will result NaN in result Series data + expected = self.series_klass( + data=['A', 'B', 'C'], + index=pd.to_timedelta([0, 10, 20], unit='s') + ) + + result = self.series_klass( + data={pd.to_timedelta(0, unit='s'): 'A', + pd.to_timedelta(10, unit='s'): 'B', + pd.to_timedelta(20, unit='s'): 'C'}, + index=pd.to_timedelta([0, 10, 20], unit='s') + ) + self._assert_series_equal(result, expected) + + def test_from_array_deprecated(self): + + with tm.assert_produces_warning(FutureWarning): + self.series_klass.from_array([1, 2, 3]) + + def test_sparse_accessor_updates_on_inplace(self): + s = pd.Series([1, 1, 2, 3], dtype="Sparse[int]") + s.drop([0, 1], inplace=True) + assert s.sparse.density == 1.0 + + +class TestSeriesMisc(TestData, SharedWithSparse): + + series_klass = Series + # SharedWithSparse tests use generic, series_klass-agnostic assertion + _assert_series_equal = staticmethod(tm.assert_series_equal) + + def test_tab_completion(self): + # GH 9910 + s = Series(list('abcd')) + # Series of str values should have .str but not .dt/.cat in __dir__ + assert 'str' in dir(s) + assert 'dt' not in dir(s) + assert 'cat' not in dir(s) + + # similarly for .dt + s = Series(date_range('1/1/2015', periods=5)) + assert 'dt' in dir(s) + assert 'str' not in dir(s) + assert 'cat' not in dir(s) + + # Similarly for .cat, but with the twist that str and dt should be + # there if the categories are of that type first cat and str. + s = Series(list('abbcd'), dtype="category") + assert 'cat' in dir(s) + assert 'str' in dir(s) # as it is a string categorical + assert 'dt' not in dir(s) + + # similar to cat and str + s = Series(date_range('1/1/2015', periods=5)).astype("category") + assert 'cat' in dir(s) + assert 'str' not in dir(s) + assert 'dt' in dir(s) # as it is a datetime categorical + + def test_tab_completion_with_categorical(self): + # test the tab completion display + ok_for_cat = ['name', 'index', 'categorical', 'categories', 'codes', + 'ordered', 'set_categories', 'add_categories', + 'remove_categories', 'rename_categories', + 'reorder_categories', 'remove_unused_categories', + 'as_ordered', 'as_unordered'] + + def get_dir(s): + results = [r for r in s.cat.__dir__() if not r.startswith('_')] + return list(sorted(set(results))) + + s = Series(list('aabbcde')).astype('category') + results = get_dir(s) + tm.assert_almost_equal(results, list(sorted(set(ok_for_cat)))) + + @pytest.mark.parametrize("index", [ + tm.makeUnicodeIndex(10), + tm.makeStringIndex(10), + tm.makeCategoricalIndex(10), + Index(['foo', 'bar', 'baz'] * 2), + tm.makeDateIndex(10), + tm.makePeriodIndex(10), + tm.makeTimedeltaIndex(10), + tm.makeIntIndex(10), + tm.makeUIntIndex(10), + tm.makeIntIndex(10), + tm.makeFloatIndex(10), + Index([True, False]), + Index(['a{}'.format(i) for i in range(101)]), + pd.MultiIndex.from_tuples(lzip('ABCD', 'EFGH')), + pd.MultiIndex.from_tuples(lzip([0, 1, 2, 3], 'EFGH')), ]) + def test_index_tab_completion(self, index): + # dir contains string-like values of the Index. + s = pd.Series(index=index) + dir_s = dir(s) + for i, x in enumerate(s.index.unique(level=0)): + if i < 100: + assert (not isinstance(x, string_types) or + not isidentifier(x) or x in dir_s) + else: + assert x not in dir_s + + def test_not_hashable(self): + s_empty = Series() + s = Series([1]) + msg = "'Series' objects are mutable, thus they cannot be hashed" + with pytest.raises(TypeError, match=msg): + hash(s_empty) + with pytest.raises(TypeError, match=msg): + hash(s) + + def test_contains(self): + tm.assert_contains_all(self.ts.index, self.ts) + + def test_iter(self): + for i, val in enumerate(self.series): + assert val == self.series[i] + + for i, val in enumerate(self.ts): + assert val == self.ts[i] + + def test_keys(self): + # HACK: By doing this in two stages, we avoid 2to3 wrapping the call + # to .keys() in a list() + getkeys = self.ts.keys + assert getkeys() is self.ts.index + + def test_values(self): + tm.assert_almost_equal(self.ts.values, self.ts, check_dtype=False) + + def test_iteritems(self): + for idx, val in compat.iteritems(self.series): + assert val == self.series[idx] + + for idx, val in compat.iteritems(self.ts): + assert val == self.ts[idx] + + # assert is lazy (genrators don't define reverse, lists do) + assert not hasattr(self.series.iteritems(), 'reverse') + + def test_items(self): + for idx, val in self.series.items(): + assert val == self.series[idx] + + for idx, val in self.ts.items(): + assert val == self.ts[idx] + + # assert is lazy (genrators don't define reverse, lists do) + assert not hasattr(self.series.items(), 'reverse') + + def test_raise_on_info(self): + s = Series(np.random.randn(10)) + msg = "'Series' object has no attribute 'info'" + with pytest.raises(AttributeError, match=msg): + s.info() + + def test_copy(self): + + for deep in [None, False, True]: + s = Series(np.arange(10), dtype='float64') + + # default deep is True + if deep is None: + s2 = s.copy() + else: + s2 = s.copy(deep=deep) + + s2[::2] = np.NaN + + if deep is None or deep is True: + # Did not modify original Series + assert np.isnan(s2[0]) + assert not np.isnan(s[0]) + else: + # we DID modify the original Series + assert np.isnan(s2[0]) + assert np.isnan(s[0]) + + # GH 11794 + # copy of tz-aware + expected = Series([Timestamp('2012/01/01', tz='UTC')]) + expected2 = Series([Timestamp('1999/01/01', tz='UTC')]) + + for deep in [None, False, True]: + + s = Series([Timestamp('2012/01/01', tz='UTC')]) + + if deep is None: + s2 = s.copy() + else: + s2 = s.copy(deep=deep) + + s2[0] = pd.Timestamp('1999/01/01', tz='UTC') + + # default deep is True + if deep is None or deep is True: + # Did not modify original Series + assert_series_equal(s2, expected2) + assert_series_equal(s, expected) + else: + # we DID modify the original Series + assert_series_equal(s2, expected2) + assert_series_equal(s, expected2) + + def test_axis_alias(self): + s = Series([1, 2, np.nan]) + assert_series_equal(s.dropna(axis='rows'), s.dropna(axis='index')) + assert s.dropna().sum('rows') == 3 + assert s._get_axis_number('rows') == 0 + assert s._get_axis_name('rows') == 'index' + + def test_class_axis(self): + # https://github.com/pandas-dev/pandas/issues/18147 + # no exception and no empty docstring + assert pydoc.getdoc(Series.index) + + def test_numpy_unique(self): + # it works! + np.unique(self.ts) + + def test_ndarray_compat(self): + + # test numpy compat with Series as sub-class of NDFrame + tsdf = DataFrame(np.random.randn(1000, 3), columns=['A', 'B', 'C'], + index=date_range('1/1/2000', periods=1000)) + + def f(x): + return x[x.idxmax()] + + result = tsdf.apply(f) + expected = tsdf.max() + tm.assert_series_equal(result, expected) + + # .item() + s = Series([1]) + result = s.item() + assert result == 1 + assert s.item() == s.iloc[0] + + # using an ndarray like function + s = Series(np.random.randn(10)) + result = Series(np.ones_like(s)) + expected = Series(1, index=range(10), dtype='float64') + tm.assert_series_equal(result, expected) + + # ravel + s = Series(np.random.randn(10)) + tm.assert_almost_equal(s.ravel(order='F'), s.values.ravel(order='F')) + + # compress + # GH 6658 + s = Series([0, 1., -1], index=list('abc')) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = np.compress(s > 0, s) + tm.assert_series_equal(result, Series([1.], index=['b'])) + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = np.compress(s < -1, s) + # result empty Index(dtype=object) as the same as original + exp = Series([], dtype='float64', index=Index([], dtype='object')) + tm.assert_series_equal(result, exp) + + s = Series([0, 1., -1], index=[.1, .2, .3]) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = np.compress(s > 0, s) + tm.assert_series_equal(result, Series([1.], index=[.2])) + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = np.compress(s < -1, s) + # result empty Float64Index as the same as original + exp = Series([], dtype='float64', index=Index([], dtype='float64')) + tm.assert_series_equal(result, exp) + + def test_str_accessor_updates_on_inplace(self): + s = pd.Series(list('abc')) + s.drop([0], inplace=True) + assert len(s.str.lower()) == 2 + + def test_str_attribute(self): + # GH9068 + methods = ['strip', 'rstrip', 'lstrip'] + s = Series([' jack', 'jill ', ' jesse ', 'frank']) + for method in methods: + expected = Series([getattr(str, method)(x) for x in s.values]) + assert_series_equal(getattr(Series.str, method)(s.str), expected) + + # str accessor only valid with string values + s = Series(range(5)) + with pytest.raises(AttributeError, match='only use .str accessor'): + s.str.repeat(2) + + def test_empty_method(self): + s_empty = pd.Series() + assert s_empty.empty + + for full_series in [pd.Series([1]), pd.Series(index=[1])]: + assert not full_series.empty + + def test_tab_complete_warning(self, ip): + # https://github.com/pandas-dev/pandas/issues/16409 + pytest.importorskip('IPython', minversion="6.0.0") + from IPython.core.completer import provisionalcompleter + + code = "import pandas as pd; s = pd.Series()" + ip.run_code(code) + with tm.assert_produces_warning(None): + with provisionalcompleter('ignore'): + list(ip.Completer.completions('s.', 1)) + + +class TestCategoricalSeries(object): + + @pytest.mark.parametrize( + "method", + [ + lambda x: x.cat.set_categories([1, 2, 3]), + lambda x: x.cat.reorder_categories([2, 3, 1], ordered=True), + lambda x: x.cat.rename_categories([1, 2, 3]), + lambda x: x.cat.remove_unused_categories(), + lambda x: x.cat.remove_categories([2]), + lambda x: x.cat.add_categories([4]), + lambda x: x.cat.as_ordered(), + lambda x: x.cat.as_unordered(), + ]) + def test_getname_categorical_accessor(self, method): + # GH 17509 + s = Series([1, 2, 3], name='A').astype('category') + expected = 'A' + result = method(s).name + assert result == expected + + def test_cat_accessor(self): + s = Series(Categorical(["a", "b", np.nan, "a"])) + tm.assert_index_equal(s.cat.categories, Index(["a", "b"])) + assert not s.cat.ordered, False + + exp = Categorical(["a", "b", np.nan, "a"], categories=["b", "a"]) + s.cat.set_categories(["b", "a"], inplace=True) + tm.assert_categorical_equal(s.values, exp) + + res = s.cat.set_categories(["b", "a"]) + tm.assert_categorical_equal(res.values, exp) + + s[:] = "a" + s = s.cat.remove_unused_categories() + tm.assert_index_equal(s.cat.categories, Index(["a"])) + + def test_cat_accessor_api(self): + # GH 9322 + from pandas.core.arrays.categorical import CategoricalAccessor + assert Series.cat is CategoricalAccessor + s = Series(list('aabbcde')).astype('category') + assert isinstance(s.cat, CategoricalAccessor) + + invalid = Series([1]) + with pytest.raises(AttributeError, match="only use .cat accessor"): + invalid.cat + assert not hasattr(invalid, 'cat') + + def test_cat_accessor_no_new_attributes(self): + # https://github.com/pandas-dev/pandas/issues/10673 + c = Series(list('aabbcde')).astype('category') + with pytest.raises(AttributeError, + match="You cannot add any new attribute"): + c.cat.xlabel = "a" + + def test_cat_accessor_updates_on_inplace(self): + s = Series(list('abc')).astype('category') + s.drop(0, inplace=True) + s.cat.remove_unused_categories(inplace=True) + assert len(s.cat.categories) == 2 + + def test_categorical_delegations(self): + + # invalid accessor + msg = r"Can only use \.cat accessor with a 'category' dtype" + with pytest.raises(AttributeError, match=msg): + Series([1, 2, 3]).cat + with pytest.raises(AttributeError, match=msg): + Series([1, 2, 3]).cat() + with pytest.raises(AttributeError, match=msg): + Series(['a', 'b', 'c']).cat + with pytest.raises(AttributeError, match=msg): + Series(np.arange(5.)).cat + with pytest.raises(AttributeError, match=msg): + Series([Timestamp('20130101')]).cat + + # Series should delegate calls to '.categories', '.codes', '.ordered' + # and the methods '.set_categories()' 'drop_unused_categories()' to the + # categorical# -*- coding: utf-8 -*- + s = Series(Categorical(["a", "b", "c", "a"], ordered=True)) + exp_categories = Index(["a", "b", "c"]) + tm.assert_index_equal(s.cat.categories, exp_categories) + s.cat.categories = [1, 2, 3] + exp_categories = Index([1, 2, 3]) + tm.assert_index_equal(s.cat.categories, exp_categories) + + exp_codes = Series([0, 1, 2, 0], dtype='int8') + tm.assert_series_equal(s.cat.codes, exp_codes) + + assert s.cat.ordered + s = s.cat.as_unordered() + assert not s.cat.ordered + s.cat.as_ordered(inplace=True) + assert s.cat.ordered + + # reorder + s = Series(Categorical(["a", "b", "c", "a"], ordered=True)) + exp_categories = Index(["c", "b", "a"]) + exp_values = np.array(["a", "b", "c", "a"], dtype=np.object_) + s = s.cat.set_categories(["c", "b", "a"]) + tm.assert_index_equal(s.cat.categories, exp_categories) + tm.assert_numpy_array_equal(s.values.__array__(), exp_values) + tm.assert_numpy_array_equal(s.__array__(), exp_values) + + # remove unused categories + s = Series(Categorical(["a", "b", "b", "a"], categories=["a", "b", "c" + ])) + exp_categories = Index(["a", "b"]) + exp_values = np.array(["a", "b", "b", "a"], dtype=np.object_) + s = s.cat.remove_unused_categories() + tm.assert_index_equal(s.cat.categories, exp_categories) + tm.assert_numpy_array_equal(s.values.__array__(), exp_values) + tm.assert_numpy_array_equal(s.__array__(), exp_values) + + # This method is likely to be confused, so test that it raises an error + # on wrong inputs: + msg = "'Series' object has no attribute 'set_categories'" + with pytest.raises(AttributeError, match=msg): + s.set_categories([4, 3, 2, 1]) + + # right: s.cat.set_categories([4,3,2,1]) + + # GH18862 (let Series.cat.rename_categories take callables) + s = Series(Categorical(["a", "b", "c", "a"], ordered=True)) + result = s.cat.rename_categories(lambda x: x.upper()) + expected = Series(Categorical(["A", "B", "C", "A"], + categories=["A", "B", "C"], + ordered=True)) + tm.assert_series_equal(result, expected) + + def test_dt_accessor_api_for_categorical(self): + # https://github.com/pandas-dev/pandas/issues/10661 + from pandas.core.indexes.accessors import Properties + + s_dr = Series(date_range('1/1/2015', periods=5, tz="MET")) + c_dr = s_dr.astype("category") + + s_pr = Series(period_range('1/1/2015', freq='D', periods=5)) + c_pr = s_pr.astype("category") + + s_tdr = Series(timedelta_range('1 days', '10 days')) + c_tdr = s_tdr.astype("category") + + # only testing field (like .day) + # and bool (is_month_start) + get_ops = lambda x: x._datetimelike_ops + + test_data = [ + ("Datetime", get_ops(DatetimeIndex), s_dr, c_dr), + ("Period", get_ops(PeriodArray), s_pr, c_pr), + ("Timedelta", get_ops(TimedeltaIndex), s_tdr, c_tdr)] + + assert isinstance(c_dr.dt, Properties) + + special_func_defs = [ + ('strftime', ("%Y-%m-%d",), {}), + ('tz_convert', ("EST",), {}), + ('round', ("D",), {}), + ('floor', ("D",), {}), + ('ceil', ("D",), {}), + ('asfreq', ("D",), {}), + # ('tz_localize', ("UTC",), {}), + ] + _special_func_names = [f[0] for f in special_func_defs] + + # the series is already localized + _ignore_names = ['tz_localize', 'components'] + + for name, attr_names, s, c in test_data: + func_names = [f + for f in dir(s.dt) + if not (f.startswith("_") or f in attr_names or f in + _special_func_names or f in _ignore_names)] + + func_defs = [(f, (), {}) for f in func_names] + for f_def in special_func_defs: + if f_def[0] in dir(s.dt): + func_defs.append(f_def) + + for func, args, kwargs in func_defs: + with warnings.catch_warnings(): + if func == 'to_period': + # dropping TZ + warnings.simplefilter("ignore", UserWarning) + res = getattr(c.dt, func)(*args, **kwargs) + exp = getattr(s.dt, func)(*args, **kwargs) + + if isinstance(res, DataFrame): + tm.assert_frame_equal(res, exp) + elif isinstance(res, Series): + tm.assert_series_equal(res, exp) + else: + tm.assert_almost_equal(res, exp) + + for attr in attr_names: + try: + res = getattr(c.dt, attr) + exp = getattr(s.dt, attr) + except Exception as e: + print(name, attr) + raise e + + if isinstance(res, DataFrame): + tm.assert_frame_equal(res, exp) + elif isinstance(res, Series): + tm.assert_series_equal(res, exp) + else: + tm.assert_almost_equal(res, exp) + + invalid = Series([1, 2, 3]).astype('category') + msg = "Can only use .dt accessor with datetimelike" + + with pytest.raises(AttributeError, match=msg): + invalid.dt + assert not hasattr(invalid, 'str') diff --git a/pandas/tests/series/test_apply.py b/pandas/tests/series/test_apply.py index 16d1466bb90fe..162a27db34cb1 100644 --- a/pandas/tests/series/test_apply.py +++ b/pandas/tests/series/test_apply.py @@ -1,43 +1,44 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 +from collections import Counter, OrderedDict, defaultdict +from itertools import chain + import numpy as np -import pandas as pd +import pytest -from pandas import (Index, Series, DataFrame, isnull) +import pandas.compat as compat from pandas.compat import lrange -from pandas import compat -from pandas.util.testing import assert_series_equal -import pandas.util.testing as tm -from .common import TestData +import pandas as pd +from pandas import DataFrame, Index, Series, isna +from pandas.conftest import _get_cython_table_params +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal -class TestSeriesApply(TestData, tm.TestCase): +class TestSeriesApply(): - def test_apply(self): + def test_apply(self, datetime_series): with np.errstate(all='ignore'): - assert_series_equal(self.ts.apply(np.sqrt), np.sqrt(self.ts)) + tm.assert_series_equal(datetime_series.apply(np.sqrt), + np.sqrt(datetime_series)) - # elementwise-apply + # element-wise apply import math - assert_series_equal(self.ts.apply(math.exp), np.exp(self.ts)) - - # how to handle Series result, #2316 - result = self.ts.apply(lambda x: Series( - [x, x ** 2], index=['x', 'x^2'])) - expected = DataFrame({'x': self.ts, 'x^2': self.ts ** 2}) - tm.assert_frame_equal(result, expected) + tm.assert_series_equal(datetime_series.apply(math.exp), + np.exp(datetime_series)) # empty series s = Series(dtype=object, name='foo', index=pd.Index([], name='bar')) rs = s.apply(lambda x: x) tm.assert_series_equal(s, rs) + # check all metadata (GH 9322) - self.assertIsNot(s, rs) - self.assertIs(s.index, rs.index) - self.assertEqual(s.dtype, rs.dtype) - self.assertEqual(s.name, rs.name) + assert s is not rs + assert s.index is rs.index + assert s.dtype == rs.dtype + assert s.name == rs.name # index but no data s = Series(index=[1, 2, 3]) @@ -62,20 +63,38 @@ def test_apply_dont_convert_dtype(self): f = lambda x: x if x > 0 else np.nan result = s.apply(f, convert_dtype=False) - self.assertEqual(result.dtype, object) + assert result.dtype == object + + def test_with_string_args(self, datetime_series): + + for arg in ['sum', 'mean', 'min', 'max', 'std']: + result = datetime_series.apply(arg) + expected = getattr(datetime_series, arg)() + assert result == expected def test_apply_args(self): s = Series(['foo,bar']) result = s.apply(str.split, args=(',', )) - self.assertEqual(result[0], ['foo', 'bar']) - tm.assertIsInstance(result[0], list) + assert result[0] == ['foo', 'bar'] + assert isinstance(result[0], list) + + def test_series_map_box_timestamps(self): + # GH#2689, GH#2627 + ser = Series(pd.date_range('1/1/2000', periods=10)) + + def func(x): + return (x.hour, x.day, x.month) + + # it works! + ser.map(func) + ser.apply(func) def test_apply_box(self): # ufunc will not be boxed. Same test cases as the test_map_box vals = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' # boxed value must be Timestamp instance res = s.apply(lambda x: '{0}_{1}_{2}'.format(x.__class__.__name__, x.day, x.tz)) @@ -85,7 +104,7 @@ def test_apply_box(self): vals = [pd.Timestamp('2011-01-01', tz='US/Eastern'), pd.Timestamp('2011-01-02', tz='US/Eastern')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns, US/Eastern]') + assert s.dtype == 'datetime64[ns, US/Eastern]' res = s.apply(lambda x: '{0}_{1}_{2}'.format(x.__class__.__name__, x.day, x.tz)) exp = pd.Series(['Timestamp_1_US/Eastern', 'Timestamp_2_US/Eastern']) @@ -94,16 +113,16 @@ def test_apply_box(self): # timedelta vals = [pd.Timedelta('1 days'), pd.Timedelta('2 days')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' res = s.apply(lambda x: '{0}_{1}'.format(x.__class__.__name__, x.days)) exp = pd.Series(['Timedelta_1', 'Timedelta_2']) tm.assert_series_equal(res, exp) - # period (object dtype, not boxed) + # period vals = [pd.Period('2011-01-01', freq='M'), pd.Period('2011-01-02', freq='M')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'object') + assert s.dtype == 'Period[M]' res = s.apply(lambda x: '{0}_{1}'.format(x.__class__.__name__, x.freqstr)) exp = pd.Series(['Period_M', 'Period_M']) @@ -136,10 +155,276 @@ def f(x): exp = pd.Series(['Asia/Tokyo'] * 25, name='XX') tm.assert_series_equal(result, exp) + def test_apply_dict_depr(self): + + tsdf = pd.DataFrame(np.random.randn(10, 3), + columns=['A', 'B', 'C'], + index=pd.date_range('1/1/2000', periods=10)) + with tm.assert_produces_warning(FutureWarning): + tsdf.A.agg({'foo': ['sum', 'mean']}) + + @pytest.mark.parametrize('series', [ + ['1-1', '1-1', np.NaN], + ['1-1', '1-2', np.NaN]]) + def test_apply_categorical_with_nan_values(self, series): + # GH 20714 bug fixed in: GH 24275 + s = pd.Series(series, dtype='category') + result = s.apply(lambda x: x.split('-')[0]) + result = result.astype(object) + expected = pd.Series(['1', '1', np.NaN], dtype='category') + expected = expected.astype(object) + tm.assert_series_equal(result, expected) + -class TestSeriesMap(TestData, tm.TestCase): +class TestSeriesAggregate(): + + def test_transform(self, string_series): + # transforming functions + + with np.errstate(all='ignore'): + + f_sqrt = np.sqrt(string_series) + f_abs = np.abs(string_series) + + # ufunc + result = string_series.transform(np.sqrt) + expected = f_sqrt.copy() + assert_series_equal(result, expected) + + result = string_series.apply(np.sqrt) + assert_series_equal(result, expected) + + # list-like + result = string_series.transform([np.sqrt]) + expected = f_sqrt.to_frame().copy() + expected.columns = ['sqrt'] + assert_frame_equal(result, expected) + + result = string_series.transform([np.sqrt]) + assert_frame_equal(result, expected) + + result = string_series.transform(['sqrt']) + assert_frame_equal(result, expected) + + # multiple items in list + # these are in the order as if we are applying both functions per + # series and then concatting + expected = pd.concat([f_sqrt, f_abs], axis=1) + expected.columns = ['sqrt', 'absolute'] + result = string_series.apply([np.sqrt, np.abs]) + assert_frame_equal(result, expected) + + result = string_series.transform(['sqrt', 'abs']) + expected.columns = ['sqrt', 'abs'] + assert_frame_equal(result, expected) + + # dict, provide renaming + expected = pd.concat([f_sqrt, f_abs], axis=1) + expected.columns = ['foo', 'bar'] + expected = expected.unstack().rename('series') + + result = string_series.apply({'foo': np.sqrt, 'bar': np.abs}) + assert_series_equal(result.reindex_like(expected), expected) + + def test_transform_and_agg_error(self, string_series): + # we are trying to transform with an aggregator + with pytest.raises(ValueError): + string_series.transform(['min', 'max']) + + with pytest.raises(ValueError): + with np.errstate(all='ignore'): + string_series.agg(['sqrt', 'max']) + + with pytest.raises(ValueError): + with np.errstate(all='ignore'): + string_series.transform(['sqrt', 'max']) + + with pytest.raises(ValueError): + with np.errstate(all='ignore'): + string_series.agg({'foo': np.sqrt, 'bar': 'sum'}) + + def test_demo(self): + # demonstration tests + s = Series(range(6), dtype='int64', name='series') + + result = s.agg(['min', 'max']) + expected = Series([0, 5], index=['min', 'max'], name='series') + tm.assert_series_equal(result, expected) + + result = s.agg({'foo': 'min'}) + expected = Series([0], index=['foo'], name='series') + tm.assert_series_equal(result, expected) + + # nested renaming + with tm.assert_produces_warning(FutureWarning): + result = s.agg({'foo': ['min', 'max']}) + + expected = DataFrame( + {'foo': [0, 5]}, + index=['min', 'max']).unstack().rename('series') + tm.assert_series_equal(result, expected) + + def test_multiple_aggregators_with_dict_api(self): + + s = Series(range(6), dtype='int64', name='series') + # nested renaming + with tm.assert_produces_warning(FutureWarning): + result = s.agg({'foo': ['min', 'max'], 'bar': ['sum', 'mean']}) + + expected = DataFrame( + {'foo': [5.0, np.nan, 0.0, np.nan], + 'bar': [np.nan, 2.5, np.nan, 15.0]}, + columns=['foo', 'bar'], + index=['max', 'mean', + 'min', 'sum']).unstack().rename('series') + tm.assert_series_equal(result.reindex_like(expected), expected) + + def test_agg_apply_evaluate_lambdas_the_same(self, string_series): + # test that we are evaluating row-by-row first + # before vectorized evaluation + result = string_series.apply(lambda x: str(x)) + expected = string_series.agg(lambda x: str(x)) + tm.assert_series_equal(result, expected) + + result = string_series.apply(str) + expected = string_series.agg(str) + tm.assert_series_equal(result, expected) + + def test_with_nested_series(self, datetime_series): + # GH 2316 + # .agg with a reducer and a transform, what to do + result = datetime_series.apply(lambda x: Series( + [x, x ** 2], index=['x', 'x^2'])) + expected = DataFrame({'x': datetime_series, + 'x^2': datetime_series ** 2}) + tm.assert_frame_equal(result, expected) + + result = datetime_series.agg(lambda x: Series( + [x, x ** 2], index=['x', 'x^2'])) + tm.assert_frame_equal(result, expected) + + def test_replicate_describe(self, string_series): + # this also tests a result set that is all scalars + expected = string_series.describe() + result = string_series.apply(OrderedDict( + [('count', 'count'), + ('mean', 'mean'), + ('std', 'std'), + ('min', 'min'), + ('25%', lambda x: x.quantile(0.25)), + ('50%', 'median'), + ('75%', lambda x: x.quantile(0.75)), + ('max', 'max')])) + assert_series_equal(result, expected) - def test_map(self): + def test_reduce(self, string_series): + # reductions with named functions + result = string_series.agg(['sum', 'mean']) + expected = Series([string_series.sum(), + string_series.mean()], + ['sum', 'mean'], + name=string_series.name) + assert_series_equal(result, expected) + + def test_non_callable_aggregates(self): + # test agg using non-callable series attributes + s = Series([1, 2, None]) + + # Calling agg w/ just a string arg same as calling s.arg + result = s.agg('size') + expected = s.size + assert result == expected + + # test when mixed w/ callable reducers + result = s.agg(['size', 'count', 'mean']) + expected = Series(OrderedDict([('size', 3.0), + ('count', 2.0), + ('mean', 1.5)])) + assert_series_equal(result[expected.index], expected) + + @pytest.mark.parametrize("series, func, expected", chain( + _get_cython_table_params(Series(), [ + ('sum', 0), + ('max', np.nan), + ('min', np.nan), + ('all', True), + ('any', False), + ('mean', np.nan), + ('prod', 1), + ('std', np.nan), + ('var', np.nan), + ('median', np.nan), + ]), + _get_cython_table_params(Series([np.nan, 1, 2, 3]), [ + ('sum', 6), + ('max', 3), + ('min', 1), + ('all', True), + ('any', True), + ('mean', 2), + ('prod', 6), + ('std', 1), + ('var', 1), + ('median', 2), + ]), + _get_cython_table_params(Series('a b c'.split()), [ + ('sum', 'abc'), + ('max', 'c'), + ('min', 'a'), + ('all', 'c'), # see GH12863 + ('any', 'a'), + ]), + )) + def test_agg_cython_table(self, series, func, expected): + # GH21224 + # test reducing functions in + # pandas.core.base.SelectionMixin._cython_table + result = series.agg(func) + if tm.is_number(expected): + assert np.isclose(result, expected, equal_nan=True) + else: + assert result == expected + + @pytest.mark.parametrize("series, func, expected", chain( + _get_cython_table_params(Series(), [ + ('cumprod', Series([], Index([]))), + ('cumsum', Series([], Index([]))), + ]), + _get_cython_table_params(Series([np.nan, 1, 2, 3]), [ + ('cumprod', Series([np.nan, 1, 2, 6])), + ('cumsum', Series([np.nan, 1, 3, 6])), + ]), + _get_cython_table_params(Series('a b c'.split()), [ + ('cumsum', Series(['a', 'ab', 'abc'])), + ]), + )) + def test_agg_cython_table_transform(self, series, func, expected): + # GH21224 + # test transforming functions in + # pandas.core.base.SelectionMixin._cython_table (cumprod, cumsum) + result = series.agg(func) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("series, func, expected", chain( + _get_cython_table_params(Series('a b c'.split()), [ + ('mean', TypeError), # mean raises TypeError + ('prod', TypeError), + ('std', TypeError), + ('var', TypeError), + ('median', TypeError), + ('cumprod', TypeError), + ]) + )) + def test_agg_cython_table_raises(self, series, func, expected): + # GH21224 + with pytest.raises(expected): + # e.g. Series('a b'.split()).cumprod() will raise + series.agg(func) + + +class TestSeriesMap(): + + def test_map(self, datetime_series): index, data = tm.getMixedTypeDict() source = Series(data['B'], index=data['C']) @@ -148,17 +433,17 @@ def test_map(self): merged = target.map(source) for k, v in compat.iteritems(merged): - self.assertEqual(v, source[target[k]]) + assert v == source[target[k]] # input could be a dict merged = target.map(source.to_dict()) for k, v in compat.iteritems(merged): - self.assertEqual(v, source[target[k]]) + assert v == source[target[k]] # function - result = self.ts.map(lambda x: x * 2) - self.assert_series_equal(result, self.ts * 2) + result = datetime_series.map(lambda x: x * 2) + tm.assert_series_equal(result, datetime_series * 2) # GH 10324 a = Series([1, 2, 3, 4]) @@ -166,9 +451,9 @@ def test_map(self): c = Series(["even", "odd", "even", "odd"]) exp = Series(["odd", "even", "odd", np.nan], dtype="category") - self.assert_series_equal(a.map(b), exp) + tm.assert_series_equal(a.map(b), exp) exp = Series(["odd", "even", "odd", np.nan]) - self.assert_series_equal(a.map(c), exp) + tm.assert_series_equal(a.map(c), exp) a = Series(['a', 'b', 'c', 'd']) b = Series([1, 2, 3, 4], @@ -176,9 +461,9 @@ def test_map(self): c = Series([1, 2, 3, 4], index=Index(['b', 'c', 'd', 'e'])) exp = Series([np.nan, 1, 2, 3]) - self.assert_series_equal(a.map(b), exp) + tm.assert_series_equal(a.map(b), exp) exp = Series([np.nan, 1, 2, 3]) - self.assert_series_equal(a.map(c), exp) + tm.assert_series_equal(a.map(c), exp) a = Series(['a', 'b', 'c', 'd']) b = Series(['B', 'C', 'D', 'E'], dtype='category', @@ -187,9 +472,17 @@ def test_map(self): exp = Series(pd.Categorical([np.nan, 'B', 'C', 'D'], categories=['B', 'C', 'D', 'E'])) - self.assert_series_equal(a.map(b), exp) + tm.assert_series_equal(a.map(b), exp) exp = Series([np.nan, 'B', 'C', 'D']) - self.assert_series_equal(a.map(c), exp) + tm.assert_series_equal(a.map(c), exp) + + @pytest.mark.parametrize("index", tm.all_index_generator(10)) + def test_map_empty(self, index): + s = Series(index) + result = s.map({}) + + expected = pd.Series(np.nan, index=s.index) + tm.assert_series_equal(result, expected) def test_map_compat(self): # related GH 8024 @@ -202,25 +495,25 @@ def test_map_int(self): left = Series({'a': 1., 'b': 2., 'c': 3., 'd': 4}) right = Series({1: 11, 2: 22, 3: 33}) - self.assertEqual(left.dtype, np.float_) - self.assertTrue(issubclass(right.dtype.type, np.integer)) + assert left.dtype == np.float_ + assert issubclass(right.dtype.type, np.integer) merged = left.map(right) - self.assertEqual(merged.dtype, np.float_) - self.assertTrue(isnull(merged['d'])) - self.assertTrue(not isnull(merged['c'])) + assert merged.dtype == np.float_ + assert isna(merged['d']) + assert not isna(merged['c']) def test_map_type_inference(self): s = Series(lrange(3)) s2 = s.map(lambda x: np.where(x == 0, 0, 1)) - self.assertTrue(issubclass(s2.dtype.type, np.integer)) + assert issubclass(s2.dtype.type, np.integer) - def test_map_decimal(self): + def test_map_decimal(self, string_series): from decimal import Decimal - result = self.series.map(lambda x: Decimal(str(x))) - self.assertEqual(result.dtype, np.object_) - tm.assertIsInstance(result[0], Decimal) + result = string_series.map(lambda x: Decimal(str(x))) + assert result.dtype == np.object_ + assert isinstance(result[0], Decimal) def test_map_na_exclusion(self): s = Series([1.5, np.nan, 3, np.nan, 5]) @@ -236,18 +529,60 @@ def test_map_dict_with_tuple_keys(self): converted to a multi-index, preventing tuple values from being mapped properly. """ + # GH 18496 df = pd.DataFrame({'a': [(1, ), (2, ), (3, 4), (5, 6)]}) label_mappings = {(1, ): 'A', (2, ): 'B', (3, 4): 'A', (5, 6): 'B'} + df['labels'] = df['a'].map(label_mappings) df['expected_labels'] = pd.Series(['A', 'B', 'A', 'B'], index=df.index) # All labels should be filled now tm.assert_series_equal(df['labels'], df['expected_labels'], check_names=False) + def test_map_counter(self): + s = Series(['a', 'b', 'c'], index=[1, 2, 3]) + counter = Counter() + counter['b'] = 5 + counter['c'] += 1 + result = s.map(counter) + expected = Series([0, 5, 1], index=[1, 2, 3]) + assert_series_equal(result, expected) + + def test_map_defaultdict(self): + s = Series([1, 2, 3], index=['a', 'b', 'c']) + default_dict = defaultdict(lambda: 'blank') + default_dict[1] = 'stuff' + result = s.map(default_dict) + expected = Series(['stuff', 'blank', 'blank'], index=['a', 'b', 'c']) + assert_series_equal(result, expected) + + def test_map_dict_subclass_with_missing(self): + """ + Test Series.map with a dictionary subclass that defines __missing__, + i.e. sets a default value (GH #15999). + """ + class DictWithMissing(dict): + def __missing__(self, key): + return 'missing' + s = Series([1, 2, 3]) + dictionary = DictWithMissing({3: 'three'}) + result = s.map(dictionary) + expected = Series(['missing', 'missing', 'three']) + assert_series_equal(result, expected) + + def test_map_dict_subclass_without_missing(self): + class DictWithoutMissing(dict): + pass + s = Series([1, 2, 3]) + dictionary = DictWithoutMissing({3: 'three'}) + result = s.map(dictionary) + expected = Series([np.nan, np.nan, 'three']) + assert_series_equal(result, expected) + def test_map_box(self): vals = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' # boxed value must be Timestamp instance res = s.map(lambda x: '{0}_{1}_{2}'.format(x.__class__.__name__, x.day, x.tz)) @@ -257,7 +592,7 @@ def test_map_box(self): vals = [pd.Timestamp('2011-01-01', tz='US/Eastern'), pd.Timestamp('2011-01-02', tz='US/Eastern')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns, US/Eastern]') + assert s.dtype == 'datetime64[ns, US/Eastern]' res = s.map(lambda x: '{0}_{1}_{2}'.format(x.__class__.__name__, x.day, x.tz)) exp = pd.Series(['Timestamp_1_US/Eastern', 'Timestamp_2_US/Eastern']) @@ -266,16 +601,16 @@ def test_map_box(self): # timedelta vals = [pd.Timedelta('1 days'), pd.Timedelta('2 days')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' res = s.map(lambda x: '{0}_{1}'.format(x.__class__.__name__, x.days)) exp = pd.Series(['Timedelta_1', 'Timedelta_2']) tm.assert_series_equal(res, exp) - # period (object dtype, not boxed) + # period vals = [pd.Period('2011-01-01', freq='M'), pd.Period('2011-01-02', freq='M')] s = pd.Series(vals) - self.assertEqual(s.dtype, 'object') + assert s.dtype == 'Period[M]' res = s.map(lambda x: '{0}_{1}'.format(x.__class__.__name__, x.freqstr)) exp = pd.Series(['Period_M', 'Period_M']) @@ -296,9 +631,9 @@ def test_map_categorical(self): result = s.map(lambda x: 'A') exp = pd.Series(['A'] * 7, name='XX', index=list('abcdefg')) tm.assert_series_equal(result, exp) - self.assertEqual(result.dtype, np.object) + assert result.dtype == np.object - with tm.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): s.map(lambda x: x, na_action='ignore') def test_map_datetimetz(self): @@ -319,7 +654,7 @@ def test_map_datetimetz(self): exp = pd.Series(list(range(24)) + [0], name='XX', dtype=np.int64) tm.assert_series_equal(result, exp) - with tm.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): s.map(lambda x: x, na_action='ignore') # not vectorized @@ -331,3 +666,14 @@ def f(x): result = s.map(f) exp = pd.Series(['Asia/Tokyo'] * 25, name='XX') tm.assert_series_equal(result, exp) + + @pytest.mark.parametrize("vals,mapping,exp", [ + (list('abc'), {np.nan: 'not NaN'}, [np.nan] * 3 + ['not NaN']), + (list('abc'), {'a': 'a letter'}, ['a letter'] + [np.nan] * 3), + (list(range(3)), {0: 42}, [42] + [np.nan] * 3)]) + def test_map_missing_mixed(self, vals, mapping, exp): + # GH20495 + s = pd.Series(vals + [np.nan]) + result = s.map(mapping) + + tm.assert_series_equal(result, pd.Series(exp)) diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py new file mode 100644 index 0000000000000..687ed59772d18 --- /dev/null +++ b/pandas/tests/series/test_arithmetic.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +import operator + +import numpy as np +import pytest + +import pandas as pd +from pandas import Series, compat +from pandas.core.indexes.period import IncompatibleFrequency +import pandas.util.testing as tm + + +def _permute(obj): + return obj.take(np.random.permutation(len(obj))) + + +class TestSeriesFlexArithmetic(object): + @pytest.mark.parametrize( + 'ts', + [ + (lambda x: x, lambda x: x * 2, False), + (lambda x: x, lambda x: x[::2], False), + (lambda x: x, lambda x: 5, True), + (lambda x: tm.makeFloatSeries(), + lambda x: tm.makeFloatSeries(), + True) + ]) + @pytest.mark.parametrize('opname', ['add', 'sub', 'mul', 'floordiv', + 'truediv', 'div', 'pow']) + def test_flex_method_equivalence(self, opname, ts): + # check that Series.{opname} behaves like Series.__{opname}__, + tser = tm.makeTimeSeries().rename('ts') + + series = ts[0](tser) + other = ts[1](tser) + check_reverse = ts[2] + + if opname == 'div' and compat.PY3: + pytest.skip('div test only for Py3') + + op = getattr(Series, opname) + + if op == 'div': + alt = operator.truediv + else: + alt = getattr(operator, opname) + + result = op(series, other) + expected = alt(series, other) + tm.assert_almost_equal(result, expected) + if check_reverse: + rop = getattr(Series, "r" + opname) + result = rop(series, other) + expected = alt(other, series) + tm.assert_almost_equal(result, expected) + + +class TestSeriesArithmetic(object): + # Some of these may end up in tests/arithmetic, but are not yet sorted + + def test_add_series_with_period_index(self): + rng = pd.period_range('1/1/2000', '1/1/2010', freq='A') + ts = Series(np.random.randn(len(rng)), index=rng) + + result = ts + ts[::2] + expected = ts + ts + expected[1::2] = np.nan + tm.assert_series_equal(result, expected) + + result = ts + _permute(ts[::2]) + tm.assert_series_equal(result, expected) + + msg = "Input has different freq=D from PeriodIndex\\(freq=A-DEC\\)" + with pytest.raises(IncompatibleFrequency, match=msg): + ts + ts.asfreq('D', how="end") + + +# ------------------------------------------------------------------ +# Comparisons + +class TestSeriesFlexComparison(object): + def test_comparison_flex_basic(self): + left = pd.Series(np.random.randn(10)) + right = pd.Series(np.random.randn(10)) + + tm.assert_series_equal(left.eq(right), left == right) + tm.assert_series_equal(left.ne(right), left != right) + tm.assert_series_equal(left.le(right), left < right) + tm.assert_series_equal(left.lt(right), left <= right) + tm.assert_series_equal(left.gt(right), left > right) + tm.assert_series_equal(left.ge(right), left >= right) + + # axis + for axis in [0, None, 'index']: + tm.assert_series_equal(left.eq(right, axis=axis), left == right) + tm.assert_series_equal(left.ne(right, axis=axis), left != right) + tm.assert_series_equal(left.le(right, axis=axis), left < right) + tm.assert_series_equal(left.lt(right, axis=axis), left <= right) + tm.assert_series_equal(left.gt(right, axis=axis), left > right) + tm.assert_series_equal(left.ge(right, axis=axis), left >= right) + + # + msg = 'No axis named 1 for object type' + for op in ['eq', 'ne', 'le', 'le', 'gt', 'ge']: + with pytest.raises(ValueError, match=msg): + getattr(left, op)(right, axis=1) + + +class TestSeriesComparison(object): + def test_comparison_different_length(self): + a = Series(['a', 'b', 'c']) + b = Series(['b', 'a']) + with pytest.raises(ValueError): + a < b + + a = Series([1, 2]) + b = Series([2, 3, 4]) + with pytest.raises(ValueError): + a == b + + @pytest.mark.parametrize('opname', ['eq', 'ne', 'gt', 'lt', 'ge', 'le']) + def test_ser_flex_cmp_return_dtypes(self, opname): + # GH#15115 + ser = Series([1, 3, 2], index=range(3)) + const = 2 + + result = getattr(ser, opname)(const).get_dtype_counts() + tm.assert_series_equal(result, Series([1], ['bool'])) + + @pytest.mark.parametrize('opname', ['eq', 'ne', 'gt', 'lt', 'ge', 'le']) + def test_ser_flex_cmp_return_dtypes_empty(self, opname): + # GH#15115 empty Series case + ser = Series([1, 3, 2], index=range(3)) + empty = ser.iloc[:0] + const = 2 + + result = getattr(empty, opname)(const).get_dtype_counts() + tm.assert_series_equal(result, Series([1], ['bool'])) + + @pytest.mark.parametrize('op', [operator.eq, operator.ne, + operator.le, operator.lt, + operator.ge, operator.gt]) + @pytest.mark.parametrize('names', [(None, None, None), + ('foo', 'bar', None), + ('baz', 'baz', 'baz')]) + def test_ser_cmp_result_names(self, names, op): + # datetime64 dtype + dti = pd.date_range('1949-06-07 03:00:00', + freq='H', periods=5, name=names[0]) + ser = Series(dti).rename(names[1]) + result = op(ser, dti) + assert result.name == names[2] + + # datetime64tz dtype + dti = dti.tz_localize('US/Central') + ser = Series(dti).rename(names[1]) + result = op(ser, dti) + assert result.name == names[2] + + # timedelta64 dtype + tdi = dti - dti.shift(1) + ser = Series(tdi).rename(names[1]) + result = op(ser, tdi) + assert result.name == names[2] + + # categorical + if op in [operator.eq, operator.ne]: + # categorical dtype comparisons raise for inequalities + cidx = tdi.astype('category') + ser = Series(cidx).rename(names[1]) + result = op(ser, cidx) + assert result.name == names[2] diff --git a/pandas/tests/series/test_asof.py b/pandas/tests/series/test_asof.py index 82914a99e2f6c..488fc894b953e 100644 --- a/pandas/tests/series/test_asof.py +++ b/pandas/tests/series/test_asof.py @@ -1,15 +1,13 @@ # coding=utf-8 import numpy as np -from pandas import (offsets, Series, notnull, - isnull, date_range, Timestamp) +import pytest +from pandas import Series, Timestamp, date_range, isna, notna, offsets import pandas.util.testing as tm -from .common import TestData - -class TestSeriesAsof(TestData, tm.TestCase): +class TestSeriesAsof(): def test_basic(self): @@ -21,21 +19,21 @@ def test_basic(self): dates = date_range('1/1/1990', periods=N * 3, freq='25s') result = ts.asof(dates) - self.assertTrue(notnull(result).all()) + assert notna(result).all() lb = ts.index[14] ub = ts.index[30] result = ts.asof(list(dates)) - self.assertTrue(notnull(result).all()) + assert notna(result).all() lb = ts.index[14] ub = ts.index[30] mask = (result.index >= lb) & (result.index < ub) rs = result[mask] - self.assertTrue((rs == ts[lb]).all()) + assert (rs == ts[lb]).all() val = result[result.index[result.index >= ub][0]] - self.assertEqual(ts[ub], val) + assert ts[ub] == val def test_scalar(self): @@ -48,20 +46,20 @@ def test_scalar(self): val1 = ts.asof(ts.index[7]) val2 = ts.asof(ts.index[19]) - self.assertEqual(val1, ts[4]) - self.assertEqual(val2, ts[14]) + assert val1 == ts[4] + assert val2 == ts[14] # accepts strings val1 = ts.asof(str(ts.index[7])) - self.assertEqual(val1, ts[4]) + assert val1 == ts[4] # in there result = ts.asof(ts.index[3]) - self.assertEqual(result, ts[3]) + assert result == ts[3] # no as of value d = ts.index[0] - offsets.BDay() - self.assertTrue(np.isnan(ts.asof(d))) + assert np.isnan(ts.asof(d)) def test_with_nan(self): # basic asof test @@ -96,19 +94,19 @@ def test_periodindex(self): dates = date_range('1/1/1990', periods=N * 3, freq='37min') result = ts.asof(dates) - self.assertTrue(notnull(result).all()) + assert notna(result).all() lb = ts.index[14] ub = ts.index[30] result = ts.asof(list(dates)) - self.assertTrue(notnull(result).all()) + assert notna(result).all() lb = ts.index[14] ub = ts.index[30] pix = PeriodIndex(result.index.values, freq='H') mask = (pix >= lb) & (pix < ub) rs = result[mask] - self.assertTrue((rs == ts[lb]).all()) + assert (rs == ts[lb]).all() ts[5:10] = np.nan ts[15:20] = np.nan @@ -116,19 +114,19 @@ def test_periodindex(self): val1 = ts.asof(ts.index[7]) val2 = ts.asof(ts.index[19]) - self.assertEqual(val1, ts[4]) - self.assertEqual(val2, ts[14]) + assert val1 == ts[4] + assert val2 == ts[14] # accepts strings val1 = ts.asof(str(ts.index[7])) - self.assertEqual(val1, ts[4]) + assert val1 == ts[4] # in there - self.assertEqual(ts.asof(ts.index[3]), ts[3]) + assert ts.asof(ts.index[3]) == ts[3] # no as of value d = ts.index[0].to_timestamp() - offsets.BDay() - self.assertTrue(isnull(ts.asof(d))) + assert isna(ts.asof(d)) def test_errors(self): @@ -138,15 +136,15 @@ def test_errors(self): Timestamp('20130102')]) # non-monotonic - self.assertFalse(s.index.is_monotonic) - with self.assertRaises(ValueError): + assert not s.index.is_monotonic + with pytest.raises(ValueError): s.asof(s.index[0]) # subset with Series N = 10 rng = date_range('1/1/1990', periods=N, freq='53s') s = Series(np.random.randn(N), index=rng) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): s.asof(s.index[0], subset='foo') def test_all_nans(self): @@ -168,7 +166,7 @@ def test_all_nans(self): # testing scalar input date = date_range('1/1/1990', periods=N * 3, freq='25s')[0] result = Series(np.nan, index=rng).asof(date) - assert isnull(result) + assert isna(result) # test name is propagated result = Series(np.nan, index=[1, 2, 3, 4], name='test').asof([4, 5]) diff --git a/pandas/tests/series/test_block_internals.py b/pandas/tests/series/test_block_internals.py new file mode 100644 index 0000000000000..e74b32181ce0f --- /dev/null +++ b/pandas/tests/series/test_block_internals.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +import pandas as pd + +# Segregated collection of methods that require the BlockManager internal data +# structure + + +class TestSeriesBlockInternals(object): + + def test_setitem_invalidates_datetime_index_freq(self): + # GH#24096 altering a datetime64tz Series inplace invalidates the + # `freq` attribute on the underlying DatetimeIndex + + dti = pd.date_range('20130101', periods=3, tz='US/Eastern') + ts = dti[1] + ser = pd.Series(dti) + assert ser._values is not dti + assert ser._values._data.base is not dti._data._data.base + assert dti.freq == 'D' + ser.iloc[1] = pd.NaT + assert ser._values.freq is None + + # check that the DatetimeIndex was not altered in place + assert ser._values is not dti + assert ser._values._data.base is not dti._data._data.base + assert dti[1] == ts + assert dti.freq == 'D' + + def test_dt64tz_setitem_does_not_mutate_dti(self): + # GH#21907, GH#24096 + dti = pd.date_range('2016-01-01', periods=10, tz='US/Pacific') + ts = dti[0] + ser = pd.Series(dti) + assert ser._values is not dti + assert ser._values._data.base is not dti._data._data.base + assert ser._data.blocks[0].values is not dti + assert (ser._data.blocks[0].values._data.base + is not dti._data._data.base) + + ser[::3] = pd.NaT + assert ser[0] is pd.NaT + assert dti[0] == ts diff --git a/pandas/tests/series/test_combine_concat.py b/pandas/tests/series/test_combine_concat.py index d4e5d36c15c68..45e3dffde60f7 100644 --- a/pandas/tests/series/test_combine_concat.py +++ b/pandas/tests/series/test_combine_concat.py @@ -3,39 +3,38 @@ from datetime import datetime -from numpy import nan import numpy as np -import pandas as pd - -from pandas import Series, DataFrame, date_range, DatetimeIndex +from numpy import nan +import pytest -from pandas import compat -from pandas.util.testing import assert_series_equal +import pandas as pd +from pandas import DataFrame, DatetimeIndex, Series, compat, date_range import pandas.util.testing as tm - -from .common import TestData +from pandas.util.testing import assert_frame_equal, assert_series_equal -class TestSeriesCombine(TestData, tm.TestCase): +class TestSeriesCombine(object): - def test_append(self): - appendedSeries = self.series.append(self.objSeries) + def test_append(self, datetime_series, string_series, object_series): + appendedSeries = string_series.append(object_series) for idx, value in compat.iteritems(appendedSeries): - if idx in self.series.index: - self.assertEqual(value, self.series[idx]) - elif idx in self.objSeries.index: - self.assertEqual(value, self.objSeries[idx]) + if idx in string_series.index: + assert value == string_series[idx] + elif idx in object_series.index: + assert value == object_series[idx] else: - self.fail("orphaned index!") + raise AssertionError("orphaned index!") - self.assertRaises(ValueError, self.ts.append, self.ts, - verify_integrity=True) + msg = "Indexes have overlapping values:" + with pytest.raises(ValueError, match=msg): + datetime_series.append(datetime_series, verify_integrity=True) - def test_append_many(self): - pieces = [self.ts[:5], self.ts[5:10], self.ts[10:]] + def test_append_many(self, datetime_series): + pieces = [datetime_series[:5], datetime_series[5:10], + datetime_series[10:]] result = pieces[0].append(pieces[1:]) - assert_series_equal(result, self.ts) + assert_series_equal(result, datetime_series) def test_append_duplicates(self): # GH 13677 @@ -53,11 +52,24 @@ def test_append_duplicates(self): exp, check_index_type=True) msg = 'Indexes have overlapping values:' - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): s1.append(s2, verify_integrity=True) - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): pd.concat([s1, s2], verify_integrity=True) + def test_combine_scalar(self): + # GH 21248 + # Note - combine() with another Series is tested elsewhere because + # it is used when testing operators + s = pd.Series([i * 10 for i in range(5)]) + result = s.combine(3, lambda x, y: x + y) + expected = pd.Series([i * 10 + 3 for i in range(5)]) + tm.assert_series_equal(result, expected) + + result = s.combine(22, lambda x, y: min(x, y)) + expected = pd.Series([min(i * 10, 22) for i in range(5)]) + tm.assert_series_equal(result, expected) + def test_combine_first(self): values = tm.makeIntIndex(20).values.astype(float) series = Series(values, index=tm.makeIntIndex(20)) @@ -68,14 +80,14 @@ def test_combine_first(self): # nothing used from the input combined = series.combine_first(series_copy) - self.assert_series_equal(combined, series) + tm.assert_series_equal(combined, series) # Holes filled from input combined = series_copy.combine_first(series) - self.assertTrue(np.isfinite(combined).all()) + assert np.isfinite(combined).all() - self.assert_series_equal(combined[::2], series[::2]) - self.assert_series_equal(combined[1::2], series_copy[1::2]) + tm.assert_series_equal(combined[::2], series[::2]) + tm.assert_series_equal(combined[1::2], series_copy[1::2]) # mixed types index = tm.makeStringIndex(20) @@ -105,8 +117,40 @@ def test_update(self): df = DataFrame([{"a": 1}, {"a": 3, "b": 2}]) df['c'] = np.nan - # this will fail as long as series is a sub-class of ndarray - # df['c'].update(Series(['foo'],index=[0])) ##### + df['c'].update(Series(['foo'], index=[0])) + expected = DataFrame([[1, np.nan, 'foo'], [3, 2., np.nan]], + columns=['a', 'b', 'c']) + assert_frame_equal(df, expected) + + @pytest.mark.parametrize('other, dtype, expected', [ + # other is int + ([61, 63], 'int32', pd.Series([10, 61, 12], dtype='int32')), + ([61, 63], 'int64', pd.Series([10, 61, 12])), + ([61, 63], float, pd.Series([10., 61., 12.])), + ([61, 63], object, pd.Series([10, 61, 12], dtype=object)), + # other is float, but can be cast to int + ([61., 63.], 'int32', pd.Series([10, 61, 12], dtype='int32')), + ([61., 63.], 'int64', pd.Series([10, 61, 12])), + ([61., 63.], float, pd.Series([10., 61., 12.])), + ([61., 63.], object, pd.Series([10, 61., 12], dtype=object)), + # others is float, cannot be cast to int + ([61.1, 63.1], 'int32', pd.Series([10., 61.1, 12.])), + ([61.1, 63.1], 'int64', pd.Series([10., 61.1, 12.])), + ([61.1, 63.1], float, pd.Series([10., 61.1, 12.])), + ([61.1, 63.1], object, pd.Series([10, 61.1, 12], dtype=object)), + # other is object, cannot be cast + ([(61,), (63,)], 'int32', pd.Series([10, (61,), 12])), + ([(61,), (63,)], 'int64', pd.Series([10, (61,), 12])), + ([(61,), (63,)], float, pd.Series([10., (61,), 12.])), + ([(61,), (63,)], object, pd.Series([10, (61,), 12])) + ]) + def test_update_dtypes(self, other, dtype, expected): + + s = Series([10, 11, 12], dtype=dtype) + other = Series(other, index=[1, 3]) + s.update(other) + + assert_series_equal(s, expected) def test_concat_empty_series_dtypes_roundtrips(self): @@ -115,24 +159,24 @@ def test_concat_empty_series_dtypes_roundtrips(self): 'M8[ns]']) for dtype in dtypes: - self.assertEqual(pd.concat([Series(dtype=dtype)]).dtype, dtype) - self.assertEqual(pd.concat([Series(dtype=dtype), - Series(dtype=dtype)]).dtype, dtype) + assert pd.concat([Series(dtype=dtype)]).dtype == dtype + assert pd.concat([Series(dtype=dtype), + Series(dtype=dtype)]).dtype == dtype def int_result_type(dtype, dtype2): - typs = set([dtype.kind, dtype2.kind]) - if not len(typs - set(['i', 'u', 'b'])) and (dtype.kind == 'i' or - dtype2.kind == 'i'): + typs = {dtype.kind, dtype2.kind} + if not len(typs - {'i', 'u', 'b'}) and (dtype.kind == 'i' or + dtype2.kind == 'i'): return 'i' - elif not len(typs - set(['u', 'b'])) and (dtype.kind == 'u' or - dtype2.kind == 'u'): + elif not len(typs - {'u', 'b'}) and (dtype.kind == 'u' or + dtype2.kind == 'u'): return 'u' return None def float_result_type(dtype, dtype2): - typs = set([dtype.kind, dtype2.kind]) - if not len(typs - set(['f', 'i', 'u'])) and (dtype.kind == 'f' or - dtype2.kind == 'f'): + typs = {dtype.kind, dtype2.kind} + if not len(typs - {'f', 'i', 'u'}) and (dtype.kind == 'f' or + dtype2.kind == 'f'): return 'f' return None @@ -153,58 +197,75 @@ def get_result_type(dtype, dtype2): expected = get_result_type(dtype, dtype2) result = pd.concat([Series(dtype=dtype), Series(dtype=dtype2) ]).dtype - self.assertEqual(result.kind, expected) + assert result.kind == expected + + def test_combine_first_dt_tz_values(self, tz_naive_fixture): + ser1 = pd.Series(pd.DatetimeIndex(['20150101', '20150102', '20150103'], + tz=tz_naive_fixture), + name='ser1') + ser2 = pd.Series(pd.DatetimeIndex(['20160514', '20160515', '20160516'], + tz=tz_naive_fixture), + index=[2, 3, 4], name='ser2') + result = ser1.combine_first(ser2) + exp_vals = pd.DatetimeIndex(['20150101', '20150102', '20150103', + '20160515', '20160516'], + tz=tz_naive_fixture) + exp = pd.Series(exp_vals, name='ser1') + assert_series_equal(exp, result) def test_concat_empty_series_dtypes(self): - # bools - self.assertEqual(pd.concat([Series(dtype=np.bool_), - Series(dtype=np.int32)]).dtype, np.int32) - self.assertEqual(pd.concat([Series(dtype=np.bool_), - Series(dtype=np.float32)]).dtype, - np.object_) - - # datetimelike - self.assertEqual(pd.concat([Series(dtype='m8[ns]'), - Series(dtype=np.bool)]).dtype, np.object_) - self.assertEqual(pd.concat([Series(dtype='m8[ns]'), - Series(dtype=np.int64)]).dtype, np.object_) - self.assertEqual(pd.concat([Series(dtype='M8[ns]'), - Series(dtype=np.bool)]).dtype, np.object_) - self.assertEqual(pd.concat([Series(dtype='M8[ns]'), - Series(dtype=np.int64)]).dtype, np.object_) - self.assertEqual(pd.concat([Series(dtype='M8[ns]'), - Series(dtype=np.bool_), - Series(dtype=np.int64)]).dtype, np.object_) + # booleans + assert pd.concat([Series(dtype=np.bool_), + Series(dtype=np.int32)]).dtype == np.int32 + assert pd.concat([Series(dtype=np.bool_), + Series(dtype=np.float32)]).dtype == np.object_ + + # datetime-like + assert pd.concat([Series(dtype='m8[ns]'), + Series(dtype=np.bool)]).dtype == np.object_ + assert pd.concat([Series(dtype='m8[ns]'), + Series(dtype=np.int64)]).dtype == np.object_ + assert pd.concat([Series(dtype='M8[ns]'), + Series(dtype=np.bool)]).dtype == np.object_ + assert pd.concat([Series(dtype='M8[ns]'), + Series(dtype=np.int64)]).dtype == np.object_ + assert pd.concat([Series(dtype='M8[ns]'), + Series(dtype=np.bool_), + Series(dtype=np.int64)]).dtype == np.object_ # categorical - self.assertEqual(pd.concat([Series(dtype='category'), - Series(dtype='category')]).dtype, - 'category') - self.assertEqual(pd.concat([Series(dtype='category'), - Series(dtype='float64')]).dtype, - 'float64') - self.assertEqual(pd.concat([Series(dtype='category'), - Series(dtype='object')]).dtype, 'object') + assert pd.concat([Series(dtype='category'), + Series(dtype='category')]).dtype == 'category' + # GH 18515 + assert pd.concat([Series(np.array([]), dtype='category'), + Series(dtype='float64')]).dtype == 'float64' + assert pd.concat([Series(dtype='category'), + Series(dtype='object')]).dtype == 'object' # sparse + # TODO: move? result = pd.concat([Series(dtype='float64').to_sparse(), Series( dtype='float64').to_sparse()]) - self.assertEqual(result.dtype, np.float64) - self.assertEqual(result.ftype, 'float64:sparse') + assert result.dtype == 'Sparse[float64]' + assert result.ftype == 'float64:sparse' result = pd.concat([Series(dtype='float64').to_sparse(), Series( dtype='float64')]) - self.assertEqual(result.dtype, np.float64) - self.assertEqual(result.ftype, 'float64:sparse') + # TODO: release-note: concat sparse dtype + expected = pd.core.sparse.api.SparseDtype(np.float64) + assert result.dtype == expected + assert result.ftype == 'float64:sparse' result = pd.concat([Series(dtype='float64').to_sparse(), Series( dtype='object')]) - self.assertEqual(result.dtype, np.object_) - self.assertEqual(result.ftype, 'object:dense') + # TODO: release-note: concat sparse dtype + expected = pd.core.sparse.api.SparseDtype('object') + assert result.dtype == expected + assert result.ftype == 'object:sparse' def test_combine_first_dt64(self): - from pandas.tseries.tools import to_datetime + from pandas.core.tools.datetimes import to_datetime s0 = to_datetime(Series(["2010", np.NaN])) s1 = to_datetime(Series([np.NaN, "2011"])) rs = s0.combine_first(s1) @@ -218,7 +279,7 @@ def test_combine_first_dt64(self): assert_series_equal(rs, xp) -class TestTimeseries(tm.TestCase): +class TestTimeseries(object): def test_append_concat(self): rng = date_range('5/8/2012 1:45', periods=10, freq='5T') @@ -243,13 +304,11 @@ def test_append_concat(self): rng2 = rng.copy() rng1.name = 'foo' rng2.name = 'bar' - self.assertEqual(rng1.append(rng1).name, 'foo') - self.assertIsNone(rng1.append(rng2).name) + assert rng1.append(rng1).name == 'foo' + assert rng1.append(rng2).name is None def test_append_concat_tz(self): - # GH 2938 - tm._skip_if_no_pytz() - + # see gh-2938 rng = date_range('5/8/2012 1:45', periods=10, freq='5T', tz='US/Eastern') rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T', @@ -270,8 +329,7 @@ def test_append_concat_tz(self): tm.assert_index_equal(appended, rng3) def test_append_concat_tz_explicit_pytz(self): - # GH 2938 - tm._skip_if_no_pytz() + # see gh-2938 from pytz import timezone as timezone rng = date_range('5/8/2012 1:45', periods=10, freq='5T', @@ -294,8 +352,7 @@ def test_append_concat_tz_explicit_pytz(self): tm.assert_index_equal(appended, rng3) def test_append_concat_tz_dateutil(self): - # GH 2938 - tm._skip_if_no_dateutil() + # see gh-2938 rng = date_range('5/8/2012 1:45', periods=10, freq='5T', tz='dateutil/US/Eastern') rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T', diff --git a/pandas/tests/series/test_constructors.py b/pandas/tests/series/test_constructors.py index 24e4355fa9f9a..8525b877618c9 100644 --- a/pandas/tests/series/test_constructors.py +++ b/pandas/tests/series/test_constructors.py @@ -1,84 +1,166 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 +from collections import OrderedDict from datetime import datetime, timedelta -from numpy import nan import numpy as np +from numpy import nan import numpy.ma as ma -import pandas as pd - -from pandas.types.common import is_categorical_dtype, is_datetime64tz_dtype -from pandas import (Index, Series, isnull, date_range, - period_range, NaT) -from pandas.core.index import MultiIndex -from pandas.tseries.index import Timestamp, DatetimeIndex +import pytest from pandas._libs import lib from pandas._libs.tslib import iNaT +from pandas.compat import PY36, long, lrange, range, zip -from pandas.compat import lrange, range, zip, OrderedDict, long -from pandas import compat -from pandas.util.testing import assert_series_equal +from pandas.core.dtypes.common import ( + is_categorical_dtype, is_datetime64tz_dtype) + +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, IntervalIndex, MultiIndex, NaT, Series, + Timestamp, date_range, isna, period_range, timedelta_range) +from pandas.api.types import CategoricalDtype +from pandas.core.arrays import period_array import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal -from .common import TestData +class TestSeriesConstructors(): -class TestSeriesConstructors(TestData, tm.TestCase): + def test_invalid_dtype(self): + # GH15520 + msg = 'not understood' + invalid_list = [pd.Timestamp, 'pd.Timestamp', list] + for dtype in invalid_list: + with pytest.raises(TypeError, match=msg): + Series([], name='time', dtype=dtype) def test_scalar_conversion(self): # Pass in scalar is disabled scalar = Series(0.5) - self.assertNotIsInstance(scalar, float) + assert not isinstance(scalar, float) - # coercion - self.assertEqual(float(Series([1.])), 1.0) - self.assertEqual(int(Series([1.])), 1) - self.assertEqual(long(Series([1.])), 1) + # Coercion + assert float(Series([1.])) == 1.0 + assert int(Series([1.])) == 1 + assert long(Series([1.])) == 1 - def test_constructor(self): - self.assertTrue(self.ts.index.is_all_dates) + def test_constructor(self, datetime_series): + empty_series = Series() + + assert datetime_series.index.is_all_dates # Pass in Series - derived = Series(self.ts) - self.assertTrue(derived.index.is_all_dates) + derived = Series(datetime_series) + assert derived.index.is_all_dates - self.assertTrue(tm.equalContents(derived.index, self.ts.index)) + assert tm.equalContents(derived.index, datetime_series.index) # Ensure new index is not created - self.assertEqual(id(self.ts.index), id(derived.index)) + assert id(datetime_series.index) == id(derived.index) # Mixed type Series mixed = Series(['hello', np.NaN], index=[0, 1]) - self.assertEqual(mixed.dtype, np.object_) - self.assertIs(mixed[1], np.NaN) + assert mixed.dtype == np.object_ + assert mixed[1] is np.NaN + + assert not empty_series.index.is_all_dates + assert not Series({}).index.is_all_dates - self.assertFalse(self.empty.index.is_all_dates) - self.assertFalse(Series({}).index.is_all_dates) - self.assertRaises(Exception, Series, np.random.randn(3, 3), - index=np.arange(3)) + # exception raised is of type Exception + with pytest.raises(Exception, match="Data must be 1-dimensional"): + Series(np.random.randn(3, 3), index=np.arange(3)) mixed.name = 'Series' rs = Series(mixed).name xp = 'Series' - self.assertEqual(rs, xp) + assert rs == xp # raise on MultiIndex GH4187 m = MultiIndex.from_arrays([[1, 2], [3, 4]]) - self.assertRaises(NotImplementedError, Series, m) + msg = "initializing a Series from a MultiIndex is not supported" + with pytest.raises(NotImplementedError, match=msg): + Series(m) - def test_constructor_empty(self): + @pytest.mark.parametrize('input_class', [list, dict, OrderedDict]) + def test_constructor_empty(self, input_class): empty = Series() - empty2 = Series([]) + empty2 = Series(input_class()) - # the are Index() and RangeIndex() which don't compare type equal + # these are Index() and RangeIndex() which don't compare type equal # but are just .equals assert_series_equal(empty, empty2, check_index_type=False) - empty = Series(index=lrange(10)) - empty2 = Series(np.nan, index=lrange(10)) - assert_series_equal(empty, empty2) + # With explicit dtype: + empty = Series(dtype='float64') + empty2 = Series(input_class(), dtype='float64') + assert_series_equal(empty, empty2, check_index_type=False) + + # GH 18515 : with dtype=category: + empty = Series(dtype='category') + empty2 = Series(input_class(), dtype='category') + assert_series_equal(empty, empty2, check_index_type=False) + + if input_class is not list: + # With index: + empty = Series(index=lrange(10)) + empty2 = Series(input_class(), index=lrange(10)) + assert_series_equal(empty, empty2) + + # With index and dtype float64: + empty = Series(np.nan, index=lrange(10)) + empty2 = Series(input_class(), index=lrange(10), dtype='float64') + assert_series_equal(empty, empty2) + + # GH 19853 : with empty string, index and dtype str + empty = Series('', dtype=str, index=range(3)) + empty2 = Series('', index=range(3)) + assert_series_equal(empty, empty2) + + @pytest.mark.parametrize('input_arg', [np.nan, float('nan')]) + def test_constructor_nan(self, input_arg): + empty = Series(dtype='float64', index=lrange(10)) + empty2 = Series(input_arg, index=lrange(10)) + + assert_series_equal(empty, empty2, check_index_type=False) + + @pytest.mark.parametrize('dtype', [ + 'f8', 'i8', 'M8[ns]', 'm8[ns]', 'category', 'object', + 'datetime64[ns, UTC]', + ]) + @pytest.mark.parametrize('index', [None, pd.Index([])]) + def test_constructor_dtype_only(self, dtype, index): + # GH-20865 + result = pd.Series(dtype=dtype, index=index) + assert result.dtype == dtype + assert len(result) == 0 + + def test_constructor_no_data_index_order(self): + result = pd.Series(index=['b', 'a', 'c']) + assert result.index.tolist() == ['b', 'a', 'c'] + + def test_constructor_no_data_string_type(self): + # GH 22477 + result = pd.Series(index=[1], dtype=str) + assert np.isnan(result.iloc[0]) + + @pytest.mark.parametrize('item', ['entry', 'ѐ', 13]) + def test_constructor_string_element_string_type(self, item): + # GH 22477 + result = pd.Series(item, index=[1], dtype=str) + assert result.iloc[0] == str(item) + + def test_constructor_dtype_str_na_values(self, string_dtype): + # https://github.com/pandas-dev/pandas/issues/21083 + ser = Series(['x', None], dtype=string_dtype) + result = ser.isna() + expected = Series([False, True]) + tm.assert_series_equal(result, expected) + assert ser.iloc[1] is None + + ser = Series(['x', np.nan], dtype=string_dtype) + assert np.isnan(ser.iloc[1]) def test_constructor_series(self): index1 = ['d', 'b', 'a', 'c'] @@ -88,12 +170,29 @@ def test_constructor_series(self): assert_series_equal(s2, s1.sort_index()) - def test_constructor_iterator(self): + def test_constructor_iterable(self): + # GH 21987 + class Iter(): + def __iter__(self): + for i in range(10): + yield i + expected = Series(list(range(10)), dtype='int64') + result = Series(Iter(), dtype='int64') + assert_series_equal(result, expected) + + def test_constructor_sequence(self): + # GH 21987 expected = Series(list(range(10)), dtype='int64') result = Series(range(10), dtype='int64') assert_series_equal(result, expected) + def test_constructor_single_str(self): + # GH 21987 + expected = Series(['abc']) + result = Series('abc') + assert_series_equal(result, expected) + def test_constructor_list_like(self): # make sure that we are coercing different @@ -105,6 +204,28 @@ def test_constructor_list_like(self): result = Series(obj, index=[0, 1, 2]) assert_series_equal(result, expected) + @pytest.mark.parametrize('input_vals', [ + ([1, 2]), + (['1', '2']), + (list(pd.date_range('1/1/2011', periods=2, freq='H'))), + (list(pd.date_range('1/1/2011', periods=2, freq='H', + tz='US/Eastern'))), + ([pd.Interval(left=0, right=5)]), + ]) + def test_constructor_list_str(self, input_vals, string_dtype): + # GH 16605 + # Ensure that data elements from a list are converted to strings + # when dtype is str, 'str', or 'U' + result = Series(input_vals, dtype=string_dtype) + expected = Series(input_vals).astype(string_dtype) + assert_series_equal(result, expected) + + def test_constructor_list_str_na(self, string_dtype): + result = Series([1.0, 2.0, np.nan], dtype=string_dtype) + expected = Series(['1.0', '2.0', np.nan], dtype=object) + assert_series_equal(result, expected) + assert np.isnan(result[2]) + def test_constructor_generator(self): gen = (i for i in range(10)) @@ -136,16 +257,134 @@ def test_constructor_categorical(self): res = Series(cat) tm.assert_categorical_equal(res.values, cat) + # can cast to a new dtype + result = Series(pd.Categorical([1, 2, 3]), + dtype='int64') + expected = pd.Series([1, 2, 3], dtype='int64') + tm.assert_series_equal(result, expected) + # GH12574 - self.assertRaises( - ValueError, lambda: Series(pd.Categorical([1, 2, 3]), - dtype='int64')) cat = Series(pd.Categorical([1, 2, 3]), dtype='category') - self.assertTrue(is_categorical_dtype(cat)) - self.assertTrue(is_categorical_dtype(cat.dtype)) + assert is_categorical_dtype(cat) + assert is_categorical_dtype(cat.dtype) s = Series([1, 2, 3], dtype='category') - self.assertTrue(is_categorical_dtype(s)) - self.assertTrue(is_categorical_dtype(s.dtype)) + assert is_categorical_dtype(s) + assert is_categorical_dtype(s.dtype) + + def test_constructor_categorical_with_coercion(self): + factor = Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) + # test basic creation / coercion of categoricals + s = Series(factor, name='A') + assert s.dtype == 'category' + assert len(s) == len(factor) + str(s.values) + str(s) + + # in a frame + df = DataFrame({'A': factor}) + result = df['A'] + tm.assert_series_equal(result, s) + result = df.iloc[:, 0] + tm.assert_series_equal(result, s) + assert len(df) == len(factor) + str(df.values) + str(df) + + df = DataFrame({'A': s}) + result = df['A'] + tm.assert_series_equal(result, s) + assert len(df) == len(factor) + str(df.values) + str(df) + + # multiples + df = DataFrame({'A': s, 'B': s, 'C': 1}) + result1 = df['A'] + result2 = df['B'] + tm.assert_series_equal(result1, s) + tm.assert_series_equal(result2, s, check_names=False) + assert result2.name == 'B' + assert len(df) == len(factor) + str(df.values) + str(df) + + # GH8623 + x = DataFrame([[1, 'John P. Doe'], [2, 'Jane Dove'], + [1, 'John P. Doe']], + columns=['person_id', 'person_name']) + x['person_name'] = Categorical(x.person_name + ) # doing this breaks transform + + expected = x.iloc[0].person_name + result = x.person_name.iloc[0] + assert result == expected + + result = x.person_name[0] + assert result == expected + + result = x.person_name.loc[0] + assert result == expected + + def test_constructor_categorical_dtype(self): + result = pd.Series(['a', 'b'], + dtype=CategoricalDtype(['a', 'b', 'c'], + ordered=True)) + assert is_categorical_dtype(result) is True + tm.assert_index_equal(result.cat.categories, pd.Index(['a', 'b', 'c'])) + assert result.cat.ordered + + result = pd.Series(['a', 'b'], dtype=CategoricalDtype(['b', 'a'])) + assert is_categorical_dtype(result) + tm.assert_index_equal(result.cat.categories, pd.Index(['b', 'a'])) + assert result.cat.ordered is False + + # GH 19565 - Check broadcasting of scalar with Categorical dtype + result = Series('a', index=[0, 1], + dtype=CategoricalDtype(['a', 'b'], ordered=True)) + expected = Series(['a', 'a'], index=[0, 1], + dtype=CategoricalDtype(['a', 'b'], ordered=True)) + tm.assert_series_equal(result, expected, check_categorical=True) + + def test_categorical_sideeffects_free(self): + # Passing a categorical to a Series and then changing values in either + # the series or the categorical should not change the values in the + # other one, IF you specify copy! + cat = Categorical(["a", "b", "c", "a"]) + s = Series(cat, copy=True) + assert s.cat is not cat + s.cat.categories = [1, 2, 3] + exp_s = np.array([1, 2, 3, 1], dtype=np.int64) + exp_cat = np.array(["a", "b", "c", "a"], dtype=np.object_) + tm.assert_numpy_array_equal(s.__array__(), exp_s) + tm.assert_numpy_array_equal(cat.__array__(), exp_cat) + + # setting + s[0] = 2 + exp_s2 = np.array([2, 2, 3, 1], dtype=np.int64) + tm.assert_numpy_array_equal(s.__array__(), exp_s2) + tm.assert_numpy_array_equal(cat.__array__(), exp_cat) + + # however, copy is False by default + # so this WILL change values + cat = Categorical(["a", "b", "c", "a"]) + s = Series(cat) + assert s.values is cat + s.cat.categories = [1, 2, 3] + exp_s = np.array([1, 2, 3, 1], dtype=np.int64) + tm.assert_numpy_array_equal(s.__array__(), exp_s) + tm.assert_numpy_array_equal(cat.__array__(), exp_s) + + s[0] = 2 + exp_s2 = np.array([2, 2, 3, 1], dtype=np.int64) + tm.assert_numpy_array_equal(s.__array__(), exp_s2) + tm.assert_numpy_array_equal(cat.__array__(), exp_s2) + + def test_unordered_compare_equal(self): + left = pd.Series(['a', 'b', 'c'], + dtype=CategoricalDtype(['a', 'b'])) + right = pd.Series(pd.Categorical(['a', 'b', np.nan], + categories=['a', 'b'])) + tm.assert_series_equal(left, right) def test_constructor_maskedarray(self): data = ma.masked_all((3, ), dtype=float) @@ -218,29 +457,66 @@ def test_constructor_maskedarray(self): datetime(2001, 1, 3)], index=index, dtype='M8[ns]') assert_series_equal(result, expected) + def test_constructor_maskedarray_hardened(self): + # Check numpy masked arrays with hard masks -- from GH24574 + data = ma.masked_all((3, ), dtype=float).harden_mask() + result = pd.Series(data) + expected = pd.Series([nan, nan, nan]) + tm.assert_series_equal(result, expected) + def test_series_ctor_plus_datetimeindex(self): rng = date_range('20090415', '20090519', freq='B') - data = dict((k, 1) for k in rng) + data = {k: 1 for k in rng} result = Series(data, index=rng) - self.assertIs(result.index, rng) + assert result.index is rng def test_constructor_default_index(self): s = Series([0, 1, 2]) tm.assert_index_equal(s.index, pd.Index(np.arange(3))) + @pytest.mark.parametrize('input', [[1, 2, 3], + (1, 2, 3), + list(range(3)), + pd.Categorical(['a', 'b', 'a']), + (i for i in range(3)), + map(lambda x: x, range(3))]) + def test_constructor_index_mismatch(self, input): + # GH 19342 + # test that construction of a Series with an index of different length + # raises an error + msg = 'Length of passed values is 3, index implies 4' + with pytest.raises(ValueError, match=msg): + Series(input, index=np.arange(4)) + + def test_constructor_numpy_scalar(self): + # GH 19342 + # construction with a numpy scalar + # should not raise + result = Series(np.array(100), index=np.arange(4), dtype='int64') + expected = Series(100, index=np.arange(4), dtype='int64') + tm.assert_series_equal(result, expected) + + def test_constructor_broadcast_list(self): + # GH 19342 + # construction with single-element container and index + # should raise + msg = "Length of passed values is 1, index implies 3" + with pytest.raises(ValueError, match=msg): + Series(['foo'], index=['a', 'b', 'c']) + def test_constructor_corner(self): df = tm.makeTimeDataFrame() objs = [df, df] s = Series(objs, index=[0, 1]) - tm.assertIsInstance(s, Series) + assert isinstance(s, Series) def test_constructor_sanitize(self): s = Series(np.array([1., 1., 8.]), dtype='i8') - self.assertEqual(s.dtype, np.dtype('i8')) + assert s.dtype == np.dtype('i8') s = Series(np.array([1., 1., np.nan]), copy=True, dtype='i8') - self.assertEqual(s.dtype, np.dtype('f8')) + assert s.dtype == np.dtype('f8') def test_constructor_copy(self): # GH15125 @@ -254,16 +530,35 @@ def test_constructor_copy(self): # changes to origin of copy does not affect the copy x[0] = 2. - self.assertFalse(x.equals(y)) - self.assertEqual(x[0], 2.) - self.assertEqual(y[0], 1.) + assert not x.equals(y) + assert x[0] == 2. + assert y[0] == 1. + + @pytest.mark.parametrize( + "index", + [ + pd.date_range('20170101', periods=3, tz='US/Eastern'), + pd.date_range('20170101', periods=3), + pd.timedelta_range('1 day', periods=3), + pd.period_range('2012Q1', periods=3, freq='Q'), + pd.Index(list('abc')), + pd.Int64Index([1, 2, 3]), + pd.RangeIndex(0, 3)], + ids=lambda x: type(x).__name__) + def test_constructor_limit_copies(self, index): + # GH 17449 + # limit copies of input + s = pd.Series(index) + + # we make 1 copy; this is just a smoke test here + assert s._data.blocks[0].values is not index def test_constructor_pass_none(self): s = Series(None, index=lrange(5)) - self.assertEqual(s.dtype, np.float64) + assert s.dtype == np.float64 s = Series(None, index=lrange(5), dtype=object) - self.assertEqual(s.dtype, np.object_) + assert s.dtype == np.object_ # GH 7431 # inference on the index @@ -274,12 +569,12 @@ def test_constructor_pass_none(self): def test_constructor_pass_nan_nat(self): # GH 13467 exp = Series([np.nan, np.nan], dtype=np.float64) - self.assertEqual(exp.dtype, np.float64) + assert exp.dtype == np.float64 tm.assert_series_equal(Series([np.nan, np.nan]), exp) tm.assert_series_equal(Series(np.array([np.nan, np.nan])), exp) exp = Series([pd.NaT, pd.NaT]) - self.assertEqual(exp.dtype, 'datetime64[ns]') + assert exp.dtype == 'datetime64[ns]' tm.assert_series_equal(Series([pd.NaT, pd.NaT]), exp) tm.assert_series_equal(Series(np.array([pd.NaT, pd.NaT])), exp) @@ -290,26 +585,44 @@ def test_constructor_pass_nan_nat(self): tm.assert_series_equal(Series(np.array([np.nan, pd.NaT])), exp) def test_constructor_cast(self): - self.assertRaises(ValueError, Series, ['a', 'b', 'c'], dtype=float) + msg = "could not convert string to float" + with pytest.raises(ValueError, match=msg): + Series(["a", "b", "c"], dtype=float) + + def test_constructor_unsigned_dtype_overflow(self, uint_dtype): + # see gh-15832 + msg = 'Trying to coerce negative values to unsigned integers' + with pytest.raises(OverflowError, match=msg): + Series([-1], dtype=uint_dtype) + + def test_constructor_coerce_float_fail(self, any_int_dtype): + # see gh-15832 + msg = "Trying to coerce float values to integers" + with pytest.raises(ValueError, match=msg): + Series([1, 2, 3.5], dtype=any_int_dtype) + + def test_constructor_coerce_float_valid(self, float_dtype): + s = Series([1, 2, 3.5], dtype=float_dtype) + expected = Series([1, 2, 3.5]).astype(float_dtype) + assert_series_equal(s, expected) - def test_constructor_dtype_nocast(self): - # 1572 + def test_constructor_dtype_no_cast(self): + # see gh-1572 s = Series([1, 2, 3]) - s2 = Series(s, dtype=np.int64) s2[1] = 5 - self.assertEqual(s[1], 5) + assert s[1] == 5 def test_constructor_datelike_coercion(self): # GH 9477 - # incorrectly infering on dateimelike looking when object dtype is + # incorrectly inferring on dateimelike looking when object dtype is # specified s = Series([Timestamp('20130101'), 'NOV'], dtype=object) - self.assertEqual(s.iloc[0], Timestamp('20130101')) - self.assertEqual(s.iloc[1], 'NOV') - self.assertTrue(s.dtype == object) + assert s.iloc[0] == Timestamp('20130101') + assert s.iloc[1] == 'NOV' + assert s.dtype == object # the dtype was being reset on the slicing and re-inferred to datetime # even thought the blocks are mixed @@ -323,30 +636,38 @@ def test_constructor_datelike_coercion(self): 'mat': mat}, index=belly) result = df.loc['3T19'] - self.assertTrue(result.dtype == object) + assert result.dtype == object result = df.loc['216'] - self.assertTrue(result.dtype == object) + assert result.dtype == object + + def test_constructor_datetimes_with_nulls(self): + # gh-15869 + for arr in [np.array([None, None, None, None, + datetime.now(), None]), + np.array([None, None, datetime.now(), None])]: + result = Series(arr) + assert result.dtype == 'M8[ns]' def test_constructor_dtype_datetime64(self): s = Series(iNaT, dtype='M8[ns]', index=lrange(5)) - self.assertTrue(isnull(s).all()) + assert isna(s).all() # in theory this should be all nulls, but since # we are not specifying a dtype is ambiguous s = Series(iNaT, index=lrange(5)) - self.assertFalse(isnull(s).all()) + assert not isna(s).all() s = Series(nan, dtype='M8[ns]', index=lrange(5)) - self.assertTrue(isnull(s).all()) + assert isna(s).all() s = Series([datetime(2001, 1, 2, 0, 0), iNaT], dtype='M8[ns]') - self.assertTrue(isnull(s[1])) - self.assertEqual(s.dtype, 'M8[ns]') + assert isna(s[1]) + assert s.dtype == 'M8[ns]' s = Series([datetime(2001, 1, 2, 0, 0), nan], dtype='M8[ns]') - self.assertTrue(isnull(s[1])) - self.assertEqual(s.dtype, 'M8[ns]') + assert isna(s[1]) + assert s.dtype == 'M8[ns]' # GH3416 dates = [ @@ -356,32 +677,35 @@ def test_constructor_dtype_datetime64(self): ] s = Series(dates) - self.assertEqual(s.dtype, 'M8[ns]') + assert s.dtype == 'M8[ns]' s.iloc[0] = np.nan - self.assertEqual(s.dtype, 'M8[ns]') - - # invalid astypes - for t in ['s', 'D', 'us', 'ms']: - self.assertRaises(TypeError, s.astype, 'M8[%s]' % t) + assert s.dtype == 'M8[ns]' # GH3414 related - self.assertRaises(TypeError, lambda x: Series( + # msg = (r"cannot astype a datetimelike from \[datetime64\[ns\]\] to" + # r" \[int32\]") + # with pytest.raises(TypeError, match=msg): + # Series(Series(dates).astype('int') / 1000000, dtype='M8[ms]') + pytest.raises(TypeError, lambda x: Series( Series(dates).astype('int') / 1000000, dtype='M8[ms]')) - self.assertRaises(TypeError, - lambda x: Series(dates, dtype='datetime64')) + + msg = (r"The 'datetime64' dtype has no unit\. Please pass in" + r" 'datetime64\[ns\]' instead\.") + with pytest.raises(ValueError, match=msg): + Series(dates, dtype='datetime64') # invalid dates can be help as object result = Series([datetime(2, 1, 1)]) - self.assertEqual(result[0], datetime(2, 1, 1, 0, 0)) + assert result[0] == datetime(2, 1, 1, 0, 0) result = Series([datetime(3000, 1, 1)]) - self.assertEqual(result[0], datetime(3000, 1, 1, 0, 0)) + assert result[0] == datetime(3000, 1, 1, 0, 0) # don't mix types result = Series([Timestamp('20130101'), 1], index=['a', 'b']) - self.assertEqual(result['a'], Timestamp('20130101')) - self.assertEqual(result['b'], 1) + assert result['a'] == Timestamp('20130101') + assert result['b'] == 1 # GH6529 # coerce datetime64 non-ns properly @@ -406,45 +730,45 @@ def test_constructor_dtype_datetime64(self): dates2 = np.array([d.date() for d in dates.to_pydatetime()], dtype=object) series1 = Series(dates2, dates) - self.assert_numpy_array_equal(series1.values, dates2) - self.assertEqual(series1.dtype, object) + tm.assert_numpy_array_equal(series1.values, dates2) + assert series1.dtype == object # these will correctly infer a datetime s = Series([None, pd.NaT, '2013-08-05 15:30:00.000001']) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' s = Series([np.nan, pd.NaT, '2013-08-05 15:30:00.000001']) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' s = Series([pd.NaT, None, '2013-08-05 15:30:00.000001']) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' s = Series([pd.NaT, np.nan, '2013-08-05 15:30:00.000001']) - self.assertEqual(s.dtype, 'datetime64[ns]') + assert s.dtype == 'datetime64[ns]' # tz-aware (UTC and other tz's) # GH 8411 dr = date_range('20130101', periods=3) - self.assertTrue(Series(dr).iloc[0].tz is None) + assert Series(dr).iloc[0].tz is None dr = date_range('20130101', periods=3, tz='UTC') - self.assertTrue(str(Series(dr).iloc[0].tz) == 'UTC') + assert str(Series(dr).iloc[0].tz) == 'UTC' dr = date_range('20130101', periods=3, tz='US/Eastern') - self.assertTrue(str(Series(dr).iloc[0].tz) == 'US/Eastern') + assert str(Series(dr).iloc[0].tz) == 'US/Eastern' # non-convertible s = Series([1479596223000, -1479590, pd.NaT]) - self.assertTrue(s.dtype == 'object') - self.assertTrue(s[2] is pd.NaT) - self.assertTrue('NaT' in str(s)) + assert s.dtype == 'object' + assert s[2] is pd.NaT + assert 'NaT' in str(s) # if we passed a NaT it remains s = Series([datetime(2010, 1, 1), datetime(2, 1, 1), pd.NaT]) - self.assertTrue(s.dtype == 'object') - self.assertTrue(s[2] is pd.NaT) - self.assertTrue('NaT' in str(s)) + assert s.dtype == 'object' + assert s[2] is pd.NaT + assert 'NaT' in str(s) # if we passed a nan it remains s = Series([datetime(2010, 1, 1), datetime(2, 1, 1), np.nan]) - self.assertTrue(s.dtype == 'object') - self.assertTrue(s[2] is np.nan) - self.assertTrue('NaN' in str(s)) + assert s.dtype == 'object' + assert s[2] is np.nan + assert 'NaN' in str(s) def test_constructor_with_datetime_tz(self): @@ -453,27 +777,27 @@ def test_constructor_with_datetime_tz(self): dr = date_range('20130101', periods=3, tz='US/Eastern') s = Series(dr) - self.assertTrue(s.dtype.name == 'datetime64[ns, US/Eastern]') - self.assertTrue(s.dtype == 'datetime64[ns, US/Eastern]') - self.assertTrue(is_datetime64tz_dtype(s.dtype)) - self.assertTrue('datetime64[ns, US/Eastern]' in str(s)) + assert s.dtype.name == 'datetime64[ns, US/Eastern]' + assert s.dtype == 'datetime64[ns, US/Eastern]' + assert is_datetime64tz_dtype(s.dtype) + assert 'datetime64[ns, US/Eastern]' in str(s) # export result = s.values - self.assertIsInstance(result, np.ndarray) - self.assertTrue(result.dtype == 'datetime64[ns]') + assert isinstance(result, np.ndarray) + assert result.dtype == 'datetime64[ns]' exp = pd.DatetimeIndex(result) exp = exp.tz_localize('UTC').tz_convert(tz=s.dt.tz) - self.assert_index_equal(dr, exp) + tm.assert_index_equal(dr, exp) # indexing result = s.iloc[0] - self.assertEqual(result, Timestamp('2013-01-01 00:00:00-0500', - tz='US/Eastern', freq='D')) + assert result == Timestamp('2013-01-01 00:00:00-0500', + tz='US/Eastern', freq='D') result = s[0] - self.assertEqual(result, Timestamp('2013-01-01 00:00:00-0500', - tz='US/Eastern', freq='D')) + assert result == Timestamp('2013-01-01 00:00:00-0500', + tz='US/Eastern', freq='D') result = s[Series([True, True, False], index=s.index)] assert_series_equal(result, s[0:2]) @@ -485,36 +809,17 @@ def test_constructor_with_datetime_tz(self): result = pd.concat([s.iloc[0:1], s.iloc[1:]]) assert_series_equal(result, s) - # astype - result = s.astype(object) - expected = Series(DatetimeIndex(s._values).asobject) - assert_series_equal(result, expected) - - result = Series(s.values).dt.tz_localize('UTC').dt.tz_convert(s.dt.tz) - assert_series_equal(result, s) - - # astype - datetime64[ns, tz] - result = Series(s.values).astype('datetime64[ns, US/Eastern]') - assert_series_equal(result, s) - - result = Series(s.values).astype(s.dtype) - assert_series_equal(result, s) - - result = s.astype('datetime64[ns, CET]') - expected = Series(date_range('20130101 06:00:00', periods=3, tz='CET')) - assert_series_equal(result, expected) - # short str - self.assertTrue('datetime64[ns, US/Eastern]' in str(s)) + assert 'datetime64[ns, US/Eastern]' in str(s) # formatting with NaT result = s.shift() - self.assertTrue('datetime64[ns, US/Eastern]' in str(result)) - self.assertTrue('NaT' in str(result)) + assert 'datetime64[ns, US/Eastern]' in str(result) + assert 'NaT' in str(result) # long str t = Series(date_range('20130101', periods=1000, tz='US/Eastern')) - self.assertTrue('datetime64[ns, US/Eastern]' in str(t)) + assert 'datetime64[ns, US/Eastern]' in str(t) result = pd.DatetimeIndex(s, freq='infer') tm.assert_index_equal(result, dr) @@ -522,19 +827,52 @@ def test_constructor_with_datetime_tz(self): # inference s = Series([pd.Timestamp('2013-01-01 13:00:00-0800', tz='US/Pacific'), pd.Timestamp('2013-01-02 14:00:00-0800', tz='US/Pacific')]) - self.assertTrue(s.dtype == 'datetime64[ns, US/Pacific]') - self.assertTrue(lib.infer_dtype(s) == 'datetime64') + assert s.dtype == 'datetime64[ns, US/Pacific]' + assert lib.infer_dtype(s, skipna=True) == 'datetime64' s = Series([pd.Timestamp('2013-01-01 13:00:00-0800', tz='US/Pacific'), pd.Timestamp('2013-01-02 14:00:00-0800', tz='US/Eastern')]) - self.assertTrue(s.dtype == 'object') - self.assertTrue(lib.infer_dtype(s) == 'datetime') + assert s.dtype == 'object' + assert lib.infer_dtype(s, skipna=True) == 'datetime' # with all NaT s = Series(pd.NaT, index=[0, 1], dtype='datetime64[ns, US/Eastern]') expected = Series(pd.DatetimeIndex(['NaT', 'NaT'], tz='US/Eastern')) assert_series_equal(s, expected) + @pytest.mark.parametrize("arr_dtype", [np.int64, np.float64]) + @pytest.mark.parametrize("dtype", ["M8", "m8"]) + @pytest.mark.parametrize("unit", ['ns', 'us', 'ms', 's', 'h', 'm', 'D']) + def test_construction_to_datetimelike_unit(self, arr_dtype, dtype, unit): + # tests all units + # gh-19223 + dtype = "{}[{}]".format(dtype, unit) + arr = np.array([1, 2, 3], dtype=arr_dtype) + s = Series(arr) + result = s.astype(dtype) + expected = Series(arr.astype(dtype)) + + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('arg', + ['2013-01-01 00:00:00', pd.NaT, np.nan, None]) + def test_constructor_with_naive_string_and_datetimetz_dtype(self, arg): + # GH 17415: With naive string + result = Series([arg], dtype='datetime64[ns, CET]') + expected = Series(pd.Timestamp(arg)).dt.tz_localize('CET') + assert_series_equal(result, expected) + + def test_construction_interval(self): + # construction from interval & array of intervals + index = IntervalIndex.from_breaks(np.arange(3), closed='right') + result = Series(index) + repr(result) + str(result) + tm.assert_index_equal(Index(result.values), index) + + result = Series(index.values) + tm.assert_index_equal(Index(result.values), index) + def test_construction_consistency(self): # make sure that we are not re-localizing upon construction @@ -550,17 +888,33 @@ def test_construction_consistency(self): result = Series(s.values, dtype=s.dtype) tm.assert_series_equal(result, s) + def test_constructor_infer_period(self): + data = [pd.Period('2000', 'D'), pd.Period('2001', 'D'), None] + result = pd.Series(data) + expected = pd.Series(period_array(data)) + tm.assert_series_equal(result, expected) + assert result.dtype == 'Period[D]' + + data = np.asarray(data, dtype=object) + tm.assert_series_equal(result, expected) + assert result.dtype == 'Period[D]' + + def test_constructor_period_incompatible_frequency(self): + data = [pd.Period('2000', 'D'), pd.Period('2001', 'A')] + result = pd.Series(data) + assert result.dtype == object + assert result.tolist() == data + def test_constructor_periodindex(self): # GH7932 # converting a PeriodIndex when put in a Series pi = period_range('20130101', periods=5, freq='D') s = Series(pi) - expected = Series(pi.asobject) + assert s.dtype == 'Period[D]' + expected = Series(pi.astype(object)) assert_series_equal(s, expected) - self.assertEqual(s.dtype, 'object') - def test_constructor_dict(self): d = {'a': 0., 'b': 1., 'c': 2.} result = Series(d, index=['b', 'c', 'd', 'a']) @@ -575,47 +929,32 @@ def test_constructor_dict(self): expected.iloc[1] = 1 assert_series_equal(result, expected) - def test_constructor_dict_multiindex(self): - check = lambda result, expected: tm.assert_series_equal( - result, expected, check_dtype=True, check_series_type=True) - d = {('a', 'a'): 0., ('b', 'a'): 1., ('b', 'c'): 2.} - _d = sorted(d.items()) - ser = Series(d) - expected = Series([x[1] for x in _d], - index=MultiIndex.from_tuples([x[0] for x in _d])) - check(ser, expected) - - d['z'] = 111. - _d.insert(0, ('z', d['z'])) - ser = Series(d) - expected = Series([x[1] for x in _d], index=Index( - [x[0] for x in _d], tupleize_cols=False)) - ser = ser.reindex(index=expected.index) - check(ser, expected) - - def test_constructor_dict_timedelta_index(self): - # GH #12169 : Resample category data with timedelta index - # construct Series from dict as data and TimedeltaIndex as index - # will result NaN in result Series data - expected = Series( - data=['A', 'B', 'C'], - index=pd.to_timedelta([0, 10, 20], unit='s') - ) - - result = Series( - data={pd.to_timedelta(0, unit='s'): 'A', - pd.to_timedelta(10, unit='s'): 'B', - pd.to_timedelta(20, unit='s'): 'C'}, - index=pd.to_timedelta([0, 10, 20], unit='s') - ) - # this should work + def test_constructor_dict_order(self): + # GH19018 + # initialization ordering: by insertion order if python>= 3.6, else + # order by value + d = {'b': 1, 'a': 0, 'c': 2} + result = Series(d) + if PY36: + expected = Series([1, 0, 2], index=list('bac')) + else: + expected = Series([0, 1, 2], index=list('abc')) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("value", [2, np.nan, None, float('nan')]) + def test_constructor_dict_nan_key(self, value): + # GH 18480 + d = {1: 'a', value: 'b', float('nan'): 'c', 4: 'd'} + result = Series(d).sort_values() + expected = Series(['a', 'b', 'c', 'd'], index=[1, value, np.nan, 4]) assert_series_equal(result, expected) - def test_constructor_subclass_dict(self): - data = tm.TestSubDict((x, 10.0 * x) for x in range(10)) - series = Series(data) - refseries = Series(dict(compat.iteritems(data))) - assert_series_equal(refseries, series) + # MultiIndex: + d = {(1, 1): 'a', (2, np.nan): 'b', (3, value): 'c'} + result = Series(d).sort_values() + expected = Series(['a', 'b', 'c'], + index=Index([(1, 1), (2, np.nan), (3, value)])) + assert_series_equal(result, expected) def test_constructor_dict_datetime64_index(self): # GH 9456 @@ -640,163 +979,166 @@ def create_data(constructor): assert_series_equal(result_datetime, expected) assert_series_equal(result_Timestamp, expected) - def test_orderedDict_ctor(self): - # GH3283 - import pandas - import random - data = OrderedDict([('col%s' % i, random.random()) for i in range(12)]) - s = pandas.Series(data) - self.assertTrue(all(s.values == list(data.values()))) - - def test_orderedDict_subclass_ctor(self): - # GH3283 - import pandas - import random - - class A(OrderedDict): - pass - - data = A([('col%s' % i, random.random()) for i in range(12)]) - s = pandas.Series(data) - self.assertTrue(all(s.values == list(data.values()))) - def test_constructor_list_of_tuples(self): data = [(1, 1), (2, 2), (2, 3)] s = Series(data) - self.assertEqual(list(s), data) + assert list(s) == data def test_constructor_tuple_of_tuples(self): data = ((1, 1), (2, 2), (2, 3)) s = Series(data) - self.assertEqual(tuple(s), data) + assert tuple(s) == data + + def test_constructor_dict_of_tuples(self): + data = {(1, 2): 3, + (None, 5): 6} + result = Series(data).sort_values() + expected = Series([3, 6], + index=MultiIndex.from_tuples([(1, 2), (None, 5)])) + tm.assert_series_equal(result, expected) def test_constructor_set(self): - values = set([1, 2, 3, 4, 5]) - self.assertRaises(TypeError, Series, values) + values = {1, 2, 3, 4, 5} + with pytest.raises(TypeError, match="'set' type is unordered"): + Series(values) values = frozenset(values) - self.assertRaises(TypeError, Series, values) + with pytest.raises(TypeError, match="'frozenset' type is unordered"): + Series(values) + # https://github.com/pandas-dev/pandas/issues/22698 + @pytest.mark.filterwarnings("ignore:elementwise comparison:FutureWarning") def test_fromDict(self): data = {'a': 0, 'b': 1, 'c': 2, 'd': 3} series = Series(data) - self.assertTrue(tm.is_sorted(series.index)) + assert tm.is_sorted(series.index) data = {'a': 0, 'b': '1', 'c': '2', 'd': datetime.now()} series = Series(data) - self.assertEqual(series.dtype, np.object_) + assert series.dtype == np.object_ data = {'a': 0, 'b': '1', 'c': '2', 'd': '3'} series = Series(data) - self.assertEqual(series.dtype, np.object_) + assert series.dtype == np.object_ data = {'a': '0', 'b': '1'} series = Series(data, dtype=float) - self.assertEqual(series.dtype, np.float64) + assert series.dtype == np.float64 - def test_fromValue(self): + def test_fromValue(self, datetime_series): - nans = Series(np.NaN, index=self.ts.index) - self.assertEqual(nans.dtype, np.float_) - self.assertEqual(len(nans), len(self.ts)) + nans = Series(np.NaN, index=datetime_series.index) + assert nans.dtype == np.float_ + assert len(nans) == len(datetime_series) - strings = Series('foo', index=self.ts.index) - self.assertEqual(strings.dtype, np.object_) - self.assertEqual(len(strings), len(self.ts)) + strings = Series('foo', index=datetime_series.index) + assert strings.dtype == np.object_ + assert len(strings) == len(datetime_series) d = datetime.now() - dates = Series(d, index=self.ts.index) - self.assertEqual(dates.dtype, 'M8[ns]') - self.assertEqual(len(dates), len(self.ts)) + dates = Series(d, index=datetime_series.index) + assert dates.dtype == 'M8[ns]' + assert len(dates) == len(datetime_series) # GH12336 # Test construction of categorical series from value - categorical = Series(0, index=self.ts.index, dtype="category") - expected = Series(0, index=self.ts.index).astype("category") - self.assertEqual(categorical.dtype, 'category') - self.assertEqual(len(categorical), len(self.ts)) + categorical = Series(0, index=datetime_series.index, dtype="category") + expected = Series(0, index=datetime_series.index).astype("category") + assert categorical.dtype == 'category' + assert len(categorical) == len(datetime_series) tm.assert_series_equal(categorical, expected) def test_constructor_dtype_timedelta64(self): # basic td = Series([timedelta(days=i) for i in range(3)]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([timedelta(days=1)]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([timedelta(days=1), timedelta(days=2), np.timedelta64( 1, 's')]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' # mixed with NaT td = Series([timedelta(days=1), NaT], dtype='m8[ns]') - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([timedelta(days=1), np.nan], dtype='m8[ns]') - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([np.timedelta64(300000000), pd.NaT], dtype='m8[ns]') - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' # improved inference # GH5689 td = Series([np.timedelta64(300000000), NaT]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' # because iNaT is int, not coerced to timedelta td = Series([np.timedelta64(300000000), iNaT]) - self.assertEqual(td.dtype, 'object') + assert td.dtype == 'object' td = Series([np.timedelta64(300000000), np.nan]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([pd.NaT, np.timedelta64(300000000)]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' td = Series([np.timedelta64(1, 's')]) - self.assertEqual(td.dtype, 'timedelta64[ns]') + assert td.dtype == 'timedelta64[ns]' # these are frequency conversion astypes # for t in ['s', 'D', 'us', 'ms']: - # self.assertRaises(TypeError, td.astype, 'm8[%s]' % t) + # pytest.raises(TypeError, td.astype, 'm8[%s]' % t) # valid astype td.astype('int64') # invalid casting - self.assertRaises(TypeError, td.astype, 'int32') + msg = (r"cannot astype a timedelta from \[timedelta64\[ns\]\] to" + r" \[int32\]") + with pytest.raises(TypeError, match=msg): + td.astype('int32') # this is an invalid casting - def f(): + msg = "Could not convert object to NumPy timedelta" + with pytest.raises(ValueError, match=msg): Series([timedelta(days=1), 'foo'], dtype='m8[ns]') - self.assertRaises(Exception, f) - # leave as object here td = Series([timedelta(days=i) for i in range(3)] + ['foo']) - self.assertEqual(td.dtype, 'object') + assert td.dtype == 'object' # these will correctly infer a timedelta s = Series([None, pd.NaT, '1 Day']) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' s = Series([np.nan, pd.NaT, '1 Day']) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' s = Series([pd.NaT, None, '1 Day']) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' s = Series([pd.NaT, np.nan, '1 Day']) - self.assertEqual(s.dtype, 'timedelta64[ns]') + assert s.dtype == 'timedelta64[ns]' + + # GH 16406 + def test_constructor_mixed_tz(self): + s = Series([Timestamp('20130101'), + Timestamp('20130101', tz='US/Eastern')]) + expected = Series([Timestamp('20130101'), + Timestamp('20130101', tz='US/Eastern')], + dtype='object') + assert_series_equal(s, expected) def test_NaT_scalar(self): series = Series([0, 1000, 2000, iNaT], dtype='M8[ns]') val = series[3] - self.assertTrue(isnull(val)) + assert isna(val) series[2] = val - self.assertTrue(isnull(series[2])) + assert isna(series[2]) def test_NaT_cast(self): # GH10747 @@ -808,26 +1150,119 @@ def test_constructor_name_hashable(self): for n in [777, 777., 'name', datetime(2001, 11, 11), (1, ), u"\u05D0"]: for data in [[1, 2, 3], np.ones(3), {'a': 0, 'b': 1}]: s = Series(data, name=n) - self.assertEqual(s.name, n) + assert s.name == n def test_constructor_name_unhashable(self): + msg = r"Series\.name must be a hashable type" for n in [['name_list'], np.ones(2), {1: 2}]: for data in [['name_list'], np.ones(2), {1: 2}]: - self.assertRaises(TypeError, Series, data, name=n) + with pytest.raises(TypeError, match=msg): + Series(data, name=n) def test_auto_conversion(self): series = Series(list(date_range('1/1/2000', periods=10))) - self.assertEqual(series.dtype, 'M8[ns]') + assert series.dtype == 'M8[ns]' - def test_constructor_cant_cast_datetime64(self): - msg = "Cannot cast datetime64 to " - with tm.assertRaisesRegexp(TypeError, msg): - Series(date_range('1/1/2000', periods=10), dtype=float) + def test_convert_non_ns(self): + # convert from a numpy array of non-ns timedelta64 + arr = np.array([1, 2, 3], dtype='timedelta64[s]') + s = Series(arr) + expected = Series(pd.timedelta_range('00:00:01', periods=3, freq='s')) + assert_series_equal(s, expected) - with tm.assertRaisesRegexp(TypeError, msg): - Series(date_range('1/1/2000', periods=10), dtype=int) + # convert from a numpy array of non-ns datetime64 + # note that creating a numpy datetime64 is in LOCAL time!!!! + # seems to work for M8[D], but not for M8[s] + + s = Series(np.array(['2013-01-01', '2013-01-02', + '2013-01-03'], dtype='datetime64[D]')) + assert_series_equal(s, Series(date_range('20130101', periods=3, + freq='D'))) + + # s = Series(np.array(['2013-01-01 00:00:01','2013-01-01 + # 00:00:02','2013-01-01 00:00:03'],dtype='datetime64[s]')) + + # assert_series_equal(s,date_range('20130101 + # 00:00:01',period=3,freq='s')) + + @pytest.mark.parametrize( + "index", + [ + date_range('1/1/2000', periods=10), + timedelta_range('1 day', periods=10), + period_range('2000-Q1', periods=10, freq='Q')], + ids=lambda x: type(x).__name__) + def test_constructor_cant_cast_datetimelike(self, index): + + # floats are not ok + msg = "Cannot cast {}.*? to ".format( + # strip Index to convert PeriodIndex -> Period + # We don't care whether the error message says + # PeriodIndex or PeriodArray + type(index).__name__.rstrip("Index") + ) + with pytest.raises(TypeError, match=msg): + Series(index, dtype=float) + + # ints are ok + # we test with np.int64 to get similar results on + # windows / 32-bit platforms + result = Series(index, dtype=np.int64) + expected = Series(index.astype(np.int64)) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "index", + [ + date_range('1/1/2000', periods=10), + timedelta_range('1 day', periods=10), + period_range('2000-Q1', periods=10, freq='Q')], + ids=lambda x: type(x).__name__) + def test_constructor_cast_object(self, index): + s = Series(index, dtype=object) + exp = Series(index).astype(object) + tm.assert_series_equal(s, exp) + + s = Series(pd.Index(index, dtype=object), dtype=object) + exp = Series(index).astype(object) + tm.assert_series_equal(s, exp) - def test_constructor_cast_object(self): - s = Series(date_range('1/1/2000', periods=10), dtype=object) - exp = Series(date_range('1/1/2000', periods=10)) + s = Series(index.astype(object), dtype=object) + exp = Series(index).astype(object) tm.assert_series_equal(s, exp) + + @pytest.mark.parametrize("dtype", [ + np.datetime64, + np.timedelta64, + ]) + def test_constructor_generic_timestamp_no_frequency(self, dtype): + # see gh-15524, gh-15987 + msg = "dtype has no unit. Please pass in" + + with pytest.raises(ValueError, match=msg): + Series([], dtype=dtype) + + @pytest.mark.parametrize("dtype,msg", [ + ("m8[ps]", "cannot convert timedeltalike"), + ("M8[ps]", "cannot convert datetimelike"), + ]) + def test_constructor_generic_timestamp_bad_frequency(self, dtype, msg): + # see gh-15524, gh-15987 + + with pytest.raises(TypeError, match=msg): + Series([], dtype=dtype) + + @pytest.mark.parametrize('dtype', [None, 'uint8', 'category']) + def test_constructor_range_dtype(self, dtype): + # GH 16804 + expected = Series([0, 1, 2, 3, 4], dtype=dtype or 'int64') + result = Series(range(5), dtype=dtype) + tm.assert_series_equal(result, expected) + + def test_constructor_tz_mixed_data(self): + # GH 13051 + dt_list = [Timestamp('2016-05-01 02:03:37'), + Timestamp('2016-04-30 19:03:37-0700', tz='US/Pacific')] + result = Series(dt_list) + expected = Series(dt_list, dtype=object) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_datetime_values.py b/pandas/tests/series/test_datetime_values.py index 89f972a33a630..a916cf300653a 100644 --- a/pandas/tests/series/test_datetime_values.py +++ b/pandas/tests/series/test_datetime_values.py @@ -1,36 +1,42 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -from datetime import datetime, date +import calendar +from datetime import date, datetime, time +import locale +import unicodedata import numpy as np -import pandas as pd +import pytest +import pytz -from pandas.types.common import is_integer_dtype, is_list_like -from pandas import (Index, Series, DataFrame, bdate_range, - date_range, period_range, timedelta_range, - PeriodIndex, Timestamp, DatetimeIndex, TimedeltaIndex) -import pandas.core.common as com +from pandas._libs.tslibs.timezones import maybe_get_tz -from pandas.util.testing import assert_series_equal -import pandas.util.testing as tm +from pandas.core.dtypes.common import is_integer_dtype, is_list_like -from .common import TestData +import pandas as pd +from pandas import ( + DataFrame, DatetimeIndex, Index, PeriodIndex, Series, TimedeltaIndex, + bdate_range, compat, date_range, period_range, timedelta_range) +from pandas.core.arrays import PeriodArray +import pandas.core.common as com +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal -class TestSeriesDatetimeValues(TestData, tm.TestCase): +class TestSeriesDatetimeValues(): def test_dt_namespace_accessor(self): # GH 7207, 11128 # test .dt namespace accessor - ok_for_period = PeriodIndex._datetimelike_ops + ok_for_period = PeriodArray._datetimelike_ops ok_for_period_methods = ['strftime', 'to_timestamp', 'asfreq'] ok_for_dt = DatetimeIndex._datetimelike_ops ok_for_dt_methods = ['to_period', 'to_pydatetime', 'tz_localize', 'tz_convert', 'normalize', 'strftime', 'round', - 'floor', 'ceil', 'weekday_name'] + 'floor', 'ceil', 'day_name', 'month_name'] ok_for_td = TimedeltaIndex._datetimelike_ops ok_for_td_methods = ['components', 'to_pytimedelta', 'total_seconds', 'round', 'floor', 'ceil'] @@ -48,7 +54,7 @@ def compare(s, name): a = getattr(s.dt, prop) b = get_expected(s, prop) if not (is_list_like(a) and is_list_like(b)): - self.assertEqual(a, b) + assert a == b else: tm.assert_series_equal(a, b) @@ -68,8 +74,8 @@ def compare(s, name): getattr(s.dt, prop) result = s.dt.to_pydatetime() - self.assertIsInstance(result, np.ndarray) - self.assertTrue(result.dtype == object) + assert isinstance(result, np.ndarray) + assert result.dtype == object result = s.dt.tz_localize('US/Eastern') exp_values = DatetimeIndex(s.values).tz_localize('US/Eastern') @@ -77,10 +83,9 @@ def compare(s, name): tm.assert_series_equal(result, expected) tz_result = result.dt.tz - self.assertEqual(str(tz_result), 'US/Eastern') + assert str(tz_result) == 'US/Eastern' freq_result = s.dt.freq - self.assertEqual(freq_result, DatetimeIndex(s.values, - freq='infer').freq) + assert freq_result == DatetimeIndex(s.values, freq='infer').freq # let's localize, then convert result = s.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') @@ -89,42 +94,6 @@ def compare(s, name): expected = Series(exp_values, index=s.index, name='xxx') tm.assert_series_equal(result, expected) - # round - s = Series(pd.to_datetime(['2012-01-01 13:00:00', - '2012-01-01 12:01:00', - '2012-01-01 08:00:00']), name='xxx') - result = s.dt.round('D') - expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02', - '2012-01-01']), name='xxx') - tm.assert_series_equal(result, expected) - - # round with tz - result = (s.dt.tz_localize('UTC') - .dt.tz_convert('US/Eastern') - .dt.round('D')) - exp_values = pd.to_datetime(['2012-01-01', '2012-01-01', - '2012-01-01']).tz_localize('US/Eastern') - expected = Series(exp_values, name='xxx') - tm.assert_series_equal(result, expected) - - # floor - s = Series(pd.to_datetime(['2012-01-01 13:00:00', - '2012-01-01 12:01:00', - '2012-01-01 08:00:00']), name='xxx') - result = s.dt.floor('D') - expected = Series(pd.to_datetime(['2012-01-01', '2012-01-01', - '2012-01-01']), name='xxx') - tm.assert_series_equal(result, expected) - - # ceil - s = Series(pd.to_datetime(['2012-01-01 13:00:00', - '2012-01-01 12:01:00', - '2012-01-01 08:00:00']), name='xxx') - result = s.dt.ceil('D') - expected = Series(pd.to_datetime(['2012-01-02', '2012-01-02', - '2012-01-02']), name='xxx') - tm.assert_series_equal(result, expected) - # datetimeindex with tz s = Series(date_range('20130101', periods=5, tz='US/Eastern'), name='xxx') @@ -138,8 +107,8 @@ def compare(s, name): getattr(s.dt, prop) result = s.dt.to_pydatetime() - self.assertIsInstance(result, np.ndarray) - self.assertTrue(result.dtype == object) + assert isinstance(result, np.ndarray) + assert result.dtype == object result = s.dt.tz_convert('CET') expected = Series(s._values.tz_convert('CET'), @@ -147,12 +116,11 @@ def compare(s, name): tm.assert_series_equal(result, expected) tz_result = result.dt.tz - self.assertEqual(str(tz_result), 'CET') + assert str(tz_result) == 'CET' freq_result = s.dt.freq - self.assertEqual(freq_result, DatetimeIndex(s.values, - freq='infer').freq) + assert freq_result == DatetimeIndex(s.values, freq='infer').freq - # timedeltaindex + # timedelta index cases = [Series(timedelta_range('1 day', periods=5), index=list('abcde'), name='xxx'), Series(timedelta_range('1 day 01:23:45', periods=5, @@ -169,20 +137,19 @@ def compare(s, name): getattr(s.dt, prop) result = s.dt.components - self.assertIsInstance(result, DataFrame) + assert isinstance(result, DataFrame) tm.assert_index_equal(result.index, s.index) result = s.dt.to_pytimedelta() - self.assertIsInstance(result, np.ndarray) - self.assertTrue(result.dtype == object) + assert isinstance(result, np.ndarray) + assert result.dtype == object result = s.dt.total_seconds() - self.assertIsInstance(result, pd.Series) - self.assertTrue(result.dtype == 'float64') + assert isinstance(result, pd.Series) + assert result.dtype == 'float64' freq_result = s.dt.freq - self.assertEqual(freq_result, TimedeltaIndex(s.values, - freq='infer').freq) + assert freq_result == TimedeltaIndex(s.values, freq='infer').freq # both index = date_range('20130101', periods=3, freq='D') @@ -216,7 +183,7 @@ def compare(s, name): getattr(s.dt, prop) freq_result = s.dt.freq - self.assertEqual(freq_result, PeriodIndex(s.values).freq) + assert freq_result == PeriodIndex(s.values).freq # test limited display api def get_dir(s): @@ -229,7 +196,7 @@ def get_dir(s): results, list(sorted(set(ok_for_dt + ok_for_dt_methods)))) s = Series(period_range('20130101', periods=5, - freq='D', name='xxx').asobject) + freq='D', name='xxx').astype(object)) results = get_dir(s) tm.assert_almost_equal( results, list(sorted(set(ok_for_period + ok_for_period_methods)))) @@ -249,24 +216,162 @@ def get_dir(s): # no setting allowed s = Series(date_range('20130101', periods=5, freq='D'), name='xxx') - with tm.assertRaisesRegexp(ValueError, "modifications"): + with pytest.raises(ValueError, match="modifications"): s.dt.hour = 5 # trying to set a copy with pd.option_context('chained_assignment', 'raise'): - - def f(): + with pytest.raises(com.SettingWithCopyError): s.dt.hour[0] = 5 - self.assertRaises(com.SettingWithCopyError, f) + @pytest.mark.parametrize('method, dates', [ + ['round', ['2012-01-02', '2012-01-02', '2012-01-01']], + ['floor', ['2012-01-01', '2012-01-01', '2012-01-01']], + ['ceil', ['2012-01-02', '2012-01-02', '2012-01-02']] + ]) + def test_dt_round(self, method, dates): + # round + s = Series(pd.to_datetime(['2012-01-01 13:00:00', + '2012-01-01 12:01:00', + '2012-01-01 08:00:00']), name='xxx') + result = getattr(s.dt, method)('D') + expected = Series(pd.to_datetime(dates), name='xxx') + tm.assert_series_equal(result, expected) + + def test_dt_round_tz(self): + s = Series(pd.to_datetime(['2012-01-01 13:00:00', + '2012-01-01 12:01:00', + '2012-01-01 08:00:00']), name='xxx') + result = (s.dt.tz_localize('UTC') + .dt.tz_convert('US/Eastern') + .dt.round('D')) + + exp_values = pd.to_datetime(['2012-01-01', '2012-01-01', + '2012-01-01']).tz_localize('US/Eastern') + expected = Series(exp_values, name='xxx') + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('method', ['ceil', 'round', 'floor']) + def test_dt_round_tz_ambiguous(self, method): + # GH 18946 round near "fall back" DST + df1 = pd.DataFrame([ + pd.to_datetime('2017-10-29 02:00:00+02:00', utc=True), + pd.to_datetime('2017-10-29 02:00:00+01:00', utc=True), + pd.to_datetime('2017-10-29 03:00:00+01:00', utc=True) + ], + columns=['date']) + df1['date'] = df1['date'].dt.tz_convert('Europe/Madrid') + # infer + result = getattr(df1.date.dt, method)('H', ambiguous='infer') + expected = df1['date'] + tm.assert_series_equal(result, expected) + + # bool-array + result = getattr(df1.date.dt, method)( + 'H', ambiguous=[True, False, False] + ) + tm.assert_series_equal(result, expected) + + # NaT + result = getattr(df1.date.dt, method)('H', ambiguous='NaT') + expected = df1['date'].copy() + expected.iloc[0:2] = pd.NaT + tm.assert_series_equal(result, expected) + + # raise + with pytest.raises(pytz.AmbiguousTimeError): + getattr(df1.date.dt, method)('H', ambiguous='raise') + + @pytest.mark.parametrize('method, ts_str, freq', [ + ['ceil', '2018-03-11 01:59:00-0600', '5min'], + ['round', '2018-03-11 01:59:00-0600', '5min'], + ['floor', '2018-03-11 03:01:00-0500', '2H']]) + def test_dt_round_tz_nonexistent(self, method, ts_str, freq): + # GH 23324 round near "spring forward" DST + s = Series([pd.Timestamp(ts_str, tz='America/Chicago')]) + result = getattr(s.dt, method)(freq, nonexistent='shift_forward') + expected = Series( + [pd.Timestamp('2018-03-11 03:00:00', tz='America/Chicago')] + ) + tm.assert_series_equal(result, expected) + + result = getattr(s.dt, method)(freq, nonexistent='NaT') + expected = Series([pd.NaT]).dt.tz_localize(result.dt.tz) + tm.assert_series_equal(result, expected) + + with pytest.raises(pytz.NonExistentTimeError, + match='2018-03-11 02:00:00'): + getattr(s.dt, method)(freq, nonexistent='raise') + + def test_dt_namespace_accessor_categorical(self): + # GH 19468 + dti = DatetimeIndex(['20171111', '20181212']).repeat(2) + s = Series(pd.Categorical(dti), name='foo') + result = s.dt.year + expected = Series([2017, 2017, 2018, 2018], name='foo') + tm.assert_series_equal(result, expected) def test_dt_accessor_no_new_attributes(self): # https://github.com/pandas-dev/pandas/issues/10673 s = Series(date_range('20130101', periods=5, freq='D')) - with tm.assertRaisesRegexp(AttributeError, - "You cannot add any new attribute"): + with pytest.raises(AttributeError, + match="You cannot add any new attribute"): s.dt.xlabel = "a" + @pytest.mark.parametrize('time_locale', [ + None] if tm.get_locales() is None else [None] + tm.get_locales()) + def test_dt_accessor_datetime_name_accessors(self, time_locale): + # Test Monday -> Sunday and January -> December, in that sequence + if time_locale is None: + # If the time_locale is None, day-name and month_name should + # return the english attributes + expected_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'] + expected_months = ['January', 'February', 'March', 'April', 'May', + 'June', 'July', 'August', 'September', + 'October', 'November', 'December'] + else: + with tm.set_locale(time_locale, locale.LC_TIME): + expected_days = calendar.day_name[:] + expected_months = calendar.month_name[1:] + + s = Series(date_range(freq='D', start=datetime(1998, 1, 1), + periods=365)) + english_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'] + for day, name, eng_name in zip(range(4, 11), + expected_days, + english_days): + name = name.capitalize() + assert s.dt.weekday_name[day] == eng_name + assert s.dt.day_name(locale=time_locale)[day] == name + s = s.append(Series([pd.NaT])) + assert np.isnan(s.dt.day_name(locale=time_locale).iloc[-1]) + + s = Series(date_range(freq='M', start='2012', end='2013')) + result = s.dt.month_name(locale=time_locale) + expected = Series([month.capitalize() for month in expected_months]) + + # work around https://github.com/pandas-dev/pandas/issues/22342 + if not compat.PY2: + result = result.str.normalize("NFD") + expected = expected.str.normalize("NFD") + + tm.assert_series_equal(result, expected) + + for s_date, expected in zip(s, expected_months): + result = s_date.month_name(locale=time_locale) + expected = expected.capitalize() + + if not compat.PY2: + result = unicodedata.normalize("NFD", result) + expected = unicodedata.normalize("NFD", expected) + + assert result == expected + + s = s.append(Series([pd.NaT])) + assert np.isnan(s.dt.month_name(locale=time_locale).iloc[-1]) + def test_strftime(self): # GH 10086 s = Series(date_range('20130101', periods=5)) @@ -306,16 +411,16 @@ def test_strftime(self): datetime_index = date_range('20150301', periods=5) result = datetime_index.strftime("%Y/%m/%d") - expected = np.array(['2015/03/01', '2015/03/02', '2015/03/03', - '2015/03/04', '2015/03/05'], dtype=np.object_) + expected = Index(['2015/03/01', '2015/03/02', '2015/03/03', + '2015/03/04', '2015/03/05'], dtype=np.object_) # dtype may be S10 or U10 depending on python version - self.assert_numpy_array_equal(result, expected, check_dtype=False) + tm.assert_index_equal(result, expected) period_index = period_range('20150301', periods=5) result = period_index.strftime("%Y/%m/%d") - expected = np.array(['2015/03/01', '2015/03/02', '2015/03/03', - '2015/03/04', '2015/03/05'], dtype='=U10') - self.assert_numpy_array_equal(result, expected) + expected = Index(['2015/03/01', '2015/03/02', '2015/03/03', + '2015/03/04', '2015/03/05'], dtype='=U10') + tm.assert_index_equal(result, expected) s = Series([datetime(2013, 1, 1, 2, 32, 59), datetime(2013, 1, 2, 14, 32, 1)]) @@ -364,31 +469,31 @@ def test_valid_dt_with_missing_values(self): def test_dt_accessor_api(self): # GH 9322 - from pandas.tseries.common import (CombinedDatetimelikeProperties, - DatetimeProperties) - self.assertIs(Series.dt, CombinedDatetimelikeProperties) + from pandas.core.indexes.accessors import ( + CombinedDatetimelikeProperties, DatetimeProperties) + assert Series.dt is CombinedDatetimelikeProperties s = Series(date_range('2000-01-01', periods=3)) - self.assertIsInstance(s.dt, DatetimeProperties) - - for s in [Series(np.arange(5)), Series(list('abcde')), - Series(np.random.randn(5))]: - with tm.assertRaisesRegexp(AttributeError, - "only use .dt accessor"): - s.dt - self.assertFalse(hasattr(s, 'dt')) - - def test_sub_of_datetime_from_TimeSeries(self): - from pandas.tseries.timedeltas import to_timedelta - from datetime import datetime - a = Timestamp(datetime(1993, 0o1, 0o7, 13, 30, 00)) - b = datetime(1993, 6, 22, 13, 30) - a = Series([a]) - result = to_timedelta(np.abs(a - b)) - self.assertEqual(result.dtype, 'timedelta64[ns]') + assert isinstance(s.dt, DatetimeProperties) + + @pytest.mark.parametrize('ser', [Series(np.arange(5)), + Series(list('abcde')), + Series(np.random.randn(5))]) + def test_dt_accessor_invalid(self, ser): + # GH#9322 check that series with incorrect dtypes don't have attr + with pytest.raises(AttributeError, match="only use .dt accessor"): + ser.dt + assert not hasattr(ser, 'dt') + + def test_dt_accessor_updates_on_inplace(self): + s = Series(pd.date_range('2018-01-01', periods=10)) + s[2] = None + s.fillna(pd.Timestamp('2018-01-01'), inplace=True) + result = s.dt.date + assert result[0] == result[2] def test_between(self): - s = Series(bdate_range('1/1/2000', periods=20).asobject) + s = Series(bdate_range('1/1/2000', periods=20).astype(object)) s[::2] = np.nan result = s[s.between(s[3], s[17])] @@ -410,3 +515,42 @@ def test_date_tz(self): date(2015, 11, 22)]) assert_series_equal(s.dt.date, expected) assert_series_equal(s.apply(lambda x: x.date()), expected) + + def test_datetime_understood(self): + # Ensures it doesn't fail to create the right series + # reported in issue#16726 + series = pd.Series(pd.date_range("2012-01-01", periods=3)) + offset = pd.offsets.DateOffset(days=6) + result = series - offset + expected = pd.Series(pd.to_datetime([ + '2011-12-26', '2011-12-27', '2011-12-28'])) + tm.assert_series_equal(result, expected) + + def test_dt_timetz_accessor(self, tz_naive_fixture): + # GH21358 + tz = maybe_get_tz(tz_naive_fixture) + + dtindex = pd.DatetimeIndex(['2014-04-04 23:56', '2014-07-18 21:24', + '2015-11-22 22:14'], tz=tz) + s = Series(dtindex) + expected = Series([time(23, 56, tzinfo=tz), time(21, 24, tzinfo=tz), + time(22, 14, tzinfo=tz)]) + result = s.dt.timetz + tm.assert_series_equal(result, expected) + + def test_setitem_with_string_index(self): + # GH 23451 + x = pd.Series([1, 2, 3], index=['Date', 'b', 'other']) + x['Date'] = date.today() + assert x.Date == date.today() + assert x['Date'] == date.today() + + def test_setitem_with_different_tz(self): + # GH#24024 + ser = pd.Series(pd.date_range('2000', periods=2, tz="US/Central")) + ser[0] = pd.Timestamp("2000", tz='US/Eastern') + expected = pd.Series([ + pd.Timestamp("2000-01-01 00:00:00-05:00", tz="US/Eastern"), + pd.Timestamp("2000-01-02 00:00:00-06:00", tz="US/Central"), + ], dtype=object) + tm.assert_series_equal(ser, expected) diff --git a/pandas/tests/series/test_dtypes.py b/pandas/tests/series/test_dtypes.py index a2aaff25516ae..d8046c4944afc 100644 --- a/pandas/tests/series/test_dtypes.py +++ b/pandas/tests/series/test_dtypes.py @@ -1,163 +1,441 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -import sys -from datetime import datetime +from datetime import datetime, timedelta import string +import sys -from numpy import nan import numpy as np +import pytest -from pandas import Series, Timestamp, Timedelta, DataFrame, date_range - +from pandas._libs.tslibs import iNaT +import pandas.compat as compat from pandas.compat import lrange, range, u -from pandas import compat -from pandas.util.testing import assert_series_equal + +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, Series, Timedelta, Timestamp, date_range) +from pandas.api.types import CategoricalDtype import pandas.util.testing as tm -from .common import TestData +class TestSeriesDtypes(object): + + def test_dt64_series_astype_object(self): + dt64ser = Series(date_range('20130101', periods=3)) + result = dt64ser.astype(object) + assert isinstance(result.iloc[0], datetime) + assert result.dtype == np.object_ + + def test_td64_series_astype_object(self): + tdser = Series(['59 Days', '59 Days', 'NaT'], dtype='timedelta64[ns]') + result = tdser.astype(object) + assert isinstance(result.iloc[0], timedelta) + assert result.dtype == np.object_ + + @pytest.mark.parametrize("dtype", ["float32", "float64", + "int64", "int32"]) + def test_astype(self, dtype): + s = Series(np.random.randn(5), name='foo') + as_typed = s.astype(dtype) -class TestSeriesDtypes(TestData, tm.TestCase): + assert as_typed.dtype == dtype + assert as_typed.name == s.name - def test_astype(self): + def test_asobject_deprecated(self): s = Series(np.random.randn(5), name='foo') + with tm.assert_produces_warning(FutureWarning): + o = s.asobject + assert isinstance(o, np.ndarray) + + def test_dtype(self, datetime_series): + + assert datetime_series.dtype == np.dtype('float64') + assert datetime_series.dtypes == np.dtype('float64') + assert datetime_series.ftype == 'float64:dense' + assert datetime_series.ftypes == 'float64:dense' + tm.assert_series_equal(datetime_series.get_dtype_counts(), + Series(1, ['float64'])) + # GH18243 - Assert .get_ftype_counts is deprecated + with tm.assert_produces_warning(FutureWarning): + tm.assert_series_equal(datetime_series.get_ftype_counts(), + Series(1, ['float64:dense'])) - for dtype in ['float32', 'float64', 'int64', 'int32']: - astyped = s.astype(dtype) - self.assertEqual(astyped.dtype, dtype) - self.assertEqual(astyped.name, s.name) - - def test_dtype(self): - - self.assertEqual(self.ts.dtype, np.dtype('float64')) - self.assertEqual(self.ts.dtypes, np.dtype('float64')) - self.assertEqual(self.ts.ftype, 'float64:dense') - self.assertEqual(self.ts.ftypes, 'float64:dense') - assert_series_equal(self.ts.get_dtype_counts(), Series(1, ['float64'])) - assert_series_equal(self.ts.get_ftype_counts(), Series( - 1, ['float64:dense'])) - - def test_astype_cast_nan_inf_int(self): - # GH14265, check nan and inf raise error when converting to int - types = [np.int32, np.int64] - values = [np.nan, np.inf] + @pytest.mark.parametrize("value", [np.nan, np.inf]) + @pytest.mark.parametrize("dtype", [np.int32, np.int64]) + def test_astype_cast_nan_inf_int(self, dtype, value): + # gh-14265: check NaN and inf raise error when converting to int msg = 'Cannot convert non-finite values \\(NA or inf\\) to integer' + s = Series([value]) - for this_type in types: - for this_val in values: - s = Series([this_val]) - with self.assertRaisesRegexp(ValueError, msg): - s.astype(this_type) + with pytest.raises(ValueError, match=msg): + s.astype(dtype) - def test_astype_cast_object_int(self): + @pytest.mark.parametrize("dtype", [int, np.int8, np.int64]) + def test_astype_cast_object_int_fail(self, dtype): arr = Series(["car", "house", "tree", "1"]) + msg = r"invalid literal for (int|long)\(\) with base 10: 'car'" + with pytest.raises(ValueError, match=msg): + arr.astype(dtype) - self.assertRaises(ValueError, arr.astype, int) - self.assertRaises(ValueError, arr.astype, np.int64) - self.assertRaises(ValueError, arr.astype, np.int8) - + def test_astype_cast_object_int(self): arr = Series(['1', '2', '3', '4'], dtype=object) result = arr.astype(int) - self.assert_series_equal(result, Series(np.arange(1, 5))) - def test_astype_datetimes(self): - import pandas._libs.tslib as tslib + tm.assert_series_equal(result, Series(np.arange(1, 5))) + + def test_astype_datetime(self): + s = Series(iNaT, dtype='M8[ns]', index=lrange(5)) - s = Series(tslib.iNaT, dtype='M8[ns]', index=lrange(5)) s = s.astype('O') - self.assertEqual(s.dtype, np.object_) + assert s.dtype == np.object_ s = Series([datetime(2001, 1, 2, 0, 0)]) + s = s.astype('O') - self.assertEqual(s.dtype, np.object_) + assert s.dtype == np.object_ s = Series([datetime(2001, 1, 2, 0, 0) for i in range(3)]) + s[1] = np.nan - self.assertEqual(s.dtype, 'M8[ns]') - s = s.astype('O') - self.assertEqual(s.dtype, np.object_) + assert s.dtype == 'M8[ns]' - def test_astype_str(self): - # GH4405 - digits = string.digits - s1 = Series([digits * 10, tm.rands(63), tm.rands(64), tm.rands(1000)]) - s2 = Series([digits * 10, tm.rands(63), tm.rands(64), nan, 1.0]) - types = (compat.text_type, np.str_) - for typ in types: - for s in (s1, s2): - res = s.astype(typ) - expec = s.map(compat.text_type) - assert_series_equal(res, expec) - - # GH9757 - # Test str and unicode on python 2.x and just str on python 3.x - for tt in set([str, compat.text_type]): - ts = Series([Timestamp('2010-01-04 00:00:00')]) - s = ts.astype(tt) - expected = Series([tt('2010-01-04')]) - assert_series_equal(s, expected) - - ts = Series([Timestamp('2010-01-04 00:00:00', tz='US/Eastern')]) - s = ts.astype(tt) - expected = Series([tt('2010-01-04 00:00:00-05:00')]) - assert_series_equal(s, expected) - - td = Series([Timedelta(1, unit='d')]) - s = td.astype(tt) - expected = Series([tt('1 days 00:00:00.000000000')]) - assert_series_equal(s, expected) + s = s.astype('O') + assert s.dtype == np.object_ + + def test_astype_datetime64tz(self): + s = Series(date_range('20130101', periods=3, tz='US/Eastern')) + + # astype + result = s.astype(object) + expected = Series(s.astype(object), dtype=object) + tm.assert_series_equal(result, expected) + + result = Series(s.values).dt.tz_localize('UTC').dt.tz_convert(s.dt.tz) + tm.assert_series_equal(result, s) + + # astype - object, preserves on construction + result = Series(s.astype(object)) + expected = s.astype(object) + tm.assert_series_equal(result, expected) + + # astype - datetime64[ns, tz] + result = Series(s.values).astype('datetime64[ns, US/Eastern]') + tm.assert_series_equal(result, s) + + result = Series(s.values).astype(s.dtype) + tm.assert_series_equal(result, s) + + result = s.astype('datetime64[ns, CET]') + expected = Series(date_range('20130101 06:00:00', periods=3, tz='CET')) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", [compat.text_type, np.str_]) + @pytest.mark.parametrize("series", [Series([string.digits * 10, + tm.rands(63), + tm.rands(64), + tm.rands(1000)]), + Series([string.digits * 10, + tm.rands(63), + tm.rands(64), np.nan, 1.0])]) + def test_astype_str_map(self, dtype, series): + # see gh-4405 + result = series.astype(dtype) + expected = series.map(compat.text_type) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("dtype", [str, compat.text_type]) + def test_astype_str_cast(self, dtype): + # see gh-9757: test str and unicode on python 2.x + # and just str on python 3.x + ts = Series([Timestamp('2010-01-04 00:00:00')]) + s = ts.astype(dtype) + + expected = Series([dtype('2010-01-04')]) + tm.assert_series_equal(s, expected) + + ts = Series([Timestamp('2010-01-04 00:00:00', tz='US/Eastern')]) + s = ts.astype(dtype) + + expected = Series([dtype('2010-01-04 00:00:00-05:00')]) + tm.assert_series_equal(s, expected) + + td = Series([Timedelta(1, unit='d')]) + s = td.astype(dtype) + + expected = Series([dtype('1 days 00:00:00.000000000')]) + tm.assert_series_equal(s, expected) def test_astype_unicode(self): - - # GH7758 - # a bit of magic is required to set default encoding encoding to utf-8 + # see gh-7758: A bit of magic is required to set + # default encoding to utf-8 digits = string.digits test_series = [ Series([digits * 10, tm.rands(63), tm.rands(64), tm.rands(1000)]), Series([u('データーサイエンス、お前はもう死んでいる')]), - ] former_encoding = None + if not compat.PY3: - # in python we can force the default encoding for this test + # In Python, we can force the default encoding for this test former_encoding = sys.getdefaultencoding() reload(sys) # noqa + sys.setdefaultencoding("utf-8") if sys.getdefaultencoding() == "utf-8": test_series.append(Series([u('野菜食べないとやばい') .encode("utf-8")])) + for s in test_series: res = s.astype("unicode") expec = s.map(compat.text_type) - assert_series_equal(res, expec) - # restore the former encoding + tm.assert_series_equal(res, expec) + + # Restore the former encoding if former_encoding is not None and former_encoding != "utf-8": reload(sys) # noqa sys.setdefaultencoding(former_encoding) - def test_astype_dict(self): - # GH7271 + @pytest.mark.parametrize("dtype_class", [dict, Series]) + def test_astype_dict_like(self, dtype_class): + # see gh-7271 s = Series(range(0, 10, 2), name='abc') - result = s.astype({'abc': str}) + dt1 = dtype_class({'abc': str}) + result = s.astype(dt1) expected = Series(['0', '2', '4', '6', '8'], name='abc') - assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) - result = s.astype({'abc': 'float64'}) + dt2 = dtype_class({'abc': 'float64'}) + result = s.astype(dt2) expected = Series([0.0, 2.0, 4.0, 6.0, 8.0], dtype='float64', name='abc') - assert_series_equal(result, expected) - - self.assertRaises(KeyError, s.astype, {'abc': str, 'def': str}) - self.assertRaises(KeyError, s.astype, {0: str}) - - def test_complexx(self): - # GH4819 - # complex access for ndarray compat + tm.assert_series_equal(result, expected) + + dt3 = dtype_class({'abc': str, 'def': str}) + msg = ("Only the Series name can be used for the key in Series dtype" + r" mappings\.") + with pytest.raises(KeyError, match=msg): + s.astype(dt3) + + dt4 = dtype_class({0: str}) + with pytest.raises(KeyError, match=msg): + s.astype(dt4) + + # GH16717 + # if dtypes provided is empty, it should error + dt5 = dtype_class({}) + with pytest.raises(KeyError, match=msg): + s.astype(dt5) + + def test_astype_categories_deprecation(self): + + # deprecated 17636 + s = Series(['a', 'b', 'a']) + expected = s.astype(CategoricalDtype(['a', 'b'], ordered=True)) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + result = s.astype('category', categories=['a', 'b'], ordered=True) + tm.assert_series_equal(result, expected) + + def test_astype_from_categorical(self): + items = ["a", "b", "c", "a"] + s = Series(items) + exp = Series(Categorical(items)) + res = s.astype('category') + tm.assert_series_equal(res, exp) + + items = [1, 2, 3, 1] + s = Series(items) + exp = Series(Categorical(items)) + res = s.astype('category') + tm.assert_series_equal(res, exp) + + df = DataFrame({"cats": [1, 2, 3, 4, 5, 6], + "vals": [1, 2, 3, 4, 5, 6]}) + cats = Categorical([1, 2, 3, 4, 5, 6]) + exp_df = DataFrame({"cats": cats, "vals": [1, 2, 3, 4, 5, 6]}) + df["cats"] = df["cats"].astype("category") + tm.assert_frame_equal(exp_df, df) + + df = DataFrame({"cats": ['a', 'b', 'b', 'a', 'a', 'd'], + "vals": [1, 2, 3, 4, 5, 6]}) + cats = Categorical(['a', 'b', 'b', 'a', 'a', 'd']) + exp_df = DataFrame({"cats": cats, "vals": [1, 2, 3, 4, 5, 6]}) + df["cats"] = df["cats"].astype("category") + tm.assert_frame_equal(exp_df, df) + + # with keywords + lst = ["a", "b", "c", "a"] + s = Series(lst) + exp = Series(Categorical(lst, ordered=True)) + res = s.astype(CategoricalDtype(None, ordered=True)) + tm.assert_series_equal(res, exp) + + exp = Series(Categorical(lst, categories=list('abcdef'), ordered=True)) + res = s.astype(CategoricalDtype(list('abcdef'), ordered=True)) + tm.assert_series_equal(res, exp) + + def test_astype_categorical_to_other(self): + + df = DataFrame({'value': np.random.randint(0, 10000, 100)}) + labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] + cat_labels = Categorical(labels, labels) + + df = df.sort_values(by=['value'], ascending=True) + df['value_group'] = pd.cut(df.value, range(0, 10500, 500), + right=False, labels=cat_labels) + + s = df['value_group'] + expected = s + tm.assert_series_equal(s.astype('category'), expected) + tm.assert_series_equal(s.astype(CategoricalDtype()), expected) + msg = (r"could not convert string to float|" + r"invalid literal for float\(\)") + with pytest.raises(ValueError, match=msg): + s.astype('float64') + + cat = Series(Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'])) + exp = Series(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) + tm.assert_series_equal(cat.astype('str'), exp) + s2 = Series(Categorical(['1', '2', '3', '4'])) + exp2 = Series([1, 2, 3, 4]).astype(int) + tm.assert_series_equal(s2.astype('int'), exp2) + + # object don't sort correctly, so just compare that we have the same + # values + def cmp(a, b): + tm.assert_almost_equal( + np.sort(np.unique(a)), np.sort(np.unique(b))) + + expected = Series(np.array(s.values), name='value_group') + cmp(s.astype('object'), expected) + cmp(s.astype(np.object_), expected) + + # array conversion + tm.assert_almost_equal(np.array(s), np.array(s.values)) + + # valid conversion + for valid in [lambda x: x.astype('category'), + lambda x: x.astype(CategoricalDtype()), + lambda x: x.astype('object').astype('category'), + lambda x: x.astype('object').astype( + CategoricalDtype()) + ]: + + result = valid(s) + # compare series values + # internal .categories can't be compared because it is sorted + tm.assert_series_equal(result, s, check_categorical=False) + + # invalid conversion (these are NOT a dtype) + msg = (r"invalid type for astype") + for invalid in [lambda x: x.astype(Categorical), + lambda x: x.astype('object').astype(Categorical)]: + with pytest.raises(TypeError, match=msg): + invalid(s) + + @pytest.mark.parametrize('name', [None, 'foo']) + @pytest.mark.parametrize('dtype_ordered', [True, False]) + @pytest.mark.parametrize('series_ordered', [True, False]) + def test_astype_categorical_to_categorical(self, name, dtype_ordered, + series_ordered): + # GH 10696/18593 + s_data = list('abcaacbab') + s_dtype = CategoricalDtype(list('bac'), ordered=series_ordered) + s = Series(s_data, dtype=s_dtype, name=name) + + # unspecified categories + dtype = CategoricalDtype(ordered=dtype_ordered) + result = s.astype(dtype) + exp_dtype = CategoricalDtype(s_dtype.categories, dtype_ordered) + expected = Series(s_data, name=name, dtype=exp_dtype) + tm.assert_series_equal(result, expected) + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = s.astype('category', ordered=dtype_ordered) + tm.assert_series_equal(result, expected) + + # different categories + dtype = CategoricalDtype(list('adc'), dtype_ordered) + result = s.astype(dtype) + expected = Series(s_data, name=name, dtype=dtype) + tm.assert_series_equal(result, expected) + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = s.astype( + 'category', categories=list('adc'), ordered=dtype_ordered) + tm.assert_series_equal(result, expected) + + if dtype_ordered is False: + # not specifying ordered, so only test once + expected = s + result = s.astype('category') + tm.assert_series_equal(result, expected) + + def test_astype_categoricaldtype(self): + s = Series(['a', 'b', 'a']) + result = s.astype(CategoricalDtype(['a', 'b'], ordered=True)) + expected = Series(Categorical(['a', 'b', 'a'], ordered=True)) + tm.assert_series_equal(result, expected) + + result = s.astype(CategoricalDtype(['a', 'b'], ordered=False)) + expected = Series(Categorical(['a', 'b', 'a'], ordered=False)) + tm.assert_series_equal(result, expected) + + result = s.astype(CategoricalDtype(['a', 'b', 'c'], ordered=False)) + expected = Series(Categorical(['a', 'b', 'a'], + categories=['a', 'b', 'c'], + ordered=False)) + tm.assert_series_equal(result, expected) + tm.assert_index_equal(result.cat.categories, Index(['a', 'b', 'c'])) + + def test_astype_categoricaldtype_with_args(self): + s = Series(['a', 'b']) + type_ = CategoricalDtype(['a', 'b']) + + msg = (r"Cannot specify a CategoricalDtype and also `categories` or" + r" `ordered`\. Use `dtype=CategoricalDtype\(categories," + r" ordered\)` instead\.") + with pytest.raises(TypeError, match=msg): + s.astype(type_, ordered=True) + with pytest.raises(TypeError, match=msg): + s.astype(type_, categories=['a', 'b']) + with pytest.raises(TypeError, match=msg): + s.astype(type_, categories=['a', 'b'], ordered=False) + + @pytest.mark.parametrize("dtype", [ + np.datetime64, + np.timedelta64, + ]) + def test_astype_generic_timestamp_no_frequency(self, dtype): + # see gh-15524, gh-15987 + data = [1] + s = Series(data) + + msg = "dtype has no unit. Please pass in" + with pytest.raises(ValueError, match=msg): + s.astype(dtype) + + @pytest.mark.parametrize("dtype", np.typecodes['All']) + def test_astype_empty_constructor_equality(self, dtype): + # see gh-15524 + + if dtype not in ( + "S", "V", # poor support (if any) currently + "M", "m" # Generic timestamps raise a ValueError. Already tested. + ): + init_empty = Series([], dtype=dtype) + as_type_empty = Series([]).astype(dtype) + tm.assert_series_equal(init_empty, as_type_empty) + + def test_complex(self): + # see gh-4819: complex access for ndarray compat a = np.arange(5, dtype=np.float64) b = Series(a + 4j * a) + tm.assert_numpy_array_equal(a, b.real) tm.assert_numpy_array_equal(4 * a, b.imag) @@ -166,23 +444,21 @@ def test_complexx(self): tm.assert_numpy_array_equal(4 * a, b.imag) def test_arg_for_errors_in_astype(self): - # issue #14878 + # see gh-14878 + s = Series([1, 2, 3]) - sr = Series([1, 2, 3]) - - with self.assertRaises(ValueError): - sr.astype(np.float64, errors=False) - - with tm.assert_produces_warning(FutureWarning): - sr.astype(np.int8, raise_on_error=True) + msg = (r"Expected value of kwarg 'errors' to be one of \['raise'," + r" 'ignore'\]\. Supplied value is 'False'") + with pytest.raises(ValueError, match=msg): + s.astype(np.float64, errors=False) - sr.astype(np.int8, errors='raise') + s.astype(np.int8, errors='raise') def test_intercept_astype_object(self): series = Series(date_range('1/1/2000', periods=10)) - # this test no longer makes sense as series is by default already - # M8[ns] + # This test no longer makes sense, as + # Series is by default already M8[ns]. expected = series.astype('object') df = DataFrame({'a': series, @@ -192,9 +468,51 @@ def test_intercept_astype_object(self): tm.assert_series_equal(df.dtypes, exp_dtypes) result = df.values.squeeze() - self.assertTrue((result[:, 0] == expected.values).all()) + assert (result[:, 0] == expected.values).all() df = DataFrame({'a': series, 'b': ['foo'] * len(series)}) result = df.values.squeeze() - self.assertTrue((result[:, 0] == expected.values).all()) + assert (result[:, 0] == expected.values).all() + + def test_series_to_categorical(self): + # see gh-16524: test conversion of Series to Categorical + series = Series(['a', 'b', 'c']) + + result = Series(series, dtype='category') + expected = Series(['a', 'b', 'c'], dtype='category') + + tm.assert_series_equal(result, expected) + + def test_infer_objects_series(self): + # GH 11221 + actual = Series(np.array([1, 2, 3], dtype='O')).infer_objects() + expected = Series([1, 2, 3]) + tm.assert_series_equal(actual, expected) + + actual = Series(np.array([1, 2, 3, None], dtype='O')).infer_objects() + expected = Series([1., 2., 3., np.nan]) + tm.assert_series_equal(actual, expected) + + # only soft conversions, unconvertable pass thru unchanged + actual = (Series(np.array([1, 2, 3, None, 'a'], dtype='O')) + .infer_objects()) + expected = Series([1, 2, 3, None, 'a']) + + assert actual.dtype == 'object' + tm.assert_series_equal(actual, expected) + + def test_is_homogeneous_type(self): + assert Series()._is_homogeneous_type + assert Series([1, 2])._is_homogeneous_type + assert Series(pd.Categorical([1, 2]))._is_homogeneous_type + + @pytest.mark.parametrize("data", [ + pd.period_range("2000", periods=4), + pd.IntervalIndex.from_breaks([1, 2, 3, 4]) + ]) + def test_values_compatibility(self, data): + # https://github.com/pandas-dev/pandas/issues/23995 + result = pd.Series(data).values + expected = np.array(data.astype(object)) + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/series/test_duplicates.py b/pandas/tests/series/test_duplicates.py new file mode 100644 index 0000000000000..a975edacc19c7 --- /dev/null +++ b/pandas/tests/series/test_duplicates.py @@ -0,0 +1,148 @@ +# coding=utf-8 + +import numpy as np +import pytest + +from pandas import Categorical, Series +import pandas.util.testing as tm + + +def test_value_counts_nunique(): + # basics.rst doc example + series = Series(np.random.randn(500)) + series[20:500] = np.nan + series[10:20] = 5000 + result = series.nunique() + assert result == 11 + + # GH 18051 + s = Series(Categorical([])) + assert s.nunique() == 0 + s = Series(Categorical([np.nan])) + assert s.nunique() == 0 + + +def test_unique(): + # GH714 also, dtype=float + s = Series([1.2345] * 100) + s[::2] = np.nan + result = s.unique() + assert len(result) == 2 + + s = Series([1.2345] * 100, dtype='f4') + s[::2] = np.nan + result = s.unique() + assert len(result) == 2 + + # NAs in object arrays #714 + s = Series(['foo'] * 100, dtype='O') + s[::2] = np.nan + result = s.unique() + assert len(result) == 2 + + # decision about None + s = Series([1, 2, 3, None, None, None], dtype=object) + result = s.unique() + expected = np.array([1, 2, 3, None], dtype=object) + tm.assert_numpy_array_equal(result, expected) + + # GH 18051 + s = Series(Categorical([])) + tm.assert_categorical_equal(s.unique(), Categorical([]), check_dtype=False) + s = Series(Categorical([np.nan])) + tm.assert_categorical_equal(s.unique(), Categorical([np.nan]), + check_dtype=False) + + +def test_unique_data_ownership(): + # it works! #1807 + Series(Series(["a", "c", "b"]).unique()).sort_values() + + +@pytest.mark.parametrize('data, expected', [ + (np.random.randint(0, 10, size=1000), False), + (np.arange(1000), True), + ([], True), + ([np.nan], True), + (['foo', 'bar', np.nan], True), + (['foo', 'foo', np.nan], False), + (['foo', 'bar', np.nan, np.nan], False)]) +def test_is_unique(data, expected): + # GH11946 / GH25180 + s = Series(data) + assert s.is_unique is expected + + +def test_is_unique_class_ne(capsys): + # GH 20661 + class Foo(object): + def __init__(self, val): + self._value = val + + def __ne__(self, other): + raise Exception("NEQ not supported") + + with capsys.disabled(): + li = [Foo(i) for i in range(5)] + s = Series(li, index=[i for i in range(5)]) + s.is_unique + captured = capsys.readouterr() + assert len(captured.err) == 0 + + +@pytest.mark.parametrize( + 'keep, expected', + [ + ('first', Series([False, False, False, False, True, True, False])), + ('last', Series([False, True, True, False, False, False, False])), + (False, Series([False, True, True, False, True, True, False])) + ]) +def test_drop_duplicates(any_numpy_dtype, keep, expected): + tc = Series([1, 0, 3, 5, 3, 0, 4], dtype=np.dtype(any_numpy_dtype)) + + if tc.dtype == 'bool': + pytest.skip('tested separately in test_drop_duplicates_bool') + + tm.assert_series_equal(tc.duplicated(keep=keep), expected) + tm.assert_series_equal(tc.drop_duplicates(keep=keep), tc[~expected]) + sc = tc.copy() + sc.drop_duplicates(keep=keep, inplace=True) + tm.assert_series_equal(sc, tc[~expected]) + + +@pytest.mark.parametrize('keep, expected', + [('first', Series([False, False, True, True])), + ('last', Series([True, True, False, False])), + (False, Series([True, True, True, True]))]) +def test_drop_duplicates_bool(keep, expected): + tc = Series([True, False, True, False]) + + tm.assert_series_equal(tc.duplicated(keep=keep), expected) + tm.assert_series_equal(tc.drop_duplicates(keep=keep), tc[~expected]) + sc = tc.copy() + sc.drop_duplicates(keep=keep, inplace=True) + tm.assert_series_equal(sc, tc[~expected]) + + +@pytest.mark.parametrize('keep, expected', [ + ('first', Series([False, False, True, False, True], name='name')), + ('last', Series([True, True, False, False, False], name='name')), + (False, Series([True, True, True, False, True], name='name')) +]) +def test_duplicated_keep(keep, expected): + s = Series(['a', 'b', 'b', 'c', 'a'], name='name') + + result = s.duplicated(keep=keep) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize('keep, expected', [ + ('first', Series([False, False, True, False, True])), + ('last', Series([True, True, False, False, False])), + (False, Series([True, True, True, False, True])) +]) +def test_duplicated_nan_none(keep, expected): + s = Series([np.nan, 3, 3, None, np.nan], dtype=object) + + result = s.duplicated(keep=keep) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_indexing.py b/pandas/tests/series/test_indexing.py deleted file mode 100644 index 0b6c0c601ac72..0000000000000 --- a/pandas/tests/series/test_indexing.py +++ /dev/null @@ -1,2688 +0,0 @@ -# coding=utf-8 -# pylint: disable-msg=E1101,W0612 - -from datetime import datetime, timedelta - -from numpy import nan -import numpy as np -import pandas as pd - -import pandas._libs.index as _index -from pandas.types.common import is_integer, is_scalar -from pandas import (Index, Series, DataFrame, isnull, - date_range, NaT, MultiIndex, - Timestamp, DatetimeIndex, Timedelta) -from pandas.core.indexing import IndexingError -from pandas.tseries.offsets import BDay -from pandas._libs import tslib, lib - -from pandas.compat import lrange, range -from pandas import compat -from pandas.util.testing import (slow, - assert_series_equal, - assert_almost_equal, - assert_frame_equal) -import pandas.util.testing as tm - -from pandas.tests.series.common import TestData - -JOIN_TYPES = ['inner', 'outer', 'left', 'right'] - - -class TestSeriesIndexing(TestData, tm.TestCase): - - def test_get(self): - - # GH 6383 - s = Series(np.array([43, 48, 60, 48, 50, 51, 50, 45, 57, 48, 56, 45, - 51, 39, 55, 43, 54, 52, 51, 54])) - - result = s.get(25, 0) - expected = 0 - self.assertEqual(result, expected) - - s = Series(np.array([43, 48, 60, 48, 50, 51, 50, 45, 57, 48, 56, - 45, 51, 39, 55, 43, 54, 52, 51, 54]), - index=pd.Float64Index( - [25.0, 36.0, 49.0, 64.0, 81.0, 100.0, - 121.0, 144.0, 169.0, 196.0, 1225.0, - 1296.0, 1369.0, 1444.0, 1521.0, 1600.0, - 1681.0, 1764.0, 1849.0, 1936.0], - dtype='object')) - - result = s.get(25, 0) - expected = 43 - self.assertEqual(result, expected) - - # GH 7407 - # with a boolean accessor - df = pd.DataFrame({'i': [0] * 3, 'b': [False] * 3}) - vc = df.i.value_counts() - result = vc.get(99, default='Missing') - self.assertEqual(result, 'Missing') - - vc = df.b.value_counts() - result = vc.get(False, default='Missing') - self.assertEqual(result, 3) - - result = vc.get(True, default='Missing') - self.assertEqual(result, 'Missing') - - def test_delitem(self): - - # GH 5542 - # should delete the item inplace - s = Series(lrange(5)) - del s[0] - - expected = Series(lrange(1, 5), index=lrange(1, 5)) - assert_series_equal(s, expected) - - del s[1] - expected = Series(lrange(2, 5), index=lrange(2, 5)) - assert_series_equal(s, expected) - - # empty - s = Series() - - def f(): - del s[0] - - self.assertRaises(KeyError, f) - - # only 1 left, del, add, del - s = Series(1) - del s[0] - assert_series_equal(s, Series(dtype='int64', index=Index( - [], dtype='int64'))) - s[0] = 1 - assert_series_equal(s, Series(1)) - del s[0] - assert_series_equal(s, Series(dtype='int64', index=Index( - [], dtype='int64'))) - - # Index(dtype=object) - s = Series(1, index=['a']) - del s['a'] - assert_series_equal(s, Series(dtype='int64', index=Index( - [], dtype='object'))) - s['a'] = 1 - assert_series_equal(s, Series(1, index=['a'])) - del s['a'] - assert_series_equal(s, Series(dtype='int64', index=Index( - [], dtype='object'))) - - def test_getitem_setitem_ellipsis(self): - s = Series(np.random.randn(10)) - - np.fix(s) - - result = s[...] - assert_series_equal(result, s) - - s[...] = 5 - self.assertTrue((result == 5).all()) - - def test_getitem_negative_out_of_bounds(self): - s = Series(tm.rands_array(5, 10), index=tm.rands_array(10, 10)) - - self.assertRaises(IndexError, s.__getitem__, -11) - self.assertRaises(IndexError, s.__setitem__, -11, 'foo') - - def test_pop(self): - # GH 6600 - df = DataFrame({'A': 0, 'B': np.arange(5, dtype='int64'), 'C': 0, }) - k = df.iloc[4] - - result = k.pop('B') - self.assertEqual(result, 4) - - expected = Series([0, 0], index=['A', 'C'], name=4) - assert_series_equal(k, expected) - - def test_getitem_get(self): - idx1 = self.series.index[5] - idx2 = self.objSeries.index[5] - - self.assertEqual(self.series[idx1], self.series.get(idx1)) - self.assertEqual(self.objSeries[idx2], self.objSeries.get(idx2)) - - self.assertEqual(self.series[idx1], self.series[5]) - self.assertEqual(self.objSeries[idx2], self.objSeries[5]) - - self.assertEqual( - self.series.get(-1), self.series.get(self.series.index[-1])) - self.assertEqual(self.series[5], self.series.get(self.series.index[5])) - - # missing - d = self.ts.index[0] - BDay() - self.assertRaises(KeyError, self.ts.__getitem__, d) - - # None - # GH 5652 - for s in [Series(), Series(index=list('abc'))]: - result = s.get(None) - self.assertIsNone(result) - - def test_iloc(self): - - s = Series(np.random.randn(10), index=lrange(0, 20, 2)) - - for i in range(len(s)): - result = s.iloc[i] - exp = s[s.index[i]] - assert_almost_equal(result, exp) - - # pass a slice - result = s.iloc[slice(1, 3)] - expected = s.loc[2:4] - assert_series_equal(result, expected) - - # test slice is a view - result[:] = 0 - self.assertTrue((s[1:3] == 0).all()) - - # list of integers - result = s.iloc[[0, 2, 3, 4, 5]] - expected = s.reindex(s.index[[0, 2, 3, 4, 5]]) - assert_series_equal(result, expected) - - def test_iloc_nonunique(self): - s = Series([0, 1, 2], index=[0, 1, 0]) - self.assertEqual(s.iloc[2], 2) - - def test_getitem_regression(self): - s = Series(lrange(5), index=lrange(5)) - result = s[lrange(5)] - assert_series_equal(result, s) - - def test_getitem_setitem_slice_bug(self): - s = Series(lrange(10), lrange(10)) - result = s[-12:] - assert_series_equal(result, s) - - result = s[-7:] - assert_series_equal(result, s[3:]) - - result = s[:-12] - assert_series_equal(result, s[:0]) - - s = Series(lrange(10), lrange(10)) - s[-12:] = 0 - self.assertTrue((s == 0).all()) - - s[:-12] = 5 - self.assertTrue((s == 0).all()) - - def test_getitem_int64(self): - idx = np.int64(5) - self.assertEqual(self.ts[idx], self.ts[5]) - - def test_getitem_fancy(self): - slice1 = self.series[[1, 2, 3]] - slice2 = self.objSeries[[1, 2, 3]] - self.assertEqual(self.series.index[2], slice1.index[1]) - self.assertEqual(self.objSeries.index[2], slice2.index[1]) - self.assertEqual(self.series[2], slice1[1]) - self.assertEqual(self.objSeries[2], slice2[1]) - - def test_getitem_boolean(self): - s = self.series - mask = s > s.median() - - # passing list is OK - result = s[list(mask)] - expected = s[mask] - assert_series_equal(result, expected) - self.assert_index_equal(result.index, s.index[mask]) - - def test_getitem_boolean_empty(self): - s = Series([], dtype=np.int64) - s.index.name = 'index_name' - s = s[s.isnull()] - self.assertEqual(s.index.name, 'index_name') - self.assertEqual(s.dtype, np.int64) - - # GH5877 - # indexing with empty series - s = Series(['A', 'B']) - expected = Series(np.nan, index=['C'], dtype=object) - result = s[Series(['C'], dtype=object)] - assert_series_equal(result, expected) - - s = Series(['A', 'B']) - expected = Series(dtype=object, index=Index([], dtype='int64')) - result = s[Series([], dtype=object)] - assert_series_equal(result, expected) - - # invalid because of the boolean indexer - # that's empty or not-aligned - def f(): - s[Series([], dtype=bool)] - - self.assertRaises(IndexingError, f) - - def f(): - s[Series([True], dtype=bool)] - - self.assertRaises(IndexingError, f) - - def test_getitem_generator(self): - gen = (x > 0 for x in self.series) - result = self.series[gen] - result2 = self.series[iter(self.series > 0)] - expected = self.series[self.series > 0] - assert_series_equal(result, expected) - assert_series_equal(result2, expected) - - def test_type_promotion(self): - # GH12599 - s = pd.Series() - s["a"] = pd.Timestamp("2016-01-01") - s["b"] = 3.0 - s["c"] = "foo" - expected = Series([pd.Timestamp("2016-01-01"), 3.0, "foo"], - index=["a", "b", "c"]) - assert_series_equal(s, expected) - - def test_getitem_boolean_object(self): - # using column from DataFrame - - s = self.series - mask = s > s.median() - omask = mask.astype(object) - - # getitem - result = s[omask] - expected = s[mask] - assert_series_equal(result, expected) - - # setitem - s2 = s.copy() - cop = s.copy() - cop[omask] = 5 - s2[mask] = 5 - assert_series_equal(cop, s2) - - # nans raise exception - omask[5:10] = np.nan - self.assertRaises(Exception, s.__getitem__, omask) - self.assertRaises(Exception, s.__setitem__, omask, 5) - - def test_getitem_setitem_boolean_corner(self): - ts = self.ts - mask_shifted = ts.shift(1, freq=BDay()) > ts.median() - - # these used to raise...?? - - self.assertRaises(Exception, ts.__getitem__, mask_shifted) - self.assertRaises(Exception, ts.__setitem__, mask_shifted, 1) - # ts[mask_shifted] - # ts[mask_shifted] = 1 - - self.assertRaises(Exception, ts.loc.__getitem__, mask_shifted) - self.assertRaises(Exception, ts.loc.__setitem__, mask_shifted, 1) - # ts.loc[mask_shifted] - # ts.loc[mask_shifted] = 2 - - def test_getitem_setitem_slice_integers(self): - s = Series(np.random.randn(8), index=[2, 4, 6, 8, 10, 12, 14, 16]) - - result = s[:4] - expected = s.reindex([2, 4, 6, 8]) - assert_series_equal(result, expected) - - s[:4] = 0 - self.assertTrue((s[:4] == 0).all()) - self.assertTrue(not (s[4:] == 0).any()) - - def test_getitem_setitem_datetime_tz_pytz(self): - tm._skip_if_no_pytz() - from pytz import timezone as tz - - from pandas import date_range - - N = 50 - # testing with timezone, GH #2785 - rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern') - ts = Series(np.random.randn(N), index=rng) - - # also test Timestamp tz handling, GH #2789 - result = ts.copy() - result["1990-01-01 09:00:00+00:00"] = 0 - result["1990-01-01 09:00:00+00:00"] = ts[4] - assert_series_equal(result, ts) - - result = ts.copy() - result["1990-01-01 03:00:00-06:00"] = 0 - result["1990-01-01 03:00:00-06:00"] = ts[4] - assert_series_equal(result, ts) - - # repeat with datetimes - result = ts.copy() - result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0 - result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4] - assert_series_equal(result, ts) - - result = ts.copy() - - # comparison dates with datetime MUST be localized! - date = tz('US/Central').localize(datetime(1990, 1, 1, 3)) - result[date] = 0 - result[date] = ts[4] - assert_series_equal(result, ts) - - def test_getitem_setitem_datetime_tz_dateutil(self): - tm._skip_if_no_dateutil() - from dateutil.tz import tzutc - from pandas._libs.tslib import _dateutil_gettz as gettz - - tz = lambda x: tzutc() if x == 'UTC' else gettz( - x) # handle special case for utc in dateutil - - from pandas import date_range - - N = 50 - - # testing with timezone, GH #2785 - rng = date_range('1/1/1990', periods=N, freq='H', - tz='America/New_York') - ts = Series(np.random.randn(N), index=rng) - - # also test Timestamp tz handling, GH #2789 - result = ts.copy() - result["1990-01-01 09:00:00+00:00"] = 0 - result["1990-01-01 09:00:00+00:00"] = ts[4] - assert_series_equal(result, ts) - - result = ts.copy() - result["1990-01-01 03:00:00-06:00"] = 0 - result["1990-01-01 03:00:00-06:00"] = ts[4] - assert_series_equal(result, ts) - - # repeat with datetimes - result = ts.copy() - result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0 - result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4] - assert_series_equal(result, ts) - - result = ts.copy() - result[datetime(1990, 1, 1, 3, tzinfo=tz('America/Chicago'))] = 0 - result[datetime(1990, 1, 1, 3, tzinfo=tz('America/Chicago'))] = ts[4] - assert_series_equal(result, ts) - - def test_getitem_setitem_datetimeindex(self): - N = 50 - # testing with timezone, GH #2785 - rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern') - ts = Series(np.random.randn(N), index=rng) - - result = ts["1990-01-01 04:00:00"] - expected = ts[4] - self.assertEqual(result, expected) - - result = ts.copy() - result["1990-01-01 04:00:00"] = 0 - result["1990-01-01 04:00:00"] = ts[4] - assert_series_equal(result, ts) - - result = ts["1990-01-01 04:00:00":"1990-01-01 07:00:00"] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts.copy() - result["1990-01-01 04:00:00":"1990-01-01 07:00:00"] = 0 - result["1990-01-01 04:00:00":"1990-01-01 07:00:00"] = ts[4:8] - assert_series_equal(result, ts) - - lb = "1990-01-01 04:00:00" - rb = "1990-01-01 07:00:00" - result = ts[(ts.index >= lb) & (ts.index <= rb)] - expected = ts[4:8] - assert_series_equal(result, expected) - - # repeat all the above with naive datetimes - result = ts[datetime(1990, 1, 1, 4)] - expected = ts[4] - self.assertEqual(result, expected) - - result = ts.copy() - result[datetime(1990, 1, 1, 4)] = 0 - result[datetime(1990, 1, 1, 4)] = ts[4] - assert_series_equal(result, ts) - - result = ts[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts.copy() - result[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] = 0 - result[datetime(1990, 1, 1, 4):datetime(1990, 1, 1, 7)] = ts[4:8] - assert_series_equal(result, ts) - - lb = datetime(1990, 1, 1, 4) - rb = datetime(1990, 1, 1, 7) - result = ts[(ts.index >= lb) & (ts.index <= rb)] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts[ts.index[4]] - expected = ts[4] - self.assertEqual(result, expected) - - result = ts[ts.index[4:8]] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts.copy() - result[ts.index[4:8]] = 0 - result[4:8] = ts[4:8] - assert_series_equal(result, ts) - - # also test partial date slicing - result = ts["1990-01-02"] - expected = ts[24:48] - assert_series_equal(result, expected) - - result = ts.copy() - result["1990-01-02"] = 0 - result["1990-01-02"] = ts[24:48] - assert_series_equal(result, ts) - - def test_getitem_setitem_periodindex(self): - from pandas import period_range - - N = 50 - rng = period_range('1/1/1990', periods=N, freq='H') - ts = Series(np.random.randn(N), index=rng) - - result = ts["1990-01-01 04"] - expected = ts[4] - self.assertEqual(result, expected) - - result = ts.copy() - result["1990-01-01 04"] = 0 - result["1990-01-01 04"] = ts[4] - assert_series_equal(result, ts) - - result = ts["1990-01-01 04":"1990-01-01 07"] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts.copy() - result["1990-01-01 04":"1990-01-01 07"] = 0 - result["1990-01-01 04":"1990-01-01 07"] = ts[4:8] - assert_series_equal(result, ts) - - lb = "1990-01-01 04" - rb = "1990-01-01 07" - result = ts[(ts.index >= lb) & (ts.index <= rb)] - expected = ts[4:8] - assert_series_equal(result, expected) - - # GH 2782 - result = ts[ts.index[4]] - expected = ts[4] - self.assertEqual(result, expected) - - result = ts[ts.index[4:8]] - expected = ts[4:8] - assert_series_equal(result, expected) - - result = ts.copy() - result[ts.index[4:8]] = 0 - result[4:8] = ts[4:8] - assert_series_equal(result, ts) - - def test_getitem_median_slice_bug(self): - index = date_range('20090415', '20090519', freq='2B') - s = Series(np.random.randn(13), index=index) - - indexer = [slice(6, 7, None)] - result = s[indexer] - expected = s[indexer[0]] - assert_series_equal(result, expected) - - def test_getitem_out_of_bounds(self): - # don't segfault, GH #495 - self.assertRaises(IndexError, self.ts.__getitem__, len(self.ts)) - - # GH #917 - s = Series([]) - self.assertRaises(IndexError, s.__getitem__, -1) - - def test_getitem_setitem_integers(self): - # caused bug without test - s = Series([1, 2, 3], ['a', 'b', 'c']) - - self.assertEqual(s.iloc[0], s['a']) - s.iloc[0] = 5 - self.assertAlmostEqual(s['a'], 5) - - def test_getitem_box_float64(self): - value = self.ts[5] - tm.assertIsInstance(value, np.float64) - - def test_getitem_ambiguous_keyerror(self): - s = Series(lrange(10), index=lrange(0, 20, 2)) - self.assertRaises(KeyError, s.__getitem__, 1) - self.assertRaises(KeyError, s.loc.__getitem__, 1) - - def test_getitem_unordered_dup(self): - obj = Series(lrange(5), index=['c', 'a', 'a', 'b', 'b']) - self.assertTrue(is_scalar(obj['c'])) - self.assertEqual(obj['c'], 0) - - def test_getitem_dups_with_missing(self): - - # breaks reindex, so need to use .loc internally - # GH 4246 - s = Series([1, 2, 3, 4], ['foo', 'bar', 'foo', 'bah']) - expected = s.loc[['foo', 'bar', 'bah', 'bam']] - result = s[['foo', 'bar', 'bah', 'bam']] - assert_series_equal(result, expected) - - def test_getitem_dups(self): - s = Series(range(5), index=['A', 'A', 'B', 'C', 'C'], dtype=np.int64) - expected = Series([3, 4], index=['C', 'C'], dtype=np.int64) - result = s['C'] - assert_series_equal(result, expected) - - def test_getitem_dataframe(self): - rng = list(range(10)) - s = pd.Series(10, index=rng) - df = pd.DataFrame(rng, index=rng) - self.assertRaises(TypeError, s.__getitem__, df > 5) - - def test_getitem_callable(self): - # GH 12533 - s = pd.Series(4, index=list('ABCD')) - result = s[lambda x: 'A'] - self.assertEqual(result, s.loc['A']) - - result = s[lambda x: ['A', 'B']] - tm.assert_series_equal(result, s.loc[['A', 'B']]) - - result = s[lambda x: [True, False, True, True]] - tm.assert_series_equal(result, s.iloc[[0, 2, 3]]) - - def test_setitem_ambiguous_keyerror(self): - s = Series(lrange(10), index=lrange(0, 20, 2)) - - # equivalent of an append - s2 = s.copy() - s2[1] = 5 - expected = s.append(Series([5], index=[1])) - assert_series_equal(s2, expected) - - s2 = s.copy() - s2.loc[1] = 5 - expected = s.append(Series([5], index=[1])) - assert_series_equal(s2, expected) - - def test_setitem_float_labels(self): - # note labels are floats - s = Series(['a', 'b', 'c'], index=[0, 0.5, 1]) - tmp = s.copy() - - s.loc[1] = 'zoo' - tmp.iloc[2] = 'zoo' - - assert_series_equal(s, tmp) - - def test_setitem_callable(self): - # GH 12533 - s = pd.Series([1, 2, 3, 4], index=list('ABCD')) - s[lambda x: 'A'] = -1 - tm.assert_series_equal(s, pd.Series([-1, 2, 3, 4], index=list('ABCD'))) - - def test_setitem_other_callable(self): - # GH 13299 - inc = lambda x: x + 1 - - s = pd.Series([1, 2, -1, 4]) - s[s < 0] = inc - - expected = pd.Series([1, 2, inc, 4]) - tm.assert_series_equal(s, expected) - - def test_slice(self): - numSlice = self.series[10:20] - numSliceEnd = self.series[-10:] - objSlice = self.objSeries[10:20] - - self.assertNotIn(self.series.index[9], numSlice.index) - self.assertNotIn(self.objSeries.index[9], objSlice.index) - - self.assertEqual(len(numSlice), len(numSlice.index)) - self.assertEqual(self.series[numSlice.index[0]], - numSlice[numSlice.index[0]]) - - self.assertEqual(numSlice.index[1], self.series.index[11]) - - self.assertTrue(tm.equalContents(numSliceEnd, np.array(self.series)[ - -10:])) - - # test return view - sl = self.series[10:20] - sl[:] = 0 - self.assertTrue((self.series[10:20] == 0).all()) - - def test_slice_can_reorder_not_uniquely_indexed(self): - s = Series(1, index=['a', 'a', 'b', 'b', 'c']) - s[::-1] # it works! - - def test_slice_float_get_set(self): - - self.assertRaises(TypeError, lambda: self.ts[4.0:10.0]) - - def f(): - self.ts[4.0:10.0] = 0 - - self.assertRaises(TypeError, f) - - self.assertRaises(TypeError, self.ts.__getitem__, slice(4.5, 10.0)) - self.assertRaises(TypeError, self.ts.__setitem__, slice(4.5, 10.0), 0) - - def test_slice_floats2(self): - s = Series(np.random.rand(10), index=np.arange(10, 20, dtype=float)) - - self.assertEqual(len(s.loc[12.0:]), 8) - self.assertEqual(len(s.loc[12.5:]), 7) - - i = np.arange(10, 20, dtype=float) - i[2] = 12.2 - s.index = i - self.assertEqual(len(s.loc[12.0:]), 8) - self.assertEqual(len(s.loc[12.5:]), 7) - - def test_slice_float64(self): - - values = np.arange(10., 50., 2) - index = Index(values) - - start, end = values[[5, 15]] - - s = Series(np.random.randn(20), index=index) - - result = s[start:end] - expected = s.iloc[5:16] - assert_series_equal(result, expected) - - result = s.loc[start:end] - assert_series_equal(result, expected) - - df = DataFrame(np.random.randn(20, 3), index=index) - - result = df[start:end] - expected = df.iloc[5:16] - tm.assert_frame_equal(result, expected) - - result = df.loc[start:end] - tm.assert_frame_equal(result, expected) - - def test_setitem(self): - self.ts[self.ts.index[5]] = np.NaN - self.ts[[1, 2, 17]] = np.NaN - self.ts[6] = np.NaN - self.assertTrue(np.isnan(self.ts[6])) - self.assertTrue(np.isnan(self.ts[2])) - self.ts[np.isnan(self.ts)] = 5 - self.assertFalse(np.isnan(self.ts[2])) - - # caught this bug when writing tests - series = Series(tm.makeIntIndex(20).astype(float), - index=tm.makeIntIndex(20)) - - series[::2] = 0 - self.assertTrue((series[::2] == 0).all()) - - # set item that's not contained - s = self.series.copy() - s['foobar'] = 1 - - app = Series([1], index=['foobar'], name='series') - expected = self.series.append(app) - assert_series_equal(s, expected) - - # Test for issue #10193 - key = pd.Timestamp('2012-01-01') - series = pd.Series() - series[key] = 47 - expected = pd.Series(47, [key]) - assert_series_equal(series, expected) - - series = pd.Series([], pd.DatetimeIndex([], freq='D')) - series[key] = 47 - expected = pd.Series(47, pd.DatetimeIndex([key], freq='D')) - assert_series_equal(series, expected) - - def test_setitem_dtypes(self): - - # change dtypes - # GH 4463 - expected = Series([np.nan, 2, 3]) - - s = Series([1, 2, 3]) - s.iloc[0] = np.nan - assert_series_equal(s, expected) - - s = Series([1, 2, 3]) - s.loc[0] = np.nan - assert_series_equal(s, expected) - - s = Series([1, 2, 3]) - s[0] = np.nan - assert_series_equal(s, expected) - - s = Series([False]) - s.loc[0] = np.nan - assert_series_equal(s, Series([np.nan])) - - s = Series([False, True]) - s.loc[0] = np.nan - assert_series_equal(s, Series([np.nan, 1.0])) - - def test_set_value(self): - idx = self.ts.index[10] - res = self.ts.set_value(idx, 0) - self.assertIs(res, self.ts) - self.assertEqual(self.ts[idx], 0) - - # equiv - s = self.series.copy() - res = s.set_value('foobar', 0) - self.assertIs(res, s) - self.assertEqual(res.index[-1], 'foobar') - self.assertEqual(res['foobar'], 0) - - s = self.series.copy() - s.loc['foobar'] = 0 - self.assertEqual(s.index[-1], 'foobar') - self.assertEqual(s['foobar'], 0) - - def test_setslice(self): - sl = self.ts[5:20] - self.assertEqual(len(sl), len(sl.index)) - self.assertTrue(sl.index.is_unique) - - def test_basic_getitem_setitem_corner(self): - # invalid tuples, e.g. self.ts[:, None] vs. self.ts[:, 2] - with tm.assertRaisesRegexp(ValueError, 'tuple-index'): - self.ts[:, 2] - with tm.assertRaisesRegexp(ValueError, 'tuple-index'): - self.ts[:, 2] = 2 - - # weird lists. [slice(0, 5)] will work but not two slices - result = self.ts[[slice(None, 5)]] - expected = self.ts[:5] - assert_series_equal(result, expected) - - # OK - self.assertRaises(Exception, self.ts.__getitem__, - [5, slice(None, None)]) - self.assertRaises(Exception, self.ts.__setitem__, - [5, slice(None, None)], 2) - - def test_basic_getitem_with_labels(self): - indices = self.ts.index[[5, 10, 15]] - - result = self.ts[indices] - expected = self.ts.reindex(indices) - assert_series_equal(result, expected) - - result = self.ts[indices[0]:indices[2]] - expected = self.ts.loc[indices[0]:indices[2]] - assert_series_equal(result, expected) - - # integer indexes, be careful - s = Series(np.random.randn(10), index=lrange(0, 20, 2)) - inds = [0, 2, 5, 7, 8] - arr_inds = np.array([0, 2, 5, 7, 8]) - result = s[inds] - expected = s.reindex(inds) - assert_series_equal(result, expected) - - result = s[arr_inds] - expected = s.reindex(arr_inds) - assert_series_equal(result, expected) - - # GH12089 - # with tz for values - s = Series(pd.date_range("2011-01-01", periods=3, tz="US/Eastern"), - index=['a', 'b', 'c']) - expected = Timestamp('2011-01-01', tz='US/Eastern') - result = s.loc['a'] - self.assertEqual(result, expected) - result = s.iloc[0] - self.assertEqual(result, expected) - result = s['a'] - self.assertEqual(result, expected) - - def test_basic_setitem_with_labels(self): - indices = self.ts.index[[5, 10, 15]] - - cp = self.ts.copy() - exp = self.ts.copy() - cp[indices] = 0 - exp.loc[indices] = 0 - assert_series_equal(cp, exp) - - cp = self.ts.copy() - exp = self.ts.copy() - cp[indices[0]:indices[2]] = 0 - exp.loc[indices[0]:indices[2]] = 0 - assert_series_equal(cp, exp) - - # integer indexes, be careful - s = Series(np.random.randn(10), index=lrange(0, 20, 2)) - inds = [0, 4, 6] - arr_inds = np.array([0, 4, 6]) - - cp = s.copy() - exp = s.copy() - s[inds] = 0 - s.loc[inds] = 0 - assert_series_equal(cp, exp) - - cp = s.copy() - exp = s.copy() - s[arr_inds] = 0 - s.loc[arr_inds] = 0 - assert_series_equal(cp, exp) - - inds_notfound = [0, 4, 5, 6] - arr_inds_notfound = np.array([0, 4, 5, 6]) - self.assertRaises(Exception, s.__setitem__, inds_notfound, 0) - self.assertRaises(Exception, s.__setitem__, arr_inds_notfound, 0) - - # GH12089 - # with tz for values - s = Series(pd.date_range("2011-01-01", periods=3, tz="US/Eastern"), - index=['a', 'b', 'c']) - s2 = s.copy() - expected = Timestamp('2011-01-03', tz='US/Eastern') - s2.loc['a'] = expected - result = s2.loc['a'] - self.assertEqual(result, expected) - - s2 = s.copy() - s2.iloc[0] = expected - result = s2.iloc[0] - self.assertEqual(result, expected) - - s2 = s.copy() - s2['a'] = expected - result = s2['a'] - self.assertEqual(result, expected) - - def test_loc_getitem(self): - inds = self.series.index[[3, 4, 7]] - assert_series_equal(self.series.loc[inds], self.series.reindex(inds)) - assert_series_equal(self.series.iloc[5::2], self.series[5::2]) - - # slice with indices - d1, d2 = self.ts.index[[5, 15]] - result = self.ts.loc[d1:d2] - expected = self.ts.truncate(d1, d2) - assert_series_equal(result, expected) - - # boolean - mask = self.series > self.series.median() - assert_series_equal(self.series.loc[mask], self.series[mask]) - - # ask for index value - self.assertEqual(self.ts.loc[d1], self.ts[d1]) - self.assertEqual(self.ts.loc[d2], self.ts[d2]) - - def test_loc_getitem_not_monotonic(self): - d1, d2 = self.ts.index[[5, 15]] - - ts2 = self.ts[::2][[1, 2, 0]] - - self.assertRaises(KeyError, ts2.loc.__getitem__, slice(d1, d2)) - self.assertRaises(KeyError, ts2.loc.__setitem__, slice(d1, d2), 0) - - def test_loc_getitem_setitem_integer_slice_keyerrors(self): - s = Series(np.random.randn(10), index=lrange(0, 20, 2)) - - # this is OK - cp = s.copy() - cp.iloc[4:10] = 0 - self.assertTrue((cp.iloc[4:10] == 0).all()) - - # so is this - cp = s.copy() - cp.iloc[3:11] = 0 - self.assertTrue((cp.iloc[3:11] == 0).values.all()) - - result = s.iloc[2:6] - result2 = s.loc[3:11] - expected = s.reindex([4, 6, 8, 10]) - - assert_series_equal(result, expected) - assert_series_equal(result2, expected) - - # non-monotonic, raise KeyError - s2 = s.iloc[lrange(5) + lrange(5, 10)[::-1]] - self.assertRaises(KeyError, s2.loc.__getitem__, slice(3, 11)) - self.assertRaises(KeyError, s2.loc.__setitem__, slice(3, 11), 0) - - def test_loc_getitem_iterator(self): - idx = iter(self.series.index[:10]) - result = self.series.loc[idx] - assert_series_equal(result, self.series[:10]) - - def test_setitem_with_tz(self): - for tz in ['US/Eastern', 'UTC', 'Asia/Tokyo']: - orig = pd.Series(pd.date_range('2016-01-01', freq='H', periods=3, - tz=tz)) - self.assertEqual(orig.dtype, 'datetime64[ns, {0}]'.format(tz)) - - # scalar - s = orig.copy() - s[1] = pd.Timestamp('2011-01-01', tz=tz) - exp = pd.Series([pd.Timestamp('2016-01-01 00:00', tz=tz), - pd.Timestamp('2011-01-01 00:00', tz=tz), - pd.Timestamp('2016-01-01 02:00', tz=tz)]) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.loc[1] = pd.Timestamp('2011-01-01', tz=tz) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.iloc[1] = pd.Timestamp('2011-01-01', tz=tz) - tm.assert_series_equal(s, exp) - - # vector - vals = pd.Series([pd.Timestamp('2011-01-01', tz=tz), - pd.Timestamp('2012-01-01', tz=tz)], index=[1, 2]) - self.assertEqual(vals.dtype, 'datetime64[ns, {0}]'.format(tz)) - - s[[1, 2]] = vals - exp = pd.Series([pd.Timestamp('2016-01-01 00:00', tz=tz), - pd.Timestamp('2011-01-01 00:00', tz=tz), - pd.Timestamp('2012-01-01 00:00', tz=tz)]) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.loc[[1, 2]] = vals - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.iloc[[1, 2]] = vals - tm.assert_series_equal(s, exp) - - def test_setitem_with_tz_dst(self): - # GH XXX - tz = 'US/Eastern' - orig = pd.Series(pd.date_range('2016-11-06', freq='H', periods=3, - tz=tz)) - self.assertEqual(orig.dtype, 'datetime64[ns, {0}]'.format(tz)) - - # scalar - s = orig.copy() - s[1] = pd.Timestamp('2011-01-01', tz=tz) - exp = pd.Series([pd.Timestamp('2016-11-06 00:00', tz=tz), - pd.Timestamp('2011-01-01 00:00', tz=tz), - pd.Timestamp('2016-11-06 02:00', tz=tz)]) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.loc[1] = pd.Timestamp('2011-01-01', tz=tz) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.iloc[1] = pd.Timestamp('2011-01-01', tz=tz) - tm.assert_series_equal(s, exp) - - # vector - vals = pd.Series([pd.Timestamp('2011-01-01', tz=tz), - pd.Timestamp('2012-01-01', tz=tz)], index=[1, 2]) - self.assertEqual(vals.dtype, 'datetime64[ns, {0}]'.format(tz)) - - s[[1, 2]] = vals - exp = pd.Series([pd.Timestamp('2016-11-06 00:00', tz=tz), - pd.Timestamp('2011-01-01 00:00', tz=tz), - pd.Timestamp('2012-01-01 00:00', tz=tz)]) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.loc[[1, 2]] = vals - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.iloc[[1, 2]] = vals - tm.assert_series_equal(s, exp) - - def test_where(self): - s = Series(np.random.randn(5)) - cond = s > 0 - - rs = s.where(cond).dropna() - rs2 = s[cond] - assert_series_equal(rs, rs2) - - rs = s.where(cond, -s) - assert_series_equal(rs, s.abs()) - - rs = s.where(cond) - assert (s.shape == rs.shape) - assert (rs is not s) - - # test alignment - cond = Series([True, False, False, True, False], index=s.index) - s2 = -(s.abs()) - - expected = s2[cond].reindex(s2.index[:3]).reindex(s2.index) - rs = s2.where(cond[:3]) - assert_series_equal(rs, expected) - - expected = s2.abs() - expected.iloc[0] = s2[0] - rs = s2.where(cond[:3], -s2) - assert_series_equal(rs, expected) - - self.assertRaises(ValueError, s.where, 1) - self.assertRaises(ValueError, s.where, cond[:3].values, -s) - - # GH 2745 - s = Series([1, 2]) - s[[True, False]] = [0, 1] - expected = Series([0, 2]) - assert_series_equal(s, expected) - - # failures - self.assertRaises(ValueError, s.__setitem__, tuple([[[True, False]]]), - [0, 2, 3]) - self.assertRaises(ValueError, s.__setitem__, tuple([[[True, False]]]), - []) - - # unsafe dtype changes - for dtype in [np.int8, np.int16, np.int32, np.int64, np.float16, - np.float32, np.float64]: - s = Series(np.arange(10), dtype=dtype) - mask = s < 5 - s[mask] = lrange(2, 7) - expected = Series(lrange(2, 7) + lrange(5, 10), dtype=dtype) - assert_series_equal(s, expected) - self.assertEqual(s.dtype, expected.dtype) - - # these are allowed operations, but are upcasted - for dtype in [np.int64, np.float64]: - s = Series(np.arange(10), dtype=dtype) - mask = s < 5 - values = [2.5, 3.5, 4.5, 5.5, 6.5] - s[mask] = values - expected = Series(values + lrange(5, 10), dtype='float64') - assert_series_equal(s, expected) - self.assertEqual(s.dtype, expected.dtype) - - # GH 9731 - s = Series(np.arange(10), dtype='int64') - mask = s > 5 - values = [2.5, 3.5, 4.5, 5.5] - s[mask] = values - expected = Series(lrange(6) + values, dtype='float64') - assert_series_equal(s, expected) - - # can't do these as we are forced to change the itemsize of the input - # to something we cannot - for dtype in [np.int8, np.int16, np.int32, np.float16, np.float32]: - s = Series(np.arange(10), dtype=dtype) - mask = s < 5 - values = [2.5, 3.5, 4.5, 5.5, 6.5] - self.assertRaises(Exception, s.__setitem__, tuple(mask), values) - - # GH3235 - s = Series(np.arange(10), dtype='int64') - mask = s < 5 - s[mask] = lrange(2, 7) - expected = Series(lrange(2, 7) + lrange(5, 10), dtype='int64') - assert_series_equal(s, expected) - self.assertEqual(s.dtype, expected.dtype) - - s = Series(np.arange(10), dtype='int64') - mask = s > 5 - s[mask] = [0] * 4 - expected = Series([0, 1, 2, 3, 4, 5] + [0] * 4, dtype='int64') - assert_series_equal(s, expected) - - s = Series(np.arange(10)) - mask = s > 5 - - def f(): - s[mask] = [5, 4, 3, 2, 1] - - self.assertRaises(ValueError, f) - - def f(): - s[mask] = [0] * 5 - - self.assertRaises(ValueError, f) - - # dtype changes - s = Series([1, 2, 3, 4]) - result = s.where(s > 2, np.nan) - expected = Series([np.nan, np.nan, 3, 4]) - assert_series_equal(result, expected) - - # GH 4667 - # setting with None changes dtype - s = Series(range(10)).astype(float) - s[8] = None - result = s[8] - self.assertTrue(isnull(result)) - - s = Series(range(10)).astype(float) - s[s > 8] = None - result = s[isnull(s)] - expected = Series(np.nan, index=[9]) - assert_series_equal(result, expected) - - def test_where_array_like(self): - # see gh-15414 - s = Series([1, 2, 3]) - cond = [False, True, True] - expected = Series([np.nan, 2, 3]) - klasses = [list, tuple, np.array, Series] - - for klass in klasses: - result = s.where(klass(cond)) - assert_series_equal(result, expected) - - def test_where_invalid_input(self): - # see gh-15414: only boolean arrays accepted - s = Series([1, 2, 3]) - msg = "Boolean array expected for the condition" - - conds = [ - [1, 0, 1], - Series([2, 5, 7]), - ["True", "False", "True"], - [Timestamp("2017-01-01"), - pd.NaT, Timestamp("2017-01-02")] - ] - - for cond in conds: - with tm.assertRaisesRegexp(ValueError, msg): - s.where(cond) - - msg = "Array conditional must be same shape as self" - with tm.assertRaisesRegexp(ValueError, msg): - s.where([True]) - - def test_where_ndframe_align(self): - msg = "Array conditional must be same shape as self" - s = Series([1, 2, 3]) - - cond = [True] - with tm.assertRaisesRegexp(ValueError, msg): - s.where(cond) - - expected = Series([1, np.nan, np.nan]) - - out = s.where(Series(cond)) - tm.assert_series_equal(out, expected) - - cond = np.array([False, True, False, True]) - with tm.assertRaisesRegexp(ValueError, msg): - s.where(cond) - - expected = Series([np.nan, 2, np.nan]) - - out = s.where(Series(cond)) - tm.assert_series_equal(out, expected) - - def test_where_setitem_invalid(self): - - # GH 2702 - # make sure correct exceptions are raised on invalid list assignment - - # slice - s = Series(list('abc')) - - def f(): - s[0:3] = list(range(27)) - - self.assertRaises(ValueError, f) - - s[0:3] = list(range(3)) - expected = Series([0, 1, 2]) - assert_series_equal(s.astype(np.int64), expected, ) - - # slice with step - s = Series(list('abcdef')) - - def f(): - s[0:4:2] = list(range(27)) - - self.assertRaises(ValueError, f) - - s = Series(list('abcdef')) - s[0:4:2] = list(range(2)) - expected = Series([0, 'b', 1, 'd', 'e', 'f']) - assert_series_equal(s, expected) - - # neg slices - s = Series(list('abcdef')) - - def f(): - s[:-1] = list(range(27)) - - self.assertRaises(ValueError, f) - - s[-3:-1] = list(range(2)) - expected = Series(['a', 'b', 'c', 0, 1, 'f']) - assert_series_equal(s, expected) - - # list - s = Series(list('abc')) - - def f(): - s[[0, 1, 2]] = list(range(27)) - - self.assertRaises(ValueError, f) - - s = Series(list('abc')) - - def f(): - s[[0, 1, 2]] = list(range(2)) - - self.assertRaises(ValueError, f) - - # scalar - s = Series(list('abc')) - s[0] = list(range(10)) - expected = Series([list(range(10)), 'b', 'c']) - assert_series_equal(s, expected) - - def test_where_broadcast(self): - # Test a variety of differently sized series - for size in range(2, 6): - # Test a variety of boolean indices - for selection in [ - # First element should be set - np.resize([True, False, False, False, False], size), - # Set alternating elements] - np.resize([True, False], size), - # No element should be set - np.resize([False], size)]: - - # Test a variety of different numbers as content - for item in [2.0, np.nan, np.finfo(np.float).max, - np.finfo(np.float).min]: - # Test numpy arrays, lists and tuples as the input to be - # broadcast - for arr in [np.array([item]), [item], (item, )]: - data = np.arange(size, dtype=float) - s = Series(data) - s[selection] = arr - # Construct the expected series by taking the source - # data or item based on the selection - expected = Series([item if use_item else data[ - i] for i, use_item in enumerate(selection)]) - assert_series_equal(s, expected) - - s = Series(data) - result = s.where(~selection, arr) - assert_series_equal(result, expected) - - def test_where_inplace(self): - s = Series(np.random.randn(5)) - cond = s > 0 - - rs = s.copy() - - rs.where(cond, inplace=True) - assert_series_equal(rs.dropna(), s[cond]) - assert_series_equal(rs, s.where(cond)) - - rs = s.copy() - rs.where(cond, -s, inplace=True) - assert_series_equal(rs, s.where(cond, -s)) - - def test_where_dups(self): - # GH 4550 - # where crashes with dups in index - s1 = Series(list(range(3))) - s2 = Series(list(range(3))) - comb = pd.concat([s1, s2]) - result = comb.where(comb < 2) - expected = Series([0, 1, np.nan, 0, 1, np.nan], - index=[0, 1, 2, 0, 1, 2]) - assert_series_equal(result, expected) - - # GH 4548 - # inplace updating not working with dups - comb[comb < 1] = 5 - expected = Series([5, 1, 2, 5, 1, 2], index=[0, 1, 2, 0, 1, 2]) - assert_series_equal(comb, expected) - - comb[comb < 2] += 10 - expected = Series([5, 11, 2, 5, 11, 2], index=[0, 1, 2, 0, 1, 2]) - assert_series_equal(comb, expected) - - def test_where_datetime(self): - s = Series(date_range('20130102', periods=2)) - expected = Series([10, 10], dtype='datetime64[ns]') - mask = np.array([False, False]) - - rs = s.where(mask, [10, 10]) - assert_series_equal(rs, expected) - - rs = s.where(mask, 10) - assert_series_equal(rs, expected) - - rs = s.where(mask, 10.0) - assert_series_equal(rs, expected) - - rs = s.where(mask, [10.0, 10.0]) - assert_series_equal(rs, expected) - - rs = s.where(mask, [10.0, np.nan]) - expected = Series([10, None], dtype='datetime64[ns]') - assert_series_equal(rs, expected) - - # GH 15701 - timestamps = ['2016-12-31 12:00:04+00:00', - '2016-12-31 12:00:04.010000+00:00'] - s = Series([pd.Timestamp(t) for t in timestamps]) - rs = s.where(Series([False, True])) - expected = Series([pd.NaT, s[1]]) - assert_series_equal(rs, expected) - - def test_where_timedelta(self): - s = Series([1, 2], dtype='timedelta64[ns]') - expected = Series([10, 10], dtype='timedelta64[ns]') - mask = np.array([False, False]) - - rs = s.where(mask, [10, 10]) - assert_series_equal(rs, expected) - - rs = s.where(mask, 10) - assert_series_equal(rs, expected) - - rs = s.where(mask, 10.0) - assert_series_equal(rs, expected) - - rs = s.where(mask, [10.0, 10.0]) - assert_series_equal(rs, expected) - - rs = s.where(mask, [10.0, np.nan]) - expected = Series([10, None], dtype='timedelta64[ns]') - assert_series_equal(rs, expected) - - def test_mask(self): - # compare with tested results in test_where - s = Series(np.random.randn(5)) - cond = s > 0 - - rs = s.where(~cond, np.nan) - assert_series_equal(rs, s.mask(cond)) - - rs = s.where(~cond) - rs2 = s.mask(cond) - assert_series_equal(rs, rs2) - - rs = s.where(~cond, -s) - rs2 = s.mask(cond, -s) - assert_series_equal(rs, rs2) - - cond = Series([True, False, False, True, False], index=s.index) - s2 = -(s.abs()) - rs = s2.where(~cond[:3]) - rs2 = s2.mask(cond[:3]) - assert_series_equal(rs, rs2) - - rs = s2.where(~cond[:3], -s2) - rs2 = s2.mask(cond[:3], -s2) - assert_series_equal(rs, rs2) - - self.assertRaises(ValueError, s.mask, 1) - self.assertRaises(ValueError, s.mask, cond[:3].values, -s) - - # dtype changes - s = Series([1, 2, 3, 4]) - result = s.mask(s > 2, np.nan) - expected = Series([1, 2, np.nan, np.nan]) - assert_series_equal(result, expected) - - def test_mask_broadcast(self): - # GH 8801 - # copied from test_where_broadcast - for size in range(2, 6): - for selection in [ - # First element should be set - np.resize([True, False, False, False, False], size), - # Set alternating elements] - np.resize([True, False], size), - # No element should be set - np.resize([False], size)]: - for item in [2.0, np.nan, np.finfo(np.float).max, - np.finfo(np.float).min]: - for arr in [np.array([item]), [item], (item, )]: - data = np.arange(size, dtype=float) - s = Series(data) - result = s.mask(selection, arr) - expected = Series([item if use_item else data[ - i] for i, use_item in enumerate(selection)]) - assert_series_equal(result, expected) - - def test_mask_inplace(self): - s = Series(np.random.randn(5)) - cond = s > 0 - - rs = s.copy() - rs.mask(cond, inplace=True) - assert_series_equal(rs.dropna(), s[~cond]) - assert_series_equal(rs, s.mask(cond)) - - rs = s.copy() - rs.mask(cond, -s, inplace=True) - assert_series_equal(rs, s.mask(cond, -s)) - - def test_ix_setitem(self): - inds = self.series.index[[3, 4, 7]] - - result = self.series.copy() - result.loc[inds] = 5 - - expected = self.series.copy() - expected[[3, 4, 7]] = 5 - assert_series_equal(result, expected) - - result.iloc[5:10] = 10 - expected[5:10] = 10 - assert_series_equal(result, expected) - - # set slice with indices - d1, d2 = self.series.index[[5, 15]] - result.loc[d1:d2] = 6 - expected[5:16] = 6 # because it's inclusive - assert_series_equal(result, expected) - - # set index value - self.series.loc[d1] = 4 - self.series.loc[d2] = 6 - self.assertEqual(self.series[d1], 4) - self.assertEqual(self.series[d2], 6) - - def test_where_numeric_with_string(self): - # GH 9280 - s = pd.Series([1, 2, 3]) - w = s.where(s > 1, 'X') - - self.assertFalse(is_integer(w[0])) - self.assertTrue(is_integer(w[1])) - self.assertTrue(is_integer(w[2])) - self.assertTrue(isinstance(w[0], str)) - self.assertTrue(w.dtype == 'object') - - w = s.where(s > 1, ['X', 'Y', 'Z']) - self.assertFalse(is_integer(w[0])) - self.assertTrue(is_integer(w[1])) - self.assertTrue(is_integer(w[2])) - self.assertTrue(isinstance(w[0], str)) - self.assertTrue(w.dtype == 'object') - - w = s.where(s > 1, np.array(['X', 'Y', 'Z'])) - self.assertFalse(is_integer(w[0])) - self.assertTrue(is_integer(w[1])) - self.assertTrue(is_integer(w[2])) - self.assertTrue(isinstance(w[0], str)) - self.assertTrue(w.dtype == 'object') - - def test_setitem_boolean(self): - mask = self.series > self.series.median() - - # similiar indexed series - result = self.series.copy() - result[mask] = self.series * 2 - expected = self.series * 2 - assert_series_equal(result[mask], expected[mask]) - - # needs alignment - result = self.series.copy() - result[mask] = (self.series * 2)[0:5] - expected = (self.series * 2)[0:5].reindex_like(self.series) - expected[-mask] = self.series[mask] - assert_series_equal(result[mask], expected[mask]) - - def test_ix_setitem_boolean(self): - mask = self.series > self.series.median() - - result = self.series.copy() - result.loc[mask] = 0 - expected = self.series - expected[mask] = 0 - assert_series_equal(result, expected) - - def test_ix_setitem_corner(self): - inds = list(self.series.index[[5, 8, 12]]) - self.series.loc[inds] = 5 - self.assertRaises(Exception, self.series.loc.__setitem__, - inds + ['foo'], 5) - - def test_get_set_boolean_different_order(self): - ordered = self.series.sort_values() - - # setting - copy = self.series.copy() - copy[ordered > 0] = 0 - - expected = self.series.copy() - expected[expected > 0] = 0 - - assert_series_equal(copy, expected) - - # getting - sel = self.series[ordered > 0] - exp = self.series[self.series > 0] - assert_series_equal(sel, exp) - - def test_setitem_na(self): - # these induce dtype changes - expected = Series([np.nan, 3, np.nan, 5, np.nan, 7, np.nan, 9, np.nan]) - s = Series([2, 3, 4, 5, 6, 7, 8, 9, 10]) - s[::2] = np.nan - assert_series_equal(s, expected) - - # get's coerced to float, right? - expected = Series([np.nan, 1, np.nan, 0]) - s = Series([True, True, False, False]) - s[::2] = np.nan - assert_series_equal(s, expected) - - expected = Series([np.nan, np.nan, np.nan, np.nan, np.nan, 5, 6, 7, 8, - 9]) - s = Series(np.arange(10)) - s[:5] = np.nan - assert_series_equal(s, expected) - - def test_basic_indexing(self): - s = Series(np.random.randn(5), index=['a', 'b', 'a', 'a', 'b']) - - self.assertRaises(IndexError, s.__getitem__, 5) - self.assertRaises(IndexError, s.__setitem__, 5, 0) - - self.assertRaises(KeyError, s.__getitem__, 'c') - - s = s.sort_index() - - self.assertRaises(IndexError, s.__getitem__, 5) - self.assertRaises(IndexError, s.__setitem__, 5, 0) - - def test_int_indexing(self): - s = Series(np.random.randn(6), index=[0, 0, 1, 1, 2, 2]) - - self.assertRaises(KeyError, s.__getitem__, 5) - - self.assertRaises(KeyError, s.__getitem__, 'c') - - # not monotonic - s = Series(np.random.randn(6), index=[2, 2, 0, 0, 1, 1]) - - self.assertRaises(KeyError, s.__getitem__, 5) - - self.assertRaises(KeyError, s.__getitem__, 'c') - - def test_datetime_indexing(self): - from pandas import date_range - - index = date_range('1/1/2000', '1/7/2000') - index = index.repeat(3) - - s = Series(len(index), index=index) - stamp = Timestamp('1/8/2000') - - self.assertRaises(KeyError, s.__getitem__, stamp) - s[stamp] = 0 - self.assertEqual(s[stamp], 0) - - # not monotonic - s = Series(len(index), index=index) - s = s[::-1] - - self.assertRaises(KeyError, s.__getitem__, stamp) - s[stamp] = 0 - self.assertEqual(s[stamp], 0) - - def test_timedelta_assignment(self): - # GH 8209 - s = Series([]) - s.loc['B'] = timedelta(1) - tm.assert_series_equal(s, Series(Timedelta('1 days'), index=['B'])) - - s = s.reindex(s.index.insert(0, 'A')) - tm.assert_series_equal(s, Series( - [np.nan, Timedelta('1 days')], index=['A', 'B'])) - - result = s.fillna(timedelta(1)) - expected = Series(Timedelta('1 days'), index=['A', 'B']) - tm.assert_series_equal(result, expected) - - s.loc['A'] = timedelta(1) - tm.assert_series_equal(s, expected) - - # GH 14155 - s = Series(10 * [np.timedelta64(10, 'm')]) - s.loc[[1, 2, 3]] = np.timedelta64(20, 'm') - expected = pd.Series(10 * [np.timedelta64(10, 'm')]) - expected.loc[[1, 2, 3]] = pd.Timedelta(np.timedelta64(20, 'm')) - tm.assert_series_equal(s, expected) - - def test_underlying_data_conversion(self): - - # GH 4080 - df = DataFrame(dict((c, [1, 2, 3]) for c in ['a', 'b', 'c'])) - df.set_index(['a', 'b', 'c'], inplace=True) - s = Series([1], index=[(2, 2, 2)]) - df['val'] = 0 - df - df['val'].update(s) - - expected = DataFrame( - dict(a=[1, 2, 3], b=[1, 2, 3], c=[1, 2, 3], val=[0, 1, 0])) - expected.set_index(['a', 'b', 'c'], inplace=True) - tm.assert_frame_equal(df, expected) - - # GH 3970 - # these are chained assignments as well - pd.set_option('chained_assignment', None) - df = DataFrame({"aa": range(5), "bb": [2.2] * 5}) - df["cc"] = 0.0 - - ck = [True] * len(df) - - df["bb"].iloc[0] = .13 - - # TODO: unused - df_tmp = df.iloc[ck] # noqa - - df["bb"].iloc[0] = .15 - self.assertEqual(df['bb'].iloc[0], 0.15) - pd.set_option('chained_assignment', 'raise') - - # GH 3217 - df = DataFrame(dict(a=[1, 3], b=[np.nan, 2])) - df['c'] = np.nan - df['c'].update(pd.Series(['foo'], index=[0])) - - expected = DataFrame(dict(a=[1, 3], b=[np.nan, 2], c=['foo', np.nan])) - tm.assert_frame_equal(df, expected) - - def test_preserveRefs(self): - seq = self.ts[[5, 10, 15]] - seq[1] = np.NaN - self.assertFalse(np.isnan(self.ts[10])) - - def test_drop(self): - - # unique - s = Series([1, 2], index=['one', 'two']) - expected = Series([1], index=['one']) - result = s.drop(['two']) - assert_series_equal(result, expected) - result = s.drop('two', axis='rows') - assert_series_equal(result, expected) - - # non-unique - # GH 5248 - s = Series([1, 1, 2], index=['one', 'two', 'one']) - expected = Series([1, 2], index=['one', 'one']) - result = s.drop(['two'], axis=0) - assert_series_equal(result, expected) - result = s.drop('two') - assert_series_equal(result, expected) - - expected = Series([1], index=['two']) - result = s.drop(['one']) - assert_series_equal(result, expected) - result = s.drop('one') - assert_series_equal(result, expected) - - # single string/tuple-like - s = Series(range(3), index=list('abc')) - self.assertRaises(ValueError, s.drop, 'bc') - self.assertRaises(ValueError, s.drop, ('a', )) - - # errors='ignore' - s = Series(range(3), index=list('abc')) - result = s.drop('bc', errors='ignore') - assert_series_equal(result, s) - result = s.drop(['a', 'd'], errors='ignore') - expected = s.iloc[1:] - assert_series_equal(result, expected) - - # bad axis - self.assertRaises(ValueError, s.drop, 'one', axis='columns') - - # GH 8522 - s = Series([2, 3], index=[True, False]) - self.assertTrue(s.index.is_object()) - result = s.drop(True) - expected = Series([3], index=[False]) - assert_series_equal(result, expected) - - def test_align(self): - def _check_align(a, b, how='left', fill=None): - aa, ab = a.align(b, join=how, fill_value=fill) - - join_index = a.index.join(b.index, how=how) - if fill is not None: - diff_a = aa.index.difference(join_index) - diff_b = ab.index.difference(join_index) - if len(diff_a) > 0: - self.assertTrue((aa.reindex(diff_a) == fill).all()) - if len(diff_b) > 0: - self.assertTrue((ab.reindex(diff_b) == fill).all()) - - ea = a.reindex(join_index) - eb = b.reindex(join_index) - - if fill is not None: - ea = ea.fillna(fill) - eb = eb.fillna(fill) - - assert_series_equal(aa, ea) - assert_series_equal(ab, eb) - self.assertEqual(aa.name, 'ts') - self.assertEqual(ea.name, 'ts') - self.assertEqual(ab.name, 'ts') - self.assertEqual(eb.name, 'ts') - - for kind in JOIN_TYPES: - _check_align(self.ts[2:], self.ts[:-5], how=kind) - _check_align(self.ts[2:], self.ts[:-5], how=kind, fill=-1) - - # empty left - _check_align(self.ts[:0], self.ts[:-5], how=kind) - _check_align(self.ts[:0], self.ts[:-5], how=kind, fill=-1) - - # empty right - _check_align(self.ts[:-5], self.ts[:0], how=kind) - _check_align(self.ts[:-5], self.ts[:0], how=kind, fill=-1) - - # both empty - _check_align(self.ts[:0], self.ts[:0], how=kind) - _check_align(self.ts[:0], self.ts[:0], how=kind, fill=-1) - - def test_align_fill_method(self): - def _check_align(a, b, how='left', method='pad', limit=None): - aa, ab = a.align(b, join=how, method=method, limit=limit) - - join_index = a.index.join(b.index, how=how) - ea = a.reindex(join_index) - eb = b.reindex(join_index) - - ea = ea.fillna(method=method, limit=limit) - eb = eb.fillna(method=method, limit=limit) - - assert_series_equal(aa, ea) - assert_series_equal(ab, eb) - - for kind in JOIN_TYPES: - for meth in ['pad', 'bfill']: - _check_align(self.ts[2:], self.ts[:-5], how=kind, method=meth) - _check_align(self.ts[2:], self.ts[:-5], how=kind, method=meth, - limit=1) - - # empty left - _check_align(self.ts[:0], self.ts[:-5], how=kind, method=meth) - _check_align(self.ts[:0], self.ts[:-5], how=kind, method=meth, - limit=1) - - # empty right - _check_align(self.ts[:-5], self.ts[:0], how=kind, method=meth) - _check_align(self.ts[:-5], self.ts[:0], how=kind, method=meth, - limit=1) - - # both empty - _check_align(self.ts[:0], self.ts[:0], how=kind, method=meth) - _check_align(self.ts[:0], self.ts[:0], how=kind, method=meth, - limit=1) - - def test_align_nocopy(self): - b = self.ts[:5].copy() - - # do copy - a = self.ts.copy() - ra, _ = a.align(b, join='left') - ra[:5] = 5 - self.assertFalse((a[:5] == 5).any()) - - # do not copy - a = self.ts.copy() - ra, _ = a.align(b, join='left', copy=False) - ra[:5] = 5 - self.assertTrue((a[:5] == 5).all()) - - # do copy - a = self.ts.copy() - b = self.ts[:5].copy() - _, rb = a.align(b, join='right') - rb[:3] = 5 - self.assertFalse((b[:3] == 5).any()) - - # do not copy - a = self.ts.copy() - b = self.ts[:5].copy() - _, rb = a.align(b, join='right', copy=False) - rb[:2] = 5 - self.assertTrue((b[:2] == 5).all()) - - def test_align_sameindex(self): - a, b = self.ts.align(self.ts, copy=False) - self.assertIs(a.index, self.ts.index) - self.assertIs(b.index, self.ts.index) - - # a, b = self.ts.align(self.ts, copy=True) - # self.assertIsNot(a.index, self.ts.index) - # self.assertIsNot(b.index, self.ts.index) - - def test_align_multiindex(self): - # GH 10665 - - midx = pd.MultiIndex.from_product([range(2), range(3), range(2)], - names=('a', 'b', 'c')) - idx = pd.Index(range(2), name='b') - s1 = pd.Series(np.arange(12, dtype='int64'), index=midx) - s2 = pd.Series(np.arange(2, dtype='int64'), index=idx) - - # these must be the same results (but flipped) - res1l, res1r = s1.align(s2, join='left') - res2l, res2r = s2.align(s1, join='right') - - expl = s1 - tm.assert_series_equal(expl, res1l) - tm.assert_series_equal(expl, res2r) - expr = pd.Series([0, 0, 1, 1, np.nan, np.nan] * 2, index=midx) - tm.assert_series_equal(expr, res1r) - tm.assert_series_equal(expr, res2l) - - res1l, res1r = s1.align(s2, join='right') - res2l, res2r = s2.align(s1, join='left') - - exp_idx = pd.MultiIndex.from_product([range(2), range(2), range(2)], - names=('a', 'b', 'c')) - expl = pd.Series([0, 1, 2, 3, 6, 7, 8, 9], index=exp_idx) - tm.assert_series_equal(expl, res1l) - tm.assert_series_equal(expl, res2r) - expr = pd.Series([0, 0, 1, 1] * 2, index=exp_idx) - tm.assert_series_equal(expr, res1r) - tm.assert_series_equal(expr, res2l) - - def test_reindex(self): - - identity = self.series.reindex(self.series.index) - - # __array_interface__ is not defined for older numpies - # and on some pythons - try: - self.assertTrue(np.may_share_memory(self.series.index, - identity.index)) - except (AttributeError): - pass - - self.assertTrue(identity.index.is_(self.series.index)) - self.assertTrue(identity.index.identical(self.series.index)) - - subIndex = self.series.index[10:20] - subSeries = self.series.reindex(subIndex) - - for idx, val in compat.iteritems(subSeries): - self.assertEqual(val, self.series[idx]) - - subIndex2 = self.ts.index[10:20] - subTS = self.ts.reindex(subIndex2) - - for idx, val in compat.iteritems(subTS): - self.assertEqual(val, self.ts[idx]) - stuffSeries = self.ts.reindex(subIndex) - - self.assertTrue(np.isnan(stuffSeries).all()) - - # This is extremely important for the Cython code to not screw up - nonContigIndex = self.ts.index[::2] - subNonContig = self.ts.reindex(nonContigIndex) - for idx, val in compat.iteritems(subNonContig): - self.assertEqual(val, self.ts[idx]) - - # return a copy the same index here - result = self.ts.reindex() - self.assertFalse((result is self.ts)) - - def test_reindex_nan(self): - ts = Series([2, 3, 5, 7], index=[1, 4, nan, 8]) - - i, j = [nan, 1, nan, 8, 4, nan], [2, 0, 2, 3, 1, 2] - assert_series_equal(ts.reindex(i), ts.iloc[j]) - - ts.index = ts.index.astype('object') - - # reindex coerces index.dtype to float, loc/iloc doesn't - assert_series_equal(ts.reindex(i), ts.iloc[j], check_index_type=False) - - def test_reindex_series_add_nat(self): - rng = date_range('1/1/2000 00:00:00', periods=10, freq='10s') - series = Series(rng) - - result = series.reindex(lrange(15)) - self.assertTrue(np.issubdtype(result.dtype, np.dtype('M8[ns]'))) - - mask = result.isnull() - self.assertTrue(mask[-5:].all()) - self.assertFalse(mask[:-5].any()) - - def test_reindex_with_datetimes(self): - rng = date_range('1/1/2000', periods=20) - ts = Series(np.random.randn(20), index=rng) - - result = ts.reindex(list(ts.index[5:10])) - expected = ts[5:10] - tm.assert_series_equal(result, expected) - - result = ts[list(ts.index[5:10])] - tm.assert_series_equal(result, expected) - - def test_reindex_corner(self): - # (don't forget to fix this) I think it's fixed - self.empty.reindex(self.ts.index, method='pad') # it works - - # corner case: pad empty series - reindexed = self.empty.reindex(self.ts.index, method='pad') - - # pass non-Index - reindexed = self.ts.reindex(list(self.ts.index)) - assert_series_equal(self.ts, reindexed) - - # bad fill method - ts = self.ts[::2] - self.assertRaises(Exception, ts.reindex, self.ts.index, method='foo') - - def test_reindex_pad(self): - - s = Series(np.arange(10), dtype='int64') - s2 = s[::2] - - reindexed = s2.reindex(s.index, method='pad') - reindexed2 = s2.reindex(s.index, method='ffill') - assert_series_equal(reindexed, reindexed2) - - expected = Series([0, 0, 2, 2, 4, 4, 6, 6, 8, 8], index=np.arange(10)) - assert_series_equal(reindexed, expected) - - # GH4604 - s = Series([1, 2, 3, 4, 5], index=['a', 'b', 'c', 'd', 'e']) - new_index = ['a', 'g', 'c', 'f'] - expected = Series([1, 1, 3, 3], index=new_index) - - # this changes dtype because the ffill happens after - result = s.reindex(new_index).ffill() - assert_series_equal(result, expected.astype('float64')) - - result = s.reindex(new_index).ffill(downcast='infer') - assert_series_equal(result, expected) - - expected = Series([1, 5, 3, 5], index=new_index) - result = s.reindex(new_index, method='ffill') - assert_series_equal(result, expected) - - # inferrence of new dtype - s = Series([True, False, False, True], index=list('abcd')) - new_index = 'agc' - result = s.reindex(list(new_index)).ffill() - expected = Series([True, True, False], index=list(new_index)) - assert_series_equal(result, expected) - - # GH4618 shifted series downcasting - s = Series(False, index=lrange(0, 5)) - result = s.shift(1).fillna(method='bfill') - expected = Series(False, index=lrange(0, 5)) - assert_series_equal(result, expected) - - def test_reindex_nearest(self): - s = Series(np.arange(10, dtype='int64')) - target = [0.1, 0.9, 1.5, 2.0] - actual = s.reindex(target, method='nearest') - expected = Series(np.around(target).astype('int64'), target) - assert_series_equal(expected, actual) - - actual = s.reindex_like(actual, method='nearest') - assert_series_equal(expected, actual) - - actual = s.reindex_like(actual, method='nearest', tolerance=1) - assert_series_equal(expected, actual) - - actual = s.reindex(target, method='nearest', tolerance=0.2) - expected = Series([0, 1, np.nan, 2], target) - assert_series_equal(expected, actual) - - def test_reindex_backfill(self): - pass - - def test_reindex_int(self): - ts = self.ts[::2] - int_ts = Series(np.zeros(len(ts), dtype=int), index=ts.index) - - # this should work fine - reindexed_int = int_ts.reindex(self.ts.index) - - # if NaNs introduced - self.assertEqual(reindexed_int.dtype, np.float_) - - # NO NaNs introduced - reindexed_int = int_ts.reindex(int_ts.index[::2]) - self.assertEqual(reindexed_int.dtype, np.int_) - - def test_reindex_bool(self): - - # A series other than float, int, string, or object - ts = self.ts[::2] - bool_ts = Series(np.zeros(len(ts), dtype=bool), index=ts.index) - - # this should work fine - reindexed_bool = bool_ts.reindex(self.ts.index) - - # if NaNs introduced - self.assertEqual(reindexed_bool.dtype, np.object_) - - # NO NaNs introduced - reindexed_bool = bool_ts.reindex(bool_ts.index[::2]) - self.assertEqual(reindexed_bool.dtype, np.bool_) - - def test_reindex_bool_pad(self): - # fail - ts = self.ts[5:] - bool_ts = Series(np.zeros(len(ts), dtype=bool), index=ts.index) - filled_bool = bool_ts.reindex(self.ts.index, method='pad') - self.assertTrue(isnull(filled_bool[:5]).all()) - - def test_reindex_like(self): - other = self.ts[::2] - assert_series_equal(self.ts.reindex(other.index), - self.ts.reindex_like(other)) - - # GH 7179 - day1 = datetime(2013, 3, 5) - day2 = datetime(2013, 5, 5) - day3 = datetime(2014, 3, 5) - - series1 = Series([5, None, None], [day1, day2, day3]) - series2 = Series([None, None], [day1, day3]) - - result = series1.reindex_like(series2, method='pad') - expected = Series([5, np.nan], index=[day1, day3]) - assert_series_equal(result, expected) - - def test_reindex_fill_value(self): - # ----------------------------------------------------------- - # floats - floats = Series([1., 2., 3.]) - result = floats.reindex([1, 2, 3]) - expected = Series([2., 3., np.nan], index=[1, 2, 3]) - assert_series_equal(result, expected) - - result = floats.reindex([1, 2, 3], fill_value=0) - expected = Series([2., 3., 0], index=[1, 2, 3]) - assert_series_equal(result, expected) - - # ----------------------------------------------------------- - # ints - ints = Series([1, 2, 3]) - - result = ints.reindex([1, 2, 3]) - expected = Series([2., 3., np.nan], index=[1, 2, 3]) - assert_series_equal(result, expected) - - # don't upcast - result = ints.reindex([1, 2, 3], fill_value=0) - expected = Series([2, 3, 0], index=[1, 2, 3]) - self.assertTrue(issubclass(result.dtype.type, np.integer)) - assert_series_equal(result, expected) - - # ----------------------------------------------------------- - # objects - objects = Series([1, 2, 3], dtype=object) - - result = objects.reindex([1, 2, 3]) - expected = Series([2, 3, np.nan], index=[1, 2, 3], dtype=object) - assert_series_equal(result, expected) - - result = objects.reindex([1, 2, 3], fill_value='foo') - expected = Series([2, 3, 'foo'], index=[1, 2, 3], dtype=object) - assert_series_equal(result, expected) - - # ------------------------------------------------------------ - # bools - bools = Series([True, False, True]) - - result = bools.reindex([1, 2, 3]) - expected = Series([False, True, np.nan], index=[1, 2, 3], dtype=object) - assert_series_equal(result, expected) - - result = bools.reindex([1, 2, 3], fill_value=False) - expected = Series([False, True, False], index=[1, 2, 3]) - assert_series_equal(result, expected) - - def test_select(self): - n = len(self.ts) - result = self.ts.select(lambda x: x >= self.ts.index[n // 2]) - expected = self.ts.reindex(self.ts.index[n // 2:]) - assert_series_equal(result, expected) - - result = self.ts.select(lambda x: x.weekday() == 2) - expected = self.ts[self.ts.index.weekday == 2] - assert_series_equal(result, expected) - - def test_cast_on_putmask(self): - - # GH 2746 - - # need to upcast - s = Series([1, 2], index=[1, 2], dtype='int64') - s[[True, False]] = Series([0], index=[1], dtype='int64') - expected = Series([0, 2], index=[1, 2], dtype='int64') - - assert_series_equal(s, expected) - - def test_type_promote_putmask(self): - - # GH8387: test that changing types does not break alignment - ts = Series(np.random.randn(100), index=np.arange(100, 0, -1)).round(5) - left, mask = ts.copy(), ts > 0 - right = ts[mask].copy().map(str) - left[mask] = right - assert_series_equal(left, ts.map(lambda t: str(t) if t > 0 else t)) - - s = Series([0, 1, 2, 0]) - mask = s > 0 - s2 = s[mask].map(str) - s[mask] = s2 - assert_series_equal(s, Series([0, '1', '2', 0])) - - s = Series([0, 'foo', 'bar', 0]) - mask = Series([False, True, True, False]) - s2 = s[mask] - s[mask] = s2 - assert_series_equal(s, Series([0, 'foo', 'bar', 0])) - - def test_head_tail(self): - assert_series_equal(self.series.head(), self.series[:5]) - assert_series_equal(self.series.head(0), self.series[0:0]) - assert_series_equal(self.series.tail(), self.series[-5:]) - assert_series_equal(self.series.tail(0), self.series[0:0]) - - def test_multilevel_preserve_name(self): - index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', - 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], - names=['first', 'second']) - s = Series(np.random.randn(len(index)), index=index, name='sth') - - result = s['foo'] - result2 = s.loc['foo'] - self.assertEqual(result.name, s.name) - self.assertEqual(result2.name, s.name) - - def test_setitem_scalar_into_readonly_backing_data(self): - # GH14359: test that you cannot mutate a read only buffer - - array = np.zeros(5) - array.flags.writeable = False # make the array immutable - series = Series(array) - - for n in range(len(series)): - with self.assertRaises(ValueError): - series[n] = 1 - - self.assertEqual( - array[n], - 0, - msg='even though the ValueError was raised, the underlying' - ' array was still mutated!', - ) - - def test_setitem_slice_into_readonly_backing_data(self): - # GH14359: test that you cannot mutate a read only buffer - - array = np.zeros(5) - array.flags.writeable = False # make the array immutable - series = Series(array) - - with self.assertRaises(ValueError): - series[1:3] = 1 - - self.assertTrue( - not array.any(), - msg='even though the ValueError was raised, the underlying' - ' array was still mutated!', - ) - - -class TestTimeSeriesDuplicates(tm.TestCase): - - def setUp(self): - dates = [datetime(2000, 1, 2), datetime(2000, 1, 2), - datetime(2000, 1, 2), datetime(2000, 1, 3), - datetime(2000, 1, 3), datetime(2000, 1, 3), - datetime(2000, 1, 4), datetime(2000, 1, 4), - datetime(2000, 1, 4), datetime(2000, 1, 5)] - - self.dups = Series(np.random.randn(len(dates)), index=dates) - - def test_constructor(self): - tm.assertIsInstance(self.dups, Series) - tm.assertIsInstance(self.dups.index, DatetimeIndex) - - def test_is_unique_monotonic(self): - self.assertFalse(self.dups.index.is_unique) - - def test_index_unique(self): - uniques = self.dups.index.unique() - expected = DatetimeIndex([datetime(2000, 1, 2), datetime(2000, 1, 3), - datetime(2000, 1, 4), datetime(2000, 1, 5)]) - self.assertEqual(uniques.dtype, 'M8[ns]') # sanity - tm.assert_index_equal(uniques, expected) - self.assertEqual(self.dups.index.nunique(), 4) - - # #2563 - self.assertTrue(isinstance(uniques, DatetimeIndex)) - - dups_local = self.dups.index.tz_localize('US/Eastern') - dups_local.name = 'foo' - result = dups_local.unique() - expected = DatetimeIndex(expected, name='foo') - expected = expected.tz_localize('US/Eastern') - self.assertTrue(result.tz is not None) - self.assertEqual(result.name, 'foo') - tm.assert_index_equal(result, expected) - - # NaT, note this is excluded - arr = [1370745748 + t for t in range(20)] + [tslib.iNaT] - idx = DatetimeIndex(arr * 3) - tm.assert_index_equal(idx.unique(), DatetimeIndex(arr)) - self.assertEqual(idx.nunique(), 20) - self.assertEqual(idx.nunique(dropna=False), 21) - - arr = [Timestamp('2013-06-09 02:42:28') + timedelta(seconds=t) - for t in range(20)] + [NaT] - idx = DatetimeIndex(arr * 3) - tm.assert_index_equal(idx.unique(), DatetimeIndex(arr)) - self.assertEqual(idx.nunique(), 20) - self.assertEqual(idx.nunique(dropna=False), 21) - - def test_index_dupes_contains(self): - d = datetime(2011, 12, 5, 20, 30) - ix = DatetimeIndex([d, d]) - self.assertTrue(d in ix) - - def test_duplicate_dates_indexing(self): - ts = self.dups - - uniques = ts.index.unique() - for date in uniques: - result = ts[date] - - mask = ts.index == date - total = (ts.index == date).sum() - expected = ts[mask] - if total > 1: - assert_series_equal(result, expected) - else: - assert_almost_equal(result, expected[0]) - - cp = ts.copy() - cp[date] = 0 - expected = Series(np.where(mask, 0, ts), index=ts.index) - assert_series_equal(cp, expected) - - self.assertRaises(KeyError, ts.__getitem__, datetime(2000, 1, 6)) - - # new index - ts[datetime(2000, 1, 6)] = 0 - self.assertEqual(ts[datetime(2000, 1, 6)], 0) - - def test_range_slice(self): - idx = DatetimeIndex(['1/1/2000', '1/2/2000', '1/2/2000', '1/3/2000', - '1/4/2000']) - - ts = Series(np.random.randn(len(idx)), index=idx) - - result = ts['1/2/2000':] - expected = ts[1:] - assert_series_equal(result, expected) - - result = ts['1/2/2000':'1/3/2000'] - expected = ts[1:4] - assert_series_equal(result, expected) - - def test_groupby_average_dup_values(self): - result = self.dups.groupby(level=0).mean() - expected = self.dups.groupby(self.dups.index).mean() - assert_series_equal(result, expected) - - def test_indexing_over_size_cutoff(self): - import datetime - # #1821 - - old_cutoff = _index._SIZE_CUTOFF - try: - _index._SIZE_CUTOFF = 1000 - - # create large list of non periodic datetime - dates = [] - sec = datetime.timedelta(seconds=1) - half_sec = datetime.timedelta(microseconds=500000) - d = datetime.datetime(2011, 12, 5, 20, 30) - n = 1100 - for i in range(n): - dates.append(d) - dates.append(d + sec) - dates.append(d + sec + half_sec) - dates.append(d + sec + sec + half_sec) - d += 3 * sec - - # duplicate some values in the list - duplicate_positions = np.random.randint(0, len(dates) - 1, 20) - for p in duplicate_positions: - dates[p + 1] = dates[p] - - df = DataFrame(np.random.randn(len(dates), 4), - index=dates, - columns=list('ABCD')) - - pos = n * 3 - timestamp = df.index[pos] - self.assertIn(timestamp, df.index) - - # it works! - df.loc[timestamp] - self.assertTrue(len(df.loc[[timestamp]]) > 0) - finally: - _index._SIZE_CUTOFF = old_cutoff - - def test_indexing_unordered(self): - # GH 2437 - rng = date_range(start='2011-01-01', end='2011-01-15') - ts = Series(np.random.rand(len(rng)), index=rng) - ts2 = pd.concat([ts[0:4], ts[-4:], ts[4:-4]]) - - for t in ts.index: - # TODO: unused? - s = str(t) # noqa - - expected = ts[t] - result = ts2[t] - self.assertTrue(expected == result) - - # GH 3448 (ranges) - def compare(slobj): - result = ts2[slobj].copy() - result = result.sort_index() - expected = ts[slobj] - assert_series_equal(result, expected) - - compare(slice('2011-01-01', '2011-01-15')) - compare(slice('2010-12-30', '2011-01-15')) - compare(slice('2011-01-01', '2011-01-16')) - - # partial ranges - compare(slice('2011-01-01', '2011-01-6')) - compare(slice('2011-01-06', '2011-01-8')) - compare(slice('2011-01-06', '2011-01-12')) - - # single values - result = ts2['2011'].sort_index() - expected = ts['2011'] - assert_series_equal(result, expected) - - # diff freq - rng = date_range(datetime(2005, 1, 1), periods=20, freq='M') - ts = Series(np.arange(len(rng)), index=rng) - ts = ts.take(np.random.permutation(20)) - - result = ts['2005'] - for t in result.index: - self.assertTrue(t.year == 2005) - - def test_indexing(self): - - idx = date_range("2001-1-1", periods=20, freq='M') - ts = Series(np.random.rand(len(idx)), index=idx) - - # getting - - # GH 3070, make sure semantics work on Series/Frame - expected = ts['2001'] - expected.name = 'A' - - df = DataFrame(dict(A=ts)) - result = df['2001']['A'] - assert_series_equal(expected, result) - - # setting - ts['2001'] = 1 - expected = ts['2001'] - expected.name = 'A' - - df.loc['2001', 'A'] = 1 - - result = df['2001']['A'] - assert_series_equal(expected, result) - - # GH3546 (not including times on the last day) - idx = date_range(start='2013-05-31 00:00', end='2013-05-31 23:00', - freq='H') - ts = Series(lrange(len(idx)), index=idx) - expected = ts['2013-05'] - assert_series_equal(expected, ts) - - idx = date_range(start='2013-05-31 00:00', end='2013-05-31 23:59', - freq='S') - ts = Series(lrange(len(idx)), index=idx) - expected = ts['2013-05'] - assert_series_equal(expected, ts) - - idx = [Timestamp('2013-05-31 00:00'), - Timestamp(datetime(2013, 5, 31, 23, 59, 59, 999999))] - ts = Series(lrange(len(idx)), index=idx) - expected = ts['2013'] - assert_series_equal(expected, ts) - - # GH14826, indexing with a seconds resolution string / datetime object - df = DataFrame(np.random.rand(5, 5), - columns=['open', 'high', 'low', 'close', 'volume'], - index=date_range('2012-01-02 18:01:00', - periods=5, tz='US/Central', freq='s')) - expected = df.loc[[df.index[2]]] - - # this is a single date, so will raise - self.assertRaises(KeyError, df.__getitem__, '2012-01-02 18:01:02', ) - self.assertRaises(KeyError, df.__getitem__, df.index[2], ) - - -class TestDatetimeIndexing(tm.TestCase): - """ - Also test support for datetime64[ns] in Series / DataFrame - """ - - def setUp(self): - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='Min') - self.series = Series(np.random.rand(len(dti)), dti) - - def test_fancy_getitem(self): - dti = DatetimeIndex(freq='WOM-1FRI', start=datetime(2005, 1, 1), - end=datetime(2010, 1, 1)) - - s = Series(np.arange(len(dti)), index=dti) - - self.assertEqual(s[48], 48) - self.assertEqual(s['1/2/2009'], 48) - self.assertEqual(s['2009-1-2'], 48) - self.assertEqual(s[datetime(2009, 1, 2)], 48) - self.assertEqual(s[lib.Timestamp(datetime(2009, 1, 2))], 48) - self.assertRaises(KeyError, s.__getitem__, '2009-1-3') - - assert_series_equal(s['3/6/2009':'2009-06-05'], - s[datetime(2009, 3, 6):datetime(2009, 6, 5)]) - - def test_fancy_setitem(self): - dti = DatetimeIndex(freq='WOM-1FRI', start=datetime(2005, 1, 1), - end=datetime(2010, 1, 1)) - - s = Series(np.arange(len(dti)), index=dti) - s[48] = -1 - self.assertEqual(s[48], -1) - s['1/2/2009'] = -2 - self.assertEqual(s[48], -2) - s['1/2/2009':'2009-06-05'] = -3 - self.assertTrue((s[48:54] == -3).all()) - - def test_dti_snap(self): - dti = DatetimeIndex(['1/1/2002', '1/2/2002', '1/3/2002', '1/4/2002', - '1/5/2002', '1/6/2002', '1/7/2002'], freq='D') - - res = dti.snap(freq='W-MON') - exp = date_range('12/31/2001', '1/7/2002', freq='w-mon') - exp = exp.repeat([3, 4]) - self.assertTrue((res == exp).all()) - - res = dti.snap(freq='B') - - exp = date_range('1/1/2002', '1/7/2002', freq='b') - exp = exp.repeat([1, 1, 1, 2, 2]) - self.assertTrue((res == exp).all()) - - def test_dti_reset_index_round_trip(self): - dti = DatetimeIndex(start='1/1/2001', end='6/1/2001', freq='D') - d1 = DataFrame({'v': np.random.rand(len(dti))}, index=dti) - d2 = d1.reset_index() - self.assertEqual(d2.dtypes[0], np.dtype('M8[ns]')) - d3 = d2.set_index('index') - assert_frame_equal(d1, d3, check_names=False) - - # #2329 - stamp = datetime(2012, 11, 22) - df = DataFrame([[stamp, 12.1]], columns=['Date', 'Value']) - df = df.set_index('Date') - - self.assertEqual(df.index[0], stamp) - self.assertEqual(df.reset_index()['Date'][0], stamp) - - def test_series_set_value(self): - # #1561 - - dates = [datetime(2001, 1, 1), datetime(2001, 1, 2)] - index = DatetimeIndex(dates) - - s = Series().set_value(dates[0], 1.) - s2 = s.set_value(dates[1], np.nan) - - exp = Series([1., np.nan], index=index) - - assert_series_equal(s2, exp) - - # s = Series(index[:1], index[:1]) - # s2 = s.set_value(dates[1], index[1]) - # self.assertEqual(s2.values.dtype, 'M8[ns]') - - @slow - def test_slice_locs_indexerror(self): - times = [datetime(2000, 1, 1) + timedelta(minutes=i * 10) - for i in range(100000)] - s = Series(lrange(100000), times) - s.loc[datetime(1900, 1, 1):datetime(2100, 1, 1)] - - def test_slicing_datetimes(self): - - # GH 7523 - - # unique - df = DataFrame(np.arange(4., dtype='float64'), - index=[datetime(2001, 1, i, 10, 00) - for i in [1, 2, 3, 4]]) - result = df.loc[datetime(2001, 1, 1, 10):] - assert_frame_equal(result, df) - result = df.loc[:datetime(2001, 1, 4, 10)] - assert_frame_equal(result, df) - result = df.loc[datetime(2001, 1, 1, 10):datetime(2001, 1, 4, 10)] - assert_frame_equal(result, df) - - result = df.loc[datetime(2001, 1, 1, 11):] - expected = df.iloc[1:] - assert_frame_equal(result, expected) - result = df.loc['20010101 11':] - assert_frame_equal(result, expected) - - # duplicates - df = pd.DataFrame(np.arange(5., dtype='float64'), - index=[datetime(2001, 1, i, 10, 00) - for i in [1, 2, 2, 3, 4]]) - - result = df.loc[datetime(2001, 1, 1, 10):] - assert_frame_equal(result, df) - result = df.loc[:datetime(2001, 1, 4, 10)] - assert_frame_equal(result, df) - result = df.loc[datetime(2001, 1, 1, 10):datetime(2001, 1, 4, 10)] - assert_frame_equal(result, df) - - result = df.loc[datetime(2001, 1, 1, 11):] - expected = df.iloc[1:] - assert_frame_equal(result, expected) - result = df.loc['20010101 11':] - assert_frame_equal(result, expected) - - def test_frame_datetime64_duplicated(self): - dates = date_range('2010-07-01', end='2010-08-05') - - tst = DataFrame({'symbol': 'AAA', 'date': dates}) - result = tst.duplicated(['date', 'symbol']) - self.assertTrue((-result).all()) - - tst = DataFrame({'date': dates}) - result = tst.duplicated() - self.assertTrue((-result).all()) - - -class TestNatIndexing(tm.TestCase): - - def setUp(self): - self.series = Series(date_range('1/1/2000', periods=10)) - - # --------------------------------------------------------------------- - # NaT support - - def test_set_none_nan(self): - self.series[3] = None - self.assertIs(self.series[3], NaT) - - self.series[3:5] = None - self.assertIs(self.series[4], NaT) - - self.series[5] = np.nan - self.assertIs(self.series[5], NaT) - - self.series[5:7] = np.nan - self.assertIs(self.series[6], NaT) - - def test_nat_operations(self): - # GH 8617 - s = Series([0, pd.NaT], dtype='m8[ns]') - exp = s[0] - self.assertEqual(s.median(), exp) - self.assertEqual(s.min(), exp) - self.assertEqual(s.max(), exp) - - def test_round_nat(self): - # GH14940 - s = Series([pd.NaT]) - expected = Series(pd.NaT) - for method in ["round", "floor", "ceil"]: - round_method = getattr(s.dt, method) - for freq in ["s", "5s", "min", "5min", "h", "5h"]: - assert_series_equal(round_method(freq), expected) diff --git a/pandas/tests/series/test_internals.py b/pandas/tests/series/test_internals.py index 4b1c303200739..26b868872ee0d 100644 --- a/pandas/tests/series/test_internals.py +++ b/pandas/tests/series/test_internals.py @@ -3,18 +3,17 @@ from datetime import datetime -from numpy import nan import numpy as np +import pytest -from pandas import Series -from pandas.tseries.index import Timestamp -import pandas._libs.lib as lib - -from pandas.util.testing import assert_series_equal +import pandas as pd +from pandas import NaT, Series, Timestamp +from pandas.core.internals.blocks import IntBlock import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal -class TestSeriesInternals(tm.TestCase): +class TestSeriesInternals(object): def test_convert_objects(self): @@ -85,7 +84,7 @@ def test_convert_objects(self): expected = Series([Timestamp('20010101'), Timestamp('20010102'), Timestamp('20010103'), - lib.NaT, lib.NaT, lib.NaT, Timestamp('20010104'), + NaT, NaT, NaT, Timestamp('20010104'), Timestamp('20010105')], dtype='M8[ns]') with tm.assert_produces_warning(FutureWarning): result = s2.convert_objects(convert_dates='coerce', @@ -101,7 +100,7 @@ def test_convert_objects(self): with tm.assert_produces_warning(FutureWarning): result = s.convert_objects(convert_dates='coerce', convert_numeric=False) - expected = Series([lib.NaT] * 2 + [Timestamp(1)] * 2) + expected = Series([NaT] * 2 + [Timestamp(1)] * 2) assert_series_equal(result, expected) # preserver if non-object @@ -114,7 +113,7 @@ def test_convert_objects(self): # r = s.copy() # r[0] = np.nan # result = r.convert_objects(convert_dates=True,convert_numeric=False) - # self.assertEqual(result.dtype, 'M8[ns]') + # assert result.dtype == 'M8[ns]' # dateutil parses some single letters into today's value as a date for x in 'abcdefghijklmnopqrstuvwxyz': @@ -147,14 +146,14 @@ def test_convert(self): # Test coercion returns correct type s = Series(['a', 'b', 'c']) results = s._convert(datetime=True, coerce=True) - expected = Series([lib.NaT] * 3) + expected = Series([NaT] * 3) assert_series_equal(results, expected) results = s._convert(numeric=True, coerce=True) expected = Series([np.nan] * 3) assert_series_equal(results, expected) - expected = Series([lib.NaT] * 3, dtype=np.dtype('m8[ns]')) + expected = Series([NaT] * 3, dtype=np.dtype('m8[ns]')) results = s._convert(timedelta=True, coerce=True) assert_series_equal(results, expected) @@ -164,15 +163,15 @@ def test_convert(self): # Test coercion with mixed types s = Series(['a', '3.1415', dt, td]) results = s._convert(datetime=True, coerce=True) - expected = Series([lib.NaT, lib.NaT, dt, lib.NaT]) + expected = Series([NaT, NaT, dt, NaT]) assert_series_equal(results, expected) results = s._convert(numeric=True, coerce=True) - expected = Series([nan, 3.1415, nan, nan]) + expected = Series([np.nan, 3.1415, np.nan, np.nan]) assert_series_equal(results, expected) results = s._convert(timedelta=True, coerce=True) - expected = Series([lib.NaT, lib.NaT, lib.NaT, td], + expected = Series([NaT, NaT, NaT, td], dtype=np.dtype('m8[ns]')) assert_series_equal(results, expected) @@ -180,7 +179,7 @@ def test_convert(self): results = s._convert(datetime=True) assert_series_equal(results, s) results = s._convert(numeric=True) - expected = Series([nan, 3.1415, nan, nan]) + expected = Series([np.nan, 3.1415, np.nan, np.nan]) assert_series_equal(results, expected) results = s._convert(timedelta=True) assert_series_equal(results, s) @@ -229,13 +228,13 @@ def test_convert(self): r['a'] = 'garbled' result = r._convert(numeric=True) expected = s.copy() - expected['a'] = nan + expected['a'] = np.nan assert_series_equal(result, expected) # GH 4119, not converting a mixed type (e.g.floats and object) s = Series([1, 'na', 3, 4]) result = s._convert(datetime=True, numeric=True) - expected = Series([1, nan, 3, 4]) + expected = Series([1, np.nan, 3, 4]) assert_series_equal(result, expected) s = Series([1, '', 3, 4]) @@ -258,7 +257,7 @@ def test_convert(self): assert_series_equal(result, expected) expected = Series([Timestamp('20010101'), Timestamp('20010102'), - Timestamp('20010103'), lib.NaT, lib.NaT, lib.NaT, + Timestamp('20010103'), NaT, NaT, NaT, Timestamp('20010104'), Timestamp('20010105')], dtype='M8[ns]') result = s2._convert(datetime=True, numeric=False, timedelta=False, @@ -269,7 +268,7 @@ def test_convert(self): s = Series(['foo', 'bar', 1, 1.0], dtype='O') result = s._convert(datetime=True, coerce=True) - expected = Series([lib.NaT] * 2 + [Timestamp(1)] * 2) + expected = Series([NaT] * 2 + [Timestamp(1)] * 2) assert_series_equal(result, expected) # preserver if non-object @@ -280,10 +279,10 @@ def test_convert(self): # r = s.copy() # r[0] = np.nan # result = r._convert(convert_dates=True,convert_numeric=False) - # self.assertEqual(result.dtype, 'M8[ns]') + # assert result.dtype == 'M8[ns]' # dateutil parses some single letters into today's value as a date - expected = Series([lib.NaT]) + expected = Series([NaT]) for x in 'abcdefghijklmnopqrstuvwxyz': s = Series([x]) result = s._convert(datetime=True, coerce=True) @@ -294,7 +293,9 @@ def test_convert(self): def test_convert_no_arg_error(self): s = Series(['1.0', '2']) - self.assertRaises(ValueError, s._convert) + msg = r"At least one of datetime, numeric or timedelta must be True\." + with pytest.raises(ValueError, match=msg): + s._convert() def test_convert_preserve_bool(self): s = Series([1, True, 3, 5], dtype=object) @@ -307,3 +308,36 @@ def test_convert_preserve_all_bool(self): r = s._convert(datetime=True, numeric=True) e = Series([False, True, False, False], dtype=bool) tm.assert_series_equal(r, e) + + def test_constructor_no_pandas_array(self): + ser = pd.Series([1, 2, 3]) + result = pd.Series(ser.array) + tm.assert_series_equal(ser, result) + assert isinstance(result._data.blocks[0], IntBlock) + + def test_from_array(self): + result = pd.Series(pd.array(['1H', '2H'], dtype='timedelta64[ns]')) + assert result._data.blocks[0].is_extension is False + + result = pd.Series(pd.array(['2015'], dtype='datetime64[ns]')) + assert result._data.blocks[0].is_extension is False + + def test_from_list_dtype(self): + result = pd.Series(['1H', '2H'], dtype='timedelta64[ns]') + assert result._data.blocks[0].is_extension is False + + result = pd.Series(['2015'], dtype='datetime64[ns]') + assert result._data.blocks[0].is_extension is False + + +def test_hasnans_unchached_for_series(): + # GH#19700 + idx = pd.Index([0, 1]) + assert idx.hasnans is False + assert 'hasnans' in idx._cache + ser = idx.to_series() + assert ser.hasnans is False + assert not hasattr(ser, '_cache') + ser.iloc[-1] = np.nan + assert ser.hasnans is True + assert Series.hasnans.__doc__ == pd.Index.hasnans.__doc__ diff --git a/pandas/tests/series/test_io.py b/pandas/tests/series/test_io.py index d514fbfc142f0..5749b0c6551d6 100644 --- a/pandas/tests/series/test_io.py +++ b/pandas/tests/series/test_io.py @@ -1,93 +1,141 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 +import collections from datetime import datetime import numpy as np -import pandas as pd +import pytest -from pandas import Series, DataFrame +from pandas.compat import StringIO, u -from pandas.compat import StringIO, u, long -from pandas.util.testing import (assert_series_equal, assert_almost_equal, - assert_frame_equal, ensure_clean) +import pandas as pd +from pandas import DataFrame, Series import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal, ensure_clean) + +from pandas.io.common import _get_handle + + +class TestSeriesToCSV(): + + def read_csv(self, path, **kwargs): + params = dict(squeeze=True, index_col=0, + header=None, parse_dates=True) + params.update(**kwargs) + + header = params.get("header") + out = pd.read_csv(path, **params) -from .common import TestData + if header is None: + out.name = out.index.name = None + return out -class TestSeriesToCSV(TestData, tm.TestCase): + def test_from_csv_deprecation(self, datetime_series): + # see gh-17812 + with ensure_clean() as path: + datetime_series.to_csv(path, header=False) - def test_from_csv(self): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + ts = self.read_csv(path) + depr_ts = Series.from_csv(path) + assert_series_equal(depr_ts, ts) + @pytest.mark.parametrize("arg", ["path", "header", "both"]) + def test_to_csv_deprecation(self, arg, datetime_series): + # see gh-19715 with ensure_clean() as path: - self.ts.to_csv(path) - ts = Series.from_csv(path) - assert_series_equal(self.ts, ts, check_names=False) - self.assertTrue(ts.name is None) - self.assertTrue(ts.index.name is None) - - # GH10483 - self.ts.to_csv(path, header=True) - ts_h = Series.from_csv(path, header=0) - self.assertTrue(ts_h.name == 'ts') - - self.series.to_csv(path) - series = Series.from_csv(path) - self.assertIsNone(series.name) - self.assertIsNone(series.index.name) - assert_series_equal(self.series, series, check_names=False) - self.assertTrue(series.name is None) - self.assertTrue(series.index.name is None) - - self.series.to_csv(path, header=True) - series_h = Series.from_csv(path, header=0) - self.assertTrue(series_h.name == 'series') - - outfile = open(path, 'w') - outfile.write('1998-01-01|1.0\n1999-01-01|2.0') - outfile.close() - series = Series.from_csv(path, sep='|') - checkseries = Series({datetime(1998, 1, 1): 1.0, - datetime(1999, 1, 1): 2.0}) - assert_series_equal(checkseries, series) - - series = Series.from_csv(path, sep='|', parse_dates=False) - checkseries = Series({'1998-01-01': 1.0, '1999-01-01': 2.0}) - assert_series_equal(checkseries, series) - - def test_to_csv(self): + if arg == "path": + kwargs = dict(path=path, header=False) + elif arg == "header": + kwargs = dict(path_or_buf=path) + else: # Both discrepancies match. + kwargs = dict(path=path) + + with tm.assert_produces_warning(FutureWarning): + datetime_series.to_csv(**kwargs) + + # Make sure roundtrip still works. + ts = self.read_csv(path) + assert_series_equal(datetime_series, ts, check_names=False) + + def test_from_csv(self, datetime_series, string_series): + + with ensure_clean() as path: + datetime_series.to_csv(path, header=False) + ts = self.read_csv(path) + assert_series_equal(datetime_series, ts, check_names=False) + + assert ts.name is None + assert ts.index.name is None + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + depr_ts = Series.from_csv(path) + assert_series_equal(depr_ts, ts) + + # see gh-10483 + datetime_series.to_csv(path, header=True) + ts_h = self.read_csv(path, header=0) + assert ts_h.name == "ts" + + string_series.to_csv(path, header=False) + series = self.read_csv(path) + assert_series_equal(string_series, series, check_names=False) + + assert series.name is None + assert series.index.name is None + + string_series.to_csv(path, header=True) + series_h = self.read_csv(path, header=0) + assert series_h.name == "series" + + with open(path, "w") as outfile: + outfile.write("1998-01-01|1.0\n1999-01-01|2.0") + + series = self.read_csv(path, sep="|") + check_series = Series({datetime(1998, 1, 1): 1.0, + datetime(1999, 1, 1): 2.0}) + assert_series_equal(check_series, series) + + series = self.read_csv(path, sep="|", parse_dates=False) + check_series = Series({"1998-01-01": 1.0, "1999-01-01": 2.0}) + assert_series_equal(check_series, series) + + def test_to_csv(self, datetime_series): import io with ensure_clean() as path: - self.ts.to_csv(path) + datetime_series.to_csv(path, header=False) with io.open(path, newline=None) as f: lines = f.readlines() assert (lines[1] != '\n') - self.ts.to_csv(path, index=False) + datetime_series.to_csv(path, index=False, header=False) arr = np.loadtxt(path) - assert_almost_equal(arr, self.ts.values) + assert_almost_equal(arr, datetime_series.values) def test_to_csv_unicode_index(self): buf = StringIO() s = Series([u("\u05d0"), "d2"], index=[u("\u05d0"), u("\u05d1")]) - s.to_csv(buf, encoding='UTF-8') + s.to_csv(buf, encoding="UTF-8", header=False) buf.seek(0) - s2 = Series.from_csv(buf, index_col=0, encoding='UTF-8') - + s2 = self.read_csv(buf, index_col=0, encoding="UTF-8") assert_series_equal(s, s2) def test_to_csv_float_format(self): with ensure_clean() as filename: ser = Series([0.123456, 0.234567, 0.567567]) - ser.to_csv(filename, float_format='%.2f') + ser.to_csv(filename, float_format="%.2f", header=False) - rs = Series.from_csv(filename) + rs = self.read_csv(filename) xp = Series([0.12, 0.23, 0.57]) assert_series_equal(rs, xp) @@ -97,50 +145,87 @@ def test_to_csv_list_entries(self): split = s.str.split(r'\s+and\s+') buf = StringIO() - split.to_csv(buf) + split.to_csv(buf, header=False) def test_to_csv_path_is_none(self): # GH 8215 # Series.to_csv() was returning None, inconsistent with # DataFrame.to_csv() which returned string s = Series([1, 2, 3]) - csv_str = s.to_csv(path=None) - self.assertIsInstance(csv_str, str) - + csv_str = s.to_csv(path_or_buf=None, header=False) + assert isinstance(csv_str, str) + + @pytest.mark.parametrize('s,encoding', [ + (Series([0.123456, 0.234567, 0.567567], index=['A', 'B', 'C'], + name='X'), None), + # GH 21241, 21118 + (Series(['abc', 'def', 'ghi'], name='X'), 'ascii'), + (Series(["123", u"你好", u"世界"], name=u"中文"), 'gb2312'), + (Series(["123", u"Γειά σου", u"Κόσμε"], name=u"Ελληνικά"), 'cp737') + ]) + def test_to_csv_compression(self, s, encoding, compression): -class TestSeriesIO(TestData, tm.TestCase): + with ensure_clean() as filename: - def test_to_frame(self): - self.ts.name = None - rs = self.ts.to_frame() - xp = pd.DataFrame(self.ts.values, index=self.ts.index) + s.to_csv(filename, compression=compression, encoding=encoding, + header=True) + # test the round trip - to_csv -> read_csv + result = pd.read_csv(filename, compression=compression, + encoding=encoding, index_col=0, squeeze=True) + assert_series_equal(s, result) + + # test the round trip using file handle - to_csv -> read_csv + f, _handles = _get_handle(filename, 'w', compression=compression, + encoding=encoding) + with f: + s.to_csv(f, encoding=encoding, header=True) + result = pd.read_csv(filename, compression=compression, + encoding=encoding, index_col=0, squeeze=True) + assert_series_equal(s, result) + + # explicitly ensure file was compressed + with tm.decompress_file(filename, compression) as fh: + text = fh.read().decode(encoding or 'utf8') + assert s.name in text + + with tm.decompress_file(filename, compression) as fh: + assert_series_equal(s, pd.read_csv(fh, + index_col=0, + squeeze=True, + encoding=encoding)) + + +class TestSeriesIO(): + + def test_to_frame(self, datetime_series): + datetime_series.name = None + rs = datetime_series.to_frame() + xp = pd.DataFrame(datetime_series.values, index=datetime_series.index) assert_frame_equal(rs, xp) - self.ts.name = 'testname' - rs = self.ts.to_frame() - xp = pd.DataFrame(dict(testname=self.ts.values), index=self.ts.index) + datetime_series.name = 'testname' + rs = datetime_series.to_frame() + xp = pd.DataFrame(dict(testname=datetime_series.values), + index=datetime_series.index) assert_frame_equal(rs, xp) - rs = self.ts.to_frame(name='testdifferent') - xp = pd.DataFrame( - dict(testdifferent=self.ts.values), index=self.ts.index) + rs = datetime_series.to_frame(name='testdifferent') + xp = pd.DataFrame(dict(testdifferent=datetime_series.values), + index=datetime_series.index) assert_frame_equal(rs, xp) - def test_to_dict(self): - self.assert_series_equal(Series(self.ts.to_dict(), name='ts'), self.ts) - def test_timeseries_periodindex(self): # GH2891 from pandas import period_range prng = period_range('1/1/2011', '1/1/2012', freq='M') ts = Series(np.random.randn(len(prng)), prng) - new_ts = self.round_trip_pickle(ts) - self.assertEqual(new_ts.index.freq, 'M') + new_ts = tm.round_trip_pickle(ts) + assert new_ts.index.freq == 'M' def test_pickle_preserve_name(self): for n in [777, 777., 'name', datetime(2001, 11, 11), (1, 2)]: unpickled = self._pickle_roundtrip_name(tm.makeTimeSeries(name=n)) - self.assertEqual(unpickled.name, n) + assert unpickled.name == n def _pickle_roundtrip_name(self, obj): @@ -163,40 +248,20 @@ class SubclassedFrame(DataFrame): s = SubclassedSeries([1, 2, 3], name='X') result = s.to_frame() - self.assertTrue(isinstance(result, SubclassedFrame)) + assert isinstance(result, SubclassedFrame) expected = SubclassedFrame({'X': [1, 2, 3]}) assert_frame_equal(result, expected) - -class TestSeriesToList(TestData, tm.TestCase): - - def test_tolist(self): - rs = self.ts.tolist() - xp = self.ts.values.tolist() - assert_almost_equal(rs, xp) - - # datetime64 - s = Series(self.ts.index) - rs = s.tolist() - self.assertEqual(self.ts.index[0], rs[0]) - - def test_tolist_np_int(self): - # GH10904 - for t in ['int8', 'int16', 'int32', 'int64']: - s = pd.Series([1], dtype=t) - self.assertIsInstance(s.tolist()[0], (int, long)) - - def test_tolist_np_uint(self): - # GH10904 - for t in ['uint8', 'uint16']: - s = pd.Series([1], dtype=t) - self.assertIsInstance(s.tolist()[0], int) - for t in ['uint32', 'uint64']: - s = pd.Series([1], dtype=t) - self.assertIsInstance(s.tolist()[0], long) - - def test_tolist_np_float(self): - # GH10904 - for t in ['float16', 'float32', 'float64']: - s = pd.Series([1], dtype=t) - self.assertIsInstance(s.tolist()[0], float) + @pytest.mark.parametrize('mapping', ( + dict, + collections.defaultdict(list), + collections.OrderedDict)) + def test_to_dict(self, mapping, datetime_series): + # GH16122 + tm.assert_series_equal( + Series(datetime_series.to_dict(mapping), name='ts'), + datetime_series) + from_method = Series(datetime_series.to_dict(collections.Counter)) + from_constructor = Series(collections + .Counter(datetime_series.iteritems())) + tm.assert_series_equal(from_method, from_constructor) diff --git a/pandas/tests/series/test_misc_api.py b/pandas/tests/series/test_misc_api.py deleted file mode 100644 index 2facbaf1fe31e..0000000000000 --- a/pandas/tests/series/test_misc_api.py +++ /dev/null @@ -1,350 +0,0 @@ -# coding=utf-8 -# pylint: disable-msg=E1101,W0612 - -import numpy as np -import pandas as pd - -from pandas import Index, Series, DataFrame, date_range -from pandas.tseries.index import Timestamp - -from pandas.compat import range -from pandas import compat -import pandas.formats.printing as printing -from pandas.util.testing import (assert_series_equal, - ensure_clean) -import pandas.util.testing as tm - -from .common import TestData - - -class SharedWithSparse(object): - - def test_scalarop_preserve_name(self): - result = self.ts * 2 - self.assertEqual(result.name, self.ts.name) - - def test_copy_name(self): - result = self.ts.copy() - self.assertEqual(result.name, self.ts.name) - - def test_copy_index_name_checking(self): - # don't want to be able to modify the index stored elsewhere after - # making a copy - - self.ts.index.name = None - self.assertIsNone(self.ts.index.name) - self.assertIs(self.ts, self.ts) - - cp = self.ts.copy() - cp.index.name = 'foo' - printing.pprint_thing(self.ts.index.name) - self.assertIsNone(self.ts.index.name) - - def test_append_preserve_name(self): - result = self.ts[:5].append(self.ts[5:]) - self.assertEqual(result.name, self.ts.name) - - def test_binop_maybe_preserve_name(self): - # names match, preserve - result = self.ts * self.ts - self.assertEqual(result.name, self.ts.name) - result = self.ts.mul(self.ts) - self.assertEqual(result.name, self.ts.name) - - result = self.ts * self.ts[:-2] - self.assertEqual(result.name, self.ts.name) - - # names don't match, don't preserve - cp = self.ts.copy() - cp.name = 'something else' - result = self.ts + cp - self.assertIsNone(result.name) - result = self.ts.add(cp) - self.assertIsNone(result.name) - - ops = ['add', 'sub', 'mul', 'div', 'truediv', 'floordiv', 'mod', 'pow'] - ops = ops + ['r' + op for op in ops] - for op in ops: - # names match, preserve - s = self.ts.copy() - result = getattr(s, op)(s) - self.assertEqual(result.name, self.ts.name) - - # names don't match, don't preserve - cp = self.ts.copy() - cp.name = 'changed' - result = getattr(s, op)(cp) - self.assertIsNone(result.name) - - def test_combine_first_name(self): - result = self.ts.combine_first(self.ts[:5]) - self.assertEqual(result.name, self.ts.name) - - def test_getitem_preserve_name(self): - result = self.ts[self.ts > 0] - self.assertEqual(result.name, self.ts.name) - - result = self.ts[[0, 2, 4]] - self.assertEqual(result.name, self.ts.name) - - result = self.ts[5:10] - self.assertEqual(result.name, self.ts.name) - - def test_pickle(self): - unp_series = self._pickle_roundtrip(self.series) - unp_ts = self._pickle_roundtrip(self.ts) - assert_series_equal(unp_series, self.series) - assert_series_equal(unp_ts, self.ts) - - def _pickle_roundtrip(self, obj): - - with ensure_clean() as path: - obj.to_pickle(path) - unpickled = pd.read_pickle(path) - return unpickled - - def test_argsort_preserve_name(self): - result = self.ts.argsort() - self.assertEqual(result.name, self.ts.name) - - def test_sort_index_name(self): - result = self.ts.sort_index(ascending=False) - self.assertEqual(result.name, self.ts.name) - - def test_to_sparse_pass_name(self): - result = self.ts.to_sparse() - self.assertEqual(result.name, self.ts.name) - - -class TestSeriesMisc(TestData, SharedWithSparse, tm.TestCase): - - def test_tab_completion(self): - # GH 9910 - s = Series(list('abcd')) - # Series of str values should have .str but not .dt/.cat in __dir__ - self.assertTrue('str' in dir(s)) - self.assertTrue('dt' not in dir(s)) - self.assertTrue('cat' not in dir(s)) - - # similiarly for .dt - s = Series(date_range('1/1/2015', periods=5)) - self.assertTrue('dt' in dir(s)) - self.assertTrue('str' not in dir(s)) - self.assertTrue('cat' not in dir(s)) - - # similiarly for .cat, but with the twist that str and dt should be - # there if the categories are of that type first cat and str - s = Series(list('abbcd'), dtype="category") - self.assertTrue('cat' in dir(s)) - self.assertTrue('str' in dir(s)) # as it is a string categorical - self.assertTrue('dt' not in dir(s)) - - # similar to cat and str - s = Series(date_range('1/1/2015', periods=5)).astype("category") - self.assertTrue('cat' in dir(s)) - self.assertTrue('str' not in dir(s)) - self.assertTrue('dt' in dir(s)) # as it is a datetime categorical - - def test_not_hashable(self): - s_empty = Series() - s = Series([1]) - self.assertRaises(TypeError, hash, s_empty) - self.assertRaises(TypeError, hash, s) - - def test_contains(self): - tm.assert_contains_all(self.ts.index, self.ts) - - def test_iter(self): - for i, val in enumerate(self.series): - self.assertEqual(val, self.series[i]) - - for i, val in enumerate(self.ts): - self.assertEqual(val, self.ts[i]) - - def test_iter_box(self): - vals = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02')] - s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns]') - for res, exp in zip(s, vals): - self.assertIsInstance(res, pd.Timestamp) - self.assertEqual(res, exp) - self.assertIsNone(res.tz) - - vals = [pd.Timestamp('2011-01-01', tz='US/Eastern'), - pd.Timestamp('2011-01-02', tz='US/Eastern')] - s = pd.Series(vals) - self.assertEqual(s.dtype, 'datetime64[ns, US/Eastern]') - for res, exp in zip(s, vals): - self.assertIsInstance(res, pd.Timestamp) - self.assertEqual(res, exp) - self.assertEqual(res.tz, exp.tz) - - # timedelta - vals = [pd.Timedelta('1 days'), pd.Timedelta('2 days')] - s = pd.Series(vals) - self.assertEqual(s.dtype, 'timedelta64[ns]') - for res, exp in zip(s, vals): - self.assertIsInstance(res, pd.Timedelta) - self.assertEqual(res, exp) - - # period (object dtype, not boxed) - vals = [pd.Period('2011-01-01', freq='M'), - pd.Period('2011-01-02', freq='M')] - s = pd.Series(vals) - self.assertEqual(s.dtype, 'object') - for res, exp in zip(s, vals): - self.assertIsInstance(res, pd.Period) - self.assertEqual(res, exp) - self.assertEqual(res.freq, 'M') - - def test_keys(self): - # HACK: By doing this in two stages, we avoid 2to3 wrapping the call - # to .keys() in a list() - getkeys = self.ts.keys - self.assertIs(getkeys(), self.ts.index) - - def test_values(self): - self.assert_almost_equal(self.ts.values, self.ts, check_dtype=False) - - def test_iteritems(self): - for idx, val in compat.iteritems(self.series): - self.assertEqual(val, self.series[idx]) - - for idx, val in compat.iteritems(self.ts): - self.assertEqual(val, self.ts[idx]) - - # assert is lazy (genrators don't define reverse, lists do) - self.assertFalse(hasattr(self.series.iteritems(), 'reverse')) - - def test_raise_on_info(self): - s = Series(np.random.randn(10)) - with tm.assertRaises(AttributeError): - s.info() - - def test_copy(self): - - for deep in [None, False, True]: - s = Series(np.arange(10), dtype='float64') - - # default deep is True - if deep is None: - s2 = s.copy() - else: - s2 = s.copy(deep=deep) - - s2[::2] = np.NaN - - if deep is None or deep is True: - # Did not modify original Series - self.assertTrue(np.isnan(s2[0])) - self.assertFalse(np.isnan(s[0])) - else: - # we DID modify the original Series - self.assertTrue(np.isnan(s2[0])) - self.assertTrue(np.isnan(s[0])) - - # GH 11794 - # copy of tz-aware - expected = Series([Timestamp('2012/01/01', tz='UTC')]) - expected2 = Series([Timestamp('1999/01/01', tz='UTC')]) - - for deep in [None, False, True]: - - s = Series([Timestamp('2012/01/01', tz='UTC')]) - - if deep is None: - s2 = s.copy() - else: - s2 = s.copy(deep=deep) - - s2[0] = pd.Timestamp('1999/01/01', tz='UTC') - - # default deep is True - if deep is None or deep is True: - # Did not modify original Series - assert_series_equal(s2, expected2) - assert_series_equal(s, expected) - else: - # we DID modify the original Series - assert_series_equal(s2, expected2) - assert_series_equal(s, expected2) - - def test_axis_alias(self): - s = Series([1, 2, np.nan]) - assert_series_equal(s.dropna(axis='rows'), s.dropna(axis='index')) - self.assertEqual(s.dropna().sum('rows'), 3) - self.assertEqual(s._get_axis_number('rows'), 0) - self.assertEqual(s._get_axis_name('rows'), 'index') - - def test_numpy_unique(self): - # it works! - np.unique(self.ts) - - def test_ndarray_compat(self): - - # test numpy compat with Series as sub-class of NDFrame - tsdf = DataFrame(np.random.randn(1000, 3), columns=['A', 'B', 'C'], - index=date_range('1/1/2000', periods=1000)) - - def f(x): - return x[x.argmax()] - - result = tsdf.apply(f) - expected = tsdf.max() - assert_series_equal(result, expected) - - # .item() - s = Series([1]) - result = s.item() - self.assertEqual(result, 1) - self.assertEqual(s.item(), s.iloc[0]) - - # using an ndarray like function - s = Series(np.random.randn(10)) - result = np.ones_like(s) - expected = Series(1, index=range(10), dtype='float64') - # assert_series_equal(result,expected) - - # ravel - s = Series(np.random.randn(10)) - tm.assert_almost_equal(s.ravel(order='F'), s.values.ravel(order='F')) - - # compress - # GH 6658 - s = Series([0, 1., -1], index=list('abc')) - result = np.compress(s > 0, s) - assert_series_equal(result, Series([1.], index=['b'])) - - result = np.compress(s < -1, s) - # result empty Index(dtype=object) as the same as original - exp = Series([], dtype='float64', index=Index([], dtype='object')) - assert_series_equal(result, exp) - - s = Series([0, 1., -1], index=[.1, .2, .3]) - result = np.compress(s > 0, s) - assert_series_equal(result, Series([1.], index=[.2])) - - result = np.compress(s < -1, s) - # result empty Float64Index as the same as original - exp = Series([], dtype='float64', index=Index([], dtype='float64')) - assert_series_equal(result, exp) - - def test_str_attribute(self): - # GH9068 - methods = ['strip', 'rstrip', 'lstrip'] - s = Series([' jack', 'jill ', ' jesse ', 'frank']) - for method in methods: - expected = Series([getattr(str, method)(x) for x in s.values]) - assert_series_equal(getattr(Series.str, method)(s.str), expected) - - # str accessor only valid with string values - s = Series(range(5)) - with self.assertRaisesRegexp(AttributeError, 'only use .str accessor'): - s.str.repeat(2) - - def test_empty_method(self): - s_empty = pd.Series() - tm.assert_equal(s_empty.empty, True) - - for full_series in [pd.Series([1]), pd.Series(index=[1])]: - tm.assert_equal(full_series.empty, False) diff --git a/pandas/tests/series/test_missing.py b/pandas/tests/series/test_missing.py index 7174283494fe7..ef9e575e60385 100644 --- a/pandas/tests/series/test_missing.py +++ b/pandas/tests/series/test_missing.py @@ -1,27 +1,32 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -import pytz -from datetime import timedelta, datetime - +from datetime import datetime, timedelta from distutils.version import LooseVersion -from numpy import nan + import numpy as np -import pandas as pd +from numpy import nan +import pytest +import pytz -from pandas import (Series, DataFrame, isnull, date_range, - MultiIndex, Index, Timestamp, NaT) -from pandas.compat import range from pandas._libs.tslib import iNaT -from pandas.util.testing import assert_series_equal, assert_frame_equal -import pandas.util.testing as tm +from pandas.compat import PY2, range +from pandas.errors import PerformanceWarning +import pandas.util._test_decorators as td -from .common import TestData +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, IntervalIndex, MultiIndex, NaT, Series, + Timestamp, date_range, isna) +from pandas.core.series import remove_na +import pandas.util.testing as tm +from pandas.util.testing import assert_frame_equal, assert_series_equal try: import scipy - _is_scipy_ge_0190 = scipy.__version__ >= LooseVersion('0.19.0') -except: + _is_scipy_ge_0190 = (LooseVersion(scipy.__version__) >= + LooseVersion('0.19.0')) +except ImportError: _is_scipy_ge_0190 = False @@ -46,29 +51,38 @@ def _simple_ts(start, end, freq='D'): return Series(np.random.randn(len(rng)), index=rng) -class TestSeriesMissingData(TestData, tm.TestCase): +class TestSeriesMissingData(): + + def test_remove_na_deprecation(self): + # see gh-16971 + with tm.assert_produces_warning(FutureWarning): + remove_na(Series([])) def test_timedelta_fillna(self): # GH 3371 - s = Series([Timestamp('20130101'), Timestamp('20130101'), Timestamp( - '20130102'), Timestamp('20130103 9:01:01')]) + s = Series([Timestamp('20130101'), Timestamp('20130101'), + Timestamp('20130102'), Timestamp('20130103 9:01:01')]) td = s.diff() # reg fillna - result = td.fillna(0) - expected = Series([timedelta(0), timedelta(0), timedelta(1), timedelta( - days=1, seconds=9 * 3600 + 60 + 1)]) + with tm.assert_produces_warning(FutureWarning): + result = td.fillna(0) + expected = Series([timedelta(0), timedelta(0), timedelta(1), + timedelta(days=1, seconds=9 * 3600 + 60 + 1)]) assert_series_equal(result, expected) - # interprested as seconds - result = td.fillna(1) - expected = Series([timedelta(seconds=1), timedelta(0), timedelta(1), + # interpreted as seconds, deprecated + with tm.assert_produces_warning(FutureWarning): + result = td.fillna(1) + expected = Series([timedelta(seconds=1), + timedelta(0), timedelta(1), timedelta(days=1, seconds=9 * 3600 + 60 + 1)]) assert_series_equal(result, expected) result = td.fillna(timedelta(days=1, seconds=1)) - expected = Series([timedelta(days=1, seconds=1), timedelta( - 0), timedelta(1), timedelta(days=1, seconds=9 * 3600 + 60 + 1)]) + expected = Series([timedelta(days=1, seconds=1), timedelta(0), + timedelta(1), + timedelta(days=1, seconds=9 * 3600 + 60 + 1)]) assert_series_equal(result, expected) result = td.fillna(np.timedelta64(int(1e9))) @@ -85,14 +99,16 @@ def test_timedelta_fillna(self): # ffill td[2] = np.nan result = td.ffill() - expected = td.fillna(0) + with tm.assert_produces_warning(FutureWarning): + expected = td.fillna(0) expected[0] = np.nan assert_series_equal(result, expected) # bfill td[2] = np.nan result = td.bfill() - expected = td.fillna(0) + with tm.assert_produces_warning(FutureWarning): + expected = td.fillna(0) expected[2] = timedelta(days=1, seconds=9 * 3600 + 60 + 1) assert_series_equal(result, expected) @@ -136,6 +152,7 @@ def test_datetime64_fillna(self): assert_series_equal(result, expected) def test_datetime64_tz_fillna(self): + for tz in ['US/Eastern', 'Asia/Tokyo']: # DatetimeBlock s = Series([Timestamp('2011-01-01 10:00'), pd.NaT, @@ -147,24 +164,24 @@ def test_datetime64_tz_fillna(self): Timestamp('2011-01-02 10:00'), Timestamp('2011-01-03 10:00'), Timestamp('2011-01-02 10:00')]) - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) # check s is not changed - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna(pd.Timestamp('2011-01-02 10:00', tz=tz)) expected = Series([Timestamp('2011-01-01 10:00'), Timestamp('2011-01-02 10:00', tz=tz), Timestamp('2011-01-03 10:00'), Timestamp('2011-01-02 10:00', tz=tz)]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna('AAA') expected = Series([Timestamp('2011-01-01 10:00'), 'AAA', Timestamp('2011-01-03 10:00'), 'AAA'], dtype=object) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna({1: pd.Timestamp('2011-01-02 10:00', tz=tz), 3: pd.Timestamp('2011-01-04 10:00')}) @@ -172,8 +189,8 @@ def test_datetime64_tz_fillna(self): Timestamp('2011-01-02 10:00', tz=tz), Timestamp('2011-01-03 10:00'), Timestamp('2011-01-04 10:00')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna({1: pd.Timestamp('2011-01-02 10:00'), 3: pd.Timestamp('2011-01-04 10:00')}) @@ -181,31 +198,31 @@ def test_datetime64_tz_fillna(self): Timestamp('2011-01-02 10:00'), Timestamp('2011-01-03 10:00'), Timestamp('2011-01-04 10:00')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) # DatetimeBlockTZ idx = pd.DatetimeIndex(['2011-01-01 10:00', pd.NaT, '2011-01-03 10:00', pd.NaT], tz=tz) s = pd.Series(idx) - self.assertEqual(s.dtype, 'datetime64[ns, {0}]'.format(tz)) - self.assert_series_equal(pd.isnull(s), null_loc) + assert s.dtype == 'datetime64[ns, {0}]'.format(tz) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna(pd.Timestamp('2011-01-02 10:00')) expected = Series([Timestamp('2011-01-01 10:00', tz=tz), Timestamp('2011-01-02 10:00'), Timestamp('2011-01-03 10:00', tz=tz), Timestamp('2011-01-02 10:00')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna(pd.Timestamp('2011-01-02 10:00', tz=tz)) idx = pd.DatetimeIndex(['2011-01-01 10:00', '2011-01-02 10:00', '2011-01-03 10:00', '2011-01-02 10:00'], tz=tz) expected = Series(idx) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna(pd.Timestamp('2011-01-02 10:00', tz=tz).to_pydatetime()) @@ -213,15 +230,15 @@ def test_datetime64_tz_fillna(self): '2011-01-03 10:00', '2011-01-02 10:00'], tz=tz) expected = Series(idx) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna('AAA') expected = Series([Timestamp('2011-01-01 10:00', tz=tz), 'AAA', Timestamp('2011-01-03 10:00', tz=tz), 'AAA'], dtype=object) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna({1: pd.Timestamp('2011-01-02 10:00', tz=tz), 3: pd.Timestamp('2011-01-04 10:00')}) @@ -229,8 +246,8 @@ def test_datetime64_tz_fillna(self): Timestamp('2011-01-02 10:00', tz=tz), Timestamp('2011-01-03 10:00', tz=tz), Timestamp('2011-01-04 10:00')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna({1: pd.Timestamp('2011-01-02 10:00', tz=tz), 3: pd.Timestamp('2011-01-04 10:00', tz=tz)}) @@ -238,8 +255,8 @@ def test_datetime64_tz_fillna(self): Timestamp('2011-01-02 10:00', tz=tz), Timestamp('2011-01-03 10:00', tz=tz), Timestamp('2011-01-04 10:00', tz=tz)]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) # filling with a naive/other zone, coerce to object result = s.fillna(Timestamp('20130101')) @@ -247,16 +264,62 @@ def test_datetime64_tz_fillna(self): Timestamp('2013-01-01'), Timestamp('2011-01-03 10:00', tz=tz), Timestamp('2013-01-01')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) result = s.fillna(Timestamp('20130101', tz='US/Pacific')) expected = Series([Timestamp('2011-01-01 10:00', tz=tz), Timestamp('2013-01-01', tz='US/Pacific'), Timestamp('2011-01-03 10:00', tz=tz), Timestamp('2013-01-01', tz='US/Pacific')]) - self.assert_series_equal(expected, result) - self.assert_series_equal(pd.isnull(s), null_loc) + tm.assert_series_equal(expected, result) + tm.assert_series_equal(pd.isna(s), null_loc) + + # with timezone + # GH 15855 + df = pd.Series([pd.Timestamp('2012-11-11 00:00:00+01:00'), pd.NaT]) + exp = pd.Series([pd.Timestamp('2012-11-11 00:00:00+01:00'), + pd.Timestamp('2012-11-11 00:00:00+01:00')]) + assert_series_equal(df.fillna(method='pad'), exp) + + df = pd.Series([pd.NaT, pd.Timestamp('2012-11-11 00:00:00+01:00')]) + exp = pd.Series([pd.Timestamp('2012-11-11 00:00:00+01:00'), + pd.Timestamp('2012-11-11 00:00:00+01:00')]) + assert_series_equal(df.fillna(method='bfill'), exp) + + def test_fillna_consistency(self): + # GH 16402 + # fillna with a tz aware to a tz-naive, should result in object + + s = Series([Timestamp('20130101'), pd.NaT]) + + result = s.fillna(Timestamp('20130101', tz='US/Eastern')) + expected = Series([Timestamp('20130101'), + Timestamp('2013-01-01', tz='US/Eastern')], + dtype='object') + assert_series_equal(result, expected) + + # where (we ignore the errors=) + result = s.where([True, False], + Timestamp('20130101', tz='US/Eastern'), + errors='ignore') + assert_series_equal(result, expected) + + result = s.where([True, False], + Timestamp('20130101', tz='US/Eastern'), + errors='ignore') + assert_series_equal(result, expected) + + # with a non-datetime + result = s.fillna('foo') + expected = Series([Timestamp('20130101'), + 'foo']) + assert_series_equal(result, expected) + + # assignment + s2 = s.copy() + s2[1] = 'foo' + assert_series_equal(s2, expected) def test_datetime64tz_fillna_round_issue(self): # GH 14872 @@ -297,16 +360,89 @@ def test_fillna_int(self): def test_fillna_raise(self): s = Series(np.random.randint(-100, 100, 50)) - self.assertRaises(TypeError, s.fillna, [1, 2]) - self.assertRaises(TypeError, s.fillna, (1, 2)) + msg = ('"value" parameter must be a scalar or dict, but you passed a' + ' "list"') + with pytest.raises(TypeError, match=msg): + s.fillna([1, 2]) + + msg = ('"value" parameter must be a scalar or dict, but you passed a' + ' "tuple"') + with pytest.raises(TypeError, match=msg): + s.fillna((1, 2)) # related GH 9217, make sure limit is an int and greater than 0 s = Series([1, 2, 3, None]) + msg = (r"Cannot specify both 'value' and 'method'\.|" + r"Limit must be greater than 0|" + "Limit must be an integer") for limit in [-1, 0, 1., 2.]: for method in ['backfill', 'bfill', 'pad', 'ffill', None]: - with tm.assertRaises(ValueError): + with pytest.raises(ValueError, match=msg): s.fillna(1, limit=limit, method=method) + def test_categorical_nan_equality(self): + cat = Series(Categorical(["a", "b", "c", np.nan])) + exp = Series([True, True, True, False]) + res = (cat == cat) + tm.assert_series_equal(res, exp) + + def test_categorical_nan_handling(self): + + # NaNs are represented as -1 in labels + s = Series(Categorical(["a", "b", np.nan, "a"])) + tm.assert_index_equal(s.cat.categories, Index(["a", "b"])) + tm.assert_numpy_array_equal(s.values.codes, + np.array([0, 1, -1, 0], dtype=np.int8)) + + @pytest.mark.parametrize('fill_value, expected_output', [ + ('a', ['a', 'a', 'b', 'a', 'a']), + ({1: 'a', 3: 'b', 4: 'b'}, ['a', 'a', 'b', 'b', 'b']), + ({1: 'a'}, ['a', 'a', 'b', np.nan, np.nan]), + ({1: 'a', 3: 'b'}, ['a', 'a', 'b', 'b', np.nan]), + (Series('a'), ['a', np.nan, 'b', np.nan, np.nan]), + (Series('a', index=[1]), ['a', 'a', 'b', np.nan, np.nan]), + (Series({1: 'a', 3: 'b'}), ['a', 'a', 'b', 'b', np.nan]), + (Series(['a', 'b'], index=[3, 4]), ['a', np.nan, 'b', 'a', 'b']) + ]) + def test_fillna_categorical(self, fill_value, expected_output): + # GH 17033 + # Test fillna for a Categorical series + data = ['a', np.nan, 'b', np.nan, np.nan] + s = Series(Categorical(data, categories=['a', 'b'])) + exp = Series(Categorical(expected_output, categories=['a', 'b'])) + tm.assert_series_equal(s.fillna(fill_value), exp) + + def test_fillna_categorical_raise(self): + data = ['a', np.nan, 'b', np.nan, np.nan] + s = Series(Categorical(data, categories=['a', 'b'])) + + with pytest.raises(ValueError, + match="fill value must be in categories"): + s.fillna('d') + + with pytest.raises(ValueError, + match="fill value must be in categories"): + s.fillna(Series('d')) + + with pytest.raises(ValueError, + match="fill value must be in categories"): + s.fillna({1: 'd', 3: 'a'}) + + msg = ('"value" parameter must be a scalar or ' + 'dict, but you passed a "list"') + with pytest.raises(TypeError, match=msg): + s.fillna(['a', 'b']) + + msg = ('"value" parameter must be a scalar or ' + 'dict, but you passed a "tuple"') + with pytest.raises(TypeError, match=msg): + s.fillna(('a', 'b')) + + msg = ('"value" parameter must be a scalar, dict ' + 'or Series, but you passed a "DataFrame"') + with pytest.raises(TypeError, match=msg): + s.fillna(DataFrame({1: ['a'], 3: ['b']})) + def test_fillna_nat(self): series = Series([0, 1, 2, iNaT], dtype='M8[ns]') @@ -344,34 +480,51 @@ def test_fillna_nat(self): assert_frame_equal(filled, expected) assert_frame_equal(filled2, expected) - def test_isnull_for_inf(self): + def test_isna_for_inf(self): + s = Series(['a', np.inf, np.nan, 1.0]) + with pd.option_context('mode.use_inf_as_na', True): + r = s.isna() + dr = s.dropna() + e = Series([False, True, True, False]) + de = Series(['a', 1.0], index=[0, 3]) + tm.assert_series_equal(r, e) + tm.assert_series_equal(dr, de) + + def test_isnull_for_inf_deprecated(self): + # gh-17115 s = Series(['a', np.inf, np.nan, 1.0]) with pd.option_context('mode.use_inf_as_null', True): - r = s.isnull() + r = s.isna() dr = s.dropna() + e = Series([False, True, True, False]) de = Series(['a', 1.0], index=[0, 3]) tm.assert_series_equal(r, e) tm.assert_series_equal(dr, de) - def test_fillna(self): + def test_fillna(self, datetime_series): ts = Series([0., 1., 2., 3., 4.], index=tm.makeDateIndex(5)) - self.assert_series_equal(ts, ts.fillna(method='ffill')) + tm.assert_series_equal(ts, ts.fillna(method='ffill')) ts[2] = np.NaN exp = Series([0., 1., 1., 3., 4.], index=ts.index) - self.assert_series_equal(ts.fillna(method='ffill'), exp) + tm.assert_series_equal(ts.fillna(method='ffill'), exp) exp = Series([0., 1., 3., 3., 4.], index=ts.index) - self.assert_series_equal(ts.fillna(method='backfill'), exp) + tm.assert_series_equal(ts.fillna(method='backfill'), exp) exp = Series([0., 1., 5., 3., 4.], index=ts.index) - self.assert_series_equal(ts.fillna(value=5), exp) + tm.assert_series_equal(ts.fillna(value=5), exp) + + msg = "Must specify a fill 'value' or 'method'" + with pytest.raises(ValueError, match=msg): + ts.fillna() - self.assertRaises(ValueError, ts.fillna) - self.assertRaises(ValueError, self.ts.fillna, value=0, method='ffill') + msg = "Cannot specify both 'value' and 'method'" + with pytest.raises(ValueError, match=msg): + datetime_series.fillna(value=0, method='ffill') # GH 5703 s1 = Series([np.nan]) @@ -441,11 +594,11 @@ def test_fillna_inplace(self): expected = x.fillna(value=0) assert_series_equal(y, expected) - def test_fillna_invalid_method(self): + def test_fillna_invalid_method(self, datetime_series): try: - self.ts.fillna(method='ffil') + datetime_series.fillna(method='ffil') except ValueError as inst: - self.assertIn('ffil', str(inst)) + assert 'ffil' in str(inst) def test_ffill(self): ts = Series([0., 1., 2., 3., 4.], index=tm.makeDateIndex(5)) @@ -470,45 +623,49 @@ def test_timedelta64_nan(self): # nan ops on timedeltas td1 = td.copy() td1[0] = np.nan - self.assertTrue(isnull(td1[0])) - self.assertEqual(td1[0].value, iNaT) + assert isna(td1[0]) + assert td1[0].value == iNaT td1[0] = td[0] - self.assertFalse(isnull(td1[0])) + assert not isna(td1[0]) td1[1] = iNaT - self.assertTrue(isnull(td1[1])) - self.assertEqual(td1[1].value, iNaT) + assert isna(td1[1]) + assert td1[1].value == iNaT td1[1] = td[1] - self.assertFalse(isnull(td1[1])) + assert not isna(td1[1]) td1[2] = NaT - self.assertTrue(isnull(td1[2])) - self.assertEqual(td1[2].value, iNaT) + assert isna(td1[2]) + assert td1[2].value == iNaT td1[2] = td[2] - self.assertFalse(isnull(td1[2])) + assert not isna(td1[2]) # boolean setting # this doesn't work, not sure numpy even supports it # result = td[(td>np.timedelta64(timedelta(days=3))) & # td= -0.5) & (self.ts <= 0.5) + # selector = -0.5 <= datetime_series <= 0.5 + # expected = (datetime_series >= -0.5) & (datetime_series <= 0.5) # assert_series_equal(selector, expected) + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_dropna_empty(self): s = Series([]) - self.assertEqual(len(s.dropna()), 0) + assert len(s.dropna()) == 0 s.dropna(inplace=True) - self.assertEqual(len(s), 0) + assert len(s) == 0 # invalid axis - self.assertRaises(ValueError, s.dropna, axis=1) + msg = ("No axis named 1 for object type" + " ") + with pytest.raises(ValueError, match=msg): + s.dropna(axis=1) def test_datetime64_tz_dropna(self): # DatetimeBlock @@ -517,55 +674,68 @@ def test_datetime64_tz_dropna(self): result = s.dropna() expected = Series([Timestamp('2011-01-01 10:00'), Timestamp('2011-01-03 10:00')], index=[0, 2]) - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) # DatetimeBlockTZ idx = pd.DatetimeIndex(['2011-01-01 10:00', pd.NaT, '2011-01-03 10:00', pd.NaT], tz='Asia/Tokyo') s = pd.Series(idx) - self.assertEqual(s.dtype, 'datetime64[ns, Asia/Tokyo]') + assert s.dtype == 'datetime64[ns, Asia/Tokyo]' result = s.dropna() expected = Series([Timestamp('2011-01-01 10:00', tz='Asia/Tokyo'), Timestamp('2011-01-03 10:00', tz='Asia/Tokyo')], index=[0, 2]) - self.assertEqual(result.dtype, 'datetime64[ns, Asia/Tokyo]') - self.assert_series_equal(result, expected) + assert result.dtype == 'datetime64[ns, Asia/Tokyo]' + tm.assert_series_equal(result, expected) def test_dropna_no_nan(self): for s in [Series([1, 2, 3], name='x'), Series( [False, True, False], name='x')]: result = s.dropna() - self.assert_series_equal(result, s) - self.assertFalse(result is s) + tm.assert_series_equal(result, s) + assert result is not s s2 = s.copy() s2.dropna(inplace=True) - self.assert_series_equal(s2, s) + tm.assert_series_equal(s2, s) - def test_valid(self): - ts = self.ts.copy() + def test_dropna_intervals(self): + s = Series([np.nan, 1, 2, 3], IntervalIndex.from_arrays( + [np.nan, 0, 1, 2], + [np.nan, 1, 2, 3])) + + result = s.dropna() + expected = s.iloc[1:] + assert_series_equal(result, expected) + + def test_valid(self, datetime_series): + ts = datetime_series.copy() ts[::2] = np.NaN - result = ts.valid() - self.assertEqual(len(result), ts.count()) + result = ts.dropna() + assert len(result) == ts.count() tm.assert_series_equal(result, ts[1::2]) - tm.assert_series_equal(result, ts[pd.notnull(ts)]) + tm.assert_series_equal(result, ts[pd.notna(ts)]) - def test_isnull(self): + def test_isna(self): ser = Series([0, 5.4, 3, nan, -0.001]) - np.array_equal(ser.isnull(), - Series([False, False, False, True, False]).values) + expected = Series([False, False, False, True, False]) + tm.assert_series_equal(ser.isna(), expected) + ser = Series(["hi", "", nan]) - np.array_equal(ser.isnull(), Series([False, False, True]).values) + expected = Series([False, False, True]) + tm.assert_series_equal(ser.isna(), expected) - def test_notnull(self): + def test_notna(self): ser = Series([0, 5.4, 3, nan, -0.001]) - np.array_equal(ser.notnull(), - Series([True, True, True, False, True]).values) + expected = Series([True, True, True, False, True]) + tm.assert_series_equal(ser.notna(), expected) + ser = Series(["hi", "", nan]) - np.array_equal(ser.notnull(), Series([True, True, False]).values) + expected = Series([True, True, False]) + tm.assert_series_equal(ser.notna(), expected) def test_pad_nan(self): x = Series([np.nan, 1., np.nan, 3., np.nan], ['z', 'a', 'b', 'c', 'd'], @@ -576,7 +746,7 @@ def test_pad_nan(self): expected = Series([np.nan, 1.0, 1.0, 3.0, 3.0], ['z', 'a', 'b', 'c', 'd'], dtype=float) assert_series_equal(x[1:], expected[1:]) - self.assertTrue(np.isnan(x[0]), np.isnan(expected[0])) + assert np.isnan(x[0]), np.isnan(expected[0]) def test_pad_require_monotonicity(self): rng = date_range('1/1/2000', '3/1/2000', freq='B') @@ -584,16 +754,18 @@ def test_pad_require_monotonicity(self): # neither monotonic increasing or decreasing rng2 = rng[[1, 0, 2]] - self.assertRaises(ValueError, rng2.get_indexer, rng, method='pad') + msg = "index must be monotonic increasing or decreasing" + with pytest.raises(ValueError, match=msg): + rng2.get_indexer(rng, method='pad') - def test_dropna_preserve_name(self): - self.ts[:5] = np.nan - result = self.ts.dropna() - self.assertEqual(result.name, self.ts.name) - name = self.ts.name - ts = self.ts.copy() + def test_dropna_preserve_name(self, datetime_series): + datetime_series[:5] = np.nan + result = datetime_series.dropna() + assert result.name == datetime_series.name + name = datetime_series.name + ts = datetime_series.copy() ts.dropna(inplace=True) - self.assertEqual(ts.name, name) + assert ts.name == name def test_fill_value_when_combine_const(self): # GH12723 @@ -626,16 +798,21 @@ def test_sparse_series_fillna_limit(self): s = Series(np.random.randn(10), index=index) ss = s[:2].reindex(index).to_sparse() - result = ss.fillna(method='pad', limit=5) - expected = ss.fillna(method='pad', limit=5) + # TODO: what is this test doing? why are result an expected + # the same call to fillna? + with tm.assert_produces_warning(PerformanceWarning): + # TODO: release-note fillna performance warning + result = ss.fillna(method='pad', limit=5) + expected = ss.fillna(method='pad', limit=5) expected = expected.to_dense() expected[-3:] = np.nan expected = expected.to_sparse() assert_series_equal(result, expected) ss = s[-2:].reindex(index).to_sparse() - result = ss.fillna(method='backfill', limit=5) - expected = ss.fillna(method='backfill') + with tm.assert_produces_warning(PerformanceWarning): + result = ss.fillna(method='backfill', limit=5) + expected = ss.fillna(method='backfill') expected = expected.to_dense() expected[:3] = np.nan expected = expected.to_sparse() @@ -647,14 +824,16 @@ def test_sparse_series_pad_backfill_limit(self): s = s.to_sparse() result = s[:2].reindex(index, method='pad', limit=5) - expected = s[:2].reindex(index).fillna(method='pad') + with tm.assert_produces_warning(PerformanceWarning): + expected = s[:2].reindex(index).fillna(method='pad') expected = expected.to_dense() expected[-3:] = np.nan expected = expected.to_sparse() assert_series_equal(result, expected) result = s[-2:].reindex(index, method='backfill', limit=5) - expected = s[-2:].reindex(index).fillna(method='backfill') + with tm.assert_produces_warning(PerformanceWarning): + expected = s[-2:].reindex(index).fillna(method='backfill') expected = expected.to_dense() expected[:3] = np.nan expected = expected.to_sparse() @@ -677,34 +856,53 @@ def test_series_pad_backfill_limit(self): assert_series_equal(result, expected) -class TestSeriesInterpolateData(TestData, tm.TestCase): +@pytest.fixture(params=['linear', 'index', 'values', 'nearest', 'slinear', + 'zero', 'quadratic', 'cubic', 'barycentric', 'krogh', + 'polynomial', 'spline', 'piecewise_polynomial', + 'from_derivatives', 'pchip', 'akima', ]) +def nontemporal_method(request): + """ Fixture that returns an (method name, required kwargs) pair. + + This fixture does not include method 'time' as a parameterization; that + method requires a Series with a DatetimeIndex, and is generally tested + separately from these non-temporal methods. + """ + method = request.param + kwargs = dict(order=1) if method in ('spline', 'polynomial') else dict() + return method, kwargs - def test_interpolate(self): - ts = Series(np.arange(len(self.ts), dtype=float), self.ts.index) + +class TestSeriesInterpolateData(): + def test_interpolate(self, datetime_series, string_series): + ts = Series(np.arange(len(datetime_series), dtype=float), + datetime_series.index) ts_copy = ts.copy() ts_copy[5:10] = np.NaN linear_interp = ts_copy.interpolate(method='linear') - self.assert_series_equal(linear_interp, ts) + tm.assert_series_equal(linear_interp, ts) - ord_ts = Series([d.toordinal() for d in self.ts.index], - index=self.ts.index).astype(float) + ord_ts = Series([d.toordinal() for d in datetime_series.index], + index=datetime_series.index).astype(float) ord_ts_copy = ord_ts.copy() ord_ts_copy[5:10] = np.NaN time_interp = ord_ts_copy.interpolate(method='time') - self.assert_series_equal(time_interp, ord_ts) - - # try time interpolation on a non-TimeSeries - # Only raises ValueError if there are NaNs. - non_ts = self.series.copy() - non_ts[0] = np.NaN - self.assertRaises(ValueError, non_ts.interpolate, method='time') - + tm.assert_series_equal(time_interp, ord_ts) + + def test_interpolate_time_raises_for_non_timeseries(self): + # When method='time' is used on a non-TimeSeries that contains a null + # value, a ValueError should be raised. + non_ts = Series([0, 1, 2, np.NaN]) + msg = ("time-weighted interpolation only works on Series.* " + "with a DatetimeIndex") + with pytest.raises(ValueError, match=msg): + non_ts.interpolate(method='time') + + @td.skip_if_no_scipy def test_interpolate_pchip(self): - tm._skip_if_no_scipy() _skip_if_no_pchip() ser = Series(np.sort(np.random.uniform(size=100))) @@ -716,8 +914,8 @@ def test_interpolate_pchip(self): # does not blow up, GH5977 interp_s[49:51] + @td.skip_if_no_scipy def test_interpolate_akima(self): - tm._skip_if_no_scipy() _skip_if_no_akima() ser = Series([10, 11, 12, 13]) @@ -731,9 +929,8 @@ def test_interpolate_akima(self): interp_s = ser.reindex(new_index).interpolate(method='akima') assert_series_equal(interp_s[1:3], expected) + @td.skip_if_no_scipy def test_interpolate_piecewise_polynomial(self): - tm._skip_if_no_scipy() - ser = Series([10, 11, 12, 13]) expected = Series([11.00, 11.25, 11.50, 11.75, @@ -746,9 +943,8 @@ def test_interpolate_piecewise_polynomial(self): method='piecewise_polynomial') assert_series_equal(interp_s[1:3], expected) + @td.skip_if_no_scipy def test_interpolate_from_derivatives(self): - tm._skip_if_no_scipy() - ser = Series([10, 11, 12, 13]) expected = Series([11.00, 11.25, 11.50, 11.75, @@ -761,19 +957,17 @@ def test_interpolate_from_derivatives(self): method='from_derivatives') assert_series_equal(interp_s[1:3], expected) - def test_interpolate_corners(self): - s = Series([np.nan, np.nan]) - assert_series_equal(s.interpolate(), s) - - s = Series([]).interpolate() - assert_series_equal(s.interpolate(), s) - - tm._skip_if_no_scipy() + @pytest.mark.parametrize("kwargs", [ + {}, + pytest.param({'method': 'polynomial', 'order': 1}, + marks=td.skip_if_no_scipy) + ]) + def test_interpolate_corners(self, kwargs): s = Series([np.nan, np.nan]) - assert_series_equal(s.interpolate(method='polynomial', order=1), s) + assert_series_equal(s.interpolate(**kwargs), s) s = Series([]).interpolate() - assert_series_equal(s.interpolate(method='polynomial', order=1), s) + assert_series_equal(s.interpolate(**kwargs), s) def test_interpolate_index_values(self): s = Series(np.nan, index=np.sort(np.random.rand(30))) @@ -784,7 +978,7 @@ def test_interpolate_index_values(self): result = s.interpolate(method='index') expected = s.copy() - bad = isnull(expected.values) + bad = isna(expected.values) good = ~bad expected = Series(np.interp(vals[bad], vals[good], s.values[good]), @@ -800,20 +994,22 @@ def test_interpolate_index_values(self): def test_interpolate_non_ts(self): s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - with tm.assertRaises(ValueError): + msg = ("time-weighted interpolation only works on Series or DataFrames" + " with a DatetimeIndex") + with pytest.raises(ValueError, match=msg): s.interpolate(method='time') - # New interpolation tests - def test_nan_interpolate(self): + @pytest.mark.parametrize("kwargs", [ + {}, + pytest.param({'method': 'polynomial', 'order': 1}, + marks=td.skip_if_no_scipy) + ]) + def test_nan_interpolate(self, kwargs): s = Series([0, 1, np.nan, 3]) - result = s.interpolate() + result = s.interpolate(**kwargs) expected = Series([0., 1., 2., 3.]) assert_series_equal(result, expected) - tm._skip_if_no_scipy() - result = s.interpolate(method='polynomial', order=1) - assert_series_equal(result, expected) - def test_nan_irregular_index(self): s = Series([1, 2, np.nan, 4], index=[1, 3, 5, 9]) result = s.interpolate() @@ -826,16 +1022,15 @@ def test_nan_str_index(self): expected = Series([0., 1., 2., 2.], index=list('abcd')) assert_series_equal(result, expected) + @td.skip_if_no_scipy def test_interp_quad(self): - tm._skip_if_no_scipy() sq = Series([1, 4, np.nan, 16], index=[1, 2, 3, 4]) result = sq.interpolate(method='quadratic') expected = Series([1., 4., 9., 16.], index=[1, 2, 3, 4]) assert_series_equal(result, expected) + @td.skip_if_no_scipy def test_interp_scipy_basic(self): - tm._skip_if_no_scipy() - s = Series([1, 3, np.nan, 12, np.nan, 25]) # slinear expected = Series([1., 3., 7.5, 12., 18.5, 25.]) @@ -883,16 +1078,35 @@ def test_interp_limit(self): result = s.interpolate(method='linear', limit=2) assert_series_equal(result, expected) - # GH 9217, make sure limit is an int and greater than 0 - methods = ['linear', 'time', 'index', 'values', 'nearest', 'zero', - 'slinear', 'quadratic', 'cubic', 'barycentric', 'krogh', - 'polynomial', 'spline', 'piecewise_polynomial', None, - 'from_derivatives', 'pchip', 'akima'] - s = pd.Series([1, 2, np.nan, np.nan, 5]) - for limit in [-1, 0, 1., 2.]: - for method in methods: - with tm.assertRaises(ValueError): - s.interpolate(limit=limit, method=method) + @pytest.mark.parametrize("limit", [-1, 0]) + def test_interpolate_invalid_nonpositive_limit(self, nontemporal_method, + limit): + # GH 9217: make sure limit is greater than zero. + s = pd.Series([1, 2, np.nan, 4]) + method, kwargs = nontemporal_method + with pytest.raises(ValueError, match="Limit must be greater than 0"): + s.interpolate(limit=limit, method=method, **kwargs) + + def test_interpolate_invalid_float_limit(self, nontemporal_method): + # GH 9217: make sure limit is an integer. + s = pd.Series([1, 2, np.nan, 4]) + method, kwargs = nontemporal_method + limit = 2.0 + with pytest.raises(ValueError, match="Limit must be an integer"): + s.interpolate(limit=limit, method=method, **kwargs) + + @pytest.mark.parametrize("invalid_method", [None, 'nonexistent_method']) + def test_interp_invalid_method(self, invalid_method): + s = Series([1, 3, np.nan, 12, np.nan, 25]) + + msg = "method must be one of.* Got '{}' instead".format(invalid_method) + with pytest.raises(ValueError, match=msg): + s.interpolate(method=invalid_method) + + # When an invalid method and invalid limit (such as -1) are + # provided, the error message reflects the invalid method. + with pytest.raises(ValueError, match=msg): + s.interpolate(method=invalid_method, limit=-1) def test_interp_limit_forward(self): s = Series([1, 3, np.nan, np.nan, np.nan, 11]) @@ -908,15 +1122,76 @@ def test_interp_limit_forward(self): limit_direction='FORWARD') assert_series_equal(result, expected) + def test_interp_unlimited(self): + # these test are for issue #16282 default Limit=None is unlimited + s = Series([np.nan, 1., 3., np.nan, np.nan, np.nan, 11., np.nan]) + expected = Series([1., 1., 3., 5., 7., 9., 11., 11.]) + result = s.interpolate(method='linear', + limit_direction='both') + assert_series_equal(result, expected) + + expected = Series([np.nan, 1., 3., 5., 7., 9., 11., 11.]) + result = s.interpolate(method='linear', + limit_direction='forward') + assert_series_equal(result, expected) + + expected = Series([1., 1., 3., 5., 7., 9., 11., np.nan]) + result = s.interpolate(method='linear', + limit_direction='backward') + assert_series_equal(result, expected) + def test_interp_limit_bad_direction(self): s = Series([1, 3, np.nan, np.nan, np.nan, 11]) - self.assertRaises(ValueError, s.interpolate, method='linear', limit=2, - limit_direction='abc') + msg = (r"Invalid limit_direction: expecting one of \['forward'," + r" 'backward', 'both'\], got 'abc'") + with pytest.raises(ValueError, match=msg): + s.interpolate(method='linear', limit=2, limit_direction='abc') # raises an error even if no limit is specified. - self.assertRaises(ValueError, s.interpolate, method='linear', - limit_direction='abc') + with pytest.raises(ValueError, match=msg): + s.interpolate(method='linear', limit_direction='abc') + + # limit_area introduced GH #16284 + def test_interp_limit_area(self): + # These tests are for issue #9218 -- fill NaNs in both directions. + s = Series([nan, nan, 3, nan, nan, nan, 7, nan, nan]) + + expected = Series([nan, nan, 3., 4., 5., 6., 7., nan, nan]) + result = s.interpolate(method='linear', limit_area='inside') + assert_series_equal(result, expected) + + expected = Series([nan, nan, 3., 4., nan, nan, 7., nan, nan]) + result = s.interpolate(method='linear', limit_area='inside', + limit=1) + + expected = Series([nan, nan, 3., 4., nan, 6., 7., nan, nan]) + result = s.interpolate(method='linear', limit_area='inside', + limit_direction='both', limit=1) + assert_series_equal(result, expected) + + expected = Series([nan, nan, 3., nan, nan, nan, 7., 7., 7.]) + result = s.interpolate(method='linear', limit_area='outside') + assert_series_equal(result, expected) + + expected = Series([nan, nan, 3., nan, nan, nan, 7., 7., nan]) + result = s.interpolate(method='linear', limit_area='outside', + limit=1) + + expected = Series([nan, 3., 3., nan, nan, nan, 7., 7., nan]) + result = s.interpolate(method='linear', limit_area='outside', + limit_direction='both', limit=1) + assert_series_equal(result, expected) + + expected = Series([3., 3., 3., nan, nan, nan, 7., nan, nan]) + result = s.interpolate(method='linear', limit_area='outside', + direction='backward') + + # raises an error even if limit type is wrong. + msg = (r"Invalid limit_area: expecting one of \['inside', 'outside'\]," + " got abc") + with pytest.raises(ValueError, match=msg): + s.interpolate(method='linear', limit_area='abc') def test_interp_limit_direction(self): # These tests are for issue #9218 -- fill NaNs in both directions. @@ -979,9 +1254,8 @@ def test_interp_limit_before_ends(self): limit_direction='both') assert_series_equal(result, expected) + @td.skip_if_no_scipy def test_interp_all_good(self): - # scipy - tm._skip_if_no_scipy() s = Series([1, 2, 3]) result = s.interpolate(method='polynomial', order=1) assert_series_equal(result, s) @@ -990,7 +1264,11 @@ def test_interp_all_good(self): result = s.interpolate() assert_series_equal(result, s) - def test_interp_multiIndex(self): + @pytest.mark.parametrize("check_scipy", [ + False, + pytest.param(True, marks=td.skip_if_no_scipy) + ]) + def test_interp_multiIndex(self, check_scipy): idx = MultiIndex.from_tuples([(0, 'a'), (1, 'b'), (2, 'c')]) s = Series([1, 2, np.nan], index=idx) @@ -999,18 +1277,20 @@ def test_interp_multiIndex(self): result = s.interpolate() assert_series_equal(result, expected) - tm._skip_if_no_scipy() - with tm.assertRaises(ValueError): - s.interpolate(method='polynomial', order=1) + msg = "Only `method=linear` interpolation is supported on MultiIndexes" + if check_scipy: + with pytest.raises(ValueError, match=msg): + s.interpolate(method='polynomial', order=1) + @td.skip_if_no_scipy def test_interp_nonmono_raise(self): - tm._skip_if_no_scipy() s = Series([1, np.nan, 3], index=[0, 2, 1]) - with tm.assertRaises(ValueError): + msg = "krogh interpolation requires that the index be monotonic" + with pytest.raises(ValueError, match=msg): s.interpolate(method='krogh') + @td.skip_if_no_scipy def test_interp_datetime64(self): - tm._skip_if_no_scipy() df = Series([1, np.nan, 3], index=date_range('1/1/2000', periods=3)) result = df.interpolate(method='nearest') expected = Series([1., 1., 3.], @@ -1024,25 +1304,32 @@ def test_interp_limit_no_nans(self): expected = s assert_series_equal(result, expected) - def test_no_order(self): - tm._skip_if_no_scipy() + @td.skip_if_no_scipy + @pytest.mark.parametrize("method", ['polynomial', 'spline']) + def test_no_order(self, method): + # see GH-10633, GH-24014 s = Series([0, 1, np.nan, 3]) - with tm.assertRaises(ValueError): - s.interpolate(method='polynomial') - with tm.assertRaises(ValueError): - s.interpolate(method='spline') + msg = "You must specify the order of the spline or polynomial" + with pytest.raises(ValueError, match=msg): + s.interpolate(method=method) + @td.skip_if_no_scipy + @pytest.mark.parametrize('order', [-1, -1.0, 0, 0.0, np.nan]) + def test_interpolate_spline_invalid_order(self, order): + s = Series([0, 1, np.nan, 3]) + msg = "order needs to be specified and greater than 0" + with pytest.raises(ValueError, match=msg): + s.interpolate(method='spline', order=order) + + @td.skip_if_no_scipy def test_spline(self): - tm._skip_if_no_scipy() s = Series([1, 2, np.nan, 4, 5, np.nan, 7]) result = s.interpolate(method='spline', order=1) expected = Series([1., 2., 3., 4., 5., 6., 7.]) assert_series_equal(result, expected) + @td.skip_if_no('scipy', min_version='0.15') def test_spline_extrapolate(self): - tm.skip_if_no_package( - 'scipy', min_version='0.15', - app='setting ext on scipy.interpolate.UnivariateSpline') s = Series([1, 2, 3, 4, np.nan, 6, np.nan]) result3 = s.interpolate(method='spline', order=1, ext=3) expected3 = Series([1., 2., 3., 4., 5., 6., 6.]) @@ -1052,33 +1339,20 @@ def test_spline_extrapolate(self): expected1 = Series([1., 2., 3., 4., 5., 6., 7.]) assert_series_equal(result1, expected1) + @td.skip_if_no_scipy def test_spline_smooth(self): - tm._skip_if_no_scipy() s = Series([1, 2, np.nan, 4, 5.1, np.nan, 7]) - self.assertNotEqual(s.interpolate(method='spline', order=3, s=0)[5], - s.interpolate(method='spline', order=3)[5]) + assert (s.interpolate(method='spline', order=3, s=0)[5] != + s.interpolate(method='spline', order=3)[5]) + @td.skip_if_no_scipy def test_spline_interpolation(self): - tm._skip_if_no_scipy() - s = Series(np.arange(10) ** 2) s[np.random.randint(0, 9, 3)] = np.nan result1 = s.interpolate(method='spline', order=1) expected1 = s.interpolate(method='spline', order=1) assert_series_equal(result1, expected1) - # GH #10633 - def test_spline_error(self): - tm._skip_if_no_scipy() - - s = pd.Series(np.arange(10) ** 2) - s[np.random.randint(0, 9, 3)] = np.nan - with tm.assertRaises(ValueError): - s.interpolate(method='spline') - - with tm.assertRaises(ValueError): - s.interpolate(method='spline', order=0) - def test_interp_timedelta64(self): # GH 6424 df = Series([1, np.nan, 3], @@ -1118,4 +1392,10 @@ def test_series_interpolate_intraday(self): new_index = index.append(index + pd.DateOffset(hours=1)).sort_values() result = ts.reindex(new_index).interpolate(method='time') - self.assert_numpy_array_equal(result.values, exp.values) + tm.assert_numpy_array_equal(result.values, exp.values) + + def test_nonzero_warning(self): + # GH 24048 + ser = pd.Series([1, 0, 3, 4]) + with tm.assert_produces_warning(FutureWarning): + ser.nonzero() diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 3d609dec7958a..b2aac441db195 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -1,936 +1,441 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -from collections import Iterable from datetime import datetime, timedelta import operator -from itertools import product, starmap -from numpy import nan, inf import numpy as np -import pandas as pd +import pytest -from pandas import (Index, Series, DataFrame, isnull, bdate_range, - NaT, date_range, timedelta_range, - _np_version_under1p8) -from pandas.tseries.index import Timestamp -from pandas.tseries.tdi import Timedelta -import pandas.core.nanops as nanops +import pandas.compat as compat +from pandas.compat import range -from pandas.compat import range, zip -from pandas import compat -from pandas.util.testing import (assert_series_equal, assert_almost_equal, - assert_frame_equal, assert_index_equal) +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, Series, bdate_range, date_range, isna) +from pandas.core import ops +import pandas.core.nanops as nanops import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) from .common import TestData -class TestSeriesOperators(TestData, tm.TestCase): +class TestSeriesLogicalOps(object): + @pytest.mark.parametrize('bool_op', [operator.and_, + operator.or_, operator.xor]) + def test_bool_operators_with_nas(self, bool_op): + # boolean &, |, ^ should work with object arrays and propagate NAs + ser = Series(bdate_range('1/1/2000', periods=10), dtype=object) + ser[::2] = np.nan - def test_series_comparison_scalars(self): - series = Series(date_range('1/1/2000', periods=10)) + mask = ser.isna() + filled = ser.fillna(ser[0]) - val = datetime(2000, 1, 4) - result = series > val - expected = Series([x > val for x in series]) - self.assert_series_equal(result, expected) + result = bool_op(ser < ser[9], ser > ser[3]) - val = series[5] - result = series > val - expected = Series([x > val for x in series]) - self.assert_series_equal(result, expected) + expected = bool_op(filled < filled[9], filled > filled[3]) + expected[mask] = False + assert_series_equal(result, expected) - def test_comparisons(self): - left = np.random.randn(10) - right = np.random.randn(10) - left[:3] = np.nan + def test_operators_bitwise(self): + # GH#9016: support bitwise op for integer types + index = list('bca') - result = nanops.nangt(left, right) - with np.errstate(invalid='ignore'): - expected = (left > right).astype('O') - expected[:3] = np.nan + s_tft = Series([True, False, True], index=index) + s_fff = Series([False, False, False], index=index) + s_tff = Series([True, False, False], index=index) + s_empty = Series([]) - assert_almost_equal(result, expected) + # TODO: unused + # s_0101 = Series([0, 1, 0, 1]) - s = Series(['a', 'b', 'c']) - s2 = Series([False, True, False]) + s_0123 = Series(range(4), dtype='int64') + s_3333 = Series([3] * 4) + s_4444 = Series([4] * 4) - # it works! - exp = Series([False, False, False]) - assert_series_equal(s == s2, exp) - assert_series_equal(s2 == s, exp) + res = s_tft & s_empty + expected = s_fff + assert_series_equal(res, expected) - def test_op_method(self): - def check(series, other, check_reverse=False): - simple_ops = ['add', 'sub', 'mul', 'floordiv', 'truediv', 'pow'] - if not compat.PY3: - simple_ops.append('div') - - for opname in simple_ops: - op = getattr(Series, opname) - - if op == 'div': - alt = operator.truediv - else: - alt = getattr(operator, opname) - - result = op(series, other) - expected = alt(series, other) - assert_almost_equal(result, expected) - if check_reverse: - rop = getattr(Series, "r" + opname) - result = rop(series, other) - expected = alt(other, series) - assert_almost_equal(result, expected) - - check(self.ts, self.ts * 2) - check(self.ts, self.ts[::2]) - check(self.ts, 5, check_reverse=True) - check(tm.makeFloatSeries(), tm.makeFloatSeries(), check_reverse=True) + res = s_tft | s_empty + expected = s_tft + assert_series_equal(res, expected) - def test_neg(self): - assert_series_equal(-self.series, -1 * self.series) + res = s_0123 & s_3333 + expected = Series(range(4), dtype='int64') + assert_series_equal(res, expected) - def test_invert(self): - assert_series_equal(-(self.series < 0), ~(self.series < 0)) - - def test_div(self): - with np.errstate(all='ignore'): - # no longer do integer div for any ops, but deal with the 0's - p = DataFrame({'first': [3, 4, 5, 8], 'second': [0, 0, 0, 3]}) - result = p['first'] / p['second'] - expected = Series( - p['first'].values.astype(float) / p['second'].values, - dtype='float64') - expected.iloc[0:3] = np.inf - assert_series_equal(result, expected) + res = s_0123 | s_4444 + expected = Series(range(4, 8), dtype='int64') + assert_series_equal(res, expected) - result = p['first'] / 0 - expected = Series(np.inf, index=p.index, name='first') - assert_series_equal(result, expected) + s_a0b1c0 = Series([1], list('b')) - p = p.astype('float64') - result = p['first'] / p['second'] - expected = Series(p['first'].values / p['second'].values) - assert_series_equal(result, expected) + res = s_tft & s_a0b1c0 + expected = s_tff.reindex(list('abc')) + assert_series_equal(res, expected) - p = DataFrame({'first': [3, 4, 5, 8], 'second': [1, 1, 1, 1]}) - result = p['first'] / p['second'] - assert_series_equal(result, p['first'].astype('float64'), - check_names=False) - self.assertTrue(result.name is None) - self.assertFalse(np.array_equal(result, p['second'] / p['first'])) - - # inf signing - s = Series([np.nan, 1., -1.]) - result = s / 0 - expected = Series([np.nan, np.inf, -np.inf]) - assert_series_equal(result, expected) + res = s_tft | s_a0b1c0 + expected = s_tft.reindex(list('abc')) + assert_series_equal(res, expected) - # float/integer issue - # GH 7785 - p = DataFrame({'first': (1, 0), 'second': (-0.01, -0.02)}) - expected = Series([-0.01, -np.inf]) + n0 = 0 + res = s_tft & n0 + expected = s_fff + assert_series_equal(res, expected) - result = p['second'].div(p['first']) - assert_series_equal(result, expected, check_names=False) + res = s_0123 & n0 + expected = Series([0] * 4) + assert_series_equal(res, expected) - result = p['second'] / p['first'] - assert_series_equal(result, expected) + n1 = 1 + res = s_tft & n1 + expected = s_tft + assert_series_equal(res, expected) - # GH 9144 - s = Series([-1, 0, 1]) + res = s_0123 & n1 + expected = Series([0, 1, 0, 1]) + assert_series_equal(res, expected) - result = 0 / s - expected = Series([0.0, nan, 0.0]) - assert_series_equal(result, expected) + s_1111 = Series([1] * 4, dtype='int8') + res = s_0123 & s_1111 + expected = Series([0, 1, 0, 1], dtype='int64') + assert_series_equal(res, expected) - result = s / 0 - expected = Series([-inf, nan, inf]) - assert_series_equal(result, expected) + res = s_0123.astype(np.int16) | s_1111.astype(np.int32) + expected = Series([1, 1, 3, 3], dtype='int32') + assert_series_equal(res, expected) - result = s // 0 - expected = Series([-inf, nan, inf]) - assert_series_equal(result, expected) + with pytest.raises(TypeError): + s_1111 & 'a' + with pytest.raises(TypeError): + s_1111 & ['a', 'b', 'c', 'd'] + with pytest.raises(TypeError): + s_0123 & np.NaN + with pytest.raises(TypeError): + s_0123 & 3.14 + with pytest.raises(TypeError): + s_0123 & [0.1, 4, 3.14, 2] - # GH 8674 - zero_array = np.array([0] * 5) - data = np.random.randn(5) - expected = pd.Series([0.] * 5) - result = zero_array / pd.Series(data) - assert_series_equal(result, expected) + # s_0123 will be all false now because of reindexing like s_tft + exp = Series([False] * 7, index=[0, 1, 2, 3, 'a', 'b', 'c']) + assert_series_equal(s_tft & s_0123, exp) - result = pd.Series(zero_array) / data - assert_series_equal(result, expected) + # s_tft will be all false now because of reindexing like s_0123 + exp = Series([False] * 7, index=[0, 1, 2, 3, 'a', 'b', 'c']) + assert_series_equal(s_0123 & s_tft, exp) - result = pd.Series(zero_array) / pd.Series(data) - assert_series_equal(result, expected) + assert_series_equal(s_0123 & False, Series([False] * 4)) + assert_series_equal(s_0123 ^ False, Series([False, True, True, True])) + assert_series_equal(s_0123 & [False], Series([False] * 4)) + assert_series_equal(s_0123 & (False), Series([False] * 4)) + assert_series_equal(s_0123 & Series([False, np.NaN, False, False]), + Series([False] * 4)) - def test_operators(self): - def _check_op(series, other, op, pos_only=False, - check_dtype=True): - left = np.abs(series) if pos_only else series - right = np.abs(other) if pos_only else other - - cython_or_numpy = op(left, right) - python = left.combine(right, op) - assert_series_equal(cython_or_numpy, python, - check_dtype=check_dtype) - - def check(series, other): - simple_ops = ['add', 'sub', 'mul', 'truediv', 'floordiv', 'mod'] - - for opname in simple_ops: - _check_op(series, other, getattr(operator, opname)) - - _check_op(series, other, operator.pow, pos_only=True) - - _check_op(series, other, lambda x, y: operator.add(y, x)) - _check_op(series, other, lambda x, y: operator.sub(y, x)) - _check_op(series, other, lambda x, y: operator.truediv(y, x)) - _check_op(series, other, lambda x, y: operator.floordiv(y, x)) - _check_op(series, other, lambda x, y: operator.mul(y, x)) - _check_op(series, other, lambda x, y: operator.pow(y, x), - pos_only=True) - _check_op(series, other, lambda x, y: operator.mod(y, x)) - - check(self.ts, self.ts * 2) - check(self.ts, self.ts * 0) - check(self.ts, self.ts[::2]) - check(self.ts, 5) - - def check_comparators(series, other, check_dtype=True): - _check_op(series, other, operator.gt, check_dtype=check_dtype) - _check_op(series, other, operator.ge, check_dtype=check_dtype) - _check_op(series, other, operator.eq, check_dtype=check_dtype) - _check_op(series, other, operator.lt, check_dtype=check_dtype) - _check_op(series, other, operator.le, check_dtype=check_dtype) - - check_comparators(self.ts, 5) - check_comparators(self.ts, self.ts + 1, check_dtype=False) - - def test_divmod(self): - def check(series, other): - results = divmod(series, other) - if isinstance(other, Iterable) and len(series) != len(other): - # if the lengths don't match, this is the test where we use - # `self.ts[::2]`. Pad every other value in `other_np` with nan. - other_np = [] - for n in other: - other_np.append(n) - other_np.append(np.nan) - else: - other_np = other - other_np = np.asarray(other_np) - with np.errstate(all='ignore'): - expecteds = divmod(series.values, np.asarray(other_np)) + s_ftft = Series([False, True, False, True]) + assert_series_equal(s_0123 & Series([0.1, 4, -3.14, 2]), s_ftft) - for result, expected in zip(results, expecteds): - # check the values, name, and index separatly - assert_almost_equal(np.asarray(result), expected) + s_abNd = Series(['a', 'b', np.NaN, 'd']) + res = s_0123 & s_abNd + expected = s_ftft + assert_series_equal(res, expected) - self.assertEqual(result.name, series.name) - assert_index_equal(result.index, series.index) + def test_scalar_na_logical_ops_corners(self): + s = Series([2, 3, 4, 5, 6, 7, 8, 9, 10]) - check(self.ts, self.ts * 2) - check(self.ts, self.ts * 0) - check(self.ts, self.ts[::2]) - check(self.ts, 5) + with pytest.raises(TypeError): + s & datetime(2005, 1, 1) - def test_operators_empty_int_corner(self): - s1 = Series([], [], dtype=np.int32) - s2 = Series({'x': 0.}) - assert_series_equal(s1 * s2, Series([np.nan], index=['x'])) + s = Series([2, 3, 4, 5, 6, 7, 8, 9, datetime(2005, 1, 1)]) + s[::2] = np.nan - def test_operators_timedelta64(self): - - # invalid ops - self.assertRaises(Exception, self.objSeries.__add__, 1) - self.assertRaises(Exception, self.objSeries.__add__, - np.array(1, dtype=np.int64)) - self.assertRaises(Exception, self.objSeries.__sub__, 1) - self.assertRaises(Exception, self.objSeries.__sub__, - np.array(1, dtype=np.int64)) - - # seriese ops - v1 = date_range('2012-1-1', periods=3, freq='D') - v2 = date_range('2012-1-2', periods=3, freq='D') - rs = Series(v2) - Series(v1) - xp = Series(1e9 * 3600 * 24, - rs.index).astype('int64').astype('timedelta64[ns]') - assert_series_equal(rs, xp) - self.assertEqual(rs.dtype, 'timedelta64[ns]') - - df = DataFrame(dict(A=v1)) - td = Series([timedelta(days=i) for i in range(3)]) - self.assertEqual(td.dtype, 'timedelta64[ns]') - - # series on the rhs - result = df['A'] - df['A'].shift() - self.assertEqual(result.dtype, 'timedelta64[ns]') - - result = df['A'] + td - self.assertEqual(result.dtype, 'M8[ns]') - - # scalar Timestamp on rhs - maxa = df['A'].max() - tm.assertIsInstance(maxa, Timestamp) - - resultb = df['A'] - df['A'].max() - self.assertEqual(resultb.dtype, 'timedelta64[ns]') - - # timestamp on lhs - result = resultb + df['A'] - values = [Timestamp('20111230'), Timestamp('20120101'), - Timestamp('20120103')] - expected = Series(values, name='A') + expected = Series(True, index=s.index) + expected[::2] = False + result = s & list(s) assert_series_equal(result, expected) - # datetimes on rhs - result = df['A'] - datetime(2001, 1, 1) - expected = Series( - [timedelta(days=4017 + i) for i in range(3)], name='A') - assert_series_equal(result, expected) - self.assertEqual(result.dtype, 'm8[ns]') - - d = datetime(2001, 1, 1, 3, 4) - resulta = df['A'] - d - self.assertEqual(resulta.dtype, 'm8[ns]') - - # roundtrip - resultb = resulta + d - assert_series_equal(df['A'], resultb) - - # timedeltas on rhs - td = timedelta(days=1) - resulta = df['A'] + td - resultb = resulta - td - assert_series_equal(resultb, df['A']) - self.assertEqual(resultb.dtype, 'M8[ns]') - - # roundtrip - td = timedelta(minutes=5, seconds=3) - resulta = df['A'] + td - resultb = resulta - td - assert_series_equal(df['A'], resultb) - self.assertEqual(resultb.dtype, 'M8[ns]') - - # inplace - value = rs[2] + np.timedelta64(timedelta(minutes=5, seconds=1)) - rs[2] += np.timedelta64(timedelta(minutes=5, seconds=1)) - self.assertEqual(rs[2], value) - - def test_operator_series_comparison_zerorank(self): - # GH 13006 - result = np.float64(0) > pd.Series([1, 2, 3]) - expected = 0.0 > pd.Series([1, 2, 3]) - self.assert_series_equal(result, expected) - result = pd.Series([1, 2, 3]) < np.float64(0) - expected = pd.Series([1, 2, 3]) < 0.0 - self.assert_series_equal(result, expected) - result = np.array([0, 1, 2])[0] > pd.Series([0, 1, 2]) - expected = 0.0 > pd.Series([1, 2, 3]) - self.assert_series_equal(result, expected) - - def test_timedeltas_with_DateOffset(self): - - # GH 4532 - # operate with pd.offsets - s = Series([Timestamp('20130101 9:01'), Timestamp('20130101 9:02')]) - - result = s + pd.offsets.Second(5) - result2 = pd.offsets.Second(5) + s - expected = Series([Timestamp('20130101 9:01:05'), Timestamp( - '20130101 9:02:05')]) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) + d = DataFrame({'A': s}) + # TODO: Fix this exception - needs to be fixed! (see GH5035) + # (previously this was a TypeError because series returned + # NotImplemented - result = s - pd.offsets.Second(5) - result2 = -pd.offsets.Second(5) + s - expected = Series([Timestamp('20130101 9:00:55'), Timestamp( - '20130101 9:01:55')]) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) + # this is an alignment issue; these are equivalent + # https://github.com/pandas-dev/pandas/issues/5284 - result = s + pd.offsets.Milli(5) - result2 = pd.offsets.Milli(5) + s - expected = Series([Timestamp('20130101 9:01:00.005'), Timestamp( - '20130101 9:02:00.005')]) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) + with pytest.raises(TypeError): + d.__and__(s, axis='columns') - result = s + pd.offsets.Minute(5) + pd.offsets.Milli(5) - expected = Series([Timestamp('20130101 9:06:00.005'), Timestamp( - '20130101 9:07:00.005')]) - assert_series_equal(result, expected) + with pytest.raises(TypeError): + s & d - # operate with np.timedelta64 correctly - result = s + np.timedelta64(1, 's') - result2 = np.timedelta64(1, 's') + s - expected = Series([Timestamp('20130101 9:01:01'), Timestamp( - '20130101 9:02:01')]) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) + # this is wrong as its not a boolean result + # result = d.__and__(s,axis='index') - result = s + np.timedelta64(5, 'ms') - result2 = np.timedelta64(5, 'ms') + s - expected = Series([Timestamp('20130101 9:01:00.005'), Timestamp( - '20130101 9:02:00.005')]) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) - - # valid DateOffsets - for do in ['Hour', 'Minute', 'Second', 'Day', 'Micro', 'Milli', - 'Nano']: - op = getattr(pd.offsets, do) - s + op(5) - op(5) + s - - def test_timedelta_series_ops(self): - # GH11925 - - s = Series(timedelta_range('1 day', periods=3)) - ts = Timestamp('2012-01-01') - expected = Series(date_range('2012-01-02', periods=3)) - assert_series_equal(ts + s, expected) - assert_series_equal(s + ts, expected) - - expected2 = Series(date_range('2011-12-31', periods=3, freq='-1D')) - assert_series_equal(ts - s, expected2) - assert_series_equal(ts + (-s), expected2) - - def test_timedelta64_operations_with_DateOffset(self): - # GH 10699 - td = Series([timedelta(minutes=5, seconds=3)] * 3) - result = td + pd.offsets.Minute(1) - expected = Series([timedelta(minutes=6, seconds=3)] * 3) - assert_series_equal(result, expected) + @pytest.mark.parametrize('op', [ + operator.and_, + operator.or_, + operator.xor, - result = td - pd.offsets.Minute(1) - expected = Series([timedelta(minutes=4, seconds=3)] * 3) - assert_series_equal(result, expected) + ]) + def test_logical_ops_with_index(self, op): + # GH#22092, GH#19792 + ser = Series([True, True, False, False]) + idx1 = Index([True, False, True, False]) + idx2 = Index([1, 0, 1, 0]) - result = td + Series([pd.offsets.Minute(1), pd.offsets.Second(3), - pd.offsets.Hour(2)]) - expected = Series([timedelta(minutes=6, seconds=3), timedelta( - minutes=5, seconds=6), timedelta(hours=2, minutes=5, seconds=3)]) - assert_series_equal(result, expected) + expected = Series([op(ser[n], idx1[n]) for n in range(len(ser))]) - result = td + pd.offsets.Minute(1) + pd.offsets.Second(12) - expected = Series([timedelta(minutes=6, seconds=15)] * 3) + result = op(ser, idx1) assert_series_equal(result, expected) - # valid DateOffsets - for do in ['Hour', 'Minute', 'Second', 'Day', 'Micro', 'Milli', - 'Nano']: - op = getattr(pd.offsets, do) - td + op(5) - op(5) + td - td - op(5) - op(5) - td - - def test_timedelta64_operations_with_timedeltas(self): - - # td operate with td - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td2 = timedelta(minutes=5, seconds=4) - result = td1 - td2 - expected = Series([timedelta(seconds=0)] * 3) - Series([timedelta( - seconds=1)] * 3) - self.assertEqual(result.dtype, 'm8[ns]') - assert_series_equal(result, expected) + expected = Series([op(ser[n], idx2[n]) for n in range(len(ser))], + dtype=bool) - result2 = td2 - td1 - expected = (Series([timedelta(seconds=1)] * 3) - Series([timedelta( - seconds=0)] * 3)) - assert_series_equal(result2, expected) - - # roundtrip - assert_series_equal(result + td2, td1) - - # Now again, using pd.to_timedelta, which should build - # a Series or a scalar, depending on input. - td1 = Series(pd.to_timedelta(['00:05:03'] * 3)) - td2 = pd.to_timedelta('00:05:04') - result = td1 - td2 - expected = Series([timedelta(seconds=0)] * 3) - Series([timedelta( - seconds=1)] * 3) - self.assertEqual(result.dtype, 'm8[ns]') + result = op(ser, idx2) assert_series_equal(result, expected) - result2 = td2 - td1 - expected = (Series([timedelta(seconds=1)] * 3) - Series([timedelta( - seconds=0)] * 3)) - assert_series_equal(result2, expected) - - # roundtrip - assert_series_equal(result + td2, td1) + @pytest.mark.parametrize("op, expected", [ + (ops.rand_, pd.Index([False, True])), + (ops.ror_, pd.Index([False, True])), + (ops.rxor, pd.Index([])), + ]) + def test_reverse_ops_with_index(self, op, expected): + # https://github.com/pandas-dev/pandas/pull/23628 + # multi-set Index ops are buggy, so let's avoid duplicates... + ser = Series([True, False]) + idx = Index([False, True]) + result = op(ser, idx) + tm.assert_index_equal(result, expected) - def test_timedelta64_operations_with_integers(self): + def test_logical_ops_label_based(self): + # GH#4947 + # logical ops should be label based - # GH 4521 - # divide/multiply by integers - startdate = Series(date_range('2013-01-01', '2013-01-03')) - enddate = Series(date_range('2013-03-01', '2013-03-03')) - - s1 = enddate - startdate - s1[2] = np.nan - s2 = Series([2, 3, 4]) - expected = Series(s1.values.astype(np.int64) / s2, dtype='m8[ns]') - expected[2] = np.nan - result = s1 / s2 - assert_series_equal(result, expected) + a = Series([True, False, True], list('bca')) + b = Series([False, True, False], list('abc')) - s2 = Series([20, 30, 40]) - expected = Series(s1.values.astype(np.int64) / s2, dtype='m8[ns]') - expected[2] = np.nan - result = s1 / s2 + expected = Series([False, True, False], list('abc')) + result = a & b assert_series_equal(result, expected) - result = s1 / 2 - expected = Series(s1.values.astype(np.int64) / 2, dtype='m8[ns]') - expected[2] = np.nan + expected = Series([True, True, False], list('abc')) + result = a | b assert_series_equal(result, expected) - s2 = Series([20, 30, 40]) - expected = Series(s1.values.astype(np.int64) * s2, dtype='m8[ns]') - expected[2] = np.nan - result = s1 * s2 + expected = Series([True, False, False], list('abc')) + result = a ^ b assert_series_equal(result, expected) - for dtype in ['int32', 'int16', 'uint32', 'uint64', 'uint32', 'uint16', - 'uint8']: - s2 = Series([20, 30, 40], dtype=dtype) - expected = Series( - s1.values.astype(np.int64) * s2.astype(np.int64), - dtype='m8[ns]') - expected[2] = np.nan - result = s1 * s2 - assert_series_equal(result, expected) + # rhs is bigger + a = Series([True, False, True], list('bca')) + b = Series([False, True, False, True], list('abcd')) - result = s1 * 2 - expected = Series(s1.values.astype(np.int64) * 2, dtype='m8[ns]') - expected[2] = np.nan + expected = Series([False, True, False, False], list('abcd')) + result = a & b assert_series_equal(result, expected) - result = s1 * -1 - expected = Series(s1.values.astype(np.int64) * -1, dtype='m8[ns]') - expected[2] = np.nan + expected = Series([True, True, False, False], list('abcd')) + result = a | b assert_series_equal(result, expected) - # invalid ops - assert_series_equal(s1 / s2.astype(float), - Series([Timedelta('2 days 22:48:00'), Timedelta( - '1 days 23:12:00'), Timedelta('NaT')])) - assert_series_equal(s1 / 2.0, - Series([Timedelta('29 days 12:00:00'), Timedelta( - '29 days 12:00:00'), Timedelta('NaT')])) - - for op in ['__add__', '__sub__']: - sop = getattr(s1, op, None) - if sop is not None: - self.assertRaises(TypeError, sop, 1) - self.assertRaises(TypeError, sop, s2.values) - - def test_timedelta64_conversions(self): - startdate = Series(date_range('2013-01-01', '2013-01-03')) - enddate = Series(date_range('2013-03-01', '2013-03-03')) + # filling - s1 = enddate - startdate - s1[2] = np.nan + # vs empty + result = a & Series([]) + expected = Series([False, False, False], list('bca')) + assert_series_equal(result, expected) - for m in [1, 3, 10]: - for unit in ['D', 'h', 'm', 's', 'ms', 'us', 'ns']: + result = a | Series([]) + expected = Series([True, False, True], list('bca')) + assert_series_equal(result, expected) - # op - expected = s1.apply(lambda x: x / np.timedelta64(m, unit)) - result = s1 / np.timedelta64(m, unit) - assert_series_equal(result, expected) + # vs non-matching + result = a & Series([1], ['z']) + expected = Series([False, False, False, False], list('abcz')) + assert_series_equal(result, expected) - if m == 1 and unit != 'ns': + result = a | Series([1], ['z']) + expected = Series([True, True, False, False], list('abcz')) + assert_series_equal(result, expected) - # astype - result = s1.astype("timedelta64[{0}]".format(unit)) - assert_series_equal(result, expected) + # identity + # we would like s[s|e] == s to hold for any e, whether empty or not + for e in [Series([]), Series([1], ['z']), + Series(np.nan, b.index), Series(np.nan, a.index)]: + result = a[a | e] + assert_series_equal(result, a[a]) - # reverse op - expected = s1.apply( - lambda x: Timedelta(np.timedelta64(m, unit)) / x) - result = np.timedelta64(m, unit) / s1 + for e in [Series(['z'])]: + result = a[a | e] + assert_series_equal(result, a[a]) - # astype - s = Series(date_range('20130101', periods=3)) - result = s.astype(object) - self.assertIsInstance(result.iloc[0], datetime) - self.assertTrue(result.dtype == np.object_) + # vs scalars + index = list('bca') + t = Series([True, False, True]) - result = s1.astype(object) - self.assertIsInstance(result.iloc[0], timedelta) - self.assertTrue(result.dtype == np.object_) + for v in [True, 1, 2]: + result = Series([True, False, True], index=index) | v + expected = Series([True, True, True], index=index) + assert_series_equal(result, expected) - def test_timedelta64_equal_timedelta_supported_ops(self): - ser = Series([Timestamp('20130301'), Timestamp('20130228 23:00:00'), - Timestamp('20130228 22:00:00'), Timestamp( - '20130228 21:00:00')]) + for v in [np.nan, 'foo']: + with pytest.raises(TypeError): + t | v - intervals = 'D', 'h', 'm', 's', 'us' + for v in [False, 0]: + result = Series([True, False, True], index=index) | v + expected = Series([True, False, True], index=index) + assert_series_equal(result, expected) - # TODO: unused - # npy16_mappings = {'D': 24 * 60 * 60 * 1000000, - # 'h': 60 * 60 * 1000000, - # 'm': 60 * 1000000, - # 's': 1000000, - # 'us': 1} - - def timedelta64(*args): - return sum(starmap(np.timedelta64, zip(args, intervals))) - - for op, d, h, m, s, us in product([operator.add, operator.sub], - *([range(2)] * 5)): - nptd = timedelta64(d, h, m, s, us) - pytd = timedelta(days=d, hours=h, minutes=m, seconds=s, - microseconds=us) - lhs = op(ser, nptd) - rhs = op(ser, pytd) - - try: - assert_series_equal(lhs, rhs) - except: - raise AssertionError( - "invalid comparsion [op->{0},d->{1},h->{2},m->{3}," - "s->{4},us->{5}]\n{6}\n{7}\n".format(op, d, h, m, s, - us, lhs, rhs)) - - def test_operators_datetimelike(self): - def run_ops(ops, get_ser, test_ser): - - # check that we are getting a TypeError - # with 'operate' (from core/ops.py) for the ops that are not - # defined - for op_str in ops: - op = getattr(get_ser, op_str, None) - with tm.assertRaisesRegexp(TypeError, 'operate'): - op(test_ser) - - # ## timedelta64 ### - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - td2 = timedelta(minutes=5, seconds=4) - ops = ['__mul__', '__floordiv__', '__pow__', '__rmul__', - '__rfloordiv__', '__rpow__'] - run_ops(ops, td1, td2) - td1 + td2 - td2 + td1 - td1 - td2 - td2 - td1 - td1 / td2 - td2 / td1 - - # ## datetime64 ### - dt1 = Series([Timestamp('20111230'), Timestamp('20120101'), - Timestamp('20120103')]) - dt1.iloc[2] = np.nan - dt2 = Series([Timestamp('20111231'), Timestamp('20120102'), - Timestamp('20120104')]) - ops = ['__add__', '__mul__', '__floordiv__', '__truediv__', '__div__', - '__pow__', '__radd__', '__rmul__', '__rfloordiv__', - '__rtruediv__', '__rdiv__', '__rpow__'] - run_ops(ops, dt1, dt2) - dt1 - dt2 - dt2 - dt1 - - # ## datetime64 with timetimedelta ### - ops = ['__mul__', '__floordiv__', '__truediv__', '__div__', '__pow__', - '__rmul__', '__rfloordiv__', '__rtruediv__', '__rdiv__', - '__rpow__'] - run_ops(ops, dt1, td1) - dt1 + td1 - td1 + dt1 - dt1 - td1 - # TODO: Decide if this ought to work. - # td1 - dt1 - - # ## timetimedelta with datetime64 ### - ops = ['__sub__', '__mul__', '__floordiv__', '__truediv__', '__div__', - '__pow__', '__rmul__', '__rfloordiv__', '__rtruediv__', - '__rdiv__', '__rpow__'] - run_ops(ops, td1, dt1) - td1 + dt1 - dt1 + td1 - - # 8260, 10763 - # datetime64 with tz - ops = ['__mul__', '__floordiv__', '__truediv__', '__div__', '__pow__', - '__rmul__', '__rfloordiv__', '__rtruediv__', '__rdiv__', - '__rpow__'] - - tz = 'US/Eastern' - dt1 = Series(date_range('2000-01-01 09:00:00', periods=5, - tz=tz), name='foo') - dt2 = dt1.copy() - dt2.iloc[2] = np.nan - td1 = Series(timedelta_range('1 days 1 min', periods=5, freq='H')) - td2 = td1.copy() - td2.iloc[1] = np.nan - run_ops(ops, dt1, td1) - - result = dt1 + td1[0] - exp = (dt1.dt.tz_localize(None) + td1[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) + for v in [True, 1]: + result = Series([True, False, True], index=index) & v + expected = Series([True, False, True], index=index) + assert_series_equal(result, expected) - result = dt2 + td2[0] - exp = (dt2.dt.tz_localize(None) + td2[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) + for v in [False, 0]: + result = Series([True, False, True], index=index) & v + expected = Series([False, False, False], index=index) + assert_series_equal(result, expected) + for v in [np.nan]: + with pytest.raises(TypeError): + t & v - # odd numpy behavior with scalar timedeltas - if not _np_version_under1p8: - result = td1[0] + dt1 - exp = (dt1.dt.tz_localize(None) + td1[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) + def test_logical_ops_df_compat(self): + # GH#1134 + s1 = pd.Series([True, False, True], index=list('ABC'), name='x') + s2 = pd.Series([True, True, False], index=list('ABD'), name='x') - result = td2[0] + dt2 - exp = (dt2.dt.tz_localize(None) + td2[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) + exp = pd.Series([True, False, False, False], + index=list('ABCD'), name='x') + assert_series_equal(s1 & s2, exp) + assert_series_equal(s2 & s1, exp) - result = dt1 - td1[0] - exp = (dt1.dt.tz_localize(None) - td1[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) - self.assertRaises(TypeError, lambda: td1[0] - dt1) + # True | np.nan => True + exp = pd.Series([True, True, True, False], + index=list('ABCD'), name='x') + assert_series_equal(s1 | s2, exp) + # np.nan | True => np.nan, filled with False + exp = pd.Series([True, True, False, False], + index=list('ABCD'), name='x') + assert_series_equal(s2 | s1, exp) - result = dt2 - td2[0] - exp = (dt2.dt.tz_localize(None) - td2[0]).dt.tz_localize(tz) - assert_series_equal(result, exp) - self.assertRaises(TypeError, lambda: td2[0] - dt2) + # DataFrame doesn't fill nan with False + exp = pd.DataFrame({'x': [True, False, np.nan, np.nan]}, + index=list('ABCD')) + assert_frame_equal(s1.to_frame() & s2.to_frame(), exp) + assert_frame_equal(s2.to_frame() & s1.to_frame(), exp) - result = dt1 + td1 - exp = (dt1.dt.tz_localize(None) + td1).dt.tz_localize(tz) - assert_series_equal(result, exp) + exp = pd.DataFrame({'x': [True, True, np.nan, np.nan]}, + index=list('ABCD')) + assert_frame_equal(s1.to_frame() | s2.to_frame(), exp) + assert_frame_equal(s2.to_frame() | s1.to_frame(), exp) - result = dt2 + td2 - exp = (dt2.dt.tz_localize(None) + td2).dt.tz_localize(tz) - assert_series_equal(result, exp) + # different length + s3 = pd.Series([True, False, True], index=list('ABC'), name='x') + s4 = pd.Series([True, True, True, True], index=list('ABCD'), name='x') - result = dt1 - td1 - exp = (dt1.dt.tz_localize(None) - td1).dt.tz_localize(tz) - assert_series_equal(result, exp) + exp = pd.Series([True, False, True, False], + index=list('ABCD'), name='x') + assert_series_equal(s3 & s4, exp) + assert_series_equal(s4 & s3, exp) - result = dt2 - td2 - exp = (dt2.dt.tz_localize(None) - td2).dt.tz_localize(tz) - assert_series_equal(result, exp) + # np.nan | True => np.nan, filled with False + exp = pd.Series([True, True, True, False], + index=list('ABCD'), name='x') + assert_series_equal(s3 | s4, exp) + # True | np.nan => True + exp = pd.Series([True, True, True, True], + index=list('ABCD'), name='x') + assert_series_equal(s4 | s3, exp) - self.assertRaises(TypeError, lambda: td1 - dt1) - self.assertRaises(TypeError, lambda: td2 - dt2) - - def test_sub_datetime_compat(self): - # GH 14088 - tm._skip_if_no_pytz() - import pytz - s = Series([datetime(2016, 8, 23, 12, tzinfo=pytz.utc), pd.NaT]) - dt = datetime(2016, 8, 22, 12, tzinfo=pytz.utc) - exp = Series([Timedelta('1 days'), pd.NaT]) - assert_series_equal(s - dt, exp) - assert_series_equal(s - Timestamp(dt), exp) - - def test_sub_single_tz(self): - # GH12290 - s1 = Series([pd.Timestamp('2016-02-10', tz='America/Sao_Paulo')]) - s2 = Series([pd.Timestamp('2016-02-08', tz='America/Sao_Paulo')]) - result = s1 - s2 - expected = Series([Timedelta('2days')]) - assert_series_equal(result, expected) - result = s2 - s1 - expected = Series([Timedelta('-2days')]) - assert_series_equal(result, expected) + exp = pd.DataFrame({'x': [True, False, True, np.nan]}, + index=list('ABCD')) + assert_frame_equal(s3.to_frame() & s4.to_frame(), exp) + assert_frame_equal(s4.to_frame() & s3.to_frame(), exp) - def test_ops_nat(self): - # GH 11349 - timedelta_series = Series([NaT, Timedelta('1s')]) - datetime_series = Series([NaT, Timestamp('19900315')]) - nat_series_dtype_timedelta = Series( - [NaT, NaT], dtype='timedelta64[ns]') - nat_series_dtype_timestamp = Series([NaT, NaT], dtype='datetime64[ns]') - single_nat_dtype_datetime = Series([NaT], dtype='datetime64[ns]') - single_nat_dtype_timedelta = Series([NaT], dtype='timedelta64[ns]') - - # subtraction - assert_series_equal(timedelta_series - NaT, nat_series_dtype_timedelta) - assert_series_equal(-NaT + timedelta_series, - nat_series_dtype_timedelta) - - assert_series_equal(timedelta_series - single_nat_dtype_timedelta, - nat_series_dtype_timedelta) - assert_series_equal(-single_nat_dtype_timedelta + timedelta_series, - nat_series_dtype_timedelta) - - assert_series_equal(datetime_series - NaT, nat_series_dtype_timestamp) - assert_series_equal(-NaT + datetime_series, nat_series_dtype_timestamp) - - assert_series_equal(datetime_series - single_nat_dtype_datetime, - nat_series_dtype_timedelta) - with tm.assertRaises(TypeError): - -single_nat_dtype_datetime + datetime_series - - assert_series_equal(datetime_series - single_nat_dtype_timedelta, - nat_series_dtype_timestamp) - assert_series_equal(-single_nat_dtype_timedelta + datetime_series, - nat_series_dtype_timestamp) - - # without a Series wrapping the NaT, it is ambiguous - # whether it is a datetime64 or timedelta64 - # defaults to interpreting it as timedelta64 - assert_series_equal(nat_series_dtype_timestamp - NaT, - nat_series_dtype_timestamp) - assert_series_equal(-NaT + nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - assert_series_equal(nat_series_dtype_timestamp - - single_nat_dtype_datetime, - nat_series_dtype_timedelta) - with tm.assertRaises(TypeError): - -single_nat_dtype_datetime + nat_series_dtype_timestamp - - assert_series_equal(nat_series_dtype_timestamp - - single_nat_dtype_timedelta, - nat_series_dtype_timestamp) - assert_series_equal(-single_nat_dtype_timedelta + - nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - with tm.assertRaises(TypeError): - timedelta_series - single_nat_dtype_datetime - - # addition - assert_series_equal(nat_series_dtype_timestamp + NaT, - nat_series_dtype_timestamp) - assert_series_equal(NaT + nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - assert_series_equal(nat_series_dtype_timestamp + - single_nat_dtype_timedelta, - nat_series_dtype_timestamp) - assert_series_equal(single_nat_dtype_timedelta + - nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - assert_series_equal(nat_series_dtype_timedelta + NaT, - nat_series_dtype_timedelta) - assert_series_equal(NaT + nat_series_dtype_timedelta, - nat_series_dtype_timedelta) - - assert_series_equal(nat_series_dtype_timedelta + - single_nat_dtype_timedelta, - nat_series_dtype_timedelta) - assert_series_equal(single_nat_dtype_timedelta + - nat_series_dtype_timedelta, - nat_series_dtype_timedelta) - - assert_series_equal(timedelta_series + NaT, nat_series_dtype_timedelta) - assert_series_equal(NaT + timedelta_series, nat_series_dtype_timedelta) - - assert_series_equal(timedelta_series + single_nat_dtype_timedelta, - nat_series_dtype_timedelta) - assert_series_equal(single_nat_dtype_timedelta + timedelta_series, - nat_series_dtype_timedelta) - - assert_series_equal(nat_series_dtype_timestamp + NaT, - nat_series_dtype_timestamp) - assert_series_equal(NaT + nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - assert_series_equal(nat_series_dtype_timestamp + - single_nat_dtype_timedelta, - nat_series_dtype_timestamp) - assert_series_equal(single_nat_dtype_timedelta + - nat_series_dtype_timestamp, - nat_series_dtype_timestamp) - - assert_series_equal(nat_series_dtype_timedelta + NaT, - nat_series_dtype_timedelta) - assert_series_equal(NaT + nat_series_dtype_timedelta, - nat_series_dtype_timedelta) - - assert_series_equal(nat_series_dtype_timedelta + - single_nat_dtype_timedelta, - nat_series_dtype_timedelta) - assert_series_equal(single_nat_dtype_timedelta + - nat_series_dtype_timedelta, - nat_series_dtype_timedelta) - - assert_series_equal(nat_series_dtype_timedelta + - single_nat_dtype_datetime, - nat_series_dtype_timestamp) - assert_series_equal(single_nat_dtype_datetime + - nat_series_dtype_timedelta, - nat_series_dtype_timestamp) - - # multiplication - assert_series_equal(nat_series_dtype_timedelta * 1.0, - nat_series_dtype_timedelta) - assert_series_equal(1.0 * nat_series_dtype_timedelta, - nat_series_dtype_timedelta) - - assert_series_equal(timedelta_series * 1, timedelta_series) - assert_series_equal(1 * timedelta_series, timedelta_series) - - assert_series_equal(timedelta_series * 1.5, - Series([NaT, Timedelta('1.5s')])) - assert_series_equal(1.5 * timedelta_series, - Series([NaT, Timedelta('1.5s')])) - - assert_series_equal(timedelta_series * nan, nat_series_dtype_timedelta) - assert_series_equal(nan * timedelta_series, nat_series_dtype_timedelta) - - with tm.assertRaises(TypeError): - datetime_series * 1 - with tm.assertRaises(TypeError): - nat_series_dtype_timestamp * 1 - with tm.assertRaises(TypeError): - datetime_series * 1.0 - with tm.assertRaises(TypeError): - nat_series_dtype_timestamp * 1.0 - - # division - assert_series_equal(timedelta_series / 2, - Series([NaT, Timedelta('0.5s')])) - assert_series_equal(timedelta_series / 2.0, - Series([NaT, Timedelta('0.5s')])) - assert_series_equal(timedelta_series / nan, nat_series_dtype_timedelta) - with tm.assertRaises(TypeError): - nat_series_dtype_timestamp / 1.0 - with tm.assertRaises(TypeError): - nat_series_dtype_timestamp / 1 + exp = pd.DataFrame({'x': [True, True, True, np.nan]}, + index=list('ABCD')) + assert_frame_equal(s3.to_frame() | s4.to_frame(), exp) + assert_frame_equal(s4.to_frame() | s3.to_frame(), exp) - def test_ops_datetimelike_align(self): - # GH 7500 - # datetimelike ops need to align - dt = Series(date_range('2012-1-1', periods=3, freq='D')) - dt.iloc[2] = np.nan - dt2 = dt[::-1] - expected = Series([timedelta(0), timedelta(0), pd.NaT]) - # name is reset - result = dt2 - dt - assert_series_equal(result, expected) +class TestSeriesComparisons(object): + def test_comparisons(self): + left = np.random.randn(10) + right = np.random.randn(10) + left[:3] = np.nan - expected = Series(expected, name=0) - result = (dt2.to_frame() - dt.to_frame())[0] - assert_series_equal(result, expected) + result = nanops.nangt(left, right) + with np.errstate(invalid='ignore'): + expected = (left > right).astype('O') + expected[:3] = np.nan - def test_object_comparisons(self): - s = Series(['a', 'b', np.nan, 'c', 'a']) + assert_almost_equal(result, expected) - result = s == 'a' - expected = Series([True, False, False, False, True]) - assert_series_equal(result, expected) + s = Series(['a', 'b', 'c']) + s2 = Series([False, True, False]) - result = s < 'a' - expected = Series([False, False, False, False, False]) - assert_series_equal(result, expected) + # it works! + exp = Series([False, False, False]) + assert_series_equal(s == s2, exp) + assert_series_equal(s2 == s, exp) - result = s != 'a' - expected = -(s == 'a') - assert_series_equal(result, expected) + def test_categorical_comparisons(self): + # GH 8938 + # allow equality comparisons + a = Series(list('abc'), dtype="category") + b = Series(list('abc'), dtype="object") + c = Series(['a', 'b', 'cc'], dtype="object") + d = Series(list('acb'), dtype="object") + e = Categorical(list('abc')) + f = Categorical(list('acb')) + + # vs scalar + assert not (a == 'a').all() + assert ((a != 'a') == ~(a == 'a')).all() + + assert not ('a' == a).all() + assert (a == 'a')[0] + assert ('a' == a)[0] + assert not ('a' != a)[0] + + # vs list-like + assert (a == a).all() + assert not (a != a).all() + + assert (a == list(a)).all() + assert (a == b).all() + assert (b == a).all() + assert ((~(a == b)) == (a != b)).all() + assert ((~(b == a)) == (b != a)).all() + + assert not (a == c).all() + assert not (c == a).all() + assert not (a == d).all() + assert not (d == a).all() + + # vs a cat-like + assert (a == e).all() + assert (e == a).all() + assert not (a == f).all() + assert not (f == a).all() + + assert ((~(a == e) == (a != e)).all()) + assert ((~(e == a) == (e != a)).all()) + assert ((~(a == f) == (a != f)).all()) + assert ((~(f == a) == (f != a)).all()) + + # non-equality is not comparable + with pytest.raises(TypeError): + a < b + with pytest.raises(TypeError): + b < a + with pytest.raises(TypeError): + a > b + with pytest.raises(TypeError): + b > a def test_comparison_tuples(self): # GH11339 @@ -970,18 +475,18 @@ def test_comparison_tuples(self): assert_series_equal(result, expected) def test_comparison_operators_with_nas(self): - s = Series(bdate_range('1/1/2000', periods=10), dtype=object) - s[::2] = np.nan + ser = Series(bdate_range('1/1/2000', periods=10), dtype=object) + ser[::2] = np.nan # test that comparisons work ops = ['lt', 'le', 'gt', 'ge', 'eq', 'ne'] for op in ops: - val = s[5] + val = ser[5] f = getattr(operator, op) - result = f(s, val) + result = f(ser, val) - expected = f(s.dropna(), val).reindex(s.index) + expected = f(ser.dropna(), val).reindex(ser.index) if op == 'ne': expected = expected.fillna(True).astype(bool) @@ -995,271 +500,78 @@ def test_comparison_operators_with_nas(self): # expected = f(val, s.dropna()).reindex(s.index) # assert_series_equal(result, expected) - # boolean &, |, ^ should work with object arrays and propagate NAs - - ops = ['and_', 'or_', 'xor'] - mask = s.isnull() - for bool_op in ops: - f = getattr(operator, bool_op) - - filled = s.fillna(s[0]) - - result = f(s < s[9], s > s[3]) - - expected = f(filled < filled[9], filled > filled[3]) - expected[mask] = False - assert_series_equal(result, expected) - - def test_comparison_object_numeric_nas(self): - s = Series(np.random.randn(10), dtype=object) - shifted = s.shift(2) - - ops = ['lt', 'le', 'gt', 'ge', 'eq', 'ne'] - for op in ops: - f = getattr(operator, op) - - result = f(s, shifted) - expected = f(s.astype(float), shifted.astype(float)) - assert_series_equal(result, expected) - - def test_comparison_invalid(self): - - # GH4968 - # invalid date/int comparisons - s = Series(range(5)) - s2 = Series(date_range('20010101', periods=5)) - - for (x, y) in [(s, s2), (s2, s)]: - self.assertRaises(TypeError, lambda: x == y) - self.assertRaises(TypeError, lambda: x != y) - self.assertRaises(TypeError, lambda: x >= y) - self.assertRaises(TypeError, lambda: x > y) - self.assertRaises(TypeError, lambda: x < y) - self.assertRaises(TypeError, lambda: x <= y) - - def test_more_na_comparisons(self): - for dtype in [None, object]: - left = Series(['a', np.nan, 'c'], dtype=dtype) - right = Series(['a', np.nan, 'd'], dtype=dtype) - - result = left == right - expected = Series([True, False, False]) - assert_series_equal(result, expected) - - result = left != right - expected = Series([False, True, True]) - assert_series_equal(result, expected) - - result = left == np.nan - expected = Series([False, False, False]) - assert_series_equal(result, expected) - - result = left != np.nan - expected = Series([True, True, True]) - assert_series_equal(result, expected) + def test_unequal_categorical_comparison_raises_type_error(self): + # unequal comparison should raise for unordered cats + cat = Series(Categorical(list("abc"))) + with pytest.raises(TypeError): + cat > "b" + + cat = Series(Categorical(list("abc"), ordered=False)) + with pytest.raises(TypeError): + cat > "b" + + # https://github.com/pandas-dev/pandas/issues/9836#issuecomment-92123057 + # and following comparisons with scalars not in categories should raise + # for unequal comps, but not for equal/not equal + cat = Series(Categorical(list("abc"), ordered=True)) + + with pytest.raises(TypeError): + cat < "d" + with pytest.raises(TypeError): + cat > "d" + with pytest.raises(TypeError): + "d" < cat + with pytest.raises(TypeError): + "d" > cat + + tm.assert_series_equal(cat == "d", Series([False, False, False])) + tm.assert_series_equal(cat != "d", Series([True, True, True])) - def test_nat_comparisons(self): - data = [([pd.Timestamp('2011-01-01'), pd.NaT, - pd.Timestamp('2011-01-03')], - [pd.NaT, pd.NaT, pd.Timestamp('2011-01-03')]), - - ([pd.Timedelta('1 days'), pd.NaT, - pd.Timedelta('3 days')], - [pd.NaT, pd.NaT, pd.Timedelta('3 days')]), - - ([pd.Period('2011-01', freq='M'), pd.NaT, - pd.Period('2011-03', freq='M')], - [pd.NaT, pd.NaT, pd.Period('2011-03', freq='M')])] - - # add lhs / rhs switched data - data = data + [(r, l) for l, r in data] - - for l, r in data: - for dtype in [None, object]: - left = Series(l, dtype=dtype) - - # Series, Index - for right in [Series(r, dtype=dtype), Index(r, dtype=dtype)]: - expected = Series([False, False, True]) - assert_series_equal(left == right, expected) - - expected = Series([True, True, False]) - assert_series_equal(left != right, expected) - - expected = Series([False, False, False]) - assert_series_equal(left < right, expected) - - expected = Series([False, False, False]) - assert_series_equal(left > right, expected) - - expected = Series([False, False, True]) - assert_series_equal(left >= right, expected) - - expected = Series([False, False, True]) - assert_series_equal(left <= right, expected) - - def test_nat_comparisons_scalar(self): - data = [[pd.Timestamp('2011-01-01'), pd.NaT, - pd.Timestamp('2011-01-03')], - - [pd.Timedelta('1 days'), pd.NaT, pd.Timedelta('3 days')], - - [pd.Period('2011-01', freq='M'), pd.NaT, - pd.Period('2011-03', freq='M')]] - - for l in data: - for dtype in [None, object]: - left = Series(l, dtype=dtype) - - expected = Series([False, False, False]) - assert_series_equal(left == pd.NaT, expected) - assert_series_equal(pd.NaT == left, expected) - - expected = Series([True, True, True]) - assert_series_equal(left != pd.NaT, expected) - assert_series_equal(pd.NaT != left, expected) - - expected = Series([False, False, False]) - assert_series_equal(left < pd.NaT, expected) - assert_series_equal(pd.NaT > left, expected) - assert_series_equal(left <= pd.NaT, expected) - assert_series_equal(pd.NaT >= left, expected) - - assert_series_equal(left > pd.NaT, expected) - assert_series_equal(pd.NaT < left, expected) - assert_series_equal(left >= pd.NaT, expected) - assert_series_equal(pd.NaT <= left, expected) - - def test_comparison_different_length(self): - a = Series(['a', 'b', 'c']) - b = Series(['b', 'a']) - self.assertRaises(ValueError, a.__lt__, b) - - a = Series([1, 2]) - b = Series([2, 3, 4]) - self.assertRaises(ValueError, a.__eq__, b) - - def test_comparison_label_based(self): - - # GH 4947 - # comparisons should be label based + def test_ne(self): + ts = Series([3, 4, 5, 6, 7], [3, 4, 5, 6, 7], dtype=float) + expected = [True, True, False, True, True] + assert tm.equalContents(ts.index != 5, expected) + assert tm.equalContents(~(ts.index == 5), expected) - a = Series([True, False, True], list('bca')) - b = Series([False, True, False], list('abc')) + def test_comp_ops_df_compat(self): + # GH 1134 + s1 = pd.Series([1, 2, 3], index=list('ABC'), name='x') + s2 = pd.Series([2, 2, 2], index=list('ABD'), name='x') - expected = Series([False, True, False], list('abc')) - result = a & b - assert_series_equal(result, expected) + s3 = pd.Series([1, 2, 3], index=list('ABC'), name='x') + s4 = pd.Series([2, 2, 2, 2], index=list('ABCD'), name='x') - expected = Series([True, True, False], list('abc')) - result = a | b - assert_series_equal(result, expected) + for left, right in [(s1, s2), (s2, s1), (s3, s4), (s4, s3)]: - expected = Series([True, False, False], list('abc')) - result = a ^ b - assert_series_equal(result, expected) + msg = "Can only compare identically-labeled Series objects" + with pytest.raises(ValueError, match=msg): + left == right - # rhs is bigger - a = Series([True, False, True], list('bca')) - b = Series([False, True, False, True], list('abcd')) + with pytest.raises(ValueError, match=msg): + left != right - expected = Series([False, True, False, False], list('abcd')) - result = a & b - assert_series_equal(result, expected) + with pytest.raises(ValueError, match=msg): + left < right - expected = Series([True, True, False, False], list('abcd')) - result = a | b - assert_series_equal(result, expected) + msg = "Can only compare identically-labeled DataFrame objects" + with pytest.raises(ValueError, match=msg): + left.to_frame() == right.to_frame() - # filling + with pytest.raises(ValueError, match=msg): + left.to_frame() != right.to_frame() - # vs empty - result = a & Series([]) - expected = Series([False, False, False], list('bca')) - assert_series_equal(result, expected) + with pytest.raises(ValueError, match=msg): + left.to_frame() < right.to_frame() - result = a | Series([]) - expected = Series([True, False, True], list('bca')) + def test_compare_series_interval_keyword(self): + # GH 25338 + s = Series(['IntervalA', 'IntervalB', 'IntervalC']) + result = s == 'IntervalA' + expected = Series([True, False, False]) assert_series_equal(result, expected) - # vs non-matching - result = a & Series([1], ['z']) - expected = Series([False, False, False, False], list('abcz')) - assert_series_equal(result, expected) - - result = a | Series([1], ['z']) - expected = Series([True, True, False, False], list('abcz')) - assert_series_equal(result, expected) - - # identity - # we would like s[s|e] == s to hold for any e, whether empty or not - for e in [Series([]), Series([1], ['z']), - Series(np.nan, b.index), Series(np.nan, a.index)]: - result = a[a | e] - assert_series_equal(result, a[a]) - - for e in [Series(['z'])]: - if compat.PY3: - with tm.assert_produces_warning(RuntimeWarning): - result = a[a | e] - else: - result = a[a | e] - assert_series_equal(result, a[a]) - - # vs scalars - index = list('bca') - t = Series([True, False, True]) - - for v in [True, 1, 2]: - result = Series([True, False, True], index=index) | v - expected = Series([True, True, True], index=index) - assert_series_equal(result, expected) - - for v in [np.nan, 'foo']: - self.assertRaises(TypeError, lambda: t | v) - - for v in [False, 0]: - result = Series([True, False, True], index=index) | v - expected = Series([True, False, True], index=index) - assert_series_equal(result, expected) - - for v in [True, 1]: - result = Series([True, False, True], index=index) & v - expected = Series([True, False, True], index=index) - assert_series_equal(result, expected) - - for v in [False, 0]: - result = Series([True, False, True], index=index) & v - expected = Series([False, False, False], index=index) - assert_series_equal(result, expected) - for v in [np.nan]: - self.assertRaises(TypeError, lambda: t & v) - - def test_comparison_flex_basic(self): - left = pd.Series(np.random.randn(10)) - right = pd.Series(np.random.randn(10)) - - assert_series_equal(left.eq(right), left == right) - assert_series_equal(left.ne(right), left != right) - assert_series_equal(left.le(right), left < right) - assert_series_equal(left.lt(right), left <= right) - assert_series_equal(left.gt(right), left > right) - assert_series_equal(left.ge(right), left >= right) - - # axis - for axis in [0, None, 'index']: - assert_series_equal(left.eq(right, axis=axis), left == right) - assert_series_equal(left.ne(right, axis=axis), left != right) - assert_series_equal(left.le(right, axis=axis), left < right) - assert_series_equal(left.lt(right, axis=axis), left <= right) - assert_series_equal(left.gt(right, axis=axis), left > right) - assert_series_equal(left.ge(right, axis=axis), left >= right) - - # - msg = 'No axis named 1 for object type' - for op in ['eq', 'ne', 'le', 'le', 'gt', 'ge']: - with tm.assertRaisesRegexp(ValueError, msg): - getattr(left, op)(right, axis=1) + +class TestSeriesFlexComparisonOps(object): def test_comparison_flex_alignment(self): left = Series([1, 3, 2], index=list('abc')) @@ -1305,158 +617,29 @@ def test_comparison_flex_alignment_fill(self): exp = pd.Series([True, True, False, False], index=list('abcd')) assert_series_equal(left.gt(right, fill_value=0), exp) - def test_return_dtypes_bool_op_costant(self): - # gh15115 - s = pd.Series([1, 3, 2], index=range(3)) - const = 2 - for op in ['eq', 'ne', 'gt', 'lt', 'ge', 'le']: - result = getattr(s, op)(const).get_dtype_counts() - self.assert_series_equal(result, Series([1], ['bool'])) - - # empty Series - empty = s.iloc[:0] - for op in ['eq', 'ne', 'gt', 'lt', 'ge', 'le']: - result = getattr(empty, op)(const).get_dtype_counts() - self.assert_series_equal(result, Series([1], ['bool'])) - - def test_operators_bitwise(self): - # GH 9016: support bitwise op for integer types - index = list('bca') - - s_tft = Series([True, False, True], index=index) - s_fff = Series([False, False, False], index=index) - s_tff = Series([True, False, False], index=index) - s_empty = Series([]) - - # TODO: unused - # s_0101 = Series([0, 1, 0, 1]) - - s_0123 = Series(range(4), dtype='int64') - s_3333 = Series([3] * 4) - s_4444 = Series([4] * 4) - - res = s_tft & s_empty - expected = s_fff - assert_series_equal(res, expected) - - res = s_tft | s_empty - expected = s_tft - assert_series_equal(res, expected) - - res = s_0123 & s_3333 - expected = Series(range(4), dtype='int64') - assert_series_equal(res, expected) - - res = s_0123 | s_4444 - expected = Series(range(4, 8), dtype='int64') - assert_series_equal(res, expected) - - s_a0b1c0 = Series([1], list('b')) - - res = s_tft & s_a0b1c0 - expected = s_tff.reindex(list('abc')) - assert_series_equal(res, expected) - - res = s_tft | s_a0b1c0 - expected = s_tft.reindex(list('abc')) - assert_series_equal(res, expected) - - n0 = 0 - res = s_tft & n0 - expected = s_fff - assert_series_equal(res, expected) - - res = s_0123 & n0 - expected = Series([0] * 4) - assert_series_equal(res, expected) - - n1 = 1 - res = s_tft & n1 - expected = s_tft - assert_series_equal(res, expected) - - res = s_0123 & n1 - expected = Series([0, 1, 0, 1]) - assert_series_equal(res, expected) - - s_1111 = Series([1] * 4, dtype='int8') - res = s_0123 & s_1111 - expected = Series([0, 1, 0, 1], dtype='int64') - assert_series_equal(res, expected) - - res = s_0123.astype(np.int16) | s_1111.astype(np.int32) - expected = Series([1, 1, 3, 3], dtype='int32') - assert_series_equal(res, expected) - - self.assertRaises(TypeError, lambda: s_1111 & 'a') - self.assertRaises(TypeError, lambda: s_1111 & ['a', 'b', 'c', 'd']) - self.assertRaises(TypeError, lambda: s_0123 & np.NaN) - self.assertRaises(TypeError, lambda: s_0123 & 3.14) - self.assertRaises(TypeError, lambda: s_0123 & [0.1, 4, 3.14, 2]) - - # s_0123 will be all false now because of reindexing like s_tft - if compat.PY3: - # unable to sort incompatible object via .union. - exp = Series([False] * 7, index=['b', 'c', 'a', 0, 1, 2, 3]) - with tm.assert_produces_warning(RuntimeWarning): - assert_series_equal(s_tft & s_0123, exp) - else: - exp = Series([False] * 7, index=[0, 1, 2, 3, 'a', 'b', 'c']) - assert_series_equal(s_tft & s_0123, exp) - - # s_tft will be all false now because of reindexing like s_0123 - if compat.PY3: - # unable to sort incompatible object via .union. - exp = Series([False] * 7, index=[0, 1, 2, 3, 'b', 'c', 'a']) - with tm.assert_produces_warning(RuntimeWarning): - assert_series_equal(s_0123 & s_tft, exp) - else: - exp = Series([False] * 7, index=[0, 1, 2, 3, 'a', 'b', 'c']) - assert_series_equal(s_0123 & s_tft, exp) - - assert_series_equal(s_0123 & False, Series([False] * 4)) - assert_series_equal(s_0123 ^ False, Series([False, True, True, True])) - assert_series_equal(s_0123 & [False], Series([False] * 4)) - assert_series_equal(s_0123 & (False), Series([False] * 4)) - assert_series_equal(s_0123 & Series([False, np.NaN, False, False]), - Series([False] * 4)) - - s_ftft = Series([False, True, False, True]) - assert_series_equal(s_0123 & Series([0.1, 4, -3.14, 2]), s_ftft) - - s_abNd = Series(['a', 'b', np.NaN, 'd']) - res = s_0123 & s_abNd - expected = s_ftft - assert_series_equal(res, expected) - - def test_scalar_na_cmp_corners(self): - s = Series([2, 3, 4, 5, 6, 7, 8, 9, 10]) - - def tester(a, b): - return a & b - - self.assertRaises(TypeError, tester, s, datetime(2005, 1, 1)) - - s = Series([2, 3, 4, 5, 6, 7, 8, 9, datetime(2005, 1, 1)]) - s[::2] = np.nan - expected = Series(True, index=s.index) - expected[::2] = False - assert_series_equal(tester(s, list(s)), expected) +class TestSeriesOperators(TestData): - d = DataFrame({'A': s}) - # TODO: Fix this exception - needs to be fixed! (see GH5035) - # (previously this was a TypeError because series returned - # NotImplemented + def test_operators_empty_int_corner(self): + s1 = Series([], [], dtype=np.int32) + s2 = Series({'x': 0.}) + assert_series_equal(s1 * s2, Series([np.nan], index=['x'])) - # this is an alignment issue; these are equivalent - # https://github.com/pandas-dev/pandas/issues/5284 + def test_ops_datetimelike_align(self): + # GH 7500 + # datetimelike ops need to align + dt = Series(date_range('2012-1-1', periods=3, freq='D')) + dt.iloc[2] = np.nan + dt2 = dt[::-1] - self.assertRaises(ValueError, lambda: d.__and__(s, axis='columns')) - self.assertRaises(ValueError, tester, s, d) + expected = Series([timedelta(0), timedelta(0), pd.NaT]) + # name is reset + result = dt2 - dt + assert_series_equal(result, expected) - # this is wrong as its not a boolean result - # result = d.__and__(s,axis='index') + expected = Series(expected, name=0) + result = (dt2.to_frame() - dt.to_frame())[0] + assert_series_equal(result, expected) def test_operators_corner(self): series = self.ts @@ -1464,10 +647,10 @@ def test_operators_corner(self): empty = Series([], index=Index([])) result = series + empty - self.assertTrue(np.isnan(result).all()) + assert np.isnan(result).all() result = empty + Series([], index=Index([])) - self.assertEqual(len(result), 0) + assert len(result) == 0 # TODO: this returned NotImplemented earlier, what to do? # deltas = Series([timedelta(1)] * 5, index=np.arange(5)) @@ -1480,259 +663,46 @@ def test_operators_corner(self): added = self.ts + int_ts expected = Series(self.ts.values[:-5] + int_ts.values, index=self.ts.index[:-5], name='ts') - self.assert_series_equal(added[:-5], expected) - - def test_operators_reverse_object(self): - # GH 56 - arr = Series(np.random.randn(10), index=np.arange(10), dtype=object) - - def _check_op(arr, op): - result = op(1., arr) - expected = op(1., arr.astype(float)) - assert_series_equal(result.astype(float), expected) - - _check_op(arr, operator.add) - _check_op(arr, operator.sub) - _check_op(arr, operator.mul) - _check_op(arr, operator.truediv) - _check_op(arr, operator.floordiv) - - def test_arith_ops_df_compat(self): - # GH 1134 - s1 = pd.Series([1, 2, 3], index=list('ABC'), name='x') - s2 = pd.Series([2, 2, 2], index=list('ABD'), name='x') - - exp = pd.Series([3.0, 4.0, np.nan, np.nan], - index=list('ABCD'), name='x') - assert_series_equal(s1 + s2, exp) - assert_series_equal(s2 + s1, exp) - - exp = pd.DataFrame({'x': [3.0, 4.0, np.nan, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s1.to_frame() + s2.to_frame(), exp) - assert_frame_equal(s2.to_frame() + s1.to_frame(), exp) - - # different length - s3 = pd.Series([1, 2, 3], index=list('ABC'), name='x') - s4 = pd.Series([2, 2, 2, 2], index=list('ABCD'), name='x') - - exp = pd.Series([3, 4, 5, np.nan], - index=list('ABCD'), name='x') - assert_series_equal(s3 + s4, exp) - assert_series_equal(s4 + s3, exp) - - exp = pd.DataFrame({'x': [3, 4, 5, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s3.to_frame() + s4.to_frame(), exp) - assert_frame_equal(s4.to_frame() + s3.to_frame(), exp) - - def test_comp_ops_df_compat(self): - # GH 1134 - s1 = pd.Series([1, 2, 3], index=list('ABC'), name='x') - s2 = pd.Series([2, 2, 2], index=list('ABD'), name='x') - - s3 = pd.Series([1, 2, 3], index=list('ABC'), name='x') - s4 = pd.Series([2, 2, 2, 2], index=list('ABCD'), name='x') - - for l, r in [(s1, s2), (s2, s1), (s3, s4), (s4, s3)]: - - msg = "Can only compare identically-labeled Series objects" - with tm.assertRaisesRegexp(ValueError, msg): - l == r - - with tm.assertRaisesRegexp(ValueError, msg): - l != r - - with tm.assertRaisesRegexp(ValueError, msg): - l < r - - msg = "Can only compare identically-labeled DataFrame objects" - with tm.assertRaisesRegexp(ValueError, msg): - l.to_frame() == r.to_frame() - - with tm.assertRaisesRegexp(ValueError, msg): - l.to_frame() != r.to_frame() - - with tm.assertRaisesRegexp(ValueError, msg): - l.to_frame() < r.to_frame() - - def test_bool_ops_df_compat(self): - # GH 1134 - s1 = pd.Series([True, False, True], index=list('ABC'), name='x') - s2 = pd.Series([True, True, False], index=list('ABD'), name='x') - - exp = pd.Series([True, False, False, False], - index=list('ABCD'), name='x') - assert_series_equal(s1 & s2, exp) - assert_series_equal(s2 & s1, exp) - - # True | np.nan => True - exp = pd.Series([True, True, True, False], - index=list('ABCD'), name='x') - assert_series_equal(s1 | s2, exp) - # np.nan | True => np.nan, filled with False - exp = pd.Series([True, True, False, False], - index=list('ABCD'), name='x') - assert_series_equal(s2 | s1, exp) - - # DataFrame doesn't fill nan with False - exp = pd.DataFrame({'x': [True, False, np.nan, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s1.to_frame() & s2.to_frame(), exp) - assert_frame_equal(s2.to_frame() & s1.to_frame(), exp) - - exp = pd.DataFrame({'x': [True, True, np.nan, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s1.to_frame() | s2.to_frame(), exp) - assert_frame_equal(s2.to_frame() | s1.to_frame(), exp) - - # different length - s3 = pd.Series([True, False, True], index=list('ABC'), name='x') - s4 = pd.Series([True, True, True, True], index=list('ABCD'), name='x') - - exp = pd.Series([True, False, True, False], - index=list('ABCD'), name='x') - assert_series_equal(s3 & s4, exp) - assert_series_equal(s4 & s3, exp) - - # np.nan | True => np.nan, filled with False - exp = pd.Series([True, True, True, False], - index=list('ABCD'), name='x') - assert_series_equal(s3 | s4, exp) - # True | np.nan => True - exp = pd.Series([True, True, True, True], - index=list('ABCD'), name='x') - assert_series_equal(s4 | s3, exp) - - exp = pd.DataFrame({'x': [True, False, True, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s3.to_frame() & s4.to_frame(), exp) - assert_frame_equal(s4.to_frame() & s3.to_frame(), exp) - - exp = pd.DataFrame({'x': [True, True, True, np.nan]}, - index=list('ABCD')) - assert_frame_equal(s3.to_frame() | s4.to_frame(), exp) - assert_frame_equal(s4.to_frame() | s3.to_frame(), exp) - - def test_series_frame_radd_bug(self): - # GH 353 - vals = Series(tm.rands_array(5, 10)) - result = 'foo_' + vals - expected = vals.map(lambda x: 'foo_' + x) - assert_series_equal(result, expected) - - frame = DataFrame({'vals': vals}) - result = 'foo_' + frame - expected = DataFrame({'vals': vals.map(lambda x: 'foo_' + x)}) - assert_frame_equal(result, expected) - - # really raise this time - with tm.assertRaises(TypeError): - datetime.now() + self.ts - - with tm.assertRaises(TypeError): - self.ts + datetime.now() - - def test_series_radd_more(self): - data = [[1, 2, 3], - [1.1, 2.2, 3.3], - [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.NaT], - ['x', 'y', 1]] - - for d in data: - for dtype in [None, object]: - s = Series(d, dtype=dtype) - with tm.assertRaises(TypeError): - 'foo_' + s - - for dtype in [None, object]: - res = 1 + pd.Series([1, 2, 3], dtype=dtype) - exp = pd.Series([2, 3, 4], dtype=dtype) - assert_series_equal(res, exp) - res = pd.Series([1, 2, 3], dtype=dtype) + 1 - assert_series_equal(res, exp) - - res = np.nan + pd.Series([1, 2, 3], dtype=dtype) - exp = pd.Series([np.nan, np.nan, np.nan], dtype=dtype) - assert_series_equal(res, exp) - res = pd.Series([1, 2, 3], dtype=dtype) + np.nan - assert_series_equal(res, exp) - - s = pd.Series([pd.Timedelta('1 days'), pd.Timedelta('2 days'), - pd.Timedelta('3 days')], dtype=dtype) - exp = pd.Series([pd.Timedelta('4 days'), pd.Timedelta('5 days'), - pd.Timedelta('6 days')]) - assert_series_equal(pd.Timedelta('3 days') + s, exp) - assert_series_equal(s + pd.Timedelta('3 days'), exp) - - s = pd.Series(['x', np.nan, 'x']) - assert_series_equal('a' + s, pd.Series(['ax', np.nan, 'ax'])) - assert_series_equal(s + 'a', pd.Series(['xa', np.nan, 'xa'])) - - def test_frame_radd_more(self): - data = [[1, 2, 3], - [1.1, 2.2, 3.3], - [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.NaT], - ['x', 'y', 1]] - - for d in data: - for dtype in [None, object]: - s = DataFrame(d, dtype=dtype) - with tm.assertRaises(TypeError): - 'foo_' + s - - for dtype in [None, object]: - res = 1 + pd.DataFrame([1, 2, 3], dtype=dtype) - exp = pd.DataFrame([2, 3, 4], dtype=dtype) - assert_frame_equal(res, exp) - res = pd.DataFrame([1, 2, 3], dtype=dtype) + 1 - assert_frame_equal(res, exp) - - res = np.nan + pd.DataFrame([1, 2, 3], dtype=dtype) - exp = pd.DataFrame([np.nan, np.nan, np.nan], dtype=dtype) - assert_frame_equal(res, exp) - res = pd.DataFrame([1, 2, 3], dtype=dtype) + np.nan - assert_frame_equal(res, exp) - - df = pd.DataFrame(['x', np.nan, 'x']) - assert_frame_equal('a' + df, pd.DataFrame(['ax', np.nan, 'ax'])) - assert_frame_equal(df + 'a', pd.DataFrame(['xa', np.nan, 'xa'])) - - def test_operators_frame(self): - # rpow does not work with DataFrame - df = DataFrame({'A': self.ts}) - - assert_series_equal(self.ts + self.ts, self.ts + df['A'], - check_names=False) - assert_series_equal(self.ts ** self.ts, self.ts ** df['A'], - check_names=False) - assert_series_equal(self.ts < self.ts, self.ts < df['A'], - check_names=False) - assert_series_equal(self.ts / self.ts, self.ts / df['A'], - check_names=False) - - def test_operators_combine(self): + tm.assert_series_equal(added[:-5], expected) + + pairings = [] + for op in ['add', 'sub', 'mul', 'pow', 'truediv', 'floordiv']: + fv = 0 + lop = getattr(Series, op) + lequiv = getattr(operator, op) + rop = getattr(Series, 'r' + op) + # bind op at definition time... + requiv = lambda x, y, op=op: getattr(operator, op)(y, x) + pairings.append((lop, lequiv, fv)) + pairings.append((rop, requiv, fv)) + if compat.PY3: + pairings.append((Series.div, operator.truediv, 1)) + pairings.append((Series.rdiv, lambda x, y: operator.truediv(y, x), 1)) + else: + pairings.append((Series.div, operator.div, 1)) + pairings.append((Series.rdiv, lambda x, y: operator.div(y, x), 1)) + + @pytest.mark.parametrize('op, equiv_op, fv', pairings) + def test_operators_combine(self, op, equiv_op, fv): def _check_fill(meth, op, a, b, fill_value=0): exp_index = a.index.union(b.index) a = a.reindex(exp_index) b = b.reindex(exp_index) - amask = isnull(a) - bmask = isnull(b) + amask = isna(a) + bmask = isna(b) exp_values = [] for i in range(len(exp_index)): with np.errstate(all='ignore'): if amask[i]: if bmask[i]: - exp_values.append(nan) + exp_values.append(np.nan) continue exp_values.append(op(fill_value, b[i])) elif bmask[i]: if amask[i]: - exp_values.append(nan) + exp_values.append(np.nan) continue exp_values.append(op(a[i], fill_value)) else: @@ -1742,41 +712,15 @@ def _check_fill(meth, op, a, b, fill_value=0): expected = Series(exp_values, exp_index) assert_series_equal(result, expected) - a = Series([nan, 1., 2., 3., nan], index=np.arange(5)) - b = Series([nan, 1, nan, 3, nan, 4.], index=np.arange(6)) - - pairings = [] - for op in ['add', 'sub', 'mul', 'pow', 'truediv', 'floordiv']: - fv = 0 - lop = getattr(Series, op) - lequiv = getattr(operator, op) - rop = getattr(Series, 'r' + op) - # bind op at definition time... - requiv = lambda x, y, op=op: getattr(operator, op)(y, x) - pairings.append((lop, lequiv, fv)) - pairings.append((rop, requiv, fv)) - - if compat.PY3: - pairings.append((Series.div, operator.truediv, 1)) - pairings.append((Series.rdiv, lambda x, y: operator.truediv(y, x), - 1)) - else: - pairings.append((Series.div, operator.div, 1)) - pairings.append((Series.rdiv, lambda x, y: operator.div(y, x), 1)) - - for op, equiv_op, fv in pairings: - result = op(a, b) - exp = equiv_op(a, b) - assert_series_equal(result, exp) - _check_fill(op, equiv_op, a, b, fill_value=fv) - # should accept axis=0 or axis='rows' - op(a, b, axis=0) + a = Series([np.nan, 1., 2., 3., np.nan], index=np.arange(5)) + b = Series([np.nan, 1, np.nan, 3, np.nan, 4.], index=np.arange(6)) - def test_ne(self): - ts = Series([3, 4, 5, 6, 7], [3, 4, 5, 6, 7], dtype=float) - expected = [True, True, False, True, True] - self.assertTrue(tm.equalContents(ts.index != 5, expected)) - self.assertTrue(tm.equalContents(~(ts.index == 5), expected)) + result = op(a, b) + exp = equiv_op(a, b) + assert_series_equal(result, exp) + _check_fill(op, equiv_op, a, b, fill_value=fv) + # should accept axis=0 or axis='rows' + op(a, b, axis=0) def test_operators_na_handling(self): from decimal import Decimal @@ -1786,68 +730,8 @@ def test_operators_na_handling(self): result = s + s.shift(1) result2 = s.shift(1) + s - self.assertTrue(isnull(result[0])) - self.assertTrue(isnull(result2[0])) - - s = Series(['foo', 'bar', 'baz', np.nan]) - result = 'prefix_' + s - expected = Series(['prefix_foo', 'prefix_bar', 'prefix_baz', np.nan]) - assert_series_equal(result, expected) - - result = s + '_suffix' - expected = Series(['foo_suffix', 'bar_suffix', 'baz_suffix', np.nan]) - assert_series_equal(result, expected) - - def test_divide_decimal(self): - """ resolves issue #9787 """ - from decimal import Decimal - - expected = Series([Decimal(5)]) - - s = Series([Decimal(10)]) - s = s / Decimal(2) - - assert_series_equal(expected, s) - - s = Series([Decimal(10)]) - s = s // Decimal(2) - - assert_series_equal(expected, s) - - def test_datetime64_with_index(self): - - # arithmetic integer ops with an index - s = Series(np.random.randn(5)) - expected = s - s.index.to_series() - result = s - s.index - assert_series_equal(result, expected) - - # GH 4629 - # arithmetic datetime64 ops with an index - s = Series(date_range('20130101', periods=5), - index=date_range('20130101', periods=5)) - expected = s - s.index.to_series() - result = s - s.index - assert_series_equal(result, expected) - - result = s - s.index.to_period() - assert_series_equal(result, expected) - - df = DataFrame(np.random.randn(5, 2), - index=date_range('20130101', periods=5)) - df['date'] = Timestamp('20130102') - df['expected'] = df['date'] - df.index.to_series() - df['result'] = df['date'] - df.index - assert_series_equal(df['result'], df['expected'], check_names=False) - - def test_dti_tz_convert_to_utc(self): - base = pd.DatetimeIndex(['2011-01-01', '2011-01-02', '2011-01-03'], - tz='UTC') - idx1 = base.tz_convert('Asia/Tokyo')[:2] - idx2 = base.tz_convert('US/Eastern')[1:] - - res = Series([1, 2], index=idx1) + Series([1, 1], index=idx2) - assert_series_equal(res, Series([np.nan, 3, np.nan], index=base)) + assert isna(result[0]) + assert isna(result2[0]) def test_op_duplicate_index(self): # GH14227 @@ -1856,3 +740,17 @@ def test_op_duplicate_index(self): result = s1 + s2 expected = pd.Series([11, 12, np.nan], index=[1, 1, 2]) assert_series_equal(result, expected) + + +class TestSeriesUnaryOps(object): + # __neg__, __pos__, __inv__ + + def test_neg(self): + ser = tm.makeStringSeries() + ser.name = 'series' + assert_series_equal(-ser, -1 * ser) + + def test_invert(self): + ser = tm.makeStringSeries() + ser.name = 'series' + assert_series_equal(-(ser < 0), ~(ser < 0)) diff --git a/pandas/tests/series/test_period.py b/pandas/tests/series/test_period.py index f1ae7765648ca..7e0feb418e8df 100644 --- a/pandas/tests/series/test_period.py +++ b/pandas/tests/series/test_period.py @@ -1,44 +1,41 @@ import numpy as np +import pytest import pandas as pd +from pandas import DataFrame, Period, Series, period_range +from pandas.core.arrays import PeriodArray import pandas.util.testing as tm -import pandas.tseries.period as period -from pandas import Series, period_range, DataFrame, Period -def _permute(obj): - return obj.take(np.random.permutation(len(obj))) +class TestSeriesPeriod(object): - -class TestSeriesPeriod(tm.TestCase): - - def setUp(self): + def setup_method(self, method): self.series = Series(period_range('2000-01-01', periods=10, freq='D')) def test_auto_conversion(self): series = Series(list(period_range('2000-01-01', periods=10, freq='D'))) - self.assertEqual(series.dtype, 'object') + assert series.dtype == 'Period[D]' series = pd.Series([pd.Period('2011-01-01', freq='D'), pd.Period('2011-02-01', freq='D')]) - self.assertEqual(series.dtype, 'object') + assert series.dtype == 'Period[D]' def test_getitem(self): - self.assertEqual(self.series[1], pd.Period('2000-01-02', freq='D')) + assert self.series[1] == pd.Period('2000-01-02', freq='D') result = self.series[[2, 4]] exp = pd.Series([pd.Period('2000-01-03', freq='D'), pd.Period('2000-01-05', freq='D')], - index=[2, 4]) - self.assert_series_equal(result, exp) - self.assertEqual(result.dtype, 'object') + index=[2, 4], dtype='Period[D]') + tm.assert_series_equal(result, exp) + assert result.dtype == 'Period[D]' - def test_isnull(self): + def test_isna(self): # GH 13737 s = Series([pd.Period('2011-01', freq='M'), pd.Period('NaT', freq='M')]) - tm.assert_series_equal(s.isnull(), Series([False, True])) - tm.assert_series_equal(s.notnull(), Series([True, False])) + tm.assert_series_equal(s.isna(), Series([False, True])) + tm.assert_series_equal(s.notna(), Series([True, False])) def test_fillna(self): # GH 13737 @@ -49,12 +46,7 @@ def test_fillna(self): exp = Series([pd.Period('2011-01', freq='M'), pd.Period('2012-01', freq='M')]) tm.assert_series_equal(res, exp) - self.assertEqual(res.dtype, 'object') - - res = s.fillna('XXX') - exp = Series([pd.Period('2011-01', freq='M'), 'XXX']) - tm.assert_series_equal(res, exp) - self.assertEqual(res.dtype, 'object') + assert res.dtype == 'Period[M]' def test_dropna(self): # GH 13737 @@ -63,17 +55,6 @@ def test_dropna(self): tm.assert_series_equal(s.dropna(), Series([pd.Period('2011-01', freq='M')])) - def test_series_comparison_scalars(self): - val = pd.Period('2000-01-04', freq='D') - result = self.series > val - expected = pd.Series([x > val for x in self.series]) - tm.assert_series_equal(result, expected) - - val = self.series[5] - result = self.series > val - expected = pd.Series([x > val for x in self.series]) - tm.assert_series_equal(result, expected) - def test_between(self): left, right = self.series[[2, 7]] result = self.series.between(left, right) @@ -83,36 +64,36 @@ def test_between(self): # --------------------------------------------------------------------- # NaT support - """ - # ToDo: Enable when support period dtype + @pytest.mark.xfail(reason="PeriodDtype Series not supported yet") def test_NaT_scalar(self): - series = Series([0, 1000, 2000, iNaT], dtype='period[D]') + series = Series([0, 1000, 2000, pd._libs.iNaT], dtype='period[D]') val = series[3] - self.assertTrue(isnull(val)) + assert pd.isna(val) series[2] = val - self.assertTrue(isnull(series[2])) + assert pd.isna(series[2]) + @pytest.mark.xfail(reason="PeriodDtype Series not supported yet") def test_NaT_cast(self): result = Series([np.nan]).astype('period[D]') - expected = Series([NaT]) + expected = Series([pd.NaT]) tm.assert_series_equal(result, expected) - """ - def test_set_none_nan(self): - # currently Period is stored as object dtype, not as NaT + def test_set_none(self): self.series[3] = None - self.assertIs(self.series[3], None) + assert self.series[3] is pd.NaT self.series[3:5] = None - self.assertIs(self.series[4], None) + assert self.series[4] is pd.NaT + def test_set_nan(self): + # Do we want to allow this? self.series[5] = np.nan - self.assertTrue(np.isnan(self.series[5])) + assert self.series[5] is pd.NaT self.series[5:7] = np.nan - self.assertTrue(np.isnan(self.series[6])) + assert self.series[6] is pd.NaT def test_intercept_astype_object(self): expected = self.series.astype('object') @@ -121,128 +102,74 @@ def test_intercept_astype_object(self): 'b': np.random.randn(len(self.series))}) result = df.values.squeeze() - self.assertTrue((result[:, 0] == expected.values).all()) + assert (result[:, 0] == expected.values).all() df = DataFrame({'a': self.series, 'b': ['foo'] * len(self.series)}) result = df.values.squeeze() - self.assertTrue((result[:, 0] == expected.values).all()) - - def test_comp_series_period_scalar(self): - # GH 13200 - for freq in ['M', '2M', '3M']: - base = Series([Period(x, freq=freq) for x in - ['2011-01', '2011-02', '2011-03', '2011-04']]) - p = Period('2011-02', freq=freq) - - exp = pd.Series([False, True, False, False]) - tm.assert_series_equal(base == p, exp) - tm.assert_series_equal(p == base, exp) - - exp = pd.Series([True, False, True, True]) - tm.assert_series_equal(base != p, exp) - tm.assert_series_equal(p != base, exp) - - exp = pd.Series([False, False, True, True]) - tm.assert_series_equal(base > p, exp) - tm.assert_series_equal(p < base, exp) - - exp = pd.Series([True, False, False, False]) - tm.assert_series_equal(base < p, exp) - tm.assert_series_equal(p > base, exp) - - exp = pd.Series([False, True, True, True]) - tm.assert_series_equal(base >= p, exp) - tm.assert_series_equal(p <= base, exp) - - exp = pd.Series([True, True, False, False]) - tm.assert_series_equal(base <= p, exp) - tm.assert_series_equal(p >= base, exp) - - # different base freq - msg = "Input has different freq=A-DEC from Period" - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - base <= Period('2011', freq='A') - - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - Period('2011', freq='A') >= base - - def test_comp_series_period_series(self): - # GH 13200 - for freq in ['M', '2M', '3M']: - base = Series([Period(x, freq=freq) for x in - ['2011-01', '2011-02', '2011-03', '2011-04']]) + assert (result[:, 0] == expected.values).all() - s = Series([Period(x, freq=freq) for x in - ['2011-02', '2011-01', '2011-03', '2011-05']]) - - exp = Series([False, False, True, False]) - tm.assert_series_equal(base == s, exp) - - exp = Series([True, True, False, True]) - tm.assert_series_equal(base != s, exp) - - exp = Series([False, True, False, False]) - tm.assert_series_equal(base > s, exp) - - exp = Series([True, False, False, True]) - tm.assert_series_equal(base < s, exp) - - exp = Series([False, True, True, False]) - tm.assert_series_equal(base >= s, exp) - - exp = Series([True, False, True, True]) - tm.assert_series_equal(base <= s, exp) - - s2 = Series([Period(x, freq='A') for x in - ['2011', '2011', '2011', '2011']]) - - # different base freq - msg = "Input has different freq=A-DEC from Period" - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - base <= s2 - - def test_comp_series_period_object(self): - # GH 13200 - base = Series([Period('2011', freq='A'), Period('2011-02', freq='M'), - Period('2013', freq='A'), Period('2011-04', freq='M')]) - - s = Series([Period('2012', freq='A'), Period('2011-01', freq='M'), - Period('2013', freq='A'), Period('2011-05', freq='M')]) - - exp = Series([False, False, True, False]) - tm.assert_series_equal(base == s, exp) - - exp = Series([True, True, False, True]) - tm.assert_series_equal(base != s, exp) - - exp = Series([False, True, False, False]) - tm.assert_series_equal(base > s, exp) - - exp = Series([True, False, False, True]) - tm.assert_series_equal(base < s, exp) - - exp = Series([False, True, True, False]) - tm.assert_series_equal(base >= s, exp) - - exp = Series([True, False, True, True]) - tm.assert_series_equal(base <= s, exp) - - def test_align_series(self): + def test_align_series(self, join_type): rng = period_range('1/1/2000', '1/1/2010', freq='A') ts = Series(np.random.randn(len(rng)), index=rng) - result = ts + ts[::2] - expected = ts + ts - expected[1::2] = np.nan + ts.align(ts[::2], join=join_type) + + def test_truncate(self): + # GH 17717 + idx1 = pd.PeriodIndex([ + pd.Period('2017-09-02'), + pd.Period('2017-09-02'), + pd.Period('2017-09-03') + ]) + series1 = pd.Series([1, 2, 3], index=idx1) + result1 = series1.truncate(after='2017-09-02') + + expected_idx1 = pd.PeriodIndex([ + pd.Period('2017-09-02'), + pd.Period('2017-09-02') + ]) + tm.assert_series_equal(result1, pd.Series([1, 2], index=expected_idx1)) + + idx2 = pd.PeriodIndex([ + pd.Period('2017-09-03'), + pd.Period('2017-09-02'), + pd.Period('2017-09-03') + ]) + series2 = pd.Series([1, 2, 3], index=idx2) + result2 = series2.sort_index().truncate(after='2017-09-02') + + expected_idx2 = pd.PeriodIndex([ + pd.Period('2017-09-02') + ]) + tm.assert_series_equal(result2, pd.Series([2], index=expected_idx2)) + + @pytest.mark.parametrize('input_vals', [ + [Period('2016-01', freq='M'), Period('2016-02', freq='M')], + [Period('2016-01-01', freq='D'), Period('2016-01-02', freq='D')], + [Period('2016-01-01 00:00:00', freq='H'), + Period('2016-01-01 01:00:00', freq='H')], + [Period('2016-01-01 00:00:00', freq='M'), + Period('2016-01-01 00:01:00', freq='M')], + [Period('2016-01-01 00:00:00', freq='S'), + Period('2016-01-01 00:00:01', freq='S')] + ]) + def test_end_time_timevalues(self, input_vals): + # GH 17157 + # Check that the time part of the Period is adjusted by end_time + # when using the dt accessor on a Series + input_vals = PeriodArray._from_sequence(np.asarray(input_vals)) + + s = Series(input_vals) + result = s.dt.end_time + expected = s.apply(lambda x: x.end_time) tm.assert_series_equal(result, expected) - result = ts + _permute(ts[::2]) + @pytest.mark.parametrize('input_vals', [ + ('2001'), ('NaT') + ]) + def test_to_period(self, input_vals): + # GH 21205 + expected = Series([input_vals], dtype='Period[D]') + result = Series([input_vals], dtype='datetime64[ns]').dt.to_period('D') tm.assert_series_equal(result, expected) - - # it works! - for kind in ['inner', 'outer', 'left', 'right']: - ts.align(ts[::2], join=kind) - msg = "Input has different freq=D from PeriodIndex\\(freq=A-DEC\\)" - with tm.assertRaisesRegexp(period.IncompatibleFrequency, msg): - ts + ts.asfreq('D', how="end") diff --git a/pandas/tests/series/test_quantile.py b/pandas/tests/series/test_quantile.py index b8d1b92081858..4f462e11e9bb9 100644 --- a/pandas/tests/series/test_quantile.py +++ b/pandas/tests/series/test_quantile.py @@ -1,59 +1,58 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 -import pytest import numpy as np -import pandas as pd +import pytest + +from pandas.core.dtypes.common import is_integer -from pandas import (Index, Series, _np_version_under1p9) -from pandas.tseries.index import Timestamp -from pandas.types.common import is_integer +import pandas as pd +from pandas import Index, Series +from pandas.core.indexes.datetimes import Timestamp import pandas.util.testing as tm from .common import TestData -class TestSeriesQuantile(TestData, tm.TestCase): +class TestSeriesQuantile(TestData): def test_quantile(self): - from numpy import percentile q = self.ts.quantile(0.1) - self.assertEqual(q, percentile(self.ts.valid(), 10)) + assert q == np.percentile(self.ts.dropna(), 10) q = self.ts.quantile(0.9) - self.assertEqual(q, percentile(self.ts.valid(), 90)) + assert q == np.percentile(self.ts.dropna(), 90) # object dtype q = Series(self.ts, dtype=object).quantile(0.9) - self.assertEqual(q, percentile(self.ts.valid(), 90)) + assert q == np.percentile(self.ts.dropna(), 90) # datetime64[ns] dtype dts = self.ts.index.to_series() q = dts.quantile(.2) - self.assertEqual(q, Timestamp('2000-01-10 19:12:00')) + assert q == Timestamp('2000-01-10 19:12:00') # timedelta64[ns] dtype tds = dts.diff() q = tds.quantile(.25) - self.assertEqual(q, pd.to_timedelta('24:00:00')) + assert q == pd.to_timedelta('24:00:00') # GH7661 result = Series([np.timedelta64('NaT')]).sum() - self.assertTrue(result is pd.NaT) + assert result == pd.Timedelta(0) msg = 'percentiles should all be in the interval \\[0, 1\\]' for invalid in [-1, 2, [0.5, -1], [0.5, 2]]: - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): self.ts.quantile(invalid) def test_quantile_multi(self): - from numpy import percentile qs = [.1, .9] result = self.ts.quantile(qs) - expected = pd.Series([percentile(self.ts.valid(), 10), - percentile(self.ts.valid(), 90)], + expected = pd.Series([np.percentile(self.ts.dropna(), 10), + np.percentile(self.ts.dropna(), 90)], index=qs, name=self.ts.name) tm.assert_series_equal(result, expected) @@ -71,59 +70,28 @@ def test_quantile_multi(self): tm.assert_series_equal(result, expected) def test_quantile_interpolation(self): - # GH #10174 - if _np_version_under1p9: - pytest.skip("Numpy version is under 1.9") - - from numpy import percentile + # see gh-10174 # interpolation = linear (default case) q = self.ts.quantile(0.1, interpolation='linear') - self.assertEqual(q, percentile(self.ts.valid(), 10)) + assert q == np.percentile(self.ts.dropna(), 10) q1 = self.ts.quantile(0.1) - self.assertEqual(q1, percentile(self.ts.valid(), 10)) + assert q1 == np.percentile(self.ts.dropna(), 10) # test with and without interpolation keyword - self.assertEqual(q, q1) + assert q == q1 def test_quantile_interpolation_dtype(self): # GH #10174 - if _np_version_under1p9: - pytest.skip("Numpy version is under 1.9") - - from numpy import percentile # interpolation = linear (default case) q = pd.Series([1, 3, 4]).quantile(0.5, interpolation='lower') - self.assertEqual(q, percentile(np.array([1, 3, 4]), 50)) - self.assertTrue(is_integer(q)) + assert q == np.percentile(np.array([1, 3, 4]), 50) + assert is_integer(q) q = pd.Series([1, 3, 4]).quantile(0.5, interpolation='higher') - self.assertEqual(q, percentile(np.array([1, 3, 4]), 50)) - self.assertTrue(is_integer(q)) - - def test_quantile_interpolation_np_lt_1p9(self): - # GH #10174 - if not _np_version_under1p9: - pytest.skip("Numpy version is greater than 1.9") - - from numpy import percentile - - # interpolation = linear (default case) - q = self.ts.quantile(0.1, interpolation='linear') - self.assertEqual(q, percentile(self.ts.valid(), 10)) - q1 = self.ts.quantile(0.1) - self.assertEqual(q1, percentile(self.ts.valid(), 10)) - - # interpolation other than linear - expErrMsg = "Interpolation methods other than " - with tm.assertRaisesRegexp(ValueError, expErrMsg): - self.ts.quantile(0.9, interpolation='nearest') - - # object dtype - with tm.assertRaisesRegexp(ValueError, expErrMsg): - q = Series(self.ts, dtype=object).quantile(0.7, - interpolation='higher') + assert q == np.percentile(np.array([1, 3, 4]), 50) + assert is_integer(q) def test_quantile_nan(self): @@ -131,14 +99,14 @@ def test_quantile_nan(self): s = pd.Series([1, 2, 3, 4, np.nan]) result = s.quantile(0.5) expected = 2.5 - self.assertEqual(result, expected) + assert result == expected # all nan/empty cases = [Series([]), Series([np.nan, np.nan])] for s in cases: res = s.quantile(0.5) - self.assertTrue(np.isnan(res)) + assert np.isnan(res) res = s.quantile([0.5]) tm.assert_series_equal(res, pd.Series([np.nan], index=[0.5])) @@ -147,51 +115,60 @@ def test_quantile_nan(self): tm.assert_series_equal(res, pd.Series([np.nan, np.nan], index=[0.2, 0.3])) - def test_quantile_box(self): - cases = [[pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.Timestamp('2011-01-03')], - [pd.Timestamp('2011-01-01', tz='US/Eastern'), - pd.Timestamp('2011-01-02', tz='US/Eastern'), - pd.Timestamp('2011-01-03', tz='US/Eastern')], - [pd.Timedelta('1 days'), pd.Timedelta('2 days'), - pd.Timedelta('3 days')], - # NaT - [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.Timestamp('2011-01-03'), pd.NaT], - [pd.Timestamp('2011-01-01', tz='US/Eastern'), - pd.Timestamp('2011-01-02', tz='US/Eastern'), - pd.Timestamp('2011-01-03', tz='US/Eastern'), pd.NaT], - [pd.Timedelta('1 days'), pd.Timedelta('2 days'), - pd.Timedelta('3 days'), pd.NaT]] - - for case in cases: - s = pd.Series(case, name='XXX') - res = s.quantile(0.5) - self.assertEqual(res, case[1]) + @pytest.mark.parametrize('case', [ + [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), + pd.Timestamp('2011-01-03')], + [pd.Timestamp('2011-01-01', tz='US/Eastern'), + pd.Timestamp('2011-01-02', tz='US/Eastern'), + pd.Timestamp('2011-01-03', tz='US/Eastern')], + [pd.Timedelta('1 days'), pd.Timedelta('2 days'), + pd.Timedelta('3 days')], + # NaT + [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), + pd.Timestamp('2011-01-03'), pd.NaT], + [pd.Timestamp('2011-01-01', tz='US/Eastern'), + pd.Timestamp('2011-01-02', tz='US/Eastern'), + pd.Timestamp('2011-01-03', tz='US/Eastern'), pd.NaT], + [pd.Timedelta('1 days'), pd.Timedelta('2 days'), + pd.Timedelta('3 days'), pd.NaT]]) + def test_quantile_box(self, case): + s = pd.Series(case, name='XXX') + res = s.quantile(0.5) + assert res == case[1] - res = s.quantile([0.5]) - exp = pd.Series([case[1]], index=[0.5], name='XXX') - tm.assert_series_equal(res, exp) + res = s.quantile([0.5]) + exp = pd.Series([case[1]], index=[0.5], name='XXX') + tm.assert_series_equal(res, exp) def test_datetime_timedelta_quantiles(self): # covers #9694 - self.assertTrue(pd.isnull(Series([], dtype='M8[ns]').quantile(.5))) - self.assertTrue(pd.isnull(Series([], dtype='m8[ns]').quantile(.5))) + assert pd.isna(Series([], dtype='M8[ns]').quantile(.5)) + assert pd.isna(Series([], dtype='m8[ns]').quantile(.5)) def test_quantile_nat(self): res = Series([pd.NaT, pd.NaT]).quantile(0.5) - self.assertTrue(res is pd.NaT) + assert res is pd.NaT res = Series([pd.NaT, pd.NaT]).quantile([0.5]) tm.assert_series_equal(res, pd.Series([pd.NaT], index=[0.5])) + @pytest.mark.parametrize('values, dtype', [ + ([0, 0, 0, 1, 2, 3], 'Sparse[int]'), + ([0., None, 1., 2.], 'Sparse[float]'), + ]) + def test_quantile_sparse(self, values, dtype): + ser = pd.Series(values, dtype=dtype) + result = ser.quantile([0.5]) + expected = pd.Series(np.asarray(ser)).quantile([0.5]) + tm.assert_series_equal(result, expected) + def test_quantile_empty(self): # floats s = Series([], dtype='float64') res = s.quantile(0.5) - self.assertTrue(np.isnan(res)) + assert np.isnan(res) res = s.quantile([0.5]) exp = Series([np.nan], index=[0.5]) @@ -201,7 +178,7 @@ def test_quantile_empty(self): s = Series([], dtype='int64') res = s.quantile(0.5) - self.assertTrue(np.isnan(res)) + assert np.isnan(res) res = s.quantile([0.5]) exp = Series([np.nan], index=[0.5]) @@ -211,7 +188,7 @@ def test_quantile_empty(self): s = Series([], dtype='datetime64[ns]') res = s.quantile(0.5) - self.assertTrue(res is pd.NaT) + assert res is pd.NaT res = s.quantile([0.5]) exp = Series([pd.NaT], index=[0.5]) diff --git a/pandas/tests/series/test_rank.py b/pandas/tests/series/test_rank.py index f47eae3adc3ae..373083c077e28 100644 --- a/pandas/tests/series/test_rank.py +++ b/pandas/tests/series/test_rank.py @@ -1,21 +1,25 @@ # -*- coding: utf-8 -*- -from pandas import compat - -import pytest - from distutils.version import LooseVersion -from numpy import nan +from itertools import chain + import numpy as np +from numpy import nan +import pytest -from pandas import (Series, date_range, NaT) +from pandas._libs.algos import Infinity, NegInfinity +from pandas._libs.tslib import iNaT +import pandas.compat as compat +from pandas.compat import PY2, product +import pandas.util._test_decorators as td -from pandas.compat import product -from pandas.util.testing import assert_series_equal -import pandas.util.testing as tm +from pandas import NaT, Series, Timestamp, date_range +from pandas.api.types import CategoricalDtype from pandas.tests.series.common import TestData +import pandas.util.testing as tm +from pandas.util.testing import assert_series_equal -class TestSeriesRank(tm.TestCase, TestData): +class TestSeriesRank(TestData): s = Series([1, 3, 4, 2, nan, 2, 1, 5, nan, 3]) results = { @@ -28,8 +32,8 @@ class TestSeriesRank(tm.TestCase, TestData): } def test_rank(self): - tm._skip_if_no_scipy() - from scipy.stats import rankdata + pytest.importorskip('scipy.stats.special') + rankdata = pytest.importorskip('scipy.stats.rankdata') self.ts[::2] = np.nan self.ts[:10][::3] = 4. @@ -123,35 +127,25 @@ def test_rank_categorical(self): exp_desc = Series([6., 5., 4., 3., 2., 1.]) ordered = Series( ['first', 'second', 'third', 'fourth', 'fifth', 'sixth'] - ).astype( - 'category', - categories=['first', 'second', 'third', - 'fourth', 'fifth', 'sixth'], - ordered=True - ) + ).astype(CategoricalDtype(categories=['first', 'second', 'third', + 'fourth', 'fifth', 'sixth'], + ordered=True)) assert_series_equal(ordered.rank(), exp) assert_series_equal(ordered.rank(ascending=False), exp_desc) # Unordered categoricals should be ranked as objects - unordered = Series( - ['first', 'second', 'third', 'fourth', 'fifth', 'sixth'], - ).astype( - 'category', - categories=['first', 'second', 'third', - 'fourth', 'fifth', 'sixth'], - ordered=False - ) + unordered = Series(['first', 'second', 'third', 'fourth', + 'fifth', 'sixth']).astype( + CategoricalDtype(categories=['first', 'second', 'third', + 'fourth', 'fifth', 'sixth'], + ordered=False)) exp_unordered = Series([2., 4., 6., 3., 1., 5.]) res = unordered.rank() assert_series_equal(res, exp_unordered) unordered1 = Series( [1, 2, 3, 4, 5, 6], - ).astype( - 'category', - categories=[1, 2, 3, 4, 5, 6], - ordered=False - ) + ).astype(CategoricalDtype([1, 2, 3, 4, 5, 6], False)) exp_unordered1 = Series([1., 2., 3., 4., 5., 6.]) res1 = unordered1.rank() assert_series_equal(res1, exp_unordered1) @@ -159,14 +153,8 @@ def test_rank_categorical(self): # Test na_option for rank data na_ser = Series( ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', np.NaN] - ).astype( - 'category', - categories=[ - 'first', 'second', 'third', 'fourth', - 'fifth', 'sixth', 'seventh' - ], - ordered=True - ) + ).astype(CategoricalDtype(['first', 'second', 'third', 'fourth', + 'fifth', 'sixth', 'seventh'], True)) exp_top = Series([2., 3., 4., 5., 6., 7., 1.]) exp_bot = Series([1., 2., 3., 4., 5., 6., 7.]) @@ -194,14 +182,19 @@ def test_rank_categorical(self): exp_keep ) + # Test invalid values for na_option + msg = "na_option must be one of 'keep', 'top', or 'bottom'" + + with pytest.raises(ValueError, match=msg): + na_ser.rank(na_option='bad', ascending=False) + + # invalid type + with pytest.raises(ValueError, match=msg): + na_ser.rank(na_option=True, ascending=False) + # Test with pct=True - na_ser = Series( - ['first', 'second', 'third', 'fourth', np.NaN], - ).astype( - 'category', - categories=['first', 'second', 'third', 'fourth'], - ordered=True - ) + na_ser = Series(['first', 'second', 'third', 'fourth', np.NaN]).astype( + CategoricalDtype(['first', 'second', 'third', 'fourth'], True)) exp_top = Series([0.4, 0.6, 0.8, 1., 0.2]) exp_bot = Series([0.2, 0.4, 0.6, 0.8, 1.]) exp_keep = Series([0.25, 0.5, 0.75, 1., np.NaN]) @@ -210,21 +203,55 @@ def test_rank_categorical(self): assert_series_equal(na_ser.rank(na_option='bottom', pct=True), exp_bot) assert_series_equal(na_ser.rank(na_option='keep', pct=True), exp_keep) + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_rank_signature(self): s = Series([0, 1]) s.rank(method='average') - self.assertRaises(ValueError, s.rank, 'average') - - def test_rank_inf(self): - pytest.skip('DataFrame.rank does not currently rank ' - 'np.inf and -np.inf properly') - - values = np.array( - [-np.inf, -50, -1, -1e-20, -1e-25, -1e-50, 0, 1e-40, 1e-20, 1e-10, - 2, 40, np.inf], dtype='float64') + msg = ("No axis named average for object type" + " ") + with pytest.raises(ValueError, match=msg): + s.rank('average') + + @pytest.mark.parametrize('contents,dtype', [ + ([-np.inf, -50, -1, -1e-20, -1e-25, -1e-50, 0, 1e-40, 1e-20, 1e-10, + 2, 40, np.inf], + 'float64'), + ([-np.inf, -50, -1, -1e-20, -1e-25, -1e-45, 0, 1e-40, 1e-20, 1e-10, + 2, 40, np.inf], + 'float32'), + ([np.iinfo(np.uint8).min, 1, 2, 100, np.iinfo(np.uint8).max], + 'uint8'), + pytest.param([np.iinfo(np.int64).min, -100, 0, 1, 9999, 100000, + 1e10, np.iinfo(np.int64).max], + 'int64', + marks=pytest.mark.xfail( + reason="iNaT is equivalent to minimum value of dtype" + "int64 pending issue GH#16674")), + ([NegInfinity(), '1', 'A', 'BA', 'Ba', 'C', Infinity()], + 'object') + ]) + def test_rank_inf(self, contents, dtype): + dtype_na_map = { + 'float64': np.nan, + 'float32': np.nan, + 'int64': iNaT, + 'object': None + } + # Insert nans at random positions if underlying dtype has missing + # value. Then adjust the expected order by adding nans accordingly + # This is for testing whether rank calculation is affected + # when values are interwined with nan values. + values = np.array(contents, dtype=dtype) + exp_order = np.array(range(len(values)), dtype='float64') + 1.0 + if dtype in dtype_na_map: + na_value = dtype_na_map[dtype] + nan_indices = np.random.choice(range(len(values)), 5) + values = np.insert(values, nan_indices, na_value) + exp_order = np.insert(exp_order, nan_indices, np.nan) + # shuffle the testing array and expected results in the same way random_order = np.random.permutation(len(values)) iseries = Series(values[random_order]) - exp = Series(random_order + 1.0, dtype='float64') + exp = Series(exp_order[random_order], dtype='float64') iranks = iseries.rank() assert_series_equal(iranks, exp) @@ -236,7 +263,7 @@ def _check(s, expected, method='average'): tm.assert_series_equal(result, Series(expected)) dtypes = [None, object] - disabled = set([(object, 'first')]) + disabled = {(object, 'first')} results = self.results for method, dtype in product(results, dtypes): @@ -245,11 +272,57 @@ def _check(s, expected, method='average'): series = s if dtype is None else s.astype(dtype) _check(series, results[method], method=method) + @td.skip_if_no_scipy + @pytest.mark.parametrize('ascending', [True, False]) + @pytest.mark.parametrize('method', ['average', 'min', 'max', 'first', + 'dense']) + @pytest.mark.parametrize('na_option', ['top', 'bottom', 'keep']) + def test_rank_tie_methods_on_infs_nans(self, method, na_option, ascending): + dtypes = [('object', None, Infinity(), NegInfinity()), + ('float64', np.nan, np.inf, -np.inf)] + chunk = 3 + disabled = {('object', 'first')} + + def _check(s, method, na_option, ascending): + exp_ranks = { + 'average': ([2, 2, 2], [5, 5, 5], [8, 8, 8]), + 'min': ([1, 1, 1], [4, 4, 4], [7, 7, 7]), + 'max': ([3, 3, 3], [6, 6, 6], [9, 9, 9]), + 'first': ([1, 2, 3], [4, 5, 6], [7, 8, 9]), + 'dense': ([1, 1, 1], [2, 2, 2], [3, 3, 3]) + } + ranks = exp_ranks[method] + if na_option == 'top': + order = [ranks[1], ranks[0], ranks[2]] + elif na_option == 'bottom': + order = [ranks[0], ranks[2], ranks[1]] + else: + order = [ranks[0], [np.nan] * chunk, ranks[1]] + expected = order if ascending else order[::-1] + expected = list(chain.from_iterable(expected)) + result = s.rank(method=method, na_option=na_option, + ascending=ascending) + tm.assert_series_equal(result, Series(expected, dtype='float64')) + + for dtype, na_value, pos_inf, neg_inf in dtypes: + in_arr = [neg_inf] * chunk + [na_value] * chunk + [pos_inf] * chunk + iseries = Series(in_arr, dtype=dtype) + if (dtype, method) in disabled: + continue + _check(iseries, method, na_option, ascending) + + def test_rank_desc_mix_nans_infs(self): + # GH 19538 + # check descending ranking when mix nans and infs + iseries = Series([1, np.nan, np.inf, -np.inf, 25]) + result = iseries.rank(ascending=False) + exp = Series([3, np.nan, 1, 4, 2], dtype='float64') + tm.assert_series_equal(result, exp) + def test_rank_methods_series(self): - tm.skip_if_no_package('scipy', min_version='0.13', - app='scipy.stats.rankdata') + pytest.importorskip('scipy.stats.special') + rankdata = pytest.importorskip('scipy.stats.rankdata') import scipy - from scipy.stats import rankdata xs = np.random.randn(9) xs = np.concatenate([xs[i:] for i in range(0, 9, 2)]) # add duplicates @@ -265,7 +338,7 @@ def test_rank_methods_series(self): sprank = rankdata(vals, m if m != 'first' else 'ordinal') expected = Series(sprank, index=index) - if LooseVersion(scipy.__version__) >= '0.17.0': + if LooseVersion(scipy.__version__) >= LooseVersion('0.17.0'): expected = expected.astype('float64') tm.assert_series_equal(result, expected) @@ -322,3 +395,115 @@ def test_rank_object_bug(self): # smoke tests Series([np.nan] * 32).astype(object).rank(ascending=True) Series([np.nan] * 32).astype(object).rank(ascending=False) + + def test_rank_modify_inplace(self): + # GH 18521 + # Check rank does not mutate series + s = Series([Timestamp('2017-01-05 10:20:27.569000'), NaT]) + expected = s.copy() + + s.rank() + result = s + assert_series_equal(result, expected) + + +# GH15630, pct should be on 100% basis when method='dense' + +@pytest.mark.parametrize('dtype', ['O', 'f8', 'i8']) +@pytest.mark.parametrize('ser, exp', [ + ([1], [1.]), + ([1, 2], [1. / 2, 2. / 2]), + ([2, 2], [1., 1.]), + ([1, 2, 3], [1. / 3, 2. / 3, 3. / 3]), + ([1, 2, 2], [1. / 2, 2. / 2, 2. / 2]), + ([4, 2, 1], [3. / 3, 2. / 3, 1. / 3],), + ([1, 1, 5, 5, 3], [1. / 3, 1. / 3, 3. / 3, 3. / 3, 2. / 3]), + ([1, 1, 3, 3, 5, 5], [1. / 3, 1. / 3, 2. / 3, 2. / 3, 3. / 3, 3. / 3]), + ([-5, -4, -3, -2, -1], [1. / 5, 2. / 5, 3. / 5, 4. / 5, 5. / 5])]) +def test_rank_dense_pct(dtype, ser, exp): + s = Series(ser).astype(dtype) + result = s.rank(method='dense', pct=True) + expected = Series(exp).astype(result.dtype) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize('dtype', ['O', 'f8', 'i8']) +@pytest.mark.parametrize('ser, exp', [ + ([1], [1.]), + ([1, 2], [1. / 2, 2. / 2]), + ([2, 2], [1. / 2, 1. / 2]), + ([1, 2, 3], [1. / 3, 2. / 3, 3. / 3]), + ([1, 2, 2], [1. / 3, 2. / 3, 2. / 3]), + ([4, 2, 1], [3. / 3, 2. / 3, 1. / 3],), + ([1, 1, 5, 5, 3], [1. / 5, 1. / 5, 4. / 5, 4. / 5, 3. / 5]), + ([1, 1, 3, 3, 5, 5], [1. / 6, 1. / 6, 3. / 6, 3. / 6, 5. / 6, 5. / 6]), + ([-5, -4, -3, -2, -1], [1. / 5, 2. / 5, 3. / 5, 4. / 5, 5. / 5])]) +def test_rank_min_pct(dtype, ser, exp): + s = Series(ser).astype(dtype) + result = s.rank(method='min', pct=True) + expected = Series(exp).astype(result.dtype) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize('dtype', ['O', 'f8', 'i8']) +@pytest.mark.parametrize('ser, exp', [ + ([1], [1.]), + ([1, 2], [1. / 2, 2. / 2]), + ([2, 2], [1., 1.]), + ([1, 2, 3], [1. / 3, 2. / 3, 3. / 3]), + ([1, 2, 2], [1. / 3, 3. / 3, 3. / 3]), + ([4, 2, 1], [3. / 3, 2. / 3, 1. / 3],), + ([1, 1, 5, 5, 3], [2. / 5, 2. / 5, 5. / 5, 5. / 5, 3. / 5]), + ([1, 1, 3, 3, 5, 5], [2. / 6, 2. / 6, 4. / 6, 4. / 6, 6. / 6, 6. / 6]), + ([-5, -4, -3, -2, -1], [1. / 5, 2. / 5, 3. / 5, 4. / 5, 5. / 5])]) +def test_rank_max_pct(dtype, ser, exp): + s = Series(ser).astype(dtype) + result = s.rank(method='max', pct=True) + expected = Series(exp).astype(result.dtype) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize('dtype', ['O', 'f8', 'i8']) +@pytest.mark.parametrize('ser, exp', [ + ([1], [1.]), + ([1, 2], [1. / 2, 2. / 2]), + ([2, 2], [1.5 / 2, 1.5 / 2]), + ([1, 2, 3], [1. / 3, 2. / 3, 3. / 3]), + ([1, 2, 2], [1. / 3, 2.5 / 3, 2.5 / 3]), + ([4, 2, 1], [3. / 3, 2. / 3, 1. / 3],), + ([1, 1, 5, 5, 3], [1.5 / 5, 1.5 / 5, 4.5 / 5, 4.5 / 5, 3. / 5]), + ([1, 1, 3, 3, 5, 5], + [1.5 / 6, 1.5 / 6, 3.5 / 6, 3.5 / 6, 5.5 / 6, 5.5 / 6]), + ([-5, -4, -3, -2, -1], [1. / 5, 2. / 5, 3. / 5, 4. / 5, 5. / 5])]) +def test_rank_average_pct(dtype, ser, exp): + s = Series(ser).astype(dtype) + result = s.rank(method='average', pct=True) + expected = Series(exp).astype(result.dtype) + assert_series_equal(result, expected) + + +@pytest.mark.parametrize('dtype', ['f8', 'i8']) +@pytest.mark.parametrize('ser, exp', [ + ([1], [1.]), + ([1, 2], [1. / 2, 2. / 2]), + ([2, 2], [1. / 2, 2. / 2.]), + ([1, 2, 3], [1. / 3, 2. / 3, 3. / 3]), + ([1, 2, 2], [1. / 3, 2. / 3, 3. / 3]), + ([4, 2, 1], [3. / 3, 2. / 3, 1. / 3],), + ([1, 1, 5, 5, 3], [1. / 5, 2. / 5, 4. / 5, 5. / 5, 3. / 5]), + ([1, 1, 3, 3, 5, 5], [1. / 6, 2. / 6, 3. / 6, 4. / 6, 5. / 6, 6. / 6]), + ([-5, -4, -3, -2, -1], [1. / 5, 2. / 5, 3. / 5, 4. / 5, 5. / 5])]) +def test_rank_first_pct(dtype, ser, exp): + s = Series(ser).astype(dtype) + result = s.rank(method='first', pct=True) + expected = Series(exp).astype(result.dtype) + assert_series_equal(result, expected) + + +@pytest.mark.single +@pytest.mark.high_memory +def test_pct_max_many_rows(): + # GH 18271 + s = Series(np.arange(2**24 + 1)) + result = s.rank(pct=True).max() + assert result == 1 diff --git a/pandas/tests/series/test_replace.py b/pandas/tests/series/test_replace.py index 5190eb110f4cf..40b28047080da 100644 --- a/pandas/tests/series/test_replace.py +++ b/pandas/tests/series/test_replace.py @@ -2,14 +2,15 @@ # pylint: disable-msg=E1101,W0612 import numpy as np +import pytest + import pandas as pd -import pandas._libs.lib as lib import pandas.util.testing as tm from .common import TestData -class TestSeriesReplace(TestData, tm.TestCase): +class TestSeriesReplace(TestData): def test_replace(self): N = 100 ser = pd.Series(np.random.randn(N)) @@ -35,18 +36,18 @@ def test_replace(self): # replace list with a single value rs = ser.replace([np.nan, 'foo', 'bar'], -1) - self.assertTrue((rs[:5] == -1).all()) - self.assertTrue((rs[6:10] == -1).all()) - self.assertTrue((rs[20:30] == -1).all()) - self.assertTrue((pd.isnull(ser[:5])).all()) + assert (rs[:5] == -1).all() + assert (rs[6:10] == -1).all() + assert (rs[20:30] == -1).all() + assert (pd.isna(ser[:5])).all() # replace with different values rs = ser.replace({np.nan: -1, 'foo': -2, 'bar': -3}) - self.assertTrue((rs[:5] == -1).all()) - self.assertTrue((rs[6:10] == -2).all()) - self.assertTrue((rs[20:30] == -3).all()) - self.assertTrue((pd.isnull(ser[:5])).all()) + assert (rs[:5] == -1).all() + assert (rs[6:10] == -2).all() + assert (rs[20:30] == -3).all() + assert (pd.isna(ser[:5])).all() # replace with different values with 2 lists rs2 = ser.replace([np.nan, 'foo', 'bar'], [-1, -2, -3]) @@ -55,14 +56,14 @@ def test_replace(self): # replace inplace ser.replace([np.nan, 'foo', 'bar'], -1, inplace=True) - self.assertTrue((ser[:5] == -1).all()) - self.assertTrue((ser[6:10] == -1).all()) - self.assertTrue((ser[20:30] == -1).all()) + assert (ser[:5] == -1).all() + assert (ser[6:10] == -1).all() + assert (ser[20:30] == -1).all() ser = pd.Series([np.nan, 0, np.inf]) tm.assert_series_equal(ser.replace(np.nan, 0), ser.fillna(0)) - ser = pd.Series([np.nan, 0, 'foo', 'bar', np.inf, None, lib.NaT]) + ser = pd.Series([np.nan, 0, 'foo', 'bar', np.inf, None, pd.NaT]) tm.assert_series_equal(ser.replace(np.nan, 0), ser.fillna(0)) filled = ser.copy() filled[4] = 0 @@ -72,11 +73,13 @@ def test_replace(self): tm.assert_series_equal(ser.replace(np.nan, 0), ser.fillna(0)) # malformed - self.assertRaises(ValueError, ser.replace, [1, 2, 3], [np.nan, 0]) + msg = r"Replacement lists must match in length\. Expecting 3 got 2" + with pytest.raises(ValueError, match=msg): + ser.replace([1, 2, 3], [np.nan, 0]) # make sure that we aren't just masking a TypeError because bools don't # implement indexing - with tm.assertRaisesRegexp(TypeError, 'Cannot compare types .+'): + with pytest.raises(TypeError, match='Cannot compare types .+'): ser.replace([1, 2], [np.nan, 0]) ser = pd.Series([0, 1, 2, 3, 4]) @@ -106,6 +109,13 @@ def test_replace_gh5319(self): pd.Timestamp('20120101')) tm.assert_series_equal(result, expected) + # GH 11792: Test with replacing NaT in a list with tz data + ts = pd.Timestamp('2015/01/01', tz='UTC') + s = pd.Series([pd.NaT, pd.Timestamp('2015/01/01', tz='UTC')]) + result = s.replace([np.nan, pd.NaT], pd.Timestamp.min) + expected = pd.Series([pd.Timestamp.min, ts], dtype=object) + tm.assert_series_equal(expected, result) + def test_replace_with_single_list(self): ser = pd.Series([0, 1, 2, 3, 4]) result = ser.replace([1, 2, 3]) @@ -117,10 +127,25 @@ def test_replace_with_single_list(self): # make sure things don't get corrupted when fillna call fails s = ser.copy() - with tm.assertRaises(ValueError): + msg = (r"Invalid fill method\. Expecting pad \(ffill\) or backfill" + r" \(bfill\)\. Got crash_cymbal") + with pytest.raises(ValueError, match=msg): s.replace([1, 2, 3], inplace=True, method='crash_cymbal') tm.assert_series_equal(s, ser) + def test_replace_with_empty_list(self): + # GH 21977 + s = pd.Series([[1], [2, 3], [], np.nan, [4]]) + expected = s + result = s.replace([], np.nan) + tm.assert_series_equal(result, expected) + + # GH 19266 + with pytest.raises(ValueError, match="cannot assign mismatch"): + s.replace({np.nan: []}) + with pytest.raises(ValueError, match="cannot assign mismatch"): + s.replace({np.nan: ['dummy', 'alt']}) + def test_replace_mixed_types(self): s = pd.Series(np.arange(5), dtype='int64') @@ -184,7 +209,7 @@ def test_replace_bool_with_bool(self): def test_replace_with_dict_with_bool_keys(self): s = pd.Series([True, False, True]) - with tm.assertRaisesRegexp(TypeError, 'Cannot compare types .+'): + with pytest.raises(TypeError, match='Cannot compare types .+'): s.replace({'asdf': 'asdb', True: 'yes'}) def test_replace2(self): @@ -198,18 +223,18 @@ def test_replace2(self): # replace list with a single value rs = ser.replace([np.nan, 'foo', 'bar'], -1) - self.assertTrue((rs[:5] == -1).all()) - self.assertTrue((rs[6:10] == -1).all()) - self.assertTrue((rs[20:30] == -1).all()) - self.assertTrue((pd.isnull(ser[:5])).all()) + assert (rs[:5] == -1).all() + assert (rs[6:10] == -1).all() + assert (rs[20:30] == -1).all() + assert (pd.isna(ser[:5])).all() # replace with different values rs = ser.replace({np.nan: -1, 'foo': -2, 'bar': -3}) - self.assertTrue((rs[:5] == -1).all()) - self.assertTrue((rs[6:10] == -2).all()) - self.assertTrue((rs[20:30] == -3).all()) - self.assertTrue((pd.isnull(ser[:5])).all()) + assert (rs[:5] == -1).all() + assert (rs[6:10] == -2).all() + assert (rs[20:30] == -3).all() + assert (pd.isna(ser[:5])).all() # replace with different values with 2 lists rs2 = ser.replace([np.nan, 'foo', 'bar'], [-1, -2, -3]) @@ -217,9 +242,9 @@ def test_replace2(self): # replace inplace ser.replace([np.nan, 'foo', 'bar'], -1, inplace=True) - self.assertTrue((ser[:5] == -1).all()) - self.assertTrue((ser[6:10] == -1).all()) - self.assertTrue((ser[20:30] == -1).all()) + assert (ser[:5] == -1).all() + assert (ser[6:10] == -1).all() + assert (ser[20:30] == -1).all() def test_replace_with_empty_dictlike(self): # GH 15289 @@ -234,6 +259,14 @@ def test_replace_string_with_number(self): expected = pd.Series([1, 2, 3]) tm.assert_series_equal(expected, result) + def test_replace_replacer_equals_replacement(self): + # GH 20656 + # make sure all replacers are matching against original values + s = pd.Series(['a', 'b']) + expected = pd.Series(['b', 'a']) + result = s.replace({'a': 'b', 'b': 'a'}) + tm.assert_series_equal(expected, result) + def test_replace_unicode_with_number(self): # GH 15743 s = pd.Series([1, 2, 3]) diff --git a/pandas/tests/series/test_repr.py b/pandas/tests/series/test_repr.py index 99a406a71b12b..842207f2a572f 100644 --- a/pandas/tests/series/test_repr.py +++ b/pandas/tests/series/test_repr.py @@ -4,25 +4,28 @@ from datetime import datetime, timedelta import numpy as np -import pandas as pd -from pandas import (Index, Series, DataFrame, date_range) -from pandas.core.index import MultiIndex +import pandas.compat as compat +from pandas.compat import lrange, range, u -from pandas.compat import StringIO, lrange, range, u -from pandas import compat +import pandas as pd +from pandas import ( + Categorical, DataFrame, Index, Series, date_range, option_context, + period_range, timedelta_range) +from pandas.core.base import StringMixin +from pandas.core.index import MultiIndex import pandas.util.testing as tm from .common import TestData -class TestSeriesRepr(TestData, tm.TestCase): +class TestSeriesRepr(TestData): def test_multilevel_name_print(self): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['first', 'second']) s = Series(lrange(0, len(index)), index=index, name='sth') expected = ["first second", "foo one 0", @@ -32,24 +35,29 @@ def test_multilevel_name_print(self): "qux one 7", " two 8", " three 9", "Name: sth, dtype: int64"] expected = "\n".join(expected) - self.assertEqual(repr(s), expected) + assert repr(s) == expected def test_name_printing(self): - # test small series + # Test small Series. s = Series([0, 1, 2]) + s.name = "test" - self.assertIn("Name: test", repr(s)) + assert "Name: test" in repr(s) + s.name = None - self.assertNotIn("Name:", repr(s)) - # test big series (diff code path) + assert "Name:" not in repr(s) + + # Test big Series (diff code path). s = Series(lrange(0, 1000)) + s.name = "test" - self.assertIn("Name: test", repr(s)) + assert "Name: test" in repr(s) + s.name = None - self.assertNotIn("Name:", repr(s)) + assert "Name:" not in repr(s) s = Series(index=date_range('20010101', '20020101'), name='test') - self.assertIn("Name: test", repr(s)) + assert "Name: test" in repr(s) def test_repr(self): str(self.ts) @@ -88,44 +96,38 @@ def test_repr(self): # 0 as name ser = Series(np.random.randn(100), name=0) rep_str = repr(ser) - self.assertIn("Name: 0", rep_str) + assert "Name: 0" in rep_str # tidy repr ser = Series(np.random.randn(1001), name=0) rep_str = repr(ser) - self.assertIn("Name: 0", rep_str) + assert "Name: 0" in rep_str ser = Series(["a\n\r\tb"], name="a\n\r\td", index=["a\n\r\tf"]) - self.assertFalse("\t" in repr(ser)) - self.assertFalse("\r" in repr(ser)) - self.assertFalse("a\n" in repr(ser)) + assert "\t" not in repr(ser) + assert "\r" not in repr(ser) + assert "a\n" not in repr(ser) # with empty series (#4651) s = Series([], dtype=np.int64, name='foo') - self.assertEqual(repr(s), 'Series([], Name: foo, dtype: int64)') + assert repr(s) == 'Series([], Name: foo, dtype: int64)' s = Series([], dtype=np.int64, name=None) - self.assertEqual(repr(s), 'Series([], dtype: int64)') + assert repr(s) == 'Series([], dtype: int64)' def test_tidy_repr(self): a = Series([u("\u05d0")] * 1000) a.name = 'title1' repr(a) # should not raise exception - def test_repr_bool_fails(self): + def test_repr_bool_fails(self, capsys): s = Series([DataFrame(np.random.randn(2, 2)) for i in range(5)]) - import sys + # It works (with no Cython exception barf)! + repr(s) - buf = StringIO() - tmp = sys.stderr - sys.stderr = buf - try: - # it works (with no Cython exception barf)! - repr(s) - finally: - sys.stderr = tmp - self.assertEqual(buf.getvalue(), '') + captured = capsys.readouterr() + assert captured.err == '' def test_repr_name_iterable_indexable(self): s = Series([1, 2, 3], name=np.int64(3)) @@ -137,8 +139,7 @@ def test_repr_name_iterable_indexable(self): repr(s) def test_repr_should_return_str(self): - # http://docs.python.org/py3k/reference/datamodel.html#object.__repr__ - # http://docs.python.org/reference/datamodel.html#object.__repr__ + # https://docs.python.org/3/reference/datamodel.html#object.__repr__ # ...The return value must be a string object. # (str on py2.x, str (unicode) on py3) @@ -146,7 +147,7 @@ def test_repr_should_return_str(self): data = [8, 5, 3, 5] index1 = [u("\u03c3"), u("\u03c4"), u("\u03c5"), u("\u03c6")] df = Series(data, index=index1) - self.assertTrue(type(df.__repr__() == str)) # both py2 / 3 + assert type(df.__repr__() == str) # both py2 / 3 def test_repr_max_rows(self): # GH 6863 @@ -174,7 +175,310 @@ def test_timeseries_repr_object_dtype(self): repr(ts) ts = tm.makeTimeSeries(1000) - self.assertTrue(repr(ts).splitlines()[-1].startswith('Freq:')) + assert repr(ts).splitlines()[-1].startswith('Freq:') ts2 = ts.iloc[np.random.randint(0, len(ts) - 1, 400)] repr(ts2).splitlines()[-1] + + def test_latex_repr(self): + result = r"""\begin{tabular}{ll} +\toprule +{} & 0 \\ +\midrule +0 & $\alpha$ \\ +1 & b \\ +2 & c \\ +\bottomrule +\end{tabular} +""" + with option_context('display.latex.escape', False, + 'display.latex.repr', True): + s = Series([r'$\alpha$', 'b', 'c']) + assert result == s._repr_latex_() + + assert s._repr_latex_() is None + + def test_index_repr_in_frame_with_nan(self): + # see gh-25061 + i = Index([1, np.nan]) + s = Series([1, 2], index=i) + exp = """1.0 1\nNaN 2\ndtype: int64""" + + assert repr(s) == exp + + +class TestCategoricalRepr(object): + + def test_categorical_repr_unicode(self): + # GH#21002 if len(index) > 60, sys.getdefaultencoding()=='ascii', + # and we are working in PY2, then rendering a Categorical could raise + # UnicodeDecodeError by trying to decode when it shouldn't + + class County(StringMixin): + name = u'San Sebastián' + state = u'PR' + + def __unicode__(self): + return self.name + u', ' + self.state + + cat = pd.Categorical([County() for n in range(61)]) + idx = pd.Index(cat) + ser = idx.to_series() + + if compat.PY3: + # no reloading of sys, just check that the default (utf8) works + # as expected + repr(ser) + str(ser) + + else: + # set sys.defaultencoding to ascii, then change it back after + # the test + with tm.set_defaultencoding('ascii'): + repr(ser) + str(ser) + + def test_categorical_repr(self): + a = Series(Categorical([1, 2, 3, 4])) + exp = u("0 1\n1 2\n2 3\n3 4\n" + + "dtype: category\nCategories (4, int64): [1, 2, 3, 4]") + + assert exp == a.__unicode__() + + a = Series(Categorical(["a", "b"] * 25)) + exp = u("0 a\n1 b\n" + " ..\n" + "48 a\n49 b\n" + + "Length: 50, dtype: category\nCategories (2, object): [a, b]") + with option_context("display.max_rows", 5): + assert exp == repr(a) + + levs = list("abcdefghijklmnopqrstuvwxyz") + a = Series(Categorical(["a", "b"], categories=levs, ordered=True)) + exp = u("0 a\n1 b\n" + "dtype: category\n" + "Categories (26, object): [a < b < c < d ... w < x < y < z]") + assert exp == a.__unicode__() + + def test_categorical_series_repr(self): + s = Series(Categorical([1, 2, 3])) + exp = """0 1 +1 2 +2 3 +dtype: category +Categories (3, int64): [1, 2, 3]""" + + assert repr(s) == exp + + s = Series(Categorical(np.arange(10))) + exp = """0 0 +1 1 +2 2 +3 3 +4 4 +5 5 +6 6 +7 7 +8 8 +9 9 +dtype: category +Categories (10, int64): [0, 1, 2, 3, ..., 6, 7, 8, 9]""" + + assert repr(s) == exp + + def test_categorical_series_repr_ordered(self): + s = Series(Categorical([1, 2, 3], ordered=True)) + exp = """0 1 +1 2 +2 3 +dtype: category +Categories (3, int64): [1 < 2 < 3]""" + + assert repr(s) == exp + + s = Series(Categorical(np.arange(10), ordered=True)) + exp = """0 0 +1 1 +2 2 +3 3 +4 4 +5 5 +6 6 +7 7 +8 8 +9 9 +dtype: category +Categories (10, int64): [0 < 1 < 2 < 3 ... 6 < 7 < 8 < 9]""" + + assert repr(s) == exp + + def test_categorical_series_repr_datetime(self): + idx = date_range('2011-01-01 09:00', freq='H', periods=5) + s = Series(Categorical(idx)) + exp = """0 2011-01-01 09:00:00 +1 2011-01-01 10:00:00 +2 2011-01-01 11:00:00 +3 2011-01-01 12:00:00 +4 2011-01-01 13:00:00 +dtype: category +Categories (5, datetime64[ns]): [2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, + 2011-01-01 12:00:00, 2011-01-01 13:00:00]""" # noqa + + assert repr(s) == exp + + idx = date_range('2011-01-01 09:00', freq='H', periods=5, + tz='US/Eastern') + s = Series(Categorical(idx)) + exp = """0 2011-01-01 09:00:00-05:00 +1 2011-01-01 10:00:00-05:00 +2 2011-01-01 11:00:00-05:00 +3 2011-01-01 12:00:00-05:00 +4 2011-01-01 13:00:00-05:00 +dtype: category +Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, + 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, + 2011-01-01 13:00:00-05:00]""" # noqa + + assert repr(s) == exp + + def test_categorical_series_repr_datetime_ordered(self): + idx = date_range('2011-01-01 09:00', freq='H', periods=5) + s = Series(Categorical(idx, ordered=True)) + exp = """0 2011-01-01 09:00:00 +1 2011-01-01 10:00:00 +2 2011-01-01 11:00:00 +3 2011-01-01 12:00:00 +4 2011-01-01 13:00:00 +dtype: category +Categories (5, datetime64[ns]): [2011-01-01 09:00:00 < 2011-01-01 10:00:00 < 2011-01-01 11:00:00 < + 2011-01-01 12:00:00 < 2011-01-01 13:00:00]""" # noqa + + assert repr(s) == exp + + idx = date_range('2011-01-01 09:00', freq='H', periods=5, + tz='US/Eastern') + s = Series(Categorical(idx, ordered=True)) + exp = """0 2011-01-01 09:00:00-05:00 +1 2011-01-01 10:00:00-05:00 +2 2011-01-01 11:00:00-05:00 +3 2011-01-01 12:00:00-05:00 +4 2011-01-01 13:00:00-05:00 +dtype: category +Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00 < 2011-01-01 10:00:00-05:00 < + 2011-01-01 11:00:00-05:00 < 2011-01-01 12:00:00-05:00 < + 2011-01-01 13:00:00-05:00]""" # noqa + + assert repr(s) == exp + + def test_categorical_series_repr_period(self): + idx = period_range('2011-01-01 09:00', freq='H', periods=5) + s = Series(Categorical(idx)) + exp = """0 2011-01-01 09:00 +1 2011-01-01 10:00 +2 2011-01-01 11:00 +3 2011-01-01 12:00 +4 2011-01-01 13:00 +dtype: category +Categories (5, period[H]): [2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, + 2011-01-01 13:00]""" # noqa + + assert repr(s) == exp + + idx = period_range('2011-01', freq='M', periods=5) + s = Series(Categorical(idx)) + exp = """0 2011-01 +1 2011-02 +2 2011-03 +3 2011-04 +4 2011-05 +dtype: category +Categories (5, period[M]): [2011-01, 2011-02, 2011-03, 2011-04, 2011-05]""" + + assert repr(s) == exp + + def test_categorical_series_repr_period_ordered(self): + idx = period_range('2011-01-01 09:00', freq='H', periods=5) + s = Series(Categorical(idx, ordered=True)) + exp = """0 2011-01-01 09:00 +1 2011-01-01 10:00 +2 2011-01-01 11:00 +3 2011-01-01 12:00 +4 2011-01-01 13:00 +dtype: category +Categories (5, period[H]): [2011-01-01 09:00 < 2011-01-01 10:00 < 2011-01-01 11:00 < 2011-01-01 12:00 < + 2011-01-01 13:00]""" # noqa + + assert repr(s) == exp + + idx = period_range('2011-01', freq='M', periods=5) + s = Series(Categorical(idx, ordered=True)) + exp = """0 2011-01 +1 2011-02 +2 2011-03 +3 2011-04 +4 2011-05 +dtype: category +Categories (5, period[M]): [2011-01 < 2011-02 < 2011-03 < 2011-04 < 2011-05]""" + + assert repr(s) == exp + + def test_categorical_series_repr_timedelta(self): + idx = timedelta_range('1 days', periods=5) + s = Series(Categorical(idx)) + exp = """0 1 days +1 2 days +2 3 days +3 4 days +4 5 days +dtype: category +Categories (5, timedelta64[ns]): [1 days, 2 days, 3 days, 4 days, 5 days]""" + + assert repr(s) == exp + + idx = timedelta_range('1 hours', periods=10) + s = Series(Categorical(idx)) + exp = """0 0 days 01:00:00 +1 1 days 01:00:00 +2 2 days 01:00:00 +3 3 days 01:00:00 +4 4 days 01:00:00 +5 5 days 01:00:00 +6 6 days 01:00:00 +7 7 days 01:00:00 +8 8 days 01:00:00 +9 9 days 01:00:00 +dtype: category +Categories (10, timedelta64[ns]): [0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, + 3 days 01:00:00, ..., 6 days 01:00:00, 7 days 01:00:00, + 8 days 01:00:00, 9 days 01:00:00]""" # noqa + + assert repr(s) == exp + + def test_categorical_series_repr_timedelta_ordered(self): + idx = timedelta_range('1 days', periods=5) + s = Series(Categorical(idx, ordered=True)) + exp = """0 1 days +1 2 days +2 3 days +3 4 days +4 5 days +dtype: category +Categories (5, timedelta64[ns]): [1 days < 2 days < 3 days < 4 days < 5 days]""" # noqa + + assert repr(s) == exp + + idx = timedelta_range('1 hours', periods=10) + s = Series(Categorical(idx, ordered=True)) + exp = """0 0 days 01:00:00 +1 1 days 01:00:00 +2 2 days 01:00:00 +3 3 days 01:00:00 +4 4 days 01:00:00 +5 5 days 01:00:00 +6 6 days 01:00:00 +7 7 days 01:00:00 +8 8 days 01:00:00 +9 9 days 01:00:00 +dtype: category +Categories (10, timedelta64[ns]): [0 days 01:00:00 < 1 days 01:00:00 < 2 days 01:00:00 < + 3 days 01:00:00 ... 6 days 01:00:00 < 7 days 01:00:00 < + 8 days 01:00:00 < 9 days 01:00:00]""" # noqa + + assert repr(s) == exp diff --git a/pandas/tests/series/test_sorting.py b/pandas/tests/series/test_sorting.py index 66ecba960ae0b..162fa4ac9ab52 100644 --- a/pandas/tests/series/test_sorting.py +++ b/pandas/tests/series/test_sorting.py @@ -1,24 +1,20 @@ # coding=utf-8 -import numpy as np import random -from pandas import (DataFrame, Series, MultiIndex) +import numpy as np +import pytest + +from pandas.compat import PY2 -from pandas.util.testing import (assert_series_equal, assert_almost_equal) +from pandas import Categorical, DataFrame, IntervalIndex, MultiIndex, Series import pandas.util.testing as tm +from pandas.util.testing import assert_almost_equal, assert_series_equal from .common import TestData -class TestSeriesSorting(TestData, tm.TestCase): - - def test_sortlevel_deprecated(self): - ts = self.ts.copy() - - # see gh-9816 - with tm.assert_produces_warning(FutureWarning): - ts.sortlevel() +class TestSeriesSorting(TestData): def test_sort_values(self): @@ -26,20 +22,20 @@ def test_sort_values(self): ser = Series([3, 2, 4, 1], ['A', 'B', 'C', 'D']) expected = Series([1, 2, 3, 4], ['D', 'B', 'A', 'C']) result = ser.sort_values() - self.assert_series_equal(expected, result) + tm.assert_series_equal(expected, result) ts = self.ts.copy() ts[:5] = np.NaN vals = ts.values result = ts.sort_values() - self.assertTrue(np.isnan(result[-5:]).all()) - self.assert_numpy_array_equal(result[:-5].values, np.sort(vals[5:])) + assert np.isnan(result[-5:]).all() + tm.assert_numpy_array_equal(result[:-5].values, np.sort(vals[5:])) # na_position result = ts.sort_values(na_position='first') - self.assertTrue(np.isnan(result[:5]).all()) - self.assert_numpy_array_equal(result[5:].values, np.sort(vals[5:])) + assert np.isnan(result[:5]).all() + tm.assert_numpy_array_equal(result[5:].values, np.sort(vals[5:])) # something object-type ser = Series(['A', 'B'], [1, 2]) @@ -48,10 +44,10 @@ def test_sort_values(self): # ascending=False ordered = ts.sort_values(ascending=False) - expected = np.sort(ts.valid().values)[::-1] - assert_almost_equal(expected, ordered.valid().values) + expected = np.sort(ts.dropna().values)[::-1] + assert_almost_equal(expected, ordered.dropna().values) ordered = ts.sort_values(ascending=False, na_position='first') - assert_almost_equal(expected, ordered.valid().values) + assert_almost_equal(expected, ordered.dropna().values) # ascending=[False] should behave the same as ascending=False ordered = ts.sort_values(ascending=[False]) @@ -61,34 +57,40 @@ def test_sort_values(self): expected = ts.sort_values(ascending=False, na_position='first') assert_series_equal(expected, ordered) - self.assertRaises(ValueError, - lambda: ts.sort_values(ascending=None)) - self.assertRaises(ValueError, - lambda: ts.sort_values(ascending=[])) - self.assertRaises(ValueError, - lambda: ts.sort_values(ascending=[1, 2, 3])) - self.assertRaises(ValueError, - lambda: ts.sort_values(ascending=[False, False])) - self.assertRaises(ValueError, - lambda: ts.sort_values(ascending='foobar')) + msg = "ascending must be boolean" + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending=None) + msg = r"Length of ascending \(0\) must be 1 for Series" + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending=[]) + msg = r"Length of ascending \(3\) must be 1 for Series" + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending=[1, 2, 3]) + msg = r"Length of ascending \(2\) must be 1 for Series" + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending=[False, False]) + msg = "ascending must be boolean" + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending='foobar') # inplace=True ts = self.ts.copy() ts.sort_values(ascending=False, inplace=True) - self.assert_series_equal(ts, self.ts.sort_values(ascending=False)) - self.assert_index_equal(ts.index, - self.ts.sort_values(ascending=False).index) + tm.assert_series_equal(ts, self.ts.sort_values(ascending=False)) + tm.assert_index_equal(ts.index, + self.ts.sort_values(ascending=False).index) # GH 5856/5853 # Series.sort_values operating on a view df = DataFrame(np.random.randn(10, 4)) s = df.iloc[:, 0] - def f(): + msg = ("This Series is a view of some other array, to sort in-place" + " you must create a copy") + with pytest.raises(ValueError, match=msg): s.sort_values(inplace=True) - self.assertRaises(ValueError, f) - + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_sort_index(self): rindex = list(self.ts.index) random.shuffle(rindex) @@ -110,13 +112,16 @@ def test_sort_index(self): sorted_series = random_order.sort_index(axis=0) assert_series_equal(sorted_series, self.ts) - self.assertRaises(ValueError, lambda: random_order.sort_values(axis=1)) + msg = ("No axis named 1 for object type" + " ") + with pytest.raises(ValueError, match=msg): + random_order.sort_values(axis=1) sorted_series = random_order.sort_index(level=0, axis=0) assert_series_equal(sorted_series, self.ts) - self.assertRaises(ValueError, - lambda: random_order.sort_index(level=0, axis=1)) + with pytest.raises(ValueError, match=msg): + random_order.sort_index(level=0, axis=1) def test_sort_index_inplace(self): @@ -127,30 +132,32 @@ def test_sort_index_inplace(self): # descending random_order = self.ts.reindex(rindex) result = random_order.sort_index(ascending=False, inplace=True) - self.assertIs(result, None, - msg='sort_index() inplace should return None') - assert_series_equal(random_order, self.ts.reindex(self.ts.index[::-1])) + + assert result is None + tm.assert_series_equal(random_order, self.ts.reindex( + self.ts.index[::-1])) # ascending random_order = self.ts.reindex(rindex) result = random_order.sort_index(ascending=True, inplace=True) - self.assertIs(result, None, - msg='sort_index() inplace should return None') - assert_series_equal(random_order, self.ts) - def test_sort_index_multiindex(self): + assert result is None + tm.assert_series_equal(random_order, self.ts) + + @pytest.mark.parametrize("level", ['A', 0]) # GH 21052 + def test_sort_index_multiindex(self, level): mi = MultiIndex.from_tuples([[1, 1, 3], [1, 1, 1]], names=list('ABC')) s = Series([1, 2], mi) backwards = s.iloc[[1, 0]] # implicit sort_remaining=True - res = s.sort_index(level='A') + res = s.sort_index(level=level) assert_series_equal(backwards, res) # GH13496 - # rows share same level='A': sort has no effect without remaining lvls - res = s.sort_index(level='A', sort_remaining=False) + # sort has no effect without remaining lvls + res = s.sort_index(level=level, sort_remaining=False) assert_series_equal(s, res) def test_sort_index_kind(self): @@ -177,3 +184,87 @@ def test_sort_index_na_position(self): expected_series_last = Series(index=[1, 2, 3, 3, 4, np.nan]) index_sorted_series = series.sort_index(na_position='last') assert_series_equal(expected_series_last, index_sorted_series) + + def test_sort_index_intervals(self): + s = Series([np.nan, 1, 2, 3], IntervalIndex.from_arrays( + [0, 1, 2, 3], + [1, 2, 3, 4])) + + result = s.sort_index() + expected = s + assert_series_equal(result, expected) + + result = s.sort_index(ascending=False) + expected = Series([3, 2, 1, np.nan], IntervalIndex.from_arrays( + [3, 2, 1, 0], + [4, 3, 2, 1])) + assert_series_equal(result, expected) + + def test_sort_values_categorical(self): + + c = Categorical(["a", "b", "b", "a"], ordered=False) + cat = Series(c.copy()) + + # sort in the categories order + expected = Series( + Categorical(["a", "a", "b", "b"], + ordered=False), index=[0, 3, 1, 2]) + result = cat.sort_values() + tm.assert_series_equal(result, expected) + + cat = Series(Categorical(["a", "c", "b", "d"], ordered=True)) + res = cat.sort_values() + exp = np.array(["a", "b", "c", "d"], dtype=np.object_) + tm.assert_numpy_array_equal(res.__array__(), exp) + + cat = Series(Categorical(["a", "c", "b", "d"], categories=[ + "a", "b", "c", "d"], ordered=True)) + res = cat.sort_values() + exp = np.array(["a", "b", "c", "d"], dtype=np.object_) + tm.assert_numpy_array_equal(res.__array__(), exp) + + res = cat.sort_values(ascending=False) + exp = np.array(["d", "c", "b", "a"], dtype=np.object_) + tm.assert_numpy_array_equal(res.__array__(), exp) + + raw_cat1 = Categorical(["a", "b", "c", "d"], + categories=["a", "b", "c", "d"], ordered=False) + raw_cat2 = Categorical(["a", "b", "c", "d"], + categories=["d", "c", "b", "a"], ordered=True) + s = ["a", "b", "c", "d"] + df = DataFrame({"unsort": raw_cat1, + "sort": raw_cat2, + "string": s, + "values": [1, 2, 3, 4]}) + + # Cats must be sorted in a dataframe + res = df.sort_values(by=["string"], ascending=False) + exp = np.array(["d", "c", "b", "a"], dtype=np.object_) + tm.assert_numpy_array_equal(res["sort"].values.__array__(), exp) + assert res["sort"].dtype == "category" + + res = df.sort_values(by=["sort"], ascending=False) + exp = df.sort_values(by=["string"], ascending=True) + tm.assert_series_equal(res["values"], exp["values"]) + assert res["sort"].dtype == "category" + assert res["unsort"].dtype == "category" + + # unordered cat, but we allow this + df.sort_values(by=["unsort"], ascending=False) + + # multi-columns sort + # GH 7848 + df = DataFrame({"id": [6, 5, 4, 3, 2, 1], + "raw_grade": ['a', 'b', 'b', 'a', 'a', 'e']}) + df["grade"] = Categorical(df["raw_grade"], ordered=True) + df['grade'] = df['grade'].cat.set_categories(['b', 'e', 'a']) + + # sorts 'grade' according to the order of the categories + result = df.sort_values(by=['grade']) + expected = df.iloc[[1, 2, 5, 0, 3, 4]] + tm.assert_frame_equal(result, expected) + + # multi + result = df.sort_values(by=['grade', 'id']) + expected = df.iloc[[2, 1, 5, 4, 3, 0]] + tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/series/test_subclass.py b/pandas/tests/series/test_subclass.py index 3b1b8aca426e1..68a162ee4c287 100644 --- a/pandas/tests/series/test_subclass.py +++ b/pandas/tests/series/test_subclass.py @@ -1,68 +1,76 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 - import numpy as np + import pandas as pd +from pandas import SparseDtype import pandas.util.testing as tm -class TestSeriesSubclassing(tm.TestCase): +class TestSeriesSubclassing(object): def test_indexing_sliced(self): s = tm.SubclassedSeries([1, 2, 3, 4], index=list('abcd')) res = s.loc[['a', 'b']] exp = tm.SubclassedSeries([1, 2], index=list('ab')) tm.assert_series_equal(res, exp) - tm.assertIsInstance(res, tm.SubclassedSeries) res = s.iloc[[2, 3]] exp = tm.SubclassedSeries([3, 4], index=list('cd')) tm.assert_series_equal(res, exp) - tm.assertIsInstance(res, tm.SubclassedSeries) res = s.loc[['a', 'b']] exp = tm.SubclassedSeries([1, 2], index=list('ab')) tm.assert_series_equal(res, exp) - tm.assertIsInstance(res, tm.SubclassedSeries) def test_to_frame(self): s = tm.SubclassedSeries([1, 2, 3, 4], index=list('abcd'), name='xxx') res = s.to_frame() exp = tm.SubclassedDataFrame({'xxx': [1, 2, 3, 4]}, index=list('abcd')) tm.assert_frame_equal(res, exp) - tm.assertIsInstance(res, tm.SubclassedDataFrame) + + def test_subclass_unstack(self): + # GH 15564 + s = tm.SubclassedSeries( + [1, 2, 3, 4], index=[list('aabb'), list('xyxy')]) + + res = s.unstack() + exp = tm.SubclassedDataFrame( + {'x': [1, 3], 'y': [2, 4]}, index=['a', 'b']) + + tm.assert_frame_equal(res, exp) -class TestSparseSeriesSubclassing(tm.TestCase): +class TestSparseSeriesSubclassing(object): def test_subclass_sparse_slice(self): # int64 s = tm.SubclassedSparseSeries([1, 2, 3, 4, 5]) exp = tm.SubclassedSparseSeries([2, 3, 4], index=[1, 2, 3]) tm.assert_sp_series_equal(s.loc[1:3], exp) - self.assertEqual(s.loc[1:3].dtype, np.int64) + assert s.loc[1:3].dtype == SparseDtype(np.int64) exp = tm.SubclassedSparseSeries([2, 3], index=[1, 2]) tm.assert_sp_series_equal(s.iloc[1:3], exp) - self.assertEqual(s.iloc[1:3].dtype, np.int64) + assert s.iloc[1:3].dtype == SparseDtype(np.int64) exp = tm.SubclassedSparseSeries([2, 3], index=[1, 2]) tm.assert_sp_series_equal(s[1:3], exp) - self.assertEqual(s[1:3].dtype, np.int64) + assert s[1:3].dtype == SparseDtype(np.int64) # float64 s = tm.SubclassedSparseSeries([1., 2., 3., 4., 5.]) exp = tm.SubclassedSparseSeries([2., 3., 4.], index=[1, 2, 3]) tm.assert_sp_series_equal(s.loc[1:3], exp) - self.assertEqual(s.loc[1:3].dtype, np.float64) + assert s.loc[1:3].dtype == SparseDtype(np.float64) exp = tm.SubclassedSparseSeries([2., 3.], index=[1, 2]) tm.assert_sp_series_equal(s.iloc[1:3], exp) - self.assertEqual(s.iloc[1:3].dtype, np.float64) + assert s.iloc[1:3].dtype == SparseDtype(np.float64) exp = tm.SubclassedSparseSeries([2., 3.], index=[1, 2]) tm.assert_sp_series_equal(s[1:3], exp) - self.assertEqual(s[1:3].dtype, np.float64) + assert s[1:3].dtype == SparseDtype(np.float64) def test_subclass_sparse_addition(self): s1 = tm.SubclassedSparseSeries([1, 3, 5]) @@ -76,25 +84,25 @@ def test_subclass_sparse_addition(self): tm.assert_sp_series_equal(s1 + s2, exp) def test_subclass_sparse_to_frame(self): - s = tm.SubclassedSparseSeries([1, 2], index=list('abcd'), name='xxx') + s = tm.SubclassedSparseSeries([1, 2], index=list('ab'), name='xxx') res = s.to_frame() exp_arr = pd.SparseArray([1, 2], dtype=np.int64, kind='block', fill_value=0) exp = tm.SubclassedSparseDataFrame({'xxx': exp_arr}, - index=list('abcd'), + index=list('ab'), default_fill_value=0) tm.assert_sp_frame_equal(res, exp) # create from int dict res = tm.SubclassedSparseDataFrame({'xxx': [1, 2]}, - index=list('abcd'), + index=list('ab'), default_fill_value=0) tm.assert_sp_frame_equal(res, exp) - s = tm.SubclassedSparseSeries([1.1, 2.1], index=list('abcd'), + s = tm.SubclassedSparseSeries([1.1, 2.1], index=list('ab'), name='xxx') res = s.to_frame() exp = tm.SubclassedSparseDataFrame({'xxx': [1.1, 2.1]}, - index=list('abcd')) + index=list('ab')) tm.assert_sp_frame_equal(res, exp) diff --git a/pandas/tests/series/test_timeseries.py b/pandas/tests/series/test_timeseries.py index ce7d5a573bfab..b6896685dd474 100644 --- a/pandas/tests/series/test_timeseries.py +++ b/pandas/tests/series/test_timeseries.py @@ -1,23 +1,29 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 +from datetime import datetime, time, timedelta + import numpy as np -from datetime import datetime, timedelta, time +import pytest -import pandas as pd -import pandas.util.testing as tm from pandas._libs.tslib import iNaT -from pandas.compat import lrange, StringIO, product -from pandas.tseries.tdi import TimedeltaIndex -from pandas.tseries.index import DatetimeIndex -from pandas.tseries.offsets import BDay, BMonthEnd -from pandas import (Index, Series, date_range, NaT, concat, DataFrame, - Timestamp, to_datetime, offsets, - timedelta_range) -from pandas.util.testing import (assert_series_equal, assert_almost_equal, - assert_frame_equal, _skip_if_has_locale) +from pandas._libs.tslibs.np_datetime import OutOfBoundsDatetime +from pandas.compat import PY2, StringIO, lrange, product +from pandas.errors import NullFrequencyError +import pandas.util._test_decorators as td +import pandas as pd +from pandas import ( + DataFrame, Index, NaT, Series, Timestamp, concat, date_range, offsets, + timedelta_range, to_datetime) +from pandas.core.indexes.datetimes import DatetimeIndex +from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.tests.series.common import TestData +import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) + +from pandas.tseries.offsets import BDay, BMonthEnd def _simple_ts(start, end, freq='D'): @@ -31,7 +37,7 @@ def assert_range_equal(left, right): assert (left.tz == right.tz) -class TestTimeSeries(TestData, tm.TestCase): +class TestTimeSeries(TestData): def test_shift(self): shifted = self.ts.shift(1) @@ -39,7 +45,7 @@ def test_shift(self): tm.assert_index_equal(shifted.index, self.ts.index) tm.assert_index_equal(unshifted.index, self.ts.index) - tm.assert_numpy_array_equal(unshifted.valid().values, + tm.assert_numpy_array_equal(unshifted.dropna().values, self.ts.values[:-1]) offset = BDay() @@ -66,14 +72,16 @@ def test_shift(self): unshifted = shifted.shift(-1) tm.assert_index_equal(shifted.index, ps.index) tm.assert_index_equal(unshifted.index, ps.index) - tm.assert_numpy_array_equal(unshifted.valid().values, ps.values[:-1]) + tm.assert_numpy_array_equal(unshifted.dropna().values, ps.values[:-1]) shifted2 = ps.shift(1, 'B') shifted3 = ps.shift(1, BDay()) assert_series_equal(shifted2, shifted3) assert_series_equal(ps, shifted2.shift(-1, 'B')) - self.assertRaises(ValueError, ps.shift, freq='D') + msg = "Given freq D does not match PeriodIndex freq B" + with pytest.raises(ValueError, match=msg): + ps.shift(freq='D') # legacy support shifted4 = ps.shift(1, freq='B') @@ -104,7 +112,10 @@ def test_shift(self): # incompat tz s2 = Series(date_range('2000-01-01 09:00:00', periods=5, tz='CET'), name='foo') - self.assertRaises(ValueError, lambda: s - s2) + msg = ("DatetimeArray subtraction must have the same timezones or no" + " timezones") + with pytest.raises(TypeError, match=msg): + s - s2 def test_shift2(self): ts = Series(np.random.randn(5), @@ -120,7 +131,42 @@ def test_shift2(self): tm.assert_index_equal(result.index, exp_index) idx = DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-04']) - self.assertRaises(ValueError, idx.shift, 1) + msg = "Cannot shift with no freq" + with pytest.raises(NullFrequencyError, match=msg): + idx.shift(1) + + def test_shift_fill_value(self): + # GH #24128 + ts = Series([1.0, 2.0, 3.0, 4.0, 5.0], + index=date_range('1/1/2000', periods=5, freq='H')) + + exp = Series([0.0, 1.0, 2.0, 3.0, 4.0], + index=date_range('1/1/2000', periods=5, freq='H')) + # check that fill value works + result = ts.shift(1, fill_value=0.0) + tm.assert_series_equal(result, exp) + + exp = Series([0.0, 0.0, 1.0, 2.0, 3.0], + index=date_range('1/1/2000', periods=5, freq='H')) + result = ts.shift(2, fill_value=0.0) + tm.assert_series_equal(result, exp) + + ts = pd.Series([1, 2, 3]) + res = ts.shift(2, fill_value=0) + assert res.dtype == ts.dtype + + def test_categorical_shift_fill_value(self): + ts = pd.Series(['a', 'b', 'c', 'd'], dtype="category") + res = ts.shift(1, fill_value='a') + expected = pd.Series(pd.Categorical(['a', 'a', 'b', 'c'], + categories=['a', 'b', 'c', 'd'], + ordered=False)) + tm.assert_equal(res, expected) + + # check for incorrect fill_value + msg = "'fill_value=f' is not present in this Categorical's categories" + with pytest.raises(ValueError, match=msg): + ts.shift(1, fill_value='f') def test_shift_dst(self): # GH 13926 @@ -129,25 +175,25 @@ def test_shift_dst(self): res = s.shift(0) tm.assert_series_equal(res, s) - self.assertEqual(res.dtype, 'datetime64[ns, US/Eastern]') + assert res.dtype == 'datetime64[ns, US/Eastern]' res = s.shift(1) - exp_vals = [NaT] + dates.asobject.values.tolist()[:9] + exp_vals = [NaT] + dates.astype(object).values.tolist()[:9] exp = Series(exp_vals) tm.assert_series_equal(res, exp) - self.assertEqual(res.dtype, 'datetime64[ns, US/Eastern]') + assert res.dtype == 'datetime64[ns, US/Eastern]' res = s.shift(-2) - exp_vals = dates.asobject.values.tolist()[2:] + [NaT, NaT] + exp_vals = dates.astype(object).values.tolist()[2:] + [NaT, NaT] exp = Series(exp_vals) tm.assert_series_equal(res, exp) - self.assertEqual(res.dtype, 'datetime64[ns, US/Eastern]') + assert res.dtype == 'datetime64[ns, US/Eastern]' for ex in [10, -10, 20, -20]: res = s.shift(ex) exp = Series([NaT] * 10, dtype='datetime64[ns, US/Eastern]') tm.assert_series_equal(res, exp) - self.assertEqual(res.dtype, 'datetime64[ns, US/Eastern]') + assert res.dtype == 'datetime64[ns, US/Eastern]' def test_tshift(self): # PeriodIndex @@ -163,7 +209,9 @@ def test_tshift(self): shifted3 = ps.tshift(freq=BDay()) assert_series_equal(shifted, shifted3) - self.assertRaises(ValueError, ps.tshift, freq='M') + msg = "Given freq M does not match PeriodIndex freq B" + with pytest.raises(ValueError, match=msg): + ps.tshift(freq='M') # DatetimeIndex shifted = self.ts.tshift(1) @@ -182,7 +230,9 @@ def test_tshift(self): assert_series_equal(unshifted, inferred_ts) no_freq = self.ts[[0, 5, 7]] - self.assertRaises(ValueError, no_freq.tshift) + msg = "Freq was not given and was not set in the index" + with pytest.raises(ValueError, match=msg): + no_freq.tshift() def test_truncate(self): offset = BDay() @@ -230,9 +280,28 @@ def test_truncate(self): truncated = ts.truncate(before=self.ts.index[-1] + offset) assert (len(truncated) == 0) - self.assertRaises(ValueError, ts.truncate, - before=self.ts.index[-1] + offset, - after=self.ts.index[0] - offset) + msg = "Truncate: 1999-12-31 00:00:00 must be after 2000-02-14 00:00:00" + with pytest.raises(ValueError, match=msg): + ts.truncate(before=self.ts.index[-1] + offset, + after=self.ts.index[0] - offset) + + def test_truncate_nonsortedindex(self): + # GH 17935 + + s = pd.Series(['a', 'b', 'c', 'd', 'e'], + index=[5, 3, 2, 9, 0]) + msg = 'truncate requires a sorted index' + + with pytest.raises(ValueError, match=msg): + s.truncate(before=3, after=9) + + rng = pd.date_range('2011-01-01', '2012-01-01', freq='W') + ts = pd.Series(np.random.randn(len(rng)), index=rng) + msg = 'truncate requires a sorted index' + + with pytest.raises(ValueError, match=msg): + ts.sort_values(ascending=False).truncate(before='2011-11', + after='2011-12') def test_asfreq(self): ts = Series([0., 1., 2.], index=[datetime(2009, 10, 30), datetime( @@ -240,25 +309,25 @@ def test_asfreq(self): daily_ts = ts.asfreq('B') monthly_ts = daily_ts.asfreq('BM') - assert_series_equal(monthly_ts, ts) + tm.assert_series_equal(monthly_ts, ts) daily_ts = ts.asfreq('B', method='pad') monthly_ts = daily_ts.asfreq('BM') - assert_series_equal(monthly_ts, ts) + tm.assert_series_equal(monthly_ts, ts) daily_ts = ts.asfreq(BDay()) monthly_ts = daily_ts.asfreq(BMonthEnd()) - assert_series_equal(monthly_ts, ts) + tm.assert_series_equal(monthly_ts, ts) result = ts[:0].asfreq('M') - self.assertEqual(len(result), 0) - self.assertIsNot(result, ts) + assert len(result) == 0 + assert result is not ts daily_ts = ts.asfreq('D', fill_value=-1) result = daily_ts.value_counts().sort_index() expected = Series([60, 1, 1, 1], index=[-1.0, 2.0, 1.0, 0.0]).sort_index() - assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) def test_asfreq_datetimeindex_empty_series(self): # GH 14320 @@ -266,7 +335,7 @@ def test_asfreq_datetimeindex_empty_series(self): ["2016-09-29 11:00"])).asfreq('H') result = Series(index=pd.DatetimeIndex(["2016-09-29 11:00"]), data=[3]).asfreq('H') - self.assert_index_equal(expected.index, result.index) + tm.assert_index_equal(expected.index, result.index) def test_diff(self): # Just run the function @@ -278,7 +347,7 @@ def test_diff(self): s = Series([a, b]) rs = s.diff() - self.assertEqual(rs[1], 1) + assert rs[1] == 1 # neg n rs = self.ts.diff(-1) @@ -323,15 +392,43 @@ def test_pct_change(self): rs = self.ts.pct_change(freq='5D') filled = self.ts.fillna(method='pad') - assert_series_equal(rs, filled / filled.shift(freq='5D') - 1) + assert_series_equal(rs, + (filled / filled.shift(freq='5D') - 1) + .reindex_like(filled)) def test_pct_change_shift_over_nas(self): s = Series([1., 1.5, np.nan, 2.5, 3.]) chg = s.pct_change() - expected = Series([np.nan, 0.5, np.nan, 2.5 / 1.5 - 1, .2]) + expected = Series([np.nan, 0.5, 0., 2.5 / 1.5 - 1, .2]) assert_series_equal(chg, expected) + @pytest.mark.parametrize("freq, periods, fill_method, limit", + [('5B', 5, None, None), + ('3B', 3, None, None), + ('3B', 3, 'bfill', None), + ('7B', 7, 'pad', 1), + ('7B', 7, 'bfill', 3), + ('14B', 14, None, None)]) + def test_pct_change_periods_freq(self, freq, periods, fill_method, limit): + # GH 7292 + rs_freq = self.ts.pct_change(freq=freq, + fill_method=fill_method, + limit=limit) + rs_periods = self.ts.pct_change(periods, + fill_method=fill_method, + limit=limit) + assert_series_equal(rs_freq, rs_periods) + + empty_ts = Series(index=self.ts.index) + rs_freq = empty_ts.pct_change(freq=freq, + fill_method=fill_method, + limit=limit) + rs_periods = empty_ts.pct_change(periods, + fill_method=fill_method, + limit=limit) + assert_series_equal(rs_freq, rs_periods) + def test_autocorr(self): # Just run the function corr1 = self.ts.autocorr() @@ -341,10 +438,10 @@ def test_autocorr(self): # corr() with lag needs Series of at least length 2 if len(self.ts) <= 2: - self.assertTrue(np.isnan(corr1)) - self.assertTrue(np.isnan(corr2)) + assert np.isnan(corr1) + assert np.isnan(corr2) else: - self.assertEqual(corr1, corr2) + assert corr1 == corr2 # Choose a random lag between 1 and length of Series - 2 # and compare the result with the Series corr() function @@ -354,34 +451,43 @@ def test_autocorr(self): # corr() with lag needs Series of at least length 2 if len(self.ts) <= 2: - self.assertTrue(np.isnan(corr1)) - self.assertTrue(np.isnan(corr2)) + assert np.isnan(corr1) + assert np.isnan(corr2) else: - self.assertEqual(corr1, corr2) + assert corr1 == corr2 def test_first_last_valid(self): ts = self.ts.copy() ts[:5] = np.NaN index = ts.first_valid_index() - self.assertEqual(index, ts.index[5]) + assert index == ts.index[5] ts[-5:] = np.NaN index = ts.last_valid_index() - self.assertEqual(index, ts.index[-6]) + assert index == ts.index[-6] ts[:] = np.nan - self.assertIsNone(ts.last_valid_index()) - self.assertIsNone(ts.first_valid_index()) + assert ts.last_valid_index() is None + assert ts.first_valid_index() is None ser = Series([], index=[]) - self.assertIsNone(ser.last_valid_index()) - self.assertIsNone(ser.first_valid_index()) + assert ser.last_valid_index() is None + assert ser.first_valid_index() is None # GH12800 empty = Series() - self.assertIsNone(empty.last_valid_index()) - self.assertIsNone(empty.first_valid_index()) + assert empty.last_valid_index() is None + assert empty.first_valid_index() is None + + # GH20499: its preserves freq with holes + ts.index = date_range("20110101", periods=len(ts), freq="B") + ts.iloc[1] = 1 + ts.iloc[-2] = 1 + assert ts.first_valid_index() == ts.index[1] + assert ts.last_valid_index() == ts.index[-2] + assert ts.first_valid_index().freq == ts.index.freq + assert ts.last_valid_index().freq == ts.index.freq def test_mpl_compat_hack(self): result = self.ts[:, np.newaxis] @@ -391,17 +497,8 @@ def test_mpl_compat_hack(self): def test_timeseries_coercion(self): idx = tm.makeDateIndex(10000) ser = Series(np.random.randn(len(idx)), idx.astype(object)) - self.assertTrue(ser.index.is_all_dates) - self.assertIsInstance(ser.index, DatetimeIndex) - - def test_empty_series_ops(self): - # see issue #13844 - a = Series(dtype='M8[ns]') - b = Series(dtype='m8[ns]') - assert_series_equal(a, a + b) - assert_series_equal(a, a - b) - assert_series_equal(a, b + a) - self.assertRaises(TypeError, lambda x, y: x - y, b, a) + assert ser.index.is_all_dates + assert isinstance(ser.index, DatetimeIndex) def test_contiguous_boolean_preserve_freq(self): rng = date_range('1/1/2000', '3/1/2000', freq='B') @@ -411,12 +508,12 @@ def test_contiguous_boolean_preserve_freq(self): masked = rng[mask] expected = rng[10:20] - self.assertIsNotNone(expected.freq) + assert expected.freq is not None assert_range_equal(masked, expected) mask[22] = True masked = rng[mask] - self.assertIsNone(masked.freq) + assert masked.freq is None def test_to_datetime_unit(self): @@ -466,9 +563,11 @@ def test_to_datetime_unit(self): Timestamp('1970-01-03')] + ['NaT'] * 3) tm.assert_index_equal(result, expected) - with self.assertRaises(ValueError): + msg = "non convertible value foo with the unit 'D'" + with pytest.raises(ValueError, match=msg): to_datetime([1, 2, 'foo'], unit='D') - with self.assertRaises(ValueError): + msg = "cannot convert input 111111111 with the unit 'D'" + with pytest.raises(OutOfBoundsDatetime, match=msg): to_datetime([1, 2, 111111111], unit='D') # coerce we can process @@ -485,7 +584,7 @@ def test_series_ctor_datetime64(self): dates = np.asarray(rng) series = Series(dates) - self.assertTrue(np.issubdtype(series.dtype, np.dtype('M8[ns]'))) + assert np.issubdtype(series.dtype, np.dtype('M8[ns]')) def test_series_repr_nat(self): series = Series([0, 1000, 2000, iNaT], dtype='M8[ns]') @@ -496,7 +595,7 @@ def test_series_repr_nat(self): '2 1970-01-01 00:00:00.000002\n' '3 NaT\n' 'dtype: datetime64[ns]') - self.assertEqual(result, expected) + assert result == expected def test_asfreq_keep_index_name(self): # GH #9854 @@ -504,8 +603,8 @@ def test_asfreq_keep_index_name(self): index = pd.date_range('20130101', periods=20, name=index_name) df = pd.DataFrame([x for x in range(20)], columns=['foo'], index=index) - self.assertEqual(index_name, df.index.name) - self.assertEqual(index_name, df.asfreq('10D').index.name) + assert index_name == df.index.name + assert index_name == df.asfreq('10D').index.name def test_promote_datetime_date(self): rng = date_range('1/1/2000', periods=20) @@ -528,7 +627,7 @@ def test_promote_datetime_date(self): result = rng.get_indexer(ts2.index) expected = rng.get_indexer(ts_slice.index) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) def test_asfreq_normalize(self): rng = date_range('1/1/2000 09:30', periods=20) @@ -553,11 +652,11 @@ def test_asfreq_normalize(self): def test_first_subset(self): ts = _simple_ts('1/1/2000', '1/1/2010', freq='12h') result = ts.first('10d') - self.assertEqual(len(result), 20) + assert len(result) == 20 ts = _simple_ts('1/1/2000', '1/1/2010') result = ts.first('10d') - self.assertEqual(len(result), 10) + assert len(result) == 10 result = ts.first('3M') expected = ts[:'3/31/2000'] @@ -570,14 +669,21 @@ def test_first_subset(self): result = ts[:0].first('3M') assert_series_equal(result, ts[:0]) + def test_first_raises(self): + # GH20725 + ser = pd.Series('a b c'.split()) + msg = "'first' only supports a DatetimeIndex index" + with pytest.raises(TypeError, match=msg): + ser.first('1D') + def test_last_subset(self): ts = _simple_ts('1/1/2000', '1/1/2010', freq='12h') result = ts.last('10d') - self.assertEqual(len(result), 20) + assert len(result) == 20 ts = _simple_ts('1/1/2000', '1/1/2010') result = ts.last('10d') - self.assertEqual(len(result), 10) + assert len(result) == 10 result = ts.last('21D') expected = ts['12/12/2009':] @@ -590,6 +696,13 @@ def test_last_subset(self): result = ts[:0].last('3M') assert_series_equal(result, ts[:0]) + def test_last_raises(self): + # GH20725 + ser = pd.Series('a b c'.split()) + msg = "'last' only supports a DatetimeIndex index" + with pytest.raises(TypeError, match=msg): + ser.last('1D') + def test_format_pre_1900_dates(self): rng = date_range('1/1/1850', '1/1/1950', freq='A-DEC') rng.format() @@ -600,9 +713,9 @@ def test_at_time(self): rng = date_range('1/1/2000', '1/5/2000', freq='5min') ts = Series(np.random.randn(len(rng)), index=rng) rs = ts.at_time(rng[1]) - self.assertTrue((rs.index.hour == rng[1].hour).all()) - self.assertTrue((rs.index.minute == rng[1].minute).all()) - self.assertTrue((rs.index.second == rng[1].second).all()) + assert (rs.index.hour == rng[1].hour).all() + assert (rs.index.minute == rng[1].minute).all() + assert (rs.index.second == rng[1].second).all() result = ts.at_time('9:30') expected = ts.at_time(time(9, 30)) @@ -636,7 +749,14 @@ def test_at_time(self): rng = date_range('1/1/2012', freq='23Min', periods=384) ts = Series(np.random.randn(len(rng)), rng) rs = ts.at_time('16:00') - self.assertEqual(len(rs), 0) + assert len(rs) == 0 + + def test_at_time_raises(self): + # GH20725 + ser = pd.Series('a b c'.split()) + msg = "Index must be DatetimeIndex" + with pytest.raises(TypeError, match=msg): + ser.at_time('00:00') def test_between(self): series = Series(date_range('1/1/2000', periods=10)) @@ -661,18 +781,18 @@ def test_between_time(self): if not inc_end: exp_len -= 4 - self.assertEqual(len(filtered), exp_len) + assert len(filtered) == exp_len for rs in filtered.index: t = rs.time() if inc_start: - self.assertTrue(t >= stime) + assert t >= stime else: - self.assertTrue(t > stime) + assert t > stime if inc_end: - self.assertTrue(t <= etime) + assert t <= etime else: - self.assertTrue(t < etime) + assert t < etime result = ts.between_time('00:00', '01:00') expected = ts.between_time(stime, etime) @@ -693,37 +813,48 @@ def test_between_time(self): if not inc_end: exp_len -= 4 - self.assertEqual(len(filtered), exp_len) + assert len(filtered) == exp_len for rs in filtered.index: t = rs.time() if inc_start: - self.assertTrue((t >= stime) or (t <= etime)) + assert (t >= stime) or (t <= etime) else: - self.assertTrue((t > stime) or (t <= etime)) + assert (t > stime) or (t <= etime) if inc_end: - self.assertTrue((t <= etime) or (t >= stime)) + assert (t <= etime) or (t >= stime) else: - self.assertTrue((t < etime) or (t >= stime)) + assert (t < etime) or (t >= stime) + + def test_between_time_raises(self): + # GH20725 + ser = pd.Series('a b c'.split()) + msg = "Index must be DatetimeIndex" + with pytest.raises(TypeError, match=msg): + ser.between_time(start_time='00:00', end_time='12:00') def test_between_time_types(self): # GH11818 rng = date_range('1/1/2000', '1/5/2000', freq='5min') - self.assertRaises(ValueError, rng.indexer_between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + msg = (r"Cannot convert arg \[datetime\.datetime\(2010, 1, 2, 1, 0\)\]" + " to a time") + with pytest.raises(ValueError, match=msg): + rng.indexer_between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) frame = DataFrame({'A': 0}, index=rng) - self.assertRaises(ValueError, frame.between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + with pytest.raises(ValueError, match=msg): + frame.between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) series = Series(0, index=rng) - self.assertRaises(ValueError, series.between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + with pytest.raises(ValueError, match=msg): + series.between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) + @td.skip_if_has_locale def test_between_time_formats(self): # GH11818 - _skip_if_has_locale() - rng = date_range('1/1/2000', '1/5/2000', freq='5min') ts = DataFrame(np.random.randn(len(rng), 2), index=rng) @@ -734,12 +865,25 @@ def test_between_time_formats(self): expected_length = 28 for time_string in strings: - self.assertEqual(len(ts.between_time(*time_string)), - expected_length, - "%s - %s" % time_string) + assert len(ts.between_time(*time_string)) == expected_length + + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") + def test_between_time_axis(self): + # issue 8839 + rng = date_range('1/1/2000', periods=100, freq='10min') + ts = Series(np.random.randn(len(rng)), index=rng) + stime, etime = ('08:00:00', '09:00:00') + expected_length = 7 + + assert len(ts.between_time(stime, etime)) == expected_length + assert len(ts.between_time(stime, etime, axis=0)) == expected_length + msg = ("No axis named 1 for object type" + " ") + with pytest.raises(ValueError, match=msg): + ts.between_time(stime, etime, axis=1) def test_to_period(self): - from pandas.tseries.period import period_range + from pandas.core.indexes.period import period_range ts = _simple_ts('1/1/2000', '1/1/2001') @@ -794,7 +938,7 @@ def test_to_csv_numpy_16_bug(self): frame.to_csv(buf) result = buf.getvalue() - self.assertIn('2000-01-01', result) + assert '2000-01-01' in result def test_series_map_box_timedelta(self): # GH 11349 @@ -815,96 +959,67 @@ def test_asfreq_resample_set_correct_freq(self): df = df.set_index(pd.to_datetime(df.date)) # testing the settings before calling .asfreq() and .resample() - self.assertEqual(df.index.freq, None) - self.assertEqual(df.index.inferred_freq, 'D') + assert df.index.freq is None + assert df.index.inferred_freq == 'D' # does .asfreq() set .freq correctly? - self.assertEqual(df.asfreq('D').index.freq, 'D') + assert df.asfreq('D').index.freq == 'D' # does .resample() set .freq correctly? - self.assertEqual(df.resample('D').asfreq().index.freq, 'D') + assert df.resample('D').asfreq().index.freq == 'D' def test_pickle(self): # GH4606 - p = self.round_trip_pickle(NaT) - self.assertTrue(p is NaT) + p = tm.round_trip_pickle(NaT) + assert p is NaT idx = pd.to_datetime(['2013-01-01', NaT, '2014-01-06']) - idx_p = self.round_trip_pickle(idx) - self.assertTrue(idx_p[0] == idx[0]) - self.assertTrue(idx_p[1] is NaT) - self.assertTrue(idx_p[2] == idx[2]) + idx_p = tm.round_trip_pickle(idx) + assert idx_p[0] == idx[0] + assert idx_p[1] is NaT + assert idx_p[2] == idx[2] # GH11002 # don't infer freq idx = date_range('1750-1-1', '2050-1-1', freq='7D') - idx_p = self.round_trip_pickle(idx) + idx_p = tm.round_trip_pickle(idx) tm.assert_index_equal(idx, idx_p) - def test_setops_preserve_freq(self): - for tz in [None, 'Asia/Tokyo', 'US/Eastern']: - rng = date_range('1/1/2000', '1/1/2002', name='idx', tz=tz) - - result = rng[:50].union(rng[50:100]) - self.assertEqual(result.name, rng.name) - self.assertEqual(result.freq, rng.freq) - self.assertEqual(result.tz, rng.tz) - - result = rng[:50].union(rng[30:100]) - self.assertEqual(result.name, rng.name) - self.assertEqual(result.freq, rng.freq) - self.assertEqual(result.tz, rng.tz) - - result = rng[:50].union(rng[60:100]) - self.assertEqual(result.name, rng.name) - self.assertIsNone(result.freq) - self.assertEqual(result.tz, rng.tz) - - result = rng[:50].intersection(rng[25:75]) - self.assertEqual(result.name, rng.name) - self.assertEqual(result.freqstr, 'D') - self.assertEqual(result.tz, rng.tz) - - nofreq = DatetimeIndex(list(rng[25:75]), name='other') - result = rng[:50].union(nofreq) - self.assertIsNone(result.name) - self.assertEqual(result.freq, rng.freq) - self.assertEqual(result.tz, rng.tz) - - result = rng[:50].intersection(nofreq) - self.assertIsNone(result.name) - self.assertEqual(result.freq, rng.freq) - self.assertEqual(result.tz, rng.tz) - - def test_min_max(self): - rng = date_range('1/1/2000', '12/31/2000') - rng2 = rng.take(np.random.permutation(len(rng))) - - the_min = rng2.min() - the_max = rng2.max() - tm.assertIsInstance(the_min, Timestamp) - tm.assertIsInstance(the_max, Timestamp) - self.assertEqual(the_min, rng[0]) - self.assertEqual(the_max, rng[-1]) - - self.assertEqual(rng.min(), rng[0]) - self.assertEqual(rng.max(), rng[-1]) - - def test_min_max_series(self): - rng = date_range('1/1/2000', periods=10, freq='4h') - lvls = ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C'] - df = DataFrame({'TS': rng, 'V': np.random.randn(len(rng)), 'L': lvls}) - - result = df.TS.max() - exp = Timestamp(df.TS.iat[-1]) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, exp) - - result = df.TS.min() - exp = Timestamp(df.TS.iat[0]) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, exp) + @pytest.mark.parametrize('tz', [None, 'Asia/Tokyo', 'US/Eastern']) + def test_setops_preserve_freq(self, tz): + rng = date_range('1/1/2000', '1/1/2002', name='idx', tz=tz) + + result = rng[:50].union(rng[50:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].union(rng[30:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].union(rng[60:100]) + assert result.name == rng.name + assert result.freq is None + assert result.tz == rng.tz + + result = rng[:50].intersection(rng[25:75]) + assert result.name == rng.name + assert result.freqstr == 'D' + assert result.tz == rng.tz + + nofreq = DatetimeIndex(list(rng[25:75]), name='other') + result = rng[:50].union(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].intersection(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz def test_from_M8_structured(self): dates = [(datetime(2012, 9, 9, 0, 0), datetime(2012, 9, 8, 15, 10))] @@ -912,23 +1027,75 @@ def test_from_M8_structured(self): dtype=[('Date', 'M8[us]'), ('Forecasting', 'M8[us]')]) df = DataFrame(arr) - self.assertEqual(df['Date'][0], dates[0][0]) - self.assertEqual(df['Forecasting'][0], dates[0][1]) + assert df['Date'][0] == dates[0][0] + assert df['Forecasting'][0] == dates[0][1] s = Series(arr['Date']) - self.assertTrue(s[0], Timestamp) - self.assertEqual(s[0], dates[0][0]) + assert isinstance(s[0], Timestamp) + assert s[0] == dates[0][0] - s = Series.from_array(arr['Date'], Index([0])) - self.assertEqual(s[0], dates[0][0]) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + s = Series.from_array(arr['Date'], Index([0])) + assert s[0] == dates[0][0] def test_get_level_values_box(self): from pandas import MultiIndex dates = date_range('1/1/2000', periods=4) levels = [dates, [0, 1]] - labels = [[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 0, 1, 0, 1]] - - index = MultiIndex(levels=levels, labels=labels) - - self.assertTrue(isinstance(index.get_level_values(0)[0], Timestamp)) + codes = [[0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 0, 1, 0, 1, 0, 1]] + + index = MultiIndex(levels=levels, codes=codes) + + assert isinstance(index.get_level_values(0)[0], Timestamp) + + def test_view_tz(self): + # GH#24024 + ser = pd.Series(pd.date_range('2000', periods=4, tz='US/Central')) + result = ser.view("i8") + expected = pd.Series([946706400000000000, + 946792800000000000, + 946879200000000000, + 946965600000000000]) + tm.assert_series_equal(result, expected) + + def test_asarray_tz_naive(self): + # This shouldn't produce a warning. + ser = pd.Series(pd.date_range('2000', periods=2)) + expected = np.array(['2000-01-01', '2000-01-02'], dtype='M8[ns]') + with tm.assert_produces_warning(None): + result = np.asarray(ser) + + tm.assert_numpy_array_equal(result, expected) + + # optionally, object + with tm.assert_produces_warning(None): + result = np.asarray(ser, dtype=object) + + expected = np.array([pd.Timestamp('2000-01-01'), + pd.Timestamp('2000-01-02')]) + tm.assert_numpy_array_equal(result, expected) + + def test_asarray_tz_aware(self): + tz = 'US/Central' + ser = pd.Series(pd.date_range('2000', periods=2, tz=tz)) + expected = np.array(['2000-01-01T06', '2000-01-02T06'], dtype='M8[ns]') + # We warn by default and return an ndarray[M8[ns]] + with tm.assert_produces_warning(FutureWarning): + result = np.asarray(ser) + + tm.assert_numpy_array_equal(result, expected) + + # Old behavior with no warning + with tm.assert_produces_warning(None): + result = np.asarray(ser, dtype="M8[ns]") + + tm.assert_numpy_array_equal(result, expected) + + # Future behavior with no warning + expected = np.array([pd.Timestamp("2000-01-01", tz=tz), + pd.Timestamp("2000-01-02", tz=tz)]) + with tm.assert_produces_warning(None): + result = np.asarray(ser, dtype=object) + + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/series/test_timezones.py b/pandas/tests/series/test_timezones.py new file mode 100644 index 0000000000000..ec644a8e93da2 --- /dev/null +++ b/pandas/tests/series/test_timezones.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +""" +Tests for Series timezone-related methods +""" +from datetime import datetime + +from dateutil.tz import tzoffset +import numpy as np +import pytest +import pytz + +from pandas._libs.tslibs import conversion, timezones +from pandas.compat import lrange + +from pandas import DatetimeIndex, Index, NaT, Series, Timestamp +from pandas.core.indexes.datetimes import date_range +import pandas.util.testing as tm + + +class TestSeriesTimezones(object): + # ----------------------------------------------------------------- + # Series.tz_localize + def test_series_tz_localize(self): + + rng = date_range('1/1/2011', periods=100, freq='H') + ts = Series(1, index=rng) + + result = ts.tz_localize('utc') + assert result.index.tz.zone == 'UTC' + + # Can't localize if already tz-aware + rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') + ts = Series(1, index=rng) + + with pytest.raises(TypeError, match='Already tz-aware'): + ts.tz_localize('US/Eastern') + + @pytest.mark.filterwarnings('ignore::FutureWarning') + def test_tz_localize_errors_deprecation(self): + # GH 22644 + tz = 'Europe/Warsaw' + n = 60 + rng = date_range(start='2015-03-29 02:00:00', periods=n, freq='min') + ts = Series(rng) + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + with pytest.raises(ValueError): + ts.dt.tz_localize(tz, errors='foo') + # make sure errors='coerce' gets mapped correctly to nonexistent + result = ts.dt.tz_localize(tz, errors='coerce') + expected = ts.dt.tz_localize(tz, nonexistent='NaT') + tm.assert_series_equal(result, expected) + + def test_series_tz_localize_ambiguous_bool(self): + # make sure that we are correctly accepting bool values as ambiguous + + # GH#14402 + ts = Timestamp('2015-11-01 01:00:03') + expected0 = Timestamp('2015-11-01 01:00:03-0500', tz='US/Central') + expected1 = Timestamp('2015-11-01 01:00:03-0600', tz='US/Central') + + ser = Series([ts]) + expected0 = Series([expected0]) + expected1 = Series([expected1]) + + with pytest.raises(pytz.AmbiguousTimeError): + ser.dt.tz_localize('US/Central') + + result = ser.dt.tz_localize('US/Central', ambiguous=True) + tm.assert_series_equal(result, expected0) + + result = ser.dt.tz_localize('US/Central', ambiguous=[True]) + tm.assert_series_equal(result, expected0) + + result = ser.dt.tz_localize('US/Central', ambiguous=False) + tm.assert_series_equal(result, expected1) + + result = ser.dt.tz_localize('US/Central', ambiguous=[False]) + tm.assert_series_equal(result, expected1) + + @pytest.mark.parametrize('tz', ['Europe/Warsaw', 'dateutil/Europe/Warsaw']) + @pytest.mark.parametrize('method, exp', [ + ['shift_forward', '2015-03-29 03:00:00'], + ['NaT', NaT], + ['raise', None], + ['foo', 'invalid'] + ]) + def test_series_tz_localize_nonexistent(self, tz, method, exp): + # GH 8917 + n = 60 + dti = date_range(start='2015-03-29 02:00:00', periods=n, freq='min') + s = Series(1, dti) + if method == 'raise': + with pytest.raises(pytz.NonExistentTimeError): + s.tz_localize(tz, nonexistent=method) + elif exp == 'invalid': + with pytest.raises(ValueError): + dti.tz_localize(tz, nonexistent=method) + else: + result = s.tz_localize(tz, nonexistent=method) + expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz)) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern']) + def test_series_tz_localize_empty(self, tzstr): + # GH#2248 + ser = Series() + + ser2 = ser.tz_localize('utc') + assert ser2.index.tz == pytz.utc + + ser2 = ser.tz_localize(tzstr) + timezones.tz_compare(ser2.index.tz, timezones.maybe_get_tz(tzstr)) + + # ----------------------------------------------------------------- + # Series.tz_convert + + def test_series_tz_convert(self): + rng = date_range('1/1/2011', periods=200, freq='D', tz='US/Eastern') + ts = Series(1, index=rng) + + result = ts.tz_convert('Europe/Berlin') + assert result.index.tz.zone == 'Europe/Berlin' + + # can't convert tz-naive + rng = date_range('1/1/2011', periods=200, freq='D') + ts = Series(1, index=rng) + + with pytest.raises(TypeError, match="Cannot convert tz-naive"): + ts.tz_convert('US/Eastern') + + def test_series_tz_convert_to_utc(self): + base = DatetimeIndex(['2011-01-01', '2011-01-02', '2011-01-03'], + tz='UTC') + idx1 = base.tz_convert('Asia/Tokyo')[:2] + idx2 = base.tz_convert('US/Eastern')[1:] + + res = Series([1, 2], index=idx1) + Series([1, 1], index=idx2) + tm.assert_series_equal(res, Series([np.nan, 3, np.nan], index=base)) + + # ----------------------------------------------------------------- + # Series.append + + def test_series_append_aware(self): + rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', + tz='US/Eastern') + rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', + tz='US/Eastern') + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex(['2011-01-01 01:00', '2011-01-01 02:00'], + tz='US/Eastern') + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + assert ts_result.index.tz == rng1.tz + + rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', tz='UTC') + rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', tz='UTC') + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex(['2011-01-01 01:00', '2011-01-01 02:00'], + tz='UTC') + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + utc = rng1.tz + assert utc == ts_result.index.tz + + # GH#7795 + # different tz coerces to object dtype, not UTC + rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', + tz='US/Eastern') + rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', + tz='US/Central') + ser1 = Series([1], index=rng1) + ser2 = Series([2], index=rng2) + ts_result = ser1.append(ser2) + exp_index = Index([Timestamp('1/1/2011 01:00', tz='US/Eastern'), + Timestamp('1/1/2011 02:00', tz='US/Central')]) + exp = Series([1, 2], index=exp_index) + tm.assert_series_equal(ts_result, exp) + + def test_series_append_aware_naive(self): + rng1 = date_range('1/1/2011 01:00', periods=1, freq='H') + rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', + tz='US/Eastern') + ser1 = Series(np.random.randn(len(rng1)), index=rng1) + ser2 = Series(np.random.randn(len(rng2)), index=rng2) + ts_result = ser1.append(ser2) + + expected = ser1.index.astype(object).append(ser2.index.astype(object)) + assert ts_result.index.equals(expected) + + # mixed + rng1 = date_range('1/1/2011 01:00', periods=1, freq='H') + rng2 = lrange(100) + ser1 = Series(np.random.randn(len(rng1)), index=rng1) + ser2 = Series(np.random.randn(len(rng2)), index=rng2) + ts_result = ser1.append(ser2) + + expected = ser1.index.astype(object).append(ser2.index) + assert ts_result.index.equals(expected) + + def test_series_append_dst(self): + rng1 = date_range('1/1/2016 01:00', periods=3, freq='H', + tz='US/Eastern') + rng2 = date_range('8/1/2016 01:00', periods=3, freq='H', + tz='US/Eastern') + ser1 = Series([1, 2, 3], index=rng1) + ser2 = Series([10, 11, 12], index=rng2) + ts_result = ser1.append(ser2) + + exp_index = DatetimeIndex(['2016-01-01 01:00', '2016-01-01 02:00', + '2016-01-01 03:00', '2016-08-01 01:00', + '2016-08-01 02:00', '2016-08-01 03:00'], + tz='US/Eastern') + exp = Series([1, 2, 3, 10, 11, 12], index=exp_index) + tm.assert_series_equal(ts_result, exp) + assert ts_result.index.tz == rng1.tz + + # ----------------------------------------------------------------- + + def test_dateutil_tzoffset_support(self): + values = [188.5, 328.25] + tzinfo = tzoffset(None, 7200) + index = [datetime(2012, 5, 11, 11, tzinfo=tzinfo), + datetime(2012, 5, 11, 12, tzinfo=tzinfo)] + series = Series(data=values, index=index) + + assert series.index.tz == tzinfo + + # it works! #2443 + repr(series.index[0]) + + @pytest.mark.parametrize('tz', ['US/Eastern', 'dateutil/US/Eastern']) + def test_tz_aware_asfreq(self, tz): + dr = date_range('2011-12-01', '2012-07-20', freq='D', tz=tz) + + ser = Series(np.random.randn(len(dr)), index=dr) + + # it works! + ser.asfreq('T') + + @pytest.mark.parametrize('tz', ['US/Eastern', 'dateutil/US/Eastern']) + def test_string_index_alias_tz_aware(self, tz): + rng = date_range('1/1/2000', periods=10, tz=tz) + ser = Series(np.random.randn(len(rng)), index=rng) + + result = ser['1/3/2000'] + tm.assert_almost_equal(result, ser[2]) + + # TODO: De-duplicate with test below + def test_series_add_tz_mismatch_converts_to_utc_duplicate(self): + rng = date_range('1/1/2011', periods=10, freq='H', tz='US/Eastern') + ser = Series(np.random.randn(len(rng)), index=rng) + + ts_moscow = ser.tz_convert('Europe/Moscow') + + result = ser + ts_moscow + assert result.index.tz is pytz.utc + + result = ts_moscow + ser + assert result.index.tz is pytz.utc + + def test_series_add_tz_mismatch_converts_to_utc(self): + rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') + + perm = np.random.permutation(100)[:90] + ser1 = Series(np.random.randn(90), + index=rng.take(perm).tz_convert('US/Eastern')) + + perm = np.random.permutation(100)[:90] + ser2 = Series(np.random.randn(90), + index=rng.take(perm).tz_convert('Europe/Berlin')) + + result = ser1 + ser2 + + uts1 = ser1.tz_convert('utc') + uts2 = ser2.tz_convert('utc') + expected = uts1 + uts2 + + assert result.index.tz == pytz.UTC + tm.assert_series_equal(result, expected) + + def test_series_add_aware_naive_raises(self): + rng = date_range('1/1/2011', periods=10, freq='H') + ser = Series(np.random.randn(len(rng)), index=rng) + + ser_utc = ser.tz_localize('utc') + + with pytest.raises(Exception): + ser + ser_utc + + with pytest.raises(Exception): + ser_utc + ser + + def test_series_align_aware(self): + idx1 = date_range('2001', periods=5, freq='H', tz='US/Eastern') + ser = Series(np.random.randn(len(idx1)), index=idx1) + ser_central = ser.tz_convert('US/Central') + # # different timezones convert to UTC + + new1, new2 = ser.align(ser_central) + assert new1.index.tz == pytz.UTC + assert new2.index.tz == pytz.UTC + + @pytest.mark.parametrize('tzstr', ['US/Eastern', 'dateutil/US/Eastern']) + def test_localized_at_time_between_time(self, tzstr): + from datetime import time + tz = timezones.maybe_get_tz(tzstr) + + rng = date_range('4/16/2012', '5/1/2012', freq='H') + ts = Series(np.random.randn(len(rng)), index=rng) + + ts_local = ts.tz_localize(tzstr) + + result = ts_local.at_time(time(10, 0)) + expected = ts.at_time(time(10, 0)).tz_localize(tzstr) + tm.assert_series_equal(result, expected) + assert timezones.tz_compare(result.index.tz, tz) + + t1, t2 = time(10, 0), time(11, 0) + result = ts_local.between_time(t1, t2) + expected = ts.between_time(t1, t2).tz_localize(tzstr) + tm.assert_series_equal(result, expected) + assert timezones.tz_compare(result.index.tz, tz) + + @pytest.mark.parametrize('tzstr', ['Europe/Berlin', + 'dateutil/Europe/Berlin']) + def test_getitem_pydatetime_tz(self, tzstr): + tz = timezones.maybe_get_tz(tzstr) + + index = date_range(start='2012-12-24 16:00', end='2012-12-24 18:00', + freq='H', tz=tzstr) + ts = Series(index=index, data=index.hour) + time_pandas = Timestamp('2012-12-24 17:00', tz=tzstr) + + dt = datetime(2012, 12, 24, 17, 0) + time_datetime = conversion.localize_pydatetime(dt, tz) + assert ts[time_pandas] == ts[time_datetime] + + def test_series_truncate_datetimeindex_tz(self): + # GH 9243 + idx = date_range('4/1/2005', '4/30/2005', freq='D', tz='US/Pacific') + s = Series(range(len(idx)), index=idx) + result = s.truncate(datetime(2005, 4, 2), datetime(2005, 4, 4)) + expected = Series([1, 2, 3], index=idx[1:4]) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('copy', [True, False]) + @pytest.mark.parametrize('method, tz', [ + ['tz_localize', None], + ['tz_convert', 'Europe/Berlin'] + ]) + def test_tz_localize_convert_copy_inplace_mutate(self, copy, method, tz): + # GH 6326 + result = Series(np.arange(0, 5), + index=date_range('20131027', periods=5, freq='1H', + tz=tz)) + getattr(result, method)('UTC', copy=copy) + expected = Series(np.arange(0, 5), + index=date_range('20131027', periods=5, freq='1H', + tz=tz)) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_validate.py b/pandas/tests/series/test_validate.py index cf0482b41c80a..8f7c16f2c3132 100644 --- a/pandas/tests/series/test_validate.py +++ b/pandas/tests/series/test_validate.py @@ -1,33 +1,19 @@ -from unittest import TestCase -from pandas.core.series import Series +import pytest -class TestSeriesValidate(TestCase): +class TestSeriesValidate(object): """Tests for error handling related to data types of method arguments.""" - s = Series([1, 2, 3, 4, 5]) - def test_validate_bool_args(self): - # Tests for error handling related to boolean arguments. - invalid_values = [1, "True", [1, 2, 3], 5.0] + @pytest.mark.parametrize("func", ["reset_index", "_set_name", + "sort_values", "sort_index", + "rename", "dropna"]) + @pytest.mark.parametrize("inplace", [1, "True", [1, 2, 3], 5.0]) + def test_validate_bool_args(self, string_series, func, inplace): + msg = "For argument \"inplace\" expected type bool" + kwargs = dict(inplace=inplace) - for value in invalid_values: - with self.assertRaises(ValueError): - self.s.reset_index(inplace=value) + if func == "_set_name": + kwargs["name"] = "hello" - with self.assertRaises(ValueError): - self.s._set_name(name='hello', inplace=value) - - with self.assertRaises(ValueError): - self.s.sort_values(inplace=value) - - with self.assertRaises(ValueError): - self.s.sort_index(inplace=value) - - with self.assertRaises(ValueError): - self.s.sort_index(inplace=value) - - with self.assertRaises(ValueError): - self.s.rename(inplace=value) - - with self.assertRaises(ValueError): - self.s.dropna(inplace=value) + with pytest.raises(ValueError, match=msg): + getattr(string_series, func)(**kwargs) diff --git a/pandas/tests/sparse/common.py b/pandas/tests/sparse/common.py index 3aeef8d436e1a..e69de29bb2d1d 100644 --- a/pandas/tests/sparse/common.py +++ b/pandas/tests/sparse/common.py @@ -1,10 +0,0 @@ -import pytest - -import pandas.util.testing as tm - - -@pytest.fixture(params=['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil']) -def spmatrix(request): - tm._skip_if_no_scipy() - from scipy import sparse - return getattr(sparse, request.param + '_matrix') diff --git a/pandas/tests/sparse/frame/__init__.py b/pandas/tests/sparse/frame/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/sparse/frame/conftest.py b/pandas/tests/sparse/frame/conftest.py new file mode 100644 index 0000000000000..3423260c1720a --- /dev/null +++ b/pandas/tests/sparse/frame/conftest.py @@ -0,0 +1,115 @@ +import numpy as np +import pytest + +from pandas import DataFrame, SparseArray, SparseDataFrame, bdate_range + +data = {'A': [np.nan, np.nan, np.nan, 0, 1, 2, 3, 4, 5, 6], + 'B': [0, 1, 2, np.nan, np.nan, np.nan, 3, 4, 5, 6], + 'C': np.arange(10, dtype=np.float64), + 'D': [0, 1, 2, 3, 4, 5, np.nan, np.nan, np.nan, np.nan]} +dates = bdate_range('1/1/2011', periods=10) + + +# fixture names must be compatible with the tests in +# tests/frame/test_api.SharedWithSparse + +@pytest.fixture +def float_frame_dense(): + """ + Fixture for dense DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; some entries are missing + """ + return DataFrame(data, index=dates) + + +@pytest.fixture +def float_frame(): + """ + Fixture for sparse DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; some entries are missing + """ + # default_kind='block' is the default + return SparseDataFrame(data, index=dates, default_kind='block') + + +@pytest.fixture +def float_frame_int_kind(): + """ + Fixture for sparse DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D'] and default_kind='integer'. + Some entries are missing. + """ + return SparseDataFrame(data, index=dates, default_kind='integer') + + +@pytest.fixture +def float_string_frame(): + """ + Fixture for sparse DataFrame of floats and strings with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D', 'foo']; some entries are missing + """ + sdf = SparseDataFrame(data, index=dates) + sdf['foo'] = SparseArray(['bar'] * len(dates)) + return sdf + + +@pytest.fixture +def float_frame_fill0_dense(): + """ + Fixture for dense DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; missing entries have been filled with 0 + """ + values = SparseDataFrame(data).values + values[np.isnan(values)] = 0 + return DataFrame(values, columns=['A', 'B', 'C', 'D'], index=dates) + + +@pytest.fixture +def float_frame_fill0(): + """ + Fixture for sparse DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; missing entries have been filled with 0 + """ + values = SparseDataFrame(data).values + values[np.isnan(values)] = 0 + return SparseDataFrame(values, columns=['A', 'B', 'C', 'D'], + default_fill_value=0, index=dates) + + +@pytest.fixture +def float_frame_fill2_dense(): + """ + Fixture for dense DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; missing entries have been filled with 2 + """ + values = SparseDataFrame(data).values + values[np.isnan(values)] = 2 + return DataFrame(values, columns=['A', 'B', 'C', 'D'], index=dates) + + +@pytest.fixture +def float_frame_fill2(): + """ + Fixture for sparse DataFrame of floats with DatetimeIndex + + Columns are ['A', 'B', 'C', 'D']; missing entries have been filled with 2 + """ + values = SparseDataFrame(data).values + values[np.isnan(values)] = 2 + return SparseDataFrame(values, columns=['A', 'B', 'C', 'D'], + default_fill_value=2, index=dates) + + +@pytest.fixture +def empty_frame(): + """ + Fixture for empty SparseDataFrame + """ + return SparseDataFrame() diff --git a/pandas/tests/sparse/frame/test_analytics.py b/pandas/tests/sparse/frame/test_analytics.py new file mode 100644 index 0000000000000..95c1c8c453d0a --- /dev/null +++ b/pandas/tests/sparse/frame/test_analytics.py @@ -0,0 +1,39 @@ +import numpy as np +import pytest + +from pandas import DataFrame, SparseDataFrame, SparseSeries +from pandas.util import testing as tm + + +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_quantile(): + # GH 17386 + data = [[1, 1], [2, 10], [3, 100], [np.nan, np.nan]] + q = 0.1 + + sparse_df = SparseDataFrame(data) + result = sparse_df.quantile(q) + + dense_df = DataFrame(data) + dense_expected = dense_df.quantile(q) + sparse_expected = SparseSeries(dense_expected) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) + + +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_quantile_multi(): + # GH 17386 + data = [[1, 1], [2, 10], [3, 100], [np.nan, np.nan]] + q = [0.1, 0.5] + + sparse_df = SparseDataFrame(data) + result = sparse_df.quantile(q) + + dense_df = DataFrame(data) + dense_expected = dense_df.quantile(q) + sparse_expected = SparseDataFrame(dense_expected) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) diff --git a/pandas/tests/sparse/frame/test_apply.py b/pandas/tests/sparse/frame/test_apply.py new file mode 100644 index 0000000000000..b5ea0a5c90e1a --- /dev/null +++ b/pandas/tests/sparse/frame/test_apply.py @@ -0,0 +1,105 @@ +import numpy as np +import pytest + +from pandas import DataFrame, Series, SparseDataFrame, bdate_range +from pandas.core import nanops +from pandas.core.sparse.api import SparseDtype +from pandas.util import testing as tm + + +@pytest.fixture +def dates(): + return bdate_range('1/1/2011', periods=10) + + +@pytest.fixture +def empty(): + return SparseDataFrame() + + +@pytest.fixture +def frame(dates): + data = {'A': [np.nan, np.nan, np.nan, 0, 1, 2, 3, 4, 5, 6], + 'B': [0, 1, 2, np.nan, np.nan, np.nan, 3, 4, 5, 6], + 'C': np.arange(10, dtype=np.float64), + 'D': [0, 1, 2, 3, 4, 5, np.nan, np.nan, np.nan, np.nan]} + + return SparseDataFrame(data, index=dates) + + +@pytest.fixture +def fill_frame(frame): + values = frame.values.copy() + values[np.isnan(values)] = 2 + + return SparseDataFrame(values, columns=['A', 'B', 'C', 'D'], + default_fill_value=2, + index=frame.index) + + +def test_apply(frame): + applied = frame.apply(np.sqrt) + assert isinstance(applied, SparseDataFrame) + tm.assert_almost_equal(applied.values, np.sqrt(frame.values)) + + # agg / broadcast + with tm.assert_produces_warning(FutureWarning): + broadcasted = frame.apply(np.sum, broadcast=True) + assert isinstance(broadcasted, SparseDataFrame) + + with tm.assert_produces_warning(FutureWarning): + exp = frame.to_dense().apply(np.sum, broadcast=True) + tm.assert_frame_equal(broadcasted.to_dense(), exp) + + applied = frame.apply(np.sum) + tm.assert_series_equal(applied, + frame.to_dense().apply(nanops.nansum).to_sparse()) + + +def test_apply_fill(fill_frame): + applied = fill_frame.apply(np.sqrt) + assert applied['A'].fill_value == np.sqrt(2) + + +def test_apply_empty(empty): + assert empty.apply(np.sqrt) is empty + + +def test_apply_nonuq(): + orig = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], + index=['a', 'a', 'c']) + sparse = orig.to_sparse() + res = sparse.apply(lambda s: s[0], axis=1) + exp = orig.apply(lambda s: s[0], axis=1) + + # dtype must be kept + assert res.dtype == SparseDtype(np.int64) + + # ToDo: apply must return subclassed dtype + assert isinstance(res, Series) + tm.assert_series_equal(res.to_dense(), exp) + + # df.T breaks + sparse = orig.T.to_sparse() + res = sparse.apply(lambda s: s[0], axis=0) # noqa + exp = orig.T.apply(lambda s: s[0], axis=0) + + # TODO: no non-unique columns supported in sparse yet + # tm.assert_series_equal(res.to_dense(), exp) + + +def test_applymap(frame): + # just test that it works + result = frame.applymap(lambda x: x * 2) + assert isinstance(result, SparseDataFrame) + + +def test_apply_keep_sparse_dtype(): + # GH 23744 + sdf = SparseDataFrame(np.array([[0, 1, 0], [0, 0, 0], [0, 0, 1]]), + columns=['b', 'a', 'c'], default_fill_value=1) + df = DataFrame(sdf) + + expected = sdf.apply(np.exp) + result = df.apply(np.exp) + tm.assert_frame_equal(expected, result) diff --git a/pandas/tests/sparse/frame/test_frame.py b/pandas/tests/sparse/frame/test_frame.py new file mode 100644 index 0000000000000..888d1fa1bfe45 --- /dev/null +++ b/pandas/tests/sparse/frame/test_frame.py @@ -0,0 +1,1407 @@ +# pylint: disable-msg=E1101,W0612 + +import operator + +import numpy as np +from numpy import nan +import pytest + +from pandas._libs.sparse import BlockIndex, IntIndex +from pandas.compat import PY2, lrange +from pandas.errors import PerformanceWarning + +import pandas as pd +from pandas import DataFrame, Panel, Series, bdate_range, compat +from pandas.core.indexes.datetimes import DatetimeIndex +from pandas.core.sparse import frame as spf +from pandas.core.sparse.api import ( + SparseArray, SparseDataFrame, SparseDtype, SparseSeries) +from pandas.tests.frame.test_api import SharedWithSparse +from pandas.util import testing as tm + +from pandas.tseries.offsets import BDay + + +class TestSparseDataFrame(SharedWithSparse): + klass = SparseDataFrame + + # SharedWithSparse tests use generic, klass-agnostic assertion + _assert_frame_equal = staticmethod(tm.assert_sp_frame_equal) + _assert_series_equal = staticmethod(tm.assert_sp_series_equal) + + def test_iterrows(self, float_frame, float_string_frame): + # Same as parent, but we don't ensure the sparse kind is the same. + for k, v in float_frame.iterrows(): + exp = float_frame.loc[k] + tm.assert_sp_series_equal(v, exp, check_kind=False) + + for k, v in float_string_frame.iterrows(): + exp = float_string_frame.loc[k] + tm.assert_sp_series_equal(v, exp, check_kind=False) + + def test_itertuples(self, float_frame): + for i, tup in enumerate(float_frame.itertuples()): + s = self.klass._constructor_sliced(tup[1:]) + s.name = tup[0] + expected = float_frame.iloc[i, :].reset_index(drop=True) + tm.assert_sp_series_equal(s, expected, check_kind=False) + + def test_fill_value_when_combine_const(self): + # GH12723 + dat = np.array([0, 1, np.nan, 3, 4, 5], dtype='float') + df = SparseDataFrame({'foo': dat}, index=range(6)) + + exp = df.fillna(0).add(2) + res = df.add(2, fill_value=0) + tm.assert_sp_frame_equal(res, exp) + + def test_values(self, empty_frame, float_frame): + empty = empty_frame.values + assert empty.shape == (0, 0) + + no_cols = SparseDataFrame(index=np.arange(10)) + mat = no_cols.values + assert mat.shape == (10, 0) + + no_index = SparseDataFrame(columns=np.arange(10)) + mat = no_index.values + assert mat.shape == (0, 10) + + def test_copy(self, float_frame): + cp = float_frame.copy() + assert isinstance(cp, SparseDataFrame) + tm.assert_sp_frame_equal(cp, float_frame) + + # as of v0.15.0 + # this is now identical (but not is_a ) + assert cp.index.identical(float_frame.index) + + def test_constructor(self, float_frame, float_frame_int_kind, + float_frame_fill0): + for col, series in compat.iteritems(float_frame): + assert isinstance(series, SparseSeries) + + assert isinstance(float_frame_int_kind['A'].sp_index, IntIndex) + + # constructed zframe from matrix above + assert float_frame_fill0['A'].fill_value == 0 + # XXX: changed asarray + expected = pd.SparseArray([0, 0, 0, 0, 1., 2., 3., 4., 5., 6.], + fill_value=0, kind='block') + tm.assert_sp_array_equal(expected, + float_frame_fill0['A'].values) + tm.assert_numpy_array_equal(np.array([0., 0., 0., 0., 1., 2., + 3., 4., 5., 6.]), + float_frame_fill0['A'].to_dense().values) + + # construct no data + sdf = SparseDataFrame(columns=np.arange(10), index=np.arange(10)) + for col, series in compat.iteritems(sdf): + assert isinstance(series, SparseSeries) + + # construct from nested dict + data = {c: s.to_dict() for c, s in compat.iteritems(float_frame)} + + sdf = SparseDataFrame(data) + tm.assert_sp_frame_equal(sdf, float_frame) + + # TODO: test data is copied from inputs + + # init dict with different index + idx = float_frame.index[:5] + cons = SparseDataFrame( + float_frame, index=idx, columns=float_frame.columns, + default_fill_value=float_frame.default_fill_value, + default_kind=float_frame.default_kind, copy=True) + reindexed = float_frame.reindex(idx) + + tm.assert_sp_frame_equal(cons, reindexed, exact_indices=False) + + # assert level parameter breaks reindex + with pytest.raises(TypeError): + float_frame.reindex(idx, level=0) + + repr(float_frame) + + def test_constructor_dict_order(self): + # GH19018 + # initialization ordering: by insertion order if python>= 3.6, else + # order by value + d = {'b': [2, 3], 'a': [0, 1]} + frame = SparseDataFrame(data=d) + if compat.PY36: + expected = SparseDataFrame(data=d, columns=list('ba')) + else: + expected = SparseDataFrame(data=d, columns=list('ab')) + tm.assert_sp_frame_equal(frame, expected) + + def test_constructor_ndarray(self, float_frame): + # no index or columns + sp = SparseDataFrame(float_frame.values) + + # 1d + sp = SparseDataFrame(float_frame['A'].values, index=float_frame.index, + columns=['A']) + tm.assert_sp_frame_equal(sp, float_frame.reindex(columns=['A'])) + + # raise on level argument + msg = "Reindex by level not supported for sparse" + with pytest.raises(TypeError, match=msg): + float_frame.reindex(columns=['A'], level=1) + + # wrong length index / columns + with pytest.raises(ValueError, match="^Index length"): + SparseDataFrame(float_frame.values, index=float_frame.index[:-1]) + + with pytest.raises(ValueError, match="^Column length"): + SparseDataFrame(float_frame.values, + columns=float_frame.columns[:-1]) + + # GH 9272 + def test_constructor_empty(self): + sp = SparseDataFrame() + assert len(sp.index) == 0 + assert len(sp.columns) == 0 + + def test_constructor_dataframe(self, float_frame): + dense = float_frame.to_dense() + sp = SparseDataFrame(dense) + tm.assert_sp_frame_equal(sp, float_frame) + + def test_constructor_convert_index_once(self): + arr = np.array([1.5, 2.5, 3.5]) + sdf = SparseDataFrame(columns=lrange(4), index=arr) + assert sdf[0].index is sdf[1].index + + def test_constructor_from_series(self): + + # GH 2873 + x = Series(np.random.randn(10000), name='a') + x = x.to_sparse(fill_value=0) + assert isinstance(x, SparseSeries) + df = SparseDataFrame(x) + assert isinstance(df, SparseDataFrame) + + x = Series(np.random.randn(10000), name='a') + y = Series(np.random.randn(10000), name='b') + x2 = x.astype(float) + x2.loc[:9998] = np.NaN + # TODO: x_sparse is unused...fix + x_sparse = x2.to_sparse(fill_value=np.NaN) # noqa + + # Currently fails too with weird ufunc error + # df1 = SparseDataFrame([x_sparse, y]) + + y.loc[:9998] = 0 + # TODO: y_sparse is unsused...fix + y_sparse = y.to_sparse(fill_value=0) # noqa + # without sparse value raises error + # df2 = SparseDataFrame([x2_sparse, y]) + + def test_constructor_from_dense_series(self): + # GH 19393 + # series with name + x = Series(np.random.randn(10000), name='a') + result = SparseDataFrame(x) + expected = x.to_frame().to_sparse() + tm.assert_sp_frame_equal(result, expected) + + # series with no name + x = Series(np.random.randn(10000)) + result = SparseDataFrame(x) + expected = x.to_frame().to_sparse() + tm.assert_sp_frame_equal(result, expected) + + def test_constructor_from_unknown_type(self): + # GH 19393 + class Unknown(object): + pass + with pytest.raises(TypeError, + match=('SparseDataFrame called with unknown type ' + '"Unknown" for data argument')): + SparseDataFrame(Unknown()) + + def test_constructor_preserve_attr(self): + # GH 13866 + arr = pd.SparseArray([1, 0, 3, 0], dtype=np.int64, fill_value=0) + assert arr.dtype == SparseDtype(np.int64) + assert arr.fill_value == 0 + + df = pd.SparseDataFrame({'x': arr}) + assert df['x'].dtype == SparseDtype(np.int64) + assert df['x'].fill_value == 0 + + s = pd.SparseSeries(arr, name='x') + assert s.dtype == SparseDtype(np.int64) + assert s.fill_value == 0 + + df = pd.SparseDataFrame(s) + assert df['x'].dtype == SparseDtype(np.int64) + assert df['x'].fill_value == 0 + + df = pd.SparseDataFrame({'x': s}) + assert df['x'].dtype == SparseDtype(np.int64) + assert df['x'].fill_value == 0 + + def test_constructor_nan_dataframe(self): + # GH 10079 + trains = np.arange(100) + thresholds = [10, 20, 30, 40, 50, 60] + tuples = [(i, j) for i in trains for j in thresholds] + index = pd.MultiIndex.from_tuples(tuples, + names=['trains', 'thresholds']) + matrix = np.empty((len(index), len(trains))) + matrix.fill(np.nan) + df = pd.DataFrame(matrix, index=index, columns=trains, dtype=float) + result = df.to_sparse() + expected = pd.SparseDataFrame(matrix, index=index, columns=trains, + dtype=float) + tm.assert_sp_frame_equal(result, expected) + + def test_type_coercion_at_construction(self): + # GH 15682 + result = pd.SparseDataFrame( + {'a': [1, 0, 0], 'b': [0, 1, 0], 'c': [0, 0, 1]}, dtype='uint8', + default_fill_value=0) + expected = pd.SparseDataFrame( + {'a': pd.SparseSeries([1, 0, 0], dtype='uint8'), + 'b': pd.SparseSeries([0, 1, 0], dtype='uint8'), + 'c': pd.SparseSeries([0, 0, 1], dtype='uint8')}, + default_fill_value=0) + tm.assert_sp_frame_equal(result, expected) + + def test_default_dtype(self): + result = pd.SparseDataFrame(columns=list('ab'), index=range(2)) + expected = pd.SparseDataFrame([[np.nan, np.nan], [np.nan, np.nan]], + columns=list('ab'), index=range(2)) + tm.assert_sp_frame_equal(result, expected) + + def test_nan_data_with_int_dtype_raises_error(self): + sdf = pd.SparseDataFrame([[np.nan, np.nan], [np.nan, np.nan]], + columns=list('ab'), index=range(2)) + msg = "Cannot convert non-finite values" + with pytest.raises(ValueError, match=msg): + pd.SparseDataFrame(sdf, dtype=np.int64) + + def test_dtypes(self): + df = DataFrame(np.random.randn(10000, 4)) + df.loc[:9998] = np.nan + sdf = df.to_sparse() + + result = sdf.get_dtype_counts() + expected = Series({'Sparse[float64, nan]': 4}) + tm.assert_series_equal(result, expected) + + def test_shape(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): + # see gh-10452 + assert float_frame.shape == (10, 4) + assert float_frame_int_kind.shape == (10, 4) + assert float_frame_fill0.shape == (10, 4) + assert float_frame_fill2.shape == (10, 4) + + def test_str(self): + df = DataFrame(np.random.randn(10000, 4)) + df.loc[:9998] = np.nan + + sdf = df.to_sparse() + str(sdf) + + def test_array_interface(self, float_frame): + res = np.sqrt(float_frame) + dres = np.sqrt(float_frame.to_dense()) + tm.assert_frame_equal(res.to_dense(), dres) + + def test_pickle(self, float_frame, float_frame_int_kind, float_frame_dense, + float_frame_fill0, float_frame_fill0_dense, + float_frame_fill2, float_frame_fill2_dense): + + def _test_roundtrip(frame, orig): + result = tm.round_trip_pickle(frame) + tm.assert_sp_frame_equal(frame, result) + tm.assert_frame_equal(result.to_dense(), orig, check_dtype=False) + + _test_roundtrip(SparseDataFrame(), DataFrame()) + _test_roundtrip(float_frame, float_frame_dense) + _test_roundtrip(float_frame_int_kind, float_frame_dense) + _test_roundtrip(float_frame_fill0, float_frame_fill0_dense) + _test_roundtrip(float_frame_fill2, float_frame_fill2_dense) + + def test_dense_to_sparse(self): + df = DataFrame({'A': [nan, nan, nan, 1, 2], + 'B': [1, 2, nan, nan, nan]}) + sdf = df.to_sparse() + assert isinstance(sdf, SparseDataFrame) + assert np.isnan(sdf.default_fill_value) + assert isinstance(sdf['A'].sp_index, BlockIndex) + tm.assert_frame_equal(sdf.to_dense(), df) + + sdf = df.to_sparse(kind='integer') + assert isinstance(sdf['A'].sp_index, IntIndex) + + df = DataFrame({'A': [0, 0, 0, 1, 2], + 'B': [1, 2, 0, 0, 0]}, dtype=float) + sdf = df.to_sparse(fill_value=0) + assert sdf.default_fill_value == 0 + tm.assert_frame_equal(sdf.to_dense(), df) + + def test_density(self): + df = SparseSeries([nan, nan, nan, 0, 1, 2, 3, 4, 5, 6]) + assert df.density == 0.7 + + df = SparseDataFrame({'A': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], + 'B': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], + 'C': np.arange(10), + 'D': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]}) + + assert df.density == 0.75 + + def test_sparse_to_dense(self): + pass + + def test_sparse_series_ops(self, float_frame): + self._check_frame_ops(float_frame) + + def test_sparse_series_ops_i(self, float_frame_int_kind): + self._check_frame_ops(float_frame_int_kind) + + def test_sparse_series_ops_z(self, float_frame_fill0): + self._check_frame_ops(float_frame_fill0) + + def test_sparse_series_ops_fill(self, float_frame_fill2): + self._check_frame_ops(float_frame_fill2) + + def _check_frame_ops(self, frame): + + def _compare_to_dense(a, b, da, db, op): + sparse_result = op(a, b) + dense_result = op(da, db) + + fill = sparse_result.default_fill_value + dense_result = dense_result.to_sparse(fill_value=fill) + tm.assert_sp_frame_equal(sparse_result, dense_result, + exact_indices=False) + + if isinstance(a, DataFrame) and isinstance(db, DataFrame): + mixed_result = op(a, db) + assert isinstance(mixed_result, SparseDataFrame) + tm.assert_sp_frame_equal(mixed_result, sparse_result, + exact_indices=False) + + opnames = ['add', 'sub', 'mul', 'truediv', 'floordiv'] + ops = [getattr(operator, name) for name in opnames] + + fidx = frame.index + + # time series operations + + series = [frame['A'], frame['B'], frame['C'], frame['D'], + frame['A'].reindex(fidx[:7]), frame['A'].reindex(fidx[::2]), + SparseSeries( + [], index=[])] + + for op in opnames: + _compare_to_dense(frame, frame[::2], frame.to_dense(), + frame[::2].to_dense(), getattr(operator, op)) + + # 2304, no auto-broadcasting + for i, s in enumerate(series): + f = lambda a, b: getattr(a, op)(b, axis='index') + _compare_to_dense(frame, s, frame.to_dense(), s.to_dense(), f) + + # rops are not implemented + # _compare_to_dense(s, frame, s.to_dense(), + # frame.to_dense(), f) + + # cross-sectional operations + series = [frame.xs(fidx[0]), frame.xs(fidx[3]), frame.xs(fidx[5]), + frame.xs(fidx[7]), frame.xs(fidx[5])[:2]] + + for op in ops: + for s in series: + _compare_to_dense(frame, s, frame.to_dense(), s, op) + _compare_to_dense(s, frame, s, frame.to_dense(), op) + + # it works! + result = frame + frame.loc[:, ['A', 'B']] # noqa + + def test_op_corners(self, float_frame, empty_frame): + empty = empty_frame + empty_frame + assert empty.empty + + foo = float_frame + empty_frame + assert isinstance(foo.index, DatetimeIndex) + tm.assert_frame_equal(foo, float_frame * np.nan) + + foo = empty_frame + float_frame + tm.assert_frame_equal(foo, float_frame * np.nan) + + def test_scalar_ops(self): + pass + + def test_getitem(self): + # 1585 select multiple columns + sdf = SparseDataFrame(index=[0, 1, 2], columns=['a', 'b', 'c']) + + result = sdf[['a', 'b']] + exp = sdf.reindex(columns=['a', 'b']) + tm.assert_sp_frame_equal(result, exp) + + with pytest.raises(KeyError, match=r"\['d'\] not in index"): + sdf[['a', 'd']] + + def test_iloc(self, float_frame): + + # GH 2227 + result = float_frame.iloc[:, 0] + assert isinstance(result, SparseSeries) + tm.assert_sp_series_equal(result, float_frame['A']) + + # preserve sparse index type. #2251 + data = {'A': [0, 1]} + iframe = SparseDataFrame(data, default_kind='integer') + tm.assert_class_equal(iframe['A'].sp_index, + iframe.iloc[:, 0].sp_index) + + def test_set_value(self, float_frame): + + # ok, as the index gets converted to object + frame = float_frame.copy() + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + res = frame.set_value('foobar', 'B', 1.5) + assert res.index.dtype == 'object' + + res = float_frame + res.index = res.index.astype(object) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + res = float_frame.set_value('foobar', 'B', 1.5) + assert res is not float_frame + assert res.index[-1] == 'foobar' + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + assert res.get_value('foobar', 'B') == 1.5 + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + res2 = res.set_value('foobar', 'qux', 1.5) + assert res2 is not res + tm.assert_index_equal(res2.columns, + pd.Index(list(float_frame.columns) + ['qux'])) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + assert res2.get_value('foobar', 'qux') == 1.5 + + def test_fancy_index_misc(self, float_frame): + # axis = 0 + sliced = float_frame.iloc[-2:, :] + expected = float_frame.reindex(index=float_frame.index[-2:]) + tm.assert_sp_frame_equal(sliced, expected) + + # axis = 1 + sliced = float_frame.iloc[:, -2:] + expected = float_frame.reindex(columns=float_frame.columns[-2:]) + tm.assert_sp_frame_equal(sliced, expected) + + def test_getitem_overload(self, float_frame): + # slicing + sl = float_frame[:20] + tm.assert_sp_frame_equal(sl, + float_frame.reindex(float_frame.index[:20])) + + # boolean indexing + d = float_frame.index[5] + indexer = float_frame.index > d + + subindex = float_frame.index[indexer] + subframe = float_frame[indexer] + + tm.assert_index_equal(subindex, subframe.index) + msg = "Item wrong length 9 instead of 10" + with pytest.raises(ValueError, match=msg): + float_frame[indexer[:-1]] + + def test_setitem(self, float_frame, float_frame_int_kind, + float_frame_dense, + float_frame_fill0, float_frame_fill0_dense, + float_frame_fill2, float_frame_fill2_dense): + + def _check_frame(frame, orig): + N = len(frame) + + # insert SparseSeries + frame['E'] = frame['A'] + assert isinstance(frame['E'], SparseSeries) + tm.assert_sp_series_equal(frame['E'], frame['A'], + check_names=False) + + # insert SparseSeries differently-indexed + to_insert = frame['A'][::2] + frame['E'] = to_insert + expected = to_insert.to_dense().reindex(frame.index) + result = frame['E'].to_dense() + tm.assert_series_equal(result, expected, check_names=False) + assert result.name == 'E' + + # insert Series + frame['F'] = frame['A'].to_dense() + assert isinstance(frame['F'], SparseSeries) + tm.assert_sp_series_equal(frame['F'], frame['A'], + check_names=False) + + # insert Series differently-indexed + to_insert = frame['A'].to_dense()[::2] + frame['G'] = to_insert + expected = to_insert.reindex(frame.index) + expected.name = 'G' + tm.assert_series_equal(frame['G'].to_dense(), expected) + + # insert ndarray + frame['H'] = np.random.randn(N) + assert isinstance(frame['H'], SparseSeries) + + to_sparsify = np.random.randn(N) + to_sparsify[N // 2:] = frame.default_fill_value + frame['I'] = to_sparsify + assert len(frame['I'].sp_values) == N // 2 + + # insert ndarray wrong size + msg = "Length of values does not match length of index" + with pytest.raises(AssertionError, match=msg): + frame['foo'] = np.random.randn(N - 1) + + # scalar value + frame['J'] = 5 + assert len(frame['J'].sp_values) == N + assert (frame['J'].sp_values == 5).all() + + frame['K'] = frame.default_fill_value + assert len(frame['K'].sp_values) == 0 + + _check_frame(float_frame, float_frame_dense) + _check_frame(float_frame_int_kind, float_frame_dense) + _check_frame(float_frame_fill0, float_frame_fill0_dense) + _check_frame(float_frame_fill2, float_frame_fill2_dense) + + @pytest.mark.parametrize('values', [ + [True, False], + [0, 1], + [1, None], + ['a', 'b'], + [pd.Timestamp('2017'), pd.NaT], + [pd.Timedelta('10s'), pd.NaT], + ]) + def test_setitem_more(self, values): + df = pd.DataFrame({"A": values}) + df['A'] = pd.SparseArray(values) + expected = pd.DataFrame({'A': pd.SparseArray(values)}) + tm.assert_frame_equal(df, expected) + + def test_setitem_corner(self, float_frame): + float_frame['a'] = float_frame['B'] + tm.assert_sp_series_equal(float_frame['a'], float_frame['B'], + check_names=False) + + def test_setitem_array(self, float_frame): + arr = float_frame['B'] + + float_frame['E'] = arr + tm.assert_sp_series_equal(float_frame['E'], float_frame['B'], + check_names=False) + + float_frame['F'] = arr[:-1] + index = float_frame.index[:-1] + tm.assert_sp_series_equal(float_frame['E'].reindex(index), + float_frame['F'].reindex(index), + check_names=False) + + def test_setitem_chained_no_consolidate(self): + # https://github.com/pandas-dev/pandas/pull/19268 + # issuecomment-361696418 + # chained setitem used to cause consolidation + sdf = pd.SparseDataFrame([[np.nan, 1], [2, np.nan]]) + with pd.option_context('mode.chained_assignment', None): + sdf[0][1] = 2 + assert len(sdf._data.blocks) == 2 + + def test_delitem(self, float_frame): + A = float_frame['A'] + C = float_frame['C'] + + del float_frame['B'] + assert 'B' not in float_frame + tm.assert_sp_series_equal(float_frame['A'], A) + tm.assert_sp_series_equal(float_frame['C'], C) + + del float_frame['D'] + assert 'D' not in float_frame + + del float_frame['A'] + assert 'A' not in float_frame + + def test_set_columns(self, float_frame): + float_frame.columns = float_frame.columns + msg = ("Length mismatch: Expected axis has 4 elements, new values have" + " 3 elements") + with pytest.raises(ValueError, match=msg): + float_frame.columns = float_frame.columns[:-1] + + def test_set_index(self, float_frame): + float_frame.index = float_frame.index + msg = ("Length mismatch: Expected axis has 10 elements, new values" + " have 9 elements") + with pytest.raises(ValueError, match=msg): + float_frame.index = float_frame.index[:-1] + + def test_ctor_reindex(self): + idx = pd.Index([0, 1, 2, 3]) + msg = "Length of passed values is 2, index implies 4" + with pytest.raises(ValueError, match=msg): + pd.SparseDataFrame({"A": [1, 2]}, index=idx) + + def test_append(self, float_frame): + a = float_frame[:5] + b = float_frame[5:] + + appended = a.append(b) + tm.assert_sp_frame_equal(appended, float_frame, exact_indices=False) + + a = float_frame.iloc[:5, :3] + b = float_frame.iloc[5:] + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + # Stacklevel is set for pd.concat, not append + appended = a.append(b) + tm.assert_sp_frame_equal(appended.iloc[:, :3], float_frame.iloc[:, :3], + exact_indices=False) + + a = a[['B', 'C', 'A']].head(2) + b = b.head(2) + + expected = pd.SparseDataFrame({ + "B": [0., 1, None, 3], + "C": [0., 1, 5, 6], + "A": [None, None, 2, 3], + "D": [None, None, 5, None], + }, index=a.index | b.index, columns=['B', 'C', 'A', 'D']) + with tm.assert_produces_warning(None): + appended = a.append(b, sort=False) + + tm.assert_frame_equal(appended, expected) + + with tm.assert_produces_warning(None): + appended = a.append(b, sort=True) + + tm.assert_sp_frame_equal(appended, expected[['A', 'B', 'C', 'D']], + consolidate_block_indices=True, + check_kind=False) + + def test_astype(self): + sparse = pd.SparseDataFrame({'A': SparseArray([1, 2, 3, 4], + dtype=np.int64), + 'B': SparseArray([4, 5, 6, 7], + dtype=np.int64)}) + assert sparse['A'].dtype == SparseDtype(np.int64) + assert sparse['B'].dtype == SparseDtype(np.int64) + + # retain fill_value + res = sparse.astype(np.float64) + exp = pd.SparseDataFrame({'A': SparseArray([1., 2., 3., 4.], + fill_value=0, + kind='integer'), + 'B': SparseArray([4., 5., 6., 7.], + fill_value=0, + kind='integer')}, + default_fill_value=np.nan) + tm.assert_sp_frame_equal(res, exp) + assert res['A'].dtype == SparseDtype(np.float64, 0) + assert res['B'].dtype == SparseDtype(np.float64, 0) + + # update fill_value + res = sparse.astype(SparseDtype(np.float64, np.nan)) + exp = pd.SparseDataFrame({'A': SparseArray([1., 2., 3., 4.], + fill_value=np.nan, + kind='integer'), + 'B': SparseArray([4., 5., 6., 7.], + fill_value=np.nan, + kind='integer')}, + default_fill_value=np.nan) + tm.assert_sp_frame_equal(res, exp) + assert res['A'].dtype == SparseDtype(np.float64, np.nan) + assert res['B'].dtype == SparseDtype(np.float64, np.nan) + + def test_astype_bool(self): + sparse = pd.SparseDataFrame({'A': SparseArray([0, 2, 0, 4], + fill_value=0, + dtype=np.int64), + 'B': SparseArray([0, 5, 0, 7], + fill_value=0, + dtype=np.int64)}, + default_fill_value=0) + assert sparse['A'].dtype == SparseDtype(np.int64) + assert sparse['B'].dtype == SparseDtype(np.int64) + + res = sparse.astype(SparseDtype(bool, False)) + exp = pd.SparseDataFrame({'A': SparseArray([False, True, False, True], + dtype=np.bool, + fill_value=False, + kind='integer'), + 'B': SparseArray([False, True, False, True], + dtype=np.bool, + fill_value=False, + kind='integer')}, + default_fill_value=False) + tm.assert_sp_frame_equal(res, exp) + assert res['A'].dtype == SparseDtype(np.bool) + assert res['B'].dtype == SparseDtype(np.bool) + + def test_astype_object(self): + # This may change in GH-23125 + df = pd.DataFrame({"A": SparseArray([0, 1]), + "B": SparseArray([0, 1])}) + result = df.astype(object) + dtype = SparseDtype(object, 0) + expected = pd.DataFrame({"A": SparseArray([0, 1], dtype=dtype), + "B": SparseArray([0, 1], dtype=dtype)}) + tm.assert_frame_equal(result, expected) + + def test_fillna(self, float_frame_fill0, float_frame_fill0_dense): + df = float_frame_fill0.reindex(lrange(5)) + dense = float_frame_fill0_dense.reindex(lrange(5)) + + result = df.fillna(0) + expected = dense.fillna(0) + tm.assert_sp_frame_equal(result, expected.to_sparse(fill_value=0), + exact_indices=False) + tm.assert_frame_equal(result.to_dense(), expected) + + result = df.copy() + result.fillna(0, inplace=True) + expected = dense.fillna(0) + + tm.assert_sp_frame_equal(result, expected.to_sparse(fill_value=0), + exact_indices=False) + tm.assert_frame_equal(result.to_dense(), expected) + + result = df.copy() + result = df['A'] + result.fillna(0, inplace=True) + + expected = dense['A'].fillna(0) + # this changes internal SparseArray repr + # tm.assert_sp_series_equal(result, expected.to_sparse(fill_value=0)) + tm.assert_series_equal(result.to_dense(), expected) + + def test_fillna_fill_value(self): + df = pd.DataFrame({'A': [1, 0, 0], 'B': [np.nan, np.nan, 4]}) + + sparse = pd.SparseDataFrame(df) + tm.assert_frame_equal(sparse.fillna(-1).to_dense(), + df.fillna(-1), check_dtype=False) + + sparse = pd.SparseDataFrame(df, default_fill_value=0) + tm.assert_frame_equal(sparse.fillna(-1).to_dense(), + df.fillna(-1), check_dtype=False) + + def test_sparse_frame_pad_backfill_limit(self): + index = np.arange(10) + df = DataFrame(np.random.randn(10, 4), index=index) + sdf = df.to_sparse() + + result = sdf[:2].reindex(index, method='pad', limit=5) + + with tm.assert_produces_warning(PerformanceWarning): + expected = sdf[:2].reindex(index).fillna(method='pad') + expected = expected.to_dense() + expected.values[-3:] = np.nan + expected = expected.to_sparse() + tm.assert_frame_equal(result, expected) + + result = sdf[-2:].reindex(index, method='backfill', limit=5) + + with tm.assert_produces_warning(PerformanceWarning): + expected = sdf[-2:].reindex(index).fillna(method='backfill') + expected = expected.to_dense() + expected.values[:3] = np.nan + expected = expected.to_sparse() + tm.assert_frame_equal(result, expected) + + def test_sparse_frame_fillna_limit(self): + index = np.arange(10) + df = DataFrame(np.random.randn(10, 4), index=index) + sdf = df.to_sparse() + + result = sdf[:2].reindex(index) + with tm.assert_produces_warning(PerformanceWarning): + result = result.fillna(method='pad', limit=5) + + with tm.assert_produces_warning(PerformanceWarning): + expected = sdf[:2].reindex(index).fillna(method='pad') + expected = expected.to_dense() + expected.values[-3:] = np.nan + expected = expected.to_sparse() + tm.assert_frame_equal(result, expected) + + result = sdf[-2:].reindex(index) + with tm.assert_produces_warning(PerformanceWarning): + result = result.fillna(method='backfill', limit=5) + + with tm.assert_produces_warning(PerformanceWarning): + expected = sdf[-2:].reindex(index).fillna(method='backfill') + expected = expected.to_dense() + expected.values[:3] = np.nan + expected = expected.to_sparse() + tm.assert_frame_equal(result, expected) + + def test_rename(self, float_frame): + result = float_frame.rename(index=str) + expected = SparseDataFrame(float_frame.values, + index=float_frame.index.strftime( + "%Y-%m-%d %H:%M:%S"), + columns=list('ABCD')) + tm.assert_sp_frame_equal(result, expected) + + result = float_frame.rename(columns=lambda x: '%s%d' % (x, 1)) + data = {'A1': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], + 'B1': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], + 'C1': np.arange(10, dtype=np.float64), + 'D1': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]} + expected = SparseDataFrame(data, index=float_frame.index) + tm.assert_sp_frame_equal(result, expected) + + def test_corr(self, float_frame): + res = float_frame.corr() + # XXX: this stays sparse + tm.assert_frame_equal(res, float_frame.to_dense().corr().to_sparse()) + + def test_describe(self, float_frame): + float_frame['foo'] = np.nan + float_frame.get_dtype_counts() + str(float_frame) + desc = float_frame.describe() # noqa + + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") + def test_join(self, float_frame): + left = float_frame.loc[:, ['A', 'B']] + right = float_frame.loc[:, ['C', 'D']] + joined = left.join(right) + tm.assert_sp_frame_equal(joined, float_frame, exact_indices=False) + + right = float_frame.loc[:, ['B', 'D']] + msg = (r"columns overlap but no suffix specified: Index\(\['B'\]," + r" dtype='object'\)") + with pytest.raises(ValueError, match=msg): + left.join(right) + + with pytest.raises(ValueError, match='Other Series must have a name'): + float_frame.join(Series( + np.random.randn(len(float_frame)), index=float_frame.index)) + + def test_reindex(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): + + def _check_frame(frame): + index = frame.index + sidx = index[::2] + sidx2 = index[:5] # noqa + + sparse_result = frame.reindex(sidx) + dense_result = frame.to_dense().reindex(sidx) + tm.assert_frame_equal(sparse_result.to_dense(), dense_result) + + tm.assert_frame_equal(frame.reindex(list(sidx)).to_dense(), + dense_result) + + sparse_result2 = sparse_result.reindex(index) + dense_result2 = dense_result.reindex(index) + tm.assert_frame_equal(sparse_result2.to_dense(), dense_result2) + + # propagate CORRECT fill value + tm.assert_almost_equal(sparse_result.default_fill_value, + frame.default_fill_value) + tm.assert_almost_equal(sparse_result['A'].fill_value, + frame['A'].fill_value) + + # length zero + length_zero = frame.reindex([]) + assert len(length_zero) == 0 + assert len(length_zero.columns) == len(frame.columns) + assert len(length_zero['A']) == 0 + + # frame being reindexed has length zero + length_n = length_zero.reindex(index) + assert len(length_n) == len(frame) + assert len(length_n.columns) == len(frame.columns) + assert len(length_n['A']) == len(frame) + + # reindex columns + reindexed = frame.reindex(columns=['A', 'B', 'Z']) + assert len(reindexed.columns) == 3 + tm.assert_almost_equal(reindexed['Z'].fill_value, + frame.default_fill_value) + assert np.isnan(reindexed['Z'].sp_values).all() + + _check_frame(float_frame) + _check_frame(float_frame_int_kind) + _check_frame(float_frame_fill0) + _check_frame(float_frame_fill2) + + # with copy=False + reindexed = float_frame.reindex(float_frame.index, copy=False) + reindexed['F'] = reindexed['A'] + assert 'F' in float_frame + + reindexed = float_frame.reindex(float_frame.index) + reindexed['G'] = reindexed['A'] + assert 'G' not in float_frame + + def test_reindex_fill_value(self, float_frame_fill0, + float_frame_fill0_dense): + rng = bdate_range('20110110', periods=20) + + result = float_frame_fill0.reindex(rng, fill_value=0) + exp = float_frame_fill0_dense.reindex(rng, fill_value=0) + exp = exp.to_sparse(float_frame_fill0.default_fill_value) + tm.assert_sp_frame_equal(result, exp) + + def test_reindex_method(self): + + sparse = SparseDataFrame(data=[[11., 12., 14.], + [21., 22., 24.], + [41., 42., 44.]], + index=[1, 2, 4], + columns=[1, 2, 4], + dtype=float) + + # Over indices + + # default method + result = sparse.reindex(index=range(6)) + expected = SparseDataFrame(data=[[nan, nan, nan], + [11., 12., 14.], + [21., 22., 24.], + [nan, nan, nan], + [41., 42., 44.], + [nan, nan, nan]], + index=range(6), + columns=[1, 2, 4], + dtype=float) + tm.assert_sp_frame_equal(result, expected) + + # method='bfill' + result = sparse.reindex(index=range(6), method='bfill') + expected = SparseDataFrame(data=[[11., 12., 14.], + [11., 12., 14.], + [21., 22., 24.], + [41., 42., 44.], + [41., 42., 44.], + [nan, nan, nan]], + index=range(6), + columns=[1, 2, 4], + dtype=float) + tm.assert_sp_frame_equal(result, expected) + + # method='ffill' + result = sparse.reindex(index=range(6), method='ffill') + expected = SparseDataFrame(data=[[nan, nan, nan], + [11., 12., 14.], + [21., 22., 24.], + [21., 22., 24.], + [41., 42., 44.], + [41., 42., 44.]], + index=range(6), + columns=[1, 2, 4], + dtype=float) + tm.assert_sp_frame_equal(result, expected) + + # Over columns + + # default method + result = sparse.reindex(columns=range(6)) + expected = SparseDataFrame(data=[[nan, 11., 12., nan, 14., nan], + [nan, 21., 22., nan, 24., nan], + [nan, 41., 42., nan, 44., nan]], + index=[1, 2, 4], + columns=range(6), + dtype=float) + tm.assert_sp_frame_equal(result, expected) + + # method='bfill' + with pytest.raises(NotImplementedError): + sparse.reindex(columns=range(6), method='bfill') + + # method='ffill' + with pytest.raises(NotImplementedError): + sparse.reindex(columns=range(6), method='ffill') + + def test_take(self, float_frame): + result = float_frame.take([1, 0, 2], axis=1) + expected = float_frame.reindex(columns=['B', 'A', 'C']) + tm.assert_sp_frame_equal(result, expected) + + def test_to_dense(self, float_frame, float_frame_int_kind, + float_frame_dense, + float_frame_fill0, float_frame_fill0_dense, + float_frame_fill2, float_frame_fill2_dense): + def _check(frame, orig): + dense_dm = frame.to_dense() + # Sparse[float] != float + tm.assert_frame_equal(frame, dense_dm, check_dtype=False) + tm.assert_frame_equal(dense_dm, orig, check_dtype=False) + + _check(float_frame, float_frame_dense) + _check(float_frame_int_kind, float_frame_dense) + _check(float_frame_fill0, float_frame_fill0_dense) + _check(float_frame_fill2, float_frame_fill2_dense) + + @pytest.mark.filterwarnings("ignore:\\nPanel:FutureWarning") + def test_stack_sparse_frame(self, float_frame, float_frame_int_kind, + float_frame_fill0, float_frame_fill2): + def _check(frame): + dense_frame = frame.to_dense() # noqa + + wp = Panel.from_dict({'foo': frame}) + from_dense_lp = wp.to_frame() + + from_sparse_lp = spf.stack_sparse_frame(frame) + + tm.assert_numpy_array_equal(from_dense_lp.values, + from_sparse_lp.values) + + _check(float_frame) + _check(float_frame_int_kind) + + # for now + msg = "This routine assumes NaN fill value" + with pytest.raises(TypeError, match=msg): + _check(float_frame_fill0) + with pytest.raises(TypeError, match=msg): + _check(float_frame_fill2) + + def test_transpose(self, float_frame, float_frame_int_kind, + float_frame_dense, + float_frame_fill0, float_frame_fill0_dense, + float_frame_fill2, float_frame_fill2_dense): + + def _check(frame, orig): + transposed = frame.T + untransposed = transposed.T + tm.assert_sp_frame_equal(frame, untransposed) + + tm.assert_frame_equal(frame.T.to_dense(), orig.T) + tm.assert_frame_equal(frame.T.T.to_dense(), orig.T.T) + tm.assert_sp_frame_equal(frame, frame.T.T, exact_indices=False) + + _check(float_frame, float_frame_dense) + _check(float_frame_int_kind, float_frame_dense) + _check(float_frame_fill0, float_frame_fill0_dense) + _check(float_frame_fill2, float_frame_fill2_dense) + + def test_shift(self, float_frame, float_frame_int_kind, float_frame_dense, + float_frame_fill0, float_frame_fill0_dense, + float_frame_fill2, float_frame_fill2_dense): + + def _check(frame, orig): + shifted = frame.shift(0) + exp = orig.shift(0) + tm.assert_frame_equal(shifted.to_dense(), exp) + + shifted = frame.shift(1) + exp = orig.shift(1) + tm.assert_frame_equal(shifted.to_dense(), exp) + + shifted = frame.shift(-2) + exp = orig.shift(-2) + tm.assert_frame_equal(shifted.to_dense(), exp) + + shifted = frame.shift(2, freq='B') + exp = orig.shift(2, freq='B') + exp = exp.to_sparse(frame.default_fill_value, + kind=frame.default_kind) + tm.assert_frame_equal(shifted, exp) + + shifted = frame.shift(2, freq=BDay()) + exp = orig.shift(2, freq=BDay()) + exp = exp.to_sparse(frame.default_fill_value, + kind=frame.default_kind) + tm.assert_frame_equal(shifted, exp) + + _check(float_frame, float_frame_dense) + _check(float_frame_int_kind, float_frame_dense) + _check(float_frame_fill0, float_frame_fill0_dense) + _check(float_frame_fill2, float_frame_fill2_dense) + + def test_count(self, float_frame): + dense_result = float_frame.to_dense().count() + + result = float_frame.count() + tm.assert_series_equal(result.to_dense(), dense_result) + + result = float_frame.count(axis=None) + tm.assert_series_equal(result.to_dense(), dense_result) + + result = float_frame.count(axis=0) + tm.assert_series_equal(result.to_dense(), dense_result) + + result = float_frame.count(axis=1) + dense_result = float_frame.to_dense().count(axis=1) + + # win32 don't check dtype + tm.assert_series_equal(result, dense_result, check_dtype=False) + + def test_numpy_transpose(self): + sdf = SparseDataFrame([1, 2, 3], index=[1, 2, 3], columns=['a']) + result = np.transpose(np.transpose(sdf)) + tm.assert_sp_frame_equal(result, sdf) + + msg = "the 'axes' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.transpose(sdf, axes=1) + + def test_combine_first(self, float_frame): + df = float_frame + + result = df[::2].combine_first(df) + + expected = df[::2].to_dense().combine_first(df.to_dense()) + expected = expected.to_sparse(fill_value=df.default_fill_value) + + tm.assert_sp_frame_equal(result, expected) + + @pytest.mark.xfail(reason="No longer supported.") + def test_combine_first_with_dense(self): + # We could support this if we allow + # pd.core.dtypes.cast.find_common_type to special case SparseDtype + # but I don't think that's worth it. + df = self.frame + + result = df[::2].combine_first(df.to_dense()) + expected = df[::2].to_dense().combine_first(df.to_dense()) + expected = expected.to_sparse(fill_value=df.default_fill_value) + + tm.assert_sp_frame_equal(result, expected) + + def test_combine_add(self, float_frame): + df = float_frame.to_dense() + df2 = df.copy() + df2['C'][:3] = np.nan + df['A'][:3] = 5.7 + + result = df.to_sparse().add(df2.to_sparse(), fill_value=0) + expected = df.add(df2, fill_value=0).to_sparse() + tm.assert_sp_frame_equal(result, expected) + + def test_isin(self): + sparse_df = DataFrame({'flag': [1., 0., 1.]}).to_sparse(fill_value=0.) + xp = sparse_df[sparse_df.flag == 1.] + rs = sparse_df[sparse_df.flag.isin([1.])] + tm.assert_frame_equal(xp, rs) + + def test_sparse_pow_issue(self): + # 2220 + df = SparseDataFrame({'A': [1.1, 3.3], 'B': [2.5, -3.9]}) + + # note : no error without nan + df = SparseDataFrame({'A': [nan, 0, 1]}) + + # note that 2 ** df works fine, also df ** 1 + result = 1 ** df + + r1 = result.take([0], 1)['A'] + r2 = result['A'] + + assert len(r2.sp_values) == len(r1.sp_values) + + def test_as_blocks(self): + df = SparseDataFrame({'A': [1.1, 3.3], 'B': [nan, -3.9]}, + dtype='float64') + + # deprecated 0.21.0 + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + df_blocks = df.blocks + assert list(df_blocks.keys()) == ['Sparse[float64, nan]'] + tm.assert_frame_equal(df_blocks['Sparse[float64, nan]'], df) + + @pytest.mark.xfail(reason='nan column names in _init_dict problematic ' + '(GH#16894)') + def test_nan_columnname(self): + # GH 8822 + nan_colname = DataFrame(Series(1.0, index=[0]), columns=[nan]) + nan_colname_sparse = nan_colname.to_sparse() + assert np.isnan(nan_colname_sparse.columns[0]) + + def test_isna(self): + # GH 8276 + df = pd.SparseDataFrame({'A': [np.nan, np.nan, 1, 2, np.nan], + 'B': [0, np.nan, np.nan, 2, np.nan]}) + + res = df.isna() + exp = pd.SparseDataFrame({'A': [True, True, False, False, True], + 'B': [False, True, True, False, True]}, + default_fill_value=True) + exp._default_fill_value = np.nan + tm.assert_sp_frame_equal(res, exp) + + # if fill_value is not nan, True can be included in sp_values + df = pd.SparseDataFrame({'A': [0, 0, 1, 2, np.nan], + 'B': [0, np.nan, 0, 2, np.nan]}, + default_fill_value=0.) + res = df.isna() + assert isinstance(res, pd.SparseDataFrame) + exp = pd.DataFrame({'A': [False, False, False, False, True], + 'B': [False, True, False, False, True]}) + tm.assert_frame_equal(res.to_dense(), exp) + + def test_notna(self): + # GH 8276 + df = pd.SparseDataFrame({'A': [np.nan, np.nan, 1, 2, np.nan], + 'B': [0, np.nan, np.nan, 2, np.nan]}) + + res = df.notna() + exp = pd.SparseDataFrame({'A': [False, False, True, True, False], + 'B': [True, False, False, True, False]}, + default_fill_value=False) + exp._default_fill_value = np.nan + tm.assert_sp_frame_equal(res, exp) + + # if fill_value is not nan, True can be included in sp_values + df = pd.SparseDataFrame({'A': [0, 0, 1, 2, np.nan], + 'B': [0, np.nan, 0, 2, np.nan]}, + default_fill_value=0.) + res = df.notna() + assert isinstance(res, pd.SparseDataFrame) + exp = pd.DataFrame({'A': [True, True, True, True, False], + 'B': [True, False, True, True, False]}) + tm.assert_frame_equal(res.to_dense(), exp) + + def test_default_fill_value_with_no_data(self): + # GH 16807 + expected = pd.SparseDataFrame([[1.0, 1.0], [1.0, 1.0]], + columns=list('ab'), index=range(2)) + result = pd.SparseDataFrame(columns=list('ab'), index=range(2), + default_fill_value=1.0) + tm.assert_frame_equal(expected, result) + + +class TestSparseDataFrameArithmetic(object): + + def test_numeric_op_scalar(self): + df = pd.DataFrame({'A': [nan, nan, 0, 1, ], + 'B': [0, 1, 2, nan], + 'C': [1., 2., 3., 4.], + 'D': [nan, nan, nan, nan]}) + sparse = df.to_sparse() + + tm.assert_sp_frame_equal(sparse + 1, (df + 1).to_sparse()) + + def test_comparison_op_scalar(self): + # GH 13001 + df = pd.DataFrame({'A': [nan, nan, 0, 1, ], + 'B': [0, 1, 2, nan], + 'C': [1., 2., 3., 4.], + 'D': [nan, nan, nan, nan]}) + sparse = df.to_sparse() + + # comparison changes internal repr, compare with dense + res = sparse > 1 + assert isinstance(res, pd.SparseDataFrame) + tm.assert_frame_equal(res.to_dense(), df > 1) + + res = sparse != 0 + assert isinstance(res, pd.SparseDataFrame) + tm.assert_frame_equal(res.to_dense(), df != 0) + + +class TestSparseDataFrameAnalytics(object): + + def test_cumsum(self, float_frame): + expected = SparseDataFrame(float_frame.to_dense().cumsum()) + + result = float_frame.cumsum() + tm.assert_sp_frame_equal(result, expected) + + result = float_frame.cumsum(axis=None) + tm.assert_sp_frame_equal(result, expected) + + result = float_frame.cumsum(axis=0) + tm.assert_sp_frame_equal(result, expected) + + def test_numpy_cumsum(self, float_frame): + result = np.cumsum(float_frame) + expected = SparseDataFrame(float_frame.to_dense().cumsum()) + tm.assert_sp_frame_equal(result, expected) + + msg = "the 'dtype' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.cumsum(float_frame, dtype=np.int64) + + msg = "the 'out' parameter is not supported" + with pytest.raises(ValueError, match=msg): + np.cumsum(float_frame, out=result) + + def test_numpy_func_call(self, float_frame): + # no exception should be raised even though + # numpy passes in 'axis=None' or `axis=-1' + funcs = ['sum', 'cumsum', 'var', + 'mean', 'prod', 'cumprod', + 'std', 'min', 'max'] + for func in funcs: + getattr(np, func)(float_frame) + + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH 17386)') + def test_quantile(self): + # GH 17386 + data = [[1, 1], [2, 10], [3, 100], [nan, nan]] + q = 0.1 + + sparse_df = SparseDataFrame(data) + result = sparse_df.quantile(q) + + dense_df = DataFrame(data) + dense_expected = dense_df.quantile(q) + sparse_expected = SparseSeries(dense_expected) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) + + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH 17386)') + def test_quantile_multi(self): + # GH 17386 + data = [[1, 1], [2, 10], [3, 100], [nan, nan]] + q = [0.1, 0.5] + + sparse_df = SparseDataFrame(data) + result = sparse_df.quantile(q) + + dense_df = DataFrame(data) + dense_expected = dense_df.quantile(q) + sparse_expected = SparseDataFrame(dense_expected) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) + + def test_assign_with_sparse_frame(self): + # GH 19163 + df = pd.DataFrame({"a": [1, 2, 3]}) + res = df.to_sparse(fill_value=False).assign(newcol=False) + exp = df.assign(newcol=False).to_sparse(fill_value=False) + + tm.assert_sp_frame_equal(res, exp) + + for column in res.columns: + assert type(res[column]) is SparseSeries + + @pytest.mark.parametrize("inplace", [True, False]) + @pytest.mark.parametrize("how", ["all", "any"]) + def test_dropna(self, inplace, how): + # Tests regression #21172. + expected = pd.SparseDataFrame({"F2": [0, 1]}) + input_df = pd.SparseDataFrame( + {"F1": [float('nan'), float('nan')], "F2": [0, 1]} + ) + result_df = input_df.dropna(axis=1, inplace=inplace, how=how) + if inplace: + result_df = input_df + tm.assert_sp_frame_equal(expected, result_df) diff --git a/pandas/tests/sparse/frame/test_indexing.py b/pandas/tests/sparse/frame/test_indexing.py new file mode 100644 index 0000000000000..2d2a7ac278dd6 --- /dev/null +++ b/pandas/tests/sparse/frame/test_indexing.py @@ -0,0 +1,109 @@ +import numpy as np +import pytest + +from pandas import DataFrame, SparseDataFrame +from pandas.util import testing as tm + +pytestmark = pytest.mark.skip("Wrong SparseBlock initialization (GH 17386)") + + +@pytest.mark.parametrize('data', [ + [[1, 1], [2, 2], [3, 3], [4, 4], [0, 0]], + [[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0], [np.nan, np.nan]], + [ + [1.0, 1.0 + 1.0j], + [2.0 + 2.0j, 2.0], + [3.0, 3.0 + 3.0j], + [4.0 + 4.0j, 4.0], + [np.nan, np.nan] + ] +]) +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_numeric_data(data): + # GH 17386 + lower_bound = 1.5 + + sparse = SparseDataFrame(data) + result = sparse.where(sparse > lower_bound) + + dense = DataFrame(data) + dense_expected = dense.where(dense > lower_bound) + sparse_expected = SparseDataFrame(dense_expected) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) + + +@pytest.mark.parametrize('data', [ + [[1, 1], [2, 2], [3, 3], [4, 4], [0, 0]], + [[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0], [np.nan, np.nan]], + [ + [1.0, 1.0 + 1.0j], + [2.0 + 2.0j, 2.0], + [3.0, 3.0 + 3.0j], + [4.0 + 4.0j, 4.0], + [np.nan, np.nan] + ] +]) +@pytest.mark.parametrize('other', [ + True, + -100, + 0.1, + 100.0 + 100.0j +]) +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_numeric_data_and_other(data, other): + # GH 17386 + lower_bound = 1.5 + + sparse = SparseDataFrame(data) + result = sparse.where(sparse > lower_bound, other) + + dense = DataFrame(data) + dense_expected = dense.where(dense > lower_bound, other) + sparse_expected = SparseDataFrame(dense_expected, + default_fill_value=other) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) + + +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_bool_data(): + # GH 17386 + data = [[False, False], [True, True], [False, False]] + cond = True + + sparse = SparseDataFrame(data) + result = sparse.where(sparse == cond) + + dense = DataFrame(data) + dense_expected = dense.where(dense == cond) + sparse_expected = SparseDataFrame(dense_expected) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) + + +@pytest.mark.parametrize('other', [ + True, + 0, + 0.1, + 100.0 + 100.0j +]) +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_bool_data_and_other(other): + # GH 17386 + data = [[False, False], [True, True], [False, False]] + cond = True + + sparse = SparseDataFrame(data) + result = sparse.where(sparse == cond, other) + + dense = DataFrame(data) + dense_expected = dense.where(dense == cond, other) + sparse_expected = SparseDataFrame(dense_expected, + default_fill_value=other) + + tm.assert_frame_equal(result, dense_expected) + tm.assert_sp_frame_equal(result, sparse_expected) diff --git a/pandas/tests/sparse/frame/test_to_csv.py b/pandas/tests/sparse/frame/test_to_csv.py new file mode 100644 index 0000000000000..ed19872f8a7ef --- /dev/null +++ b/pandas/tests/sparse/frame/test_to_csv.py @@ -0,0 +1,21 @@ +import numpy as np +import pytest + +from pandas import SparseDataFrame, read_csv +from pandas.util import testing as tm + + +class TestSparseDataFrameToCsv(object): + fill_values = [np.nan, 0, None, 1] + + @pytest.mark.parametrize('fill_value', fill_values) + def test_to_csv_sparse_dataframe(self, fill_value): + # GH19384 + sdf = SparseDataFrame({'a': type(self).fill_values}, + default_fill_value=fill_value) + + with tm.ensure_clean('sparse_df.csv') as path: + sdf.to_csv(path, index=False) + df = read_csv(path, skip_blank_lines=False) + + tm.assert_sp_frame_equal(df.to_sparse(fill_value=fill_value), sdf) diff --git a/pandas/tests/sparse/frame/test_to_from_scipy.py b/pandas/tests/sparse/frame/test_to_from_scipy.py new file mode 100644 index 0000000000000..bdb2cd022b451 --- /dev/null +++ b/pandas/tests/sparse/frame/test_to_from_scipy.py @@ -0,0 +1,185 @@ +from distutils.version import LooseVersion + +import numpy as np +import pytest + +from pandas.core.dtypes.common import is_bool_dtype + +import pandas as pd +from pandas import SparseDataFrame, SparseSeries +from pandas.core.sparse.api import SparseDtype +from pandas.util import testing as tm + +scipy = pytest.importorskip('scipy') +ignore_matrix_warning = pytest.mark.filterwarnings( + "ignore:the matrix subclass:PendingDeprecationWarning" +) + + +@pytest.mark.parametrize('index', [None, list('abc')]) # noqa: F811 +@pytest.mark.parametrize('columns', [None, list('def')]) +@pytest.mark.parametrize('fill_value', [None, 0, np.nan]) +@pytest.mark.parametrize('dtype', [bool, int, float, np.uint16]) +@ignore_matrix_warning +def test_from_to_scipy(spmatrix, index, columns, fill_value, dtype): + # GH 4343 + # Make one ndarray and from it one sparse matrix, both to be used for + # constructing frames and comparing results + arr = np.eye(3, dtype=dtype) + # GH 16179 + arr[0, 1] = dtype(2) + try: + spm = spmatrix(arr) + assert spm.dtype == arr.dtype + except (TypeError, AssertionError): + # If conversion to sparse fails for this spmatrix type and arr.dtype, + # then the combination is not currently supported in NumPy, so we + # can just skip testing it thoroughly + return + + sdf = SparseDataFrame(spm, index=index, columns=columns, + default_fill_value=fill_value) + + # Expected result construction is kind of tricky for all + # dtype-fill_value combinations; easiest to cast to something generic + # and except later on + rarr = arr.astype(object) + rarr[arr == 0] = np.nan + expected = SparseDataFrame(rarr, index=index, columns=columns).fillna( + fill_value if fill_value is not None else np.nan) + + # Assert frame is as expected + sdf_obj = sdf.astype(object) + tm.assert_sp_frame_equal(sdf_obj, expected) + tm.assert_frame_equal(sdf_obj.to_dense(), expected.to_dense()) + + # Assert spmatrices equal + assert dict(sdf.to_coo().todok()) == dict(spm.todok()) + + # Ensure dtype is preserved if possible + # XXX: verify this + res_dtype = bool if is_bool_dtype(dtype) else dtype + tm.assert_contains_all(sdf.dtypes.apply(lambda dtype: dtype.subtype), + {np.dtype(res_dtype)}) + assert sdf.to_coo().dtype == res_dtype + + # However, adding a str column results in an upcast to object + sdf['strings'] = np.arange(len(sdf)).astype(str) + assert sdf.to_coo().dtype == np.object_ + + +@pytest.mark.parametrize('fill_value', [None, 0, np.nan]) # noqa: F811 +@ignore_matrix_warning +@pytest.mark.filterwarnings("ignore:object dtype is not supp:UserWarning") +def test_from_to_scipy_object(spmatrix, fill_value): + # GH 4343 + dtype = object + columns = list('cd') + index = list('ab') + + if (spmatrix is scipy.sparse.dok_matrix and LooseVersion( + scipy.__version__) >= LooseVersion('0.19.0')): + pytest.skip("dok_matrix from object does not work in SciPy >= 0.19") + + # Make one ndarray and from it one sparse matrix, both to be used for + # constructing frames and comparing results + arr = np.eye(2, dtype=dtype) + try: + spm = spmatrix(arr) + assert spm.dtype == arr.dtype + except (TypeError, AssertionError): + # If conversion to sparse fails for this spmatrix type and arr.dtype, + # then the combination is not currently supported in NumPy, so we + # can just skip testing it thoroughly + return + + sdf = SparseDataFrame(spm, index=index, columns=columns, + default_fill_value=fill_value) + + # Expected result construction is kind of tricky for all + # dtype-fill_value combinations; easiest to cast to something generic + # and except later on + rarr = arr.astype(object) + rarr[arr == 0] = np.nan + expected = SparseDataFrame(rarr, index=index, columns=columns).fillna( + fill_value if fill_value is not None else np.nan) + + # Assert frame is as expected + sdf_obj = sdf.astype(SparseDtype(object, fill_value)) + tm.assert_sp_frame_equal(sdf_obj, expected) + tm.assert_frame_equal(sdf_obj.to_dense(), expected.to_dense()) + + # Assert spmatrices equal + assert dict(sdf.to_coo().todok()) == dict(spm.todok()) + + # Ensure dtype is preserved if possible + res_dtype = object + tm.assert_contains_all(sdf.dtypes.apply(lambda dtype: dtype.subtype), + {np.dtype(res_dtype)}) + assert sdf.to_coo().dtype == res_dtype + + +@ignore_matrix_warning +def test_from_scipy_correct_ordering(spmatrix): + # GH 16179 + arr = np.arange(1, 5).reshape(2, 2) + try: + spm = spmatrix(arr) + assert spm.dtype == arr.dtype + except (TypeError, AssertionError): + # If conversion to sparse fails for this spmatrix type and arr.dtype, + # then the combination is not currently supported in NumPy, so we + # can just skip testing it thoroughly + return + + sdf = SparseDataFrame(spm) + expected = SparseDataFrame(arr) + tm.assert_sp_frame_equal(sdf, expected) + tm.assert_frame_equal(sdf.to_dense(), expected.to_dense()) + + +@ignore_matrix_warning +def test_from_scipy_fillna(spmatrix): + # GH 16112 + arr = np.eye(3) + arr[1:, 0] = np.nan + + try: + spm = spmatrix(arr) + assert spm.dtype == arr.dtype + except (TypeError, AssertionError): + # If conversion to sparse fails for this spmatrix type and arr.dtype, + # then the combination is not currently supported in NumPy, so we + # can just skip testing it thoroughly + return + + sdf = SparseDataFrame(spm).fillna(-1.0) + + # Returning frame should fill all nan values with -1.0 + expected = SparseDataFrame({ + 0: SparseSeries([1., -1, -1]), + 1: SparseSeries([np.nan, 1, np.nan]), + 2: SparseSeries([np.nan, np.nan, 1]), + }, default_fill_value=-1) + + # fill_value is expected to be what .fillna() above was called with + # We don't use -1 as initial fill_value in expected SparseSeries + # construction because this way we obtain "compressed" SparseArrays, + # avoiding having to construct them ourselves + for col in expected: + expected[col].fill_value = -1 + + tm.assert_sp_frame_equal(sdf, expected) + + +def test_index_names_multiple_nones(): + # https://github.com/pandas-dev/pandas/pull/24092 + sparse = pytest.importorskip("scipy.sparse") + + s = (pd.Series(1, index=pd.MultiIndex.from_product([['A', 'B'], [0, 1]])) + .to_sparse()) + result, _, _ = s.to_coo() + assert isinstance(result, sparse.coo_matrix) + result = result.toarray() + expected = np.ones((2, 2), dtype="int64") + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/sparse/series/__init__.py b/pandas/tests/sparse/series/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/sparse/series/test_indexing.py b/pandas/tests/sparse/series/test_indexing.py new file mode 100644 index 0000000000000..0f4235d7cc3fe --- /dev/null +++ b/pandas/tests/sparse/series/test_indexing.py @@ -0,0 +1,111 @@ +import numpy as np +import pytest + +from pandas import Series, SparseSeries +from pandas.util import testing as tm + +pytestmark = pytest.mark.skip("Wrong SparseBlock initialization (GH 17386)") + + +@pytest.mark.parametrize('data', [ + [1, 1, 2, 2, 3, 3, 4, 4, 0, 0], + [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, np.nan, np.nan], + [ + 1.0, 1.0 + 1.0j, + 2.0 + 2.0j, 2.0, + 3.0, 3.0 + 3.0j, + 4.0 + 4.0j, 4.0, + np.nan, np.nan + ] +]) +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_numeric_data(data): + # GH 17386 + lower_bound = 1.5 + + sparse = SparseSeries(data) + result = sparse.where(sparse > lower_bound) + + dense = Series(data) + dense_expected = dense.where(dense > lower_bound) + sparse_expected = SparseSeries(dense_expected) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) + + +@pytest.mark.parametrize('data', [ + [1, 1, 2, 2, 3, 3, 4, 4, 0, 0], + [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, np.nan, np.nan], + [ + 1.0, 1.0 + 1.0j, + 2.0 + 2.0j, 2.0, + 3.0, 3.0 + 3.0j, + 4.0 + 4.0j, 4.0, + np.nan, np.nan + ] +]) +@pytest.mark.parametrize('other', [ + True, + -100, + 0.1, + 100.0 + 100.0j +]) +@pytest.mark.skip(reason='Wrong SparseBlock initialization ' + '(Segfault) ' + '(GH 17386)') +def test_where_with_numeric_data_and_other(data, other): + # GH 17386 + lower_bound = 1.5 + + sparse = SparseSeries(data) + result = sparse.where(sparse > lower_bound, other) + + dense = Series(data) + dense_expected = dense.where(dense > lower_bound, other) + sparse_expected = SparseSeries(dense_expected, fill_value=other) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) + + +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)') +def test_where_with_bool_data(): + # GH 17386 + data = [False, False, True, True, False, False] + cond = True + + sparse = SparseSeries(data) + result = sparse.where(sparse == cond) + + dense = Series(data) + dense_expected = dense.where(dense == cond) + sparse_expected = SparseSeries(dense_expected) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) + + +@pytest.mark.parametrize('other', [ + True, + 0, + 0.1, + 100.0 + 100.0j +]) +@pytest.mark.skip(reason='Wrong SparseBlock initialization ' + '(Segfault) ' + '(GH 17386)') +def test_where_with_bool_data_and_other(other): + # GH 17386 + data = [False, False, True, True, False, False] + cond = True + + sparse = SparseSeries(data) + result = sparse.where(sparse == cond, other) + + dense = Series(data) + dense_expected = dense.where(dense == cond, other) + sparse_expected = SparseSeries(dense_expected, fill_value=other) + + tm.assert_series_equal(result, dense_expected) + tm.assert_sp_series_equal(result, sparse_expected) diff --git a/pandas/tests/sparse/test_series.py b/pandas/tests/sparse/series/test_series.py similarity index 70% rename from pandas/tests/sparse/test_series.py rename to pandas/tests/sparse/series/test_series.py index 8aa85a5b7f396..93cf629f20957 100644 --- a/pandas/tests/sparse/test_series.py +++ b/pandas/tests/sparse/series/test_series.py @@ -1,24 +1,26 @@ # pylint: disable-msg=E1101,W0612 +from datetime import datetime import operator -from numpy import nan import numpy as np -import pandas as pd +from numpy import nan +import pytest -from pandas import Series, DataFrame, bdate_range -from pandas.core.common import isnull -from pandas.tseries.offsets import BDay -import pandas.util.testing as tm -from pandas.compat import range -from pandas import compat -from pandas.tools.util import cartesian_product +from pandas._libs.sparse import BlockIndex, IntIndex +from pandas.compat import PY36, range +from pandas.errors import PerformanceWarning +import pandas.util._test_decorators as td -import pandas.sparse.frame as spf +import pandas as pd +from pandas import ( + DataFrame, Series, SparseDtype, SparseSeries, bdate_range, compat, isna) +from pandas.core.reshape.util import cartesian_product +import pandas.core.sparse.frame as spf +from pandas.tests.series.test_api import SharedWithSparse +import pandas.util.testing as tm -from pandas.sparse.libsparse import BlockIndex, IntIndex -from pandas.sparse.api import SparseSeries -from pandas.tests.series.test_misc_api import SharedWithSparse +from pandas.tseries.offsets import BDay def _test_data1(): @@ -55,9 +57,13 @@ def _test_data2_zero(): return arr, index -class TestSparseSeries(tm.TestCase, SharedWithSparse): +class TestSparseSeries(SharedWithSparse): - def setUp(self): + series_klass = SparseSeries + # SharedWithSparse tests use generic, series_klass-agnostic assertion + _assert_series_equal = staticmethod(tm.assert_sp_series_equal) + + def setup_method(self, method): arr, index = _test_data1() date_index = bdate_range('1/1/2011', periods=len(index)) @@ -87,26 +93,56 @@ def setUp(self): self.ziseries2 = SparseSeries(arr, index=index, kind='integer', fill_value=0) + def test_constructor_dict_input(self): + # gh-16905 + constructor_dict = {1: 1.} + index = [0, 1, 2] + + # Series with index passed in + series = pd.Series(constructor_dict) + expected = SparseSeries(series, index=index) + + result = SparseSeries(constructor_dict, index=index) + tm.assert_sp_series_equal(result, expected) + + # Series with index and dictionary with no index + expected = SparseSeries(series) + + result = SparseSeries(constructor_dict) + tm.assert_sp_series_equal(result, expected) + + def test_constructor_dict_order(self): + # GH19018 + # initialization ordering: by insertion order if python>= 3.6, else + # order by value + d = {'b': 1, 'a': 0, 'c': 2} + result = SparseSeries(d) + if PY36: + expected = SparseSeries([1, 0, 2], index=list('bac')) + else: + expected = SparseSeries([0, 1, 2], index=list('abc')) + tm.assert_sp_series_equal(result, expected) + def test_constructor_dtype(self): arr = SparseSeries([np.nan, 1, 2, np.nan]) - self.assertEqual(arr.dtype, np.float64) - self.assertTrue(np.isnan(arr.fill_value)) + assert arr.dtype == SparseDtype(np.float64) + assert np.isnan(arr.fill_value) arr = SparseSeries([np.nan, 1, 2, np.nan], fill_value=0) - self.assertEqual(arr.dtype, np.float64) - self.assertEqual(arr.fill_value, 0) + assert arr.dtype == SparseDtype(np.float64, 0) + assert arr.fill_value == 0 arr = SparseSeries([0, 1, 2, 4], dtype=np.int64, fill_value=np.nan) - self.assertEqual(arr.dtype, np.int64) - self.assertTrue(np.isnan(arr.fill_value)) + assert arr.dtype == SparseDtype(np.int64, np.nan) + assert np.isnan(arr.fill_value) arr = SparseSeries([0, 1, 2, 4], dtype=np.int64) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) + assert arr.dtype == SparseDtype(np.int64, 0) + assert arr.fill_value == 0 arr = SparseSeries([0, 1, 2, 4], fill_value=0, dtype=np.int64) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) + assert arr.dtype == SparseDtype(np.int64, 0) + assert arr.fill_value == 0 def test_iteration_and_str(self): [x for x in self.bseries] @@ -122,11 +158,6 @@ def test_construct_DataFrame_with_sp_series(self): df.dtypes str(df) - tm.assert_sp_series_equal(df['col'], self.bseries, check_names=False) - - result = df.iloc[:, 0] - tm.assert_sp_series_equal(result, self.bseries, check_names=False) - # blocking expected = Series({'col': 'float64:sparse'}) result = df.ftypes @@ -134,12 +165,12 @@ def test_construct_DataFrame_with_sp_series(self): def test_constructor_preserve_attr(self): arr = pd.SparseArray([1, 0, 3, 0], dtype=np.int64, fill_value=0) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) + assert arr.dtype == SparseDtype(np.int64) + assert arr.fill_value == 0 s = pd.SparseSeries(arr, name='x') - self.assertEqual(s.dtype, np.int64) - self.assertEqual(s.fill_value, 0) + assert s.dtype == SparseDtype(np.int64) + assert s.fill_value == 0 def test_series_density(self): # GH2803 @@ -147,22 +178,13 @@ def test_series_density(self): ts[2:-2] = nan sts = ts.to_sparse() density = sts.density # don't die - self.assertEqual(density, 4 / 10.0) + assert density == 4 / 10.0 def test_sparse_to_dense(self): arr, index = _test_data1() series = self.bseries.to_dense() tm.assert_series_equal(series, Series(arr, name='bseries')) - # see gh-14647 - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - series = self.bseries.to_dense(sparse_only=True) - - indexer = np.isfinite(arr) - exp = Series(arr[indexer], index=index[indexer], name='bseries') - tm.assert_series_equal(series, exp) - series = self.iseries.to_dense() tm.assert_series_equal(series, Series(arr, name='iseries')) @@ -202,12 +224,12 @@ def test_dense_to_sparse(self): iseries = series.to_sparse(kind='integer') tm.assert_sp_series_equal(bseries, self.bseries) tm.assert_sp_series_equal(iseries, self.iseries, check_names=False) - self.assertEqual(iseries.name, self.bseries.name) + assert iseries.name == self.bseries.name - self.assertEqual(len(series), len(bseries)) - self.assertEqual(len(series), len(iseries)) - self.assertEqual(series.shape, bseries.shape) - self.assertEqual(series.shape, iseries.shape) + assert len(series) == len(bseries) + assert len(series) == len(iseries) + assert series.shape == bseries.shape + assert series.shape == iseries.shape # non-NaN fill value series = self.zbseries.to_dense() @@ -215,26 +237,26 @@ def test_dense_to_sparse(self): ziseries = series.to_sparse(kind='integer', fill_value=0) tm.assert_sp_series_equal(zbseries, self.zbseries) tm.assert_sp_series_equal(ziseries, self.ziseries, check_names=False) - self.assertEqual(ziseries.name, self.zbseries.name) + assert ziseries.name == self.zbseries.name - self.assertEqual(len(series), len(zbseries)) - self.assertEqual(len(series), len(ziseries)) - self.assertEqual(series.shape, zbseries.shape) - self.assertEqual(series.shape, ziseries.shape) + assert len(series) == len(zbseries) + assert len(series) == len(ziseries) + assert series.shape == zbseries.shape + assert series.shape == ziseries.shape def test_to_dense_preserve_name(self): assert (self.bseries.name is not None) result = self.bseries.to_dense() - self.assertEqual(result.name, self.bseries.name) + assert result.name == self.bseries.name def test_constructor(self): # test setup guys - self.assertTrue(np.isnan(self.bseries.fill_value)) - tm.assertIsInstance(self.bseries.sp_index, BlockIndex) - self.assertTrue(np.isnan(self.iseries.fill_value)) - tm.assertIsInstance(self.iseries.sp_index, IntIndex) + assert np.isnan(self.bseries.fill_value) + assert isinstance(self.bseries.sp_index, BlockIndex) + assert np.isnan(self.iseries.fill_value) + assert isinstance(self.iseries.sp_index, IntIndex) - self.assertEqual(self.zbseries.fill_value, 0) + assert self.zbseries.fill_value == 0 tm.assert_numpy_array_equal(self.zbseries.values.values, self.bseries.to_dense().fillna(0).values) @@ -243,13 +265,13 @@ def _check_const(sparse, name): # use passed series name result = SparseSeries(sparse) tm.assert_sp_series_equal(result, sparse) - self.assertEqual(sparse.name, name) - self.assertEqual(result.name, name) + assert sparse.name == name + assert result.name == name # use passed name result = SparseSeries(sparse, name='x') tm.assert_sp_series_equal(result, sparse, check_names=False) - self.assertEqual(result.name, 'x') + assert result.name == 'x' _check_const(self.bseries, 'bseries') _check_const(self.iseries, 'iseries') @@ -258,7 +280,7 @@ def _check_const(sparse, name): # Sparse time series works date_index = bdate_range('1/1/2000', periods=len(self.bseries)) s5 = SparseSeries(self.bseries, index=date_index) - tm.assertIsInstance(s5, SparseSeries) + assert isinstance(s5, SparseSeries) # pass Series bseries2 = SparseSeries(self.bseries.to_dense()) @@ -270,31 +292,31 @@ def _check_const(sparse, name): values = np.ones(self.bseries.npoints) sp = SparseSeries(values, sparse_index=self.bseries.sp_index) sp.sp_values[:5] = 97 - self.assertEqual(values[0], 97) + assert values[0] == 97 - self.assertEqual(len(sp), 20) - self.assertEqual(sp.shape, (20, )) + assert len(sp) == 20 + assert sp.shape == (20, ) # but can make it copy! sp = SparseSeries(values, sparse_index=self.bseries.sp_index, copy=True) sp.sp_values[:5] = 100 - self.assertEqual(values[0], 97) + assert values[0] == 97 - self.assertEqual(len(sp), 20) - self.assertEqual(sp.shape, (20, )) + assert len(sp) == 20 + assert sp.shape == (20, ) def test_constructor_scalar(self): data = 5 sp = SparseSeries(data, np.arange(100)) sp = sp.reindex(np.arange(200)) - self.assertTrue((sp.loc[:99] == data).all()) - self.assertTrue(isnull(sp.loc[100:]).all()) + assert (sp.loc[:99] == data).all() + assert isna(sp.loc[100:]).all() data = np.nan sp = SparseSeries(data, np.arange(100)) - self.assertEqual(len(sp), 100) - self.assertEqual(sp.shape, (100, )) + assert len(sp) == 100 + assert sp.shape == (100, ) def test_constructor_ndarray(self): pass @@ -303,20 +325,20 @@ def test_constructor_nonnan(self): arr = [0, 0, 0, nan, nan] sp_series = SparseSeries(arr, fill_value=0) tm.assert_numpy_array_equal(sp_series.values.values, np.array(arr)) - self.assertEqual(len(sp_series), 5) - self.assertEqual(sp_series.shape, (5, )) + assert len(sp_series) == 5 + assert sp_series.shape == (5, ) - # GH 9272 def test_constructor_empty(self): + # see gh-9272 sp = SparseSeries() - self.assertEqual(len(sp.index), 0) - self.assertEqual(sp.shape, (0, )) + assert len(sp.index) == 0 + assert sp.shape == (0, ) def test_copy_astype(self): cop = self.bseries.astype(np.float64) - self.assertIsNot(cop, self.bseries) - self.assertIs(cop.sp_index, self.bseries.sp_index) - self.assertEqual(cop.dtype, np.float64) + assert cop is not self.bseries + assert cop.sp_index is self.bseries.sp_index + assert cop.dtype == SparseDtype(np.float64) cop2 = self.iseries.copy() @@ -325,8 +347,8 @@ def test_copy_astype(self): # test that data is copied cop[:5] = 97 - self.assertEqual(cop.sp_values[0], 97) - self.assertNotEqual(self.bseries.sp_values[0], 97) + assert cop.sp_values[0] == 97 + assert self.bseries.sp_values[0] != 97 # correct fill value zbcop = self.zbseries.copy() @@ -338,23 +360,27 @@ def test_copy_astype(self): # no deep copy view = self.bseries.copy(deep=False) view.sp_values[:5] = 5 - self.assertTrue((self.bseries.sp_values[:5] == 5).all()) + assert (self.bseries.sp_values[:5] == 5).all() def test_shape(self): - # GH 10452 - self.assertEqual(self.bseries.shape, (20, )) - self.assertEqual(self.btseries.shape, (20, )) - self.assertEqual(self.iseries.shape, (20, )) + # see gh-10452 + assert self.bseries.shape == (20, ) + assert self.btseries.shape == (20, ) + assert self.iseries.shape == (20, ) - self.assertEqual(self.bseries2.shape, (15, )) - self.assertEqual(self.iseries2.shape, (15, )) + assert self.bseries2.shape == (15, ) + assert self.iseries2.shape == (15, ) - self.assertEqual(self.zbseries2.shape, (15, )) - self.assertEqual(self.ziseries2.shape, (15, )) + assert self.zbseries2.shape == (15, ) + assert self.ziseries2.shape == (15, ) def test_astype(self): - with tm.assertRaises(ValueError): - self.bseries.astype(np.int64) + result = self.bseries.astype(SparseDtype(np.int64, 0)) + expected = (self.bseries.to_dense() + .fillna(0) + .astype(np.int64) + .to_sparse(fill_value=0)) + tm.assert_sp_series_equal(result, expected) def test_astype_all(self): orig = pd.Series(np.array([1, 2, 3])) @@ -363,13 +389,14 @@ def test_astype_all(self): types = [np.float64, np.float32, np.int64, np.int32, np.int16, np.int8] for typ in types: - res = s.astype(typ) - self.assertEqual(res.dtype, typ) + dtype = SparseDtype(typ) + res = s.astype(dtype) + assert res.dtype == dtype tm.assert_series_equal(res.to_dense(), orig.astype(typ)) def test_kind(self): - self.assertEqual(self.bseries.kind, 'block') - self.assertEqual(self.iseries.kind, 'integer') + assert self.bseries.kind == 'block' + assert self.iseries.kind == 'integer' def test_to_frame(self): # GH 9850 @@ -390,7 +417,7 @@ def test_to_frame(self): def test_pickle(self): def _test_roundtrip(series): - unpickled = self.round_trip_pickle(series) + unpickled = tm.round_trip_pickle(series) tm.assert_sp_series_equal(series, unpickled) tm.assert_series_equal(series.to_dense(), unpickled.to_dense()) @@ -425,44 +452,52 @@ def _check_getitem(sp, dense): _check_getitem(self.ziseries, self.ziseries.to_dense()) # exception handling - self.assertRaises(Exception, self.bseries.__getitem__, - len(self.bseries) + 1) + with pytest.raises(IndexError, match="Out of bounds access"): + self.bseries[len(self.bseries) + 1] # index not contained - self.assertRaises(Exception, self.btseries.__getitem__, - self.btseries.index[-1] + BDay()) + msg = r"Timestamp\('2011-01-31 00:00:00', freq='B'\)" + with pytest.raises(KeyError, match=msg): + self.btseries[self.btseries.index[-1] + BDay()] def test_get_get_value(self): tm.assert_almost_equal(self.bseries.get(10), self.bseries[10]) - self.assertIsNone(self.bseries.get(len(self.bseries) + 1)) + assert self.bseries.get(len(self.bseries) + 1) is None dt = self.btseries.index[10] result = self.btseries.get(dt) expected = self.btseries.to_dense()[dt] tm.assert_almost_equal(result, expected) - tm.assert_almost_equal(self.bseries.get_value(10), self.bseries[10]) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + tm.assert_almost_equal( + self.bseries.get_value(10), self.bseries[10]) def test_set_value(self): idx = self.btseries.index[7] - self.btseries.set_value(idx, 0) - self.assertEqual(self.btseries[idx], 0) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + self.btseries.set_value(idx, 0) + assert self.btseries[idx] == 0 - self.iseries.set_value('foobar', 0) - self.assertEqual(self.iseries.index[-1], 'foobar') - self.assertEqual(self.iseries['foobar'], 0) + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + self.iseries.set_value('foobar', 0) + assert self.iseries.index[-1] == 'foobar' + assert self.iseries['foobar'] == 0 def test_getitem_slice(self): idx = self.bseries.index res = self.bseries[::2] - tm.assertIsInstance(res, SparseSeries) + assert isinstance(res, SparseSeries) expected = self.bseries.reindex(idx[::2]) tm.assert_sp_series_equal(res, expected) res = self.bseries[:5] - tm.assertIsInstance(res, SparseSeries) + assert isinstance(res, SparseSeries) tm.assert_sp_series_equal(res, self.bseries.reindex(idx[:5])) res = self.bseries[5:] @@ -479,7 +514,7 @@ def _compare_with_dense(sp): def _compare(idx): dense_result = dense.take(idx).values sparse_result = sp.take(idx) - self.assertIsInstance(sparse_result, SparseSeries) + assert isinstance(sparse_result, SparseSeries) tm.assert_almost_equal(dense_result, sparse_result.values.values) @@ -489,13 +524,21 @@ def _compare(idx): self._check_all(_compare_with_dense) - self.assertRaises(Exception, self.bseries.take, - [0, len(self.bseries) + 1]) + msg = "index 21 is out of bounds for size 20" + with pytest.raises(IndexError, match=msg): + self.bseries.take([0, len(self.bseries) + 1]) # Corner case + # XXX: changed test. Why wsa this considered a corner case? sp = SparseSeries(np.ones(10) * nan) exp = pd.Series(np.repeat(nan, 5)) - tm.assert_series_equal(sp.take([0, 1, 2, 3, 4]), exp) + tm.assert_series_equal(sp.take([0, 1, 2, 3, 4]), exp.to_sparse()) + + with tm.assert_produces_warning(FutureWarning): + sp.take([1, 5], convert=True) + + with tm.assert_produces_warning(FutureWarning): + sp.take([1, 5], convert=False) def test_numpy_take(self): sp = SparseSeries([1.0, 2.0, 3.0]) @@ -505,16 +548,16 @@ def test_numpy_take(self): np.take(sp.to_dense(), indices, axis=0)) msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.take, - sp, indices, out=np.empty(sp.shape)) + with pytest.raises(ValueError, match=msg): + np.take(sp, indices, out=np.empty(sp.shape)) msg = "the 'mode' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.take, - sp, indices, mode='clip') + with pytest.raises(ValueError, match=msg): + np.take(sp, indices, out=None, mode='clip') def test_setitem(self): self.bseries[5] = 7. - self.assertEqual(self.bseries[5], 7.) + assert self.bseries[5] == 7. def test_setslice(self): self.bseries[5:10] = 7. @@ -586,35 +629,61 @@ def _check_inplace_op(iop, op): _check_inplace_op(getattr(operator, "i%s" % op), getattr(operator, op)) + @pytest.mark.parametrize("values, op, fill_value", [ + ([True, False, False, True], operator.invert, True), + ([True, False, False, True], operator.invert, False), + ([0, 1, 2, 3], operator.pos, 0), + ([0, 1, 2, 3], operator.neg, 0), + ([0, np.nan, 2, 3], operator.pos, np.nan), + ([0, np.nan, 2, 3], operator.neg, np.nan), + ]) + def test_unary_operators(self, values, op, fill_value): + # https://github.com/pandas-dev/pandas/issues/22835 + values = np.asarray(values) + if op is operator.invert: + new_fill_value = not fill_value + else: + new_fill_value = op(fill_value) + s = SparseSeries(values, + fill_value=fill_value, + index=['a', 'b', 'c', 'd'], + name='name') + result = op(s) + expected = SparseSeries(op(values), + fill_value=new_fill_value, + index=['a', 'b', 'c', 'd'], + name='name') + tm.assert_sp_series_equal(result, expected) + def test_abs(self): s = SparseSeries([1, 2, -3], name='x') expected = SparseSeries([1, 2, 3], name='x') result = s.abs() tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' result = abs(s) tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' result = np.abs(s) tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' s = SparseSeries([1, -2, 2, -3], fill_value=-2, name='x') expected = SparseSeries([1, 2, 3], sparse_index=s.sp_index, fill_value=2, name='x') result = s.abs() tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' result = abs(s) tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' result = np.abs(s) tm.assert_sp_series_equal(result, expected) - self.assertEqual(result.name, 'x') + assert result.name == 'x' def test_reindex(self): def _compare_with_series(sps, new_index): @@ -639,7 +708,7 @@ def _compare_with_series(sps, new_index): # special cases same_index = self.bseries.reindex(self.bseries.index) tm.assert_sp_series_equal(self.bseries, same_index) - self.assertIsNot(same_index, self.bseries) + assert same_index is not self.bseries # corner cases sp = SparseSeries([], index=[]) @@ -650,7 +719,7 @@ def _compare_with_series(sps, new_index): # with copy=False reindexed = self.bseries.reindex(self.bseries.index, copy=True) reindexed.sp_values[:] = 1. - self.assertTrue((self.bseries.sp_values != 1.).all()) + assert (self.bseries.sp_values != 1.).all() reindexed = self.bseries.reindex(self.bseries.index, copy=False) reindexed.sp_values[:] = 1. @@ -663,7 +732,7 @@ def _check(values, index1, index2, fill_value): first_series = SparseSeries(values, sparse_index=index1, fill_value=fill_value) reindexed = first_series.sparse_reindex(index2) - self.assertIs(reindexed.sp_index, index2) + assert reindexed.sp_index is index2 int_indices1 = index1.to_int_index().indices int_indices2 = index2.to_int_index().indices @@ -702,9 +771,9 @@ def _check_all(values, first, second): first_series = SparseSeries(values1, sparse_index=IntIndex(length, index1), fill_value=nan) - with tm.assertRaisesRegexp(TypeError, - 'new index must be a SparseIndex'): - reindexed = first_series.sparse_reindex(0) # noqa + with pytest.raises(TypeError, + match='new index must be a SparseIndex'): + first_series.sparse_reindex(0) def test_repr(self): # TODO: These aren't used @@ -728,7 +797,7 @@ def _compare_with_dense(obj, op): sparse_result = getattr(obj, op)() series = obj.to_dense() dense_result = getattr(series, op)() - self.assertEqual(sparse_result, dense_result) + assert sparse_result == dense_result to_compare = ['count', 'sum', 'mean', 'std', 'var', 'skew'] @@ -758,26 +827,26 @@ def _compare_all(obj): def test_dropna(self): sp = SparseSeries([0, 0, 0, nan, nan, 5, 6], fill_value=0) - sp_valid = sp.valid() + sp_valid = sp.dropna() - expected = sp.to_dense().valid() + expected = sp.to_dense().dropna() expected = expected[expected != 0] exp_arr = pd.SparseArray(expected.values, fill_value=0, kind='block') tm.assert_sp_array_equal(sp_valid.values, exp_arr) - self.assert_index_equal(sp_valid.index, expected.index) - self.assertEqual(len(sp_valid.sp_values), 2) + tm.assert_index_equal(sp_valid.index, expected.index) + assert len(sp_valid.sp_values) == 2 result = self.bseries.dropna() expected = self.bseries.to_dense().dropna() - self.assertNotIsInstance(result, SparseSeries) + assert not isinstance(result, SparseSeries) tm.assert_series_equal(result, expected) def test_homogenize(self): def _check_matches(indices, expected): - data = {} - for i, idx in enumerate(indices): - data[i] = SparseSeries(idx.to_int_index().indices, - sparse_index=idx, fill_value=np.nan) + data = {i: SparseSeries(idx.to_int_index().indices, + sparse_index=idx, fill_value=np.nan) + for i, idx in enumerate(indices)} + # homogenized is only valid with NaN fill values homogenized = spf.homogenize(data) @@ -796,7 +865,7 @@ def _check_matches(indices, expected): # must have NaN fill value data = {'a': SparseSeries(np.arange(7), sparse_index=expected2, fill_value=0)} - with tm.assertRaisesRegexp(TypeError, "NaN fill value"): + with pytest.raises(TypeError, match="NaN fill value"): spf.homogenize(data) def test_fill_value_corner(self): @@ -804,13 +873,13 @@ def test_fill_value_corner(self): cop.fill_value = 0 result = self.bseries / cop - self.assertTrue(np.isnan(result.fill_value)) + assert np.isnan(result.fill_value) cop2 = self.zbseries.copy() cop2.fill_value = 1 result = cop2 / cop # 1 / 0 is inf - self.assertTrue(np.isinf(result.fill_value)) + assert np.isinf(result.fill_value) def test_fill_value_when_combine_const(self): # GH12723 @@ -818,13 +887,13 @@ def test_fill_value_when_combine_const(self): exp = s.fillna(0).add(2) res = s.add(2, fill_value=0) - self.assert_series_equal(res, exp) + tm.assert_series_equal(res, exp) def test_shift(self): series = SparseSeries([nan, 1., 2., 3., nan, nan], index=np.arange(6)) shifted = series.shift(0) - self.assertIsNot(shifted, series) + # assert shifted is not series tm.assert_sp_series_equal(shifted, series) f = lambda s: s.shift(1) @@ -846,10 +915,14 @@ def test_shift_nan(self): orig = pd.Series([np.nan, 2, np.nan, 4, 0, np.nan, 0]) sparse = orig.to_sparse() - tm.assert_sp_series_equal(sparse.shift(0), orig.shift(0).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(1), orig.shift(1).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(2), orig.shift(2).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(3), orig.shift(3).to_sparse()) + tm.assert_sp_series_equal(sparse.shift(0), orig.shift(0).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(1), orig.shift(1).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(2), orig.shift(2).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(3), orig.shift(3).to_sparse(), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(-1), orig.shift(-1).to_sparse()) tm.assert_sp_series_equal(sparse.shift(-2), orig.shift(-2).to_sparse()) @@ -857,23 +930,32 @@ def test_shift_nan(self): tm.assert_sp_series_equal(sparse.shift(-4), orig.shift(-4).to_sparse()) sparse = orig.to_sparse(fill_value=0) - tm.assert_sp_series_equal(sparse.shift(0), - orig.shift(0).to_sparse(fill_value=0)) + tm.assert_sp_series_equal( + sparse.shift(0), + orig.shift(0).to_sparse(fill_value=sparse.fill_value) + ) tm.assert_sp_series_equal(sparse.shift(1), - orig.shift(1).to_sparse(fill_value=0)) + orig.shift(1).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(2), - orig.shift(2).to_sparse(fill_value=0)) + orig.shift(2).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(3), - orig.shift(3).to_sparse(fill_value=0)) + orig.shift(3).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(-1), - orig.shift(-1).to_sparse(fill_value=0)) + orig.shift(-1).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(-2), - orig.shift(-2).to_sparse(fill_value=0)) + orig.shift(-2).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(-3), - orig.shift(-3).to_sparse(fill_value=0)) + orig.shift(-3).to_sparse(fill_value=0), + check_kind=False) tm.assert_sp_series_equal(sparse.shift(-4), - orig.shift(-4).to_sparse(fill_value=0)) + orig.shift(-4).to_sparse(fill_value=0), + check_kind=False) def test_shift_dtype(self): # GH 12908 @@ -886,39 +968,47 @@ def test_shift_dtype(self): tm.assert_sp_series_equal(sparse.shift(0), orig.shift(0).to_sparse(fill_value=np.nan)) # shift(1) or more span changes dtype to float64 - tm.assert_sp_series_equal(sparse.shift(1), orig.shift(1).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(2), orig.shift(2).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(3), orig.shift(3).to_sparse()) + # XXX: SparseSeries doesn't need to shift dtype here. + # Do we want to astype in shift, for backwards compat? + # If not, document it. + tm.assert_sp_series_equal(sparse.shift(1).astype('f8'), + orig.shift(1).to_sparse(kind='integer')) + tm.assert_sp_series_equal(sparse.shift(2).astype('f8'), + orig.shift(2).to_sparse(kind='integer')) + tm.assert_sp_series_equal(sparse.shift(3).astype('f8'), + orig.shift(3).to_sparse(kind='integer')) + + tm.assert_sp_series_equal(sparse.shift(-1).astype('f8'), + orig.shift(-1).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(-2).astype('f8'), + orig.shift(-2).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(-3).astype('f8'), + orig.shift(-3).to_sparse(), + check_kind=False) + tm.assert_sp_series_equal(sparse.shift(-4).astype('f8'), + orig.shift(-4).to_sparse(), + check_kind=False) + + @pytest.mark.parametrize("fill_value", [ + 0, + 1, + np.nan + ]) + @pytest.mark.parametrize("periods", [0, 1, 2, 3, -1, -2, -3, -4]) + def test_shift_dtype_fill_value(self, fill_value, periods): + # GH 12908 + orig = pd.Series([1, 0, 0, 4], dtype=np.dtype('int64')) - tm.assert_sp_series_equal(sparse.shift(-1), orig.shift(-1).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(-2), orig.shift(-2).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(-3), orig.shift(-3).to_sparse()) - tm.assert_sp_series_equal(sparse.shift(-4), orig.shift(-4).to_sparse()) + sparse = orig.to_sparse(fill_value=fill_value) - def test_shift_dtype_fill_value(self): - # GH 12908 - orig = pd.Series([1, 0, 0, 4], dtype=np.int64) - - for v in [0, 1, np.nan]: - sparse = orig.to_sparse(fill_value=v) - - tm.assert_sp_series_equal(sparse.shift(0), - orig.shift(0).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(1), - orig.shift(1).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(2), - orig.shift(2).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(3), - orig.shift(3).to_sparse(fill_value=v)) - - tm.assert_sp_series_equal(sparse.shift(-1), - orig.shift(-1).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(-2), - orig.shift(-2).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(-3), - orig.shift(-3).to_sparse(fill_value=v)) - tm.assert_sp_series_equal(sparse.shift(-4), - orig.shift(-4).to_sparse(fill_value=v)) + result = sparse.shift(periods) + expected = orig.shift(periods).to_sparse(fill_value=fill_value) + + tm.assert_sp_series_equal(result, expected, + check_kind=False, + consolidate_block_indices=True) def test_combine_first(self): s = self.bseries @@ -932,10 +1022,21 @@ def test_combine_first(self): tm.assert_sp_series_equal(result, result2) tm.assert_sp_series_equal(result, expected) + @pytest.mark.parametrize('deep', [True, False]) + @pytest.mark.parametrize('fill_value', [0, 1, np.nan, None]) + def test_memory_usage_deep(self, deep, fill_value): + values = [1.0] + [fill_value] * 20 + sparse_series = SparseSeries(values, fill_value=fill_value) + dense_series = Series(values) + sparse_usage = sparse_series.memory_usage(deep=deep) + dense_usage = dense_series.memory_usage(deep=deep) + + assert sparse_usage < dense_usage -class TestSparseHandlingMultiIndexes(tm.TestCase): - def setUp(self): +class TestSparseHandlingMultiIndexes(object): + + def setup_method(self, method): miindex = pd.MultiIndex.from_product( [["x", "y"], ["10", "20"]], names=['row-foo', 'row-bar']) micol = pd.MultiIndex.from_product( @@ -959,11 +1060,14 @@ def test_round_trip_preserve_multiindex_names(self): check_names=True) -class TestSparseSeriesScipyInteraction(tm.TestCase): +@td.skip_if_no_scipy +@pytest.mark.filterwarnings( + "ignore:the matrix subclass:PendingDeprecationWarning" +) +class TestSparseSeriesScipyInteraction(object): # Issue 8048: add SparseSeries coo methods - def setUp(self): - tm._skip_if_no_scipy() + def setup_method(self, method): import scipy.sparse # SparseSeries inputs used in tests, the tests rely on the order self.sparse_series = [] @@ -1036,25 +1140,35 @@ def test_to_coo_text_names_text_row_levels_nosort(self): def test_to_coo_bad_partition_nonnull_intersection(self): ss = self.sparse_series[0] - self.assertRaises(ValueError, ss.to_coo, ['A', 'B', 'C'], ['C', 'D']) + msg = "Is not a partition because intersection is not null" + with pytest.raises(ValueError, match=msg): + ss.to_coo(['A', 'B', 'C'], ['C', 'D']) def test_to_coo_bad_partition_small_union(self): ss = self.sparse_series[0] - self.assertRaises(ValueError, ss.to_coo, ['A'], ['C', 'D']) + msg = "Is not a partition because union is not the whole" + with pytest.raises(ValueError, match=msg): + ss.to_coo(['A'], ['C', 'D']) def test_to_coo_nlevels_less_than_two(self): ss = self.sparse_series[0] ss.index = np.arange(len(ss.index)) - self.assertRaises(ValueError, ss.to_coo) + msg = "to_coo requires MultiIndex with nlevels > 2" + with pytest.raises(ValueError, match=msg): + ss.to_coo() def test_to_coo_bad_ilevel(self): ss = self.sparse_series[0] - self.assertRaises(KeyError, ss.to_coo, ['A', 'B'], ['C', 'D', 'E']) + with pytest.raises(KeyError, match="Level E not found"): + ss.to_coo(['A', 'B'], ['C', 'D', 'E']) def test_to_coo_duplicate_index_entries(self): ss = pd.concat([self.sparse_series[0], self.sparse_series[0]]).to_sparse() - self.assertRaises(ValueError, ss.to_coo, ['A', 'B'], ['C', 'D']) + msg = ("Duplicate index entries are not allowed in to_coo" + " transformation") + with pytest.raises(ValueError, match=msg): + ss.to_coo(['A', 'B'], ['C', 'D']) def test_from_coo_dense_index(self): ss = SparseSeries.from_coo(self.coo_matrices[0], dense_index=True) @@ -1070,7 +1184,6 @@ def test_from_coo_nodense_index(self): def test_from_coo_long_repr(self): # GH 13114 # test it doesn't raise error. Formatting is tested in test_format - tm._skip_if_no_scipy() import scipy.sparse sparse = SparseSeries.from_coo(scipy.sparse.rand(350, 18)) @@ -1096,8 +1209,8 @@ def _check_results_to_coo(self, results, check): # or compare directly as difference of sparse # assert(abs(A - A_result).max() < 1e-12) # max is failing in python # 2.6 - self.assertEqual(il, il_result) - self.assertEqual(jl, jl_result) + assert il == il_result + assert jl == jl_result def test_concat(self): val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) @@ -1118,7 +1231,8 @@ def test_concat(self): res = pd.concat([sparse1, sparse2]) exp = pd.concat([pd.Series(val1), pd.Series(val2)]) exp = pd.SparseSeries(exp, fill_value=0, kind=kind) - tm.assert_sp_series_equal(res, exp) + tm.assert_sp_series_equal(res, exp, + consolidate_block_indices=True) def test_concat_axis1(self): val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) @@ -1141,12 +1255,14 @@ def test_concat_different_fill(self): sparse1 = pd.SparseSeries(val1, name='x', kind=kind) sparse2 = pd.SparseSeries(val2, name='y', kind=kind, fill_value=0) - res = pd.concat([sparse1, sparse2]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse1, sparse2]) exp = pd.concat([pd.Series(val1), pd.Series(val2)]) exp = pd.SparseSeries(exp, kind=kind) tm.assert_sp_series_equal(res, exp) - res = pd.concat([sparse2, sparse1]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse2, sparse1]) exp = pd.concat([pd.Series(val2), pd.Series(val1)]) exp = pd.SparseSeries(exp, kind=kind, fill_value=0) tm.assert_sp_series_equal(res, exp) @@ -1161,7 +1277,7 @@ def test_concat_axis1_different_fill(self): res = pd.concat([sparse1, sparse2], axis=1) exp = pd.concat([pd.Series(val1, name='x'), pd.Series(val2, name='y')], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) def test_concat_different_kind(self): @@ -1171,12 +1287,14 @@ def test_concat_different_kind(self): sparse1 = pd.SparseSeries(val1, name='x', kind='integer') sparse2 = pd.SparseSeries(val2, name='y', kind='block', fill_value=0) - res = pd.concat([sparse1, sparse2]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse1, sparse2]) exp = pd.concat([pd.Series(val1), pd.Series(val2)]) exp = pd.SparseSeries(exp, kind='integer') tm.assert_sp_series_equal(res, exp) - res = pd.concat([sparse2, sparse1]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse2, sparse1]) exp = pd.concat([pd.Series(val2), pd.Series(val1)]) exp = pd.SparseSeries(exp, kind='block', fill_value=0) tm.assert_sp_series_equal(res, exp) @@ -1197,21 +1315,21 @@ def test_concat_sparse_dense(self): res = pd.concat([dense, sparse, dense]) exp = pd.concat([dense, pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind) - tm.assert_sp_series_equal(res, exp) + exp = exp.astype("Sparse") + tm.assert_series_equal(res, exp) sparse = pd.SparseSeries(val1, name='x', kind=kind, fill_value=0) dense = pd.Series(val2, name='y') res = pd.concat([sparse, dense]) exp = pd.concat([pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind, fill_value=0) - tm.assert_sp_series_equal(res, exp) + exp = exp.astype(SparseDtype(exp.dtype, 0)) + tm.assert_series_equal(res, exp) res = pd.concat([dense, sparse, dense]) exp = pd.concat([dense, pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind, fill_value=0) - tm.assert_sp_series_equal(res, exp) + exp = exp.astype(SparseDtype(exp.dtype, 0)) + tm.assert_series_equal(res, exp) def test_value_counts(self): vals = [1, 2, nan, 0, nan, 1, 2, nan, nan, 1, 2, 0, 1, 1] @@ -1267,11 +1385,11 @@ def test_value_counts_int(self): tm.assert_series_equal(sparse.value_counts(dropna=False), dense.value_counts(dropna=False)) - def test_isnull(self): + def test_isna(self): # GH 8276 s = pd.SparseSeries([np.nan, np.nan, 1, 2, np.nan], name='xxx') - res = s.isnull() + res = s.isna() exp = pd.SparseSeries([True, True, False, False, True], name='xxx', fill_value=True) tm.assert_sp_series_equal(res, exp) @@ -1279,16 +1397,16 @@ def test_isnull(self): # if fill_value is not nan, True can be included in sp_values s = pd.SparseSeries([np.nan, 0., 1., 2., 0.], name='xxx', fill_value=0.) - res = s.isnull() - tm.assertIsInstance(res, pd.SparseSeries) + res = s.isna() + assert isinstance(res, pd.SparseSeries) exp = pd.Series([True, False, False, False, False], name='xxx') tm.assert_series_equal(res.to_dense(), exp) - def test_isnotnull(self): + def test_notna(self): # GH 8276 s = pd.SparseSeries([np.nan, np.nan, 1, 2, np.nan], name='xxx') - res = s.isnotnull() + res = s.notna() exp = pd.SparseSeries([False, False, True, True, False], name='xxx', fill_value=False) tm.assert_sp_series_equal(res, exp) @@ -1296,8 +1414,8 @@ def test_isnotnull(self): # if fill_value is not nan, True can be included in sp_values s = pd.SparseSeries([np.nan, 0., 1., 2., 0.], name='xxx', fill_value=0.) - res = s.isnotnull() - tm.assertIsInstance(res, pd.SparseSeries) + res = s.notna() + assert isinstance(res, pd.SparseSeries) exp = pd.Series([False, True, True, True, True], name='xxx') tm.assert_series_equal(res.to_dense(), exp) @@ -1309,9 +1427,9 @@ def _dense_series_compare(s, f): tm.assert_series_equal(result.to_dense(), dense_result) -class TestSparseSeriesAnalytics(tm.TestCase): +class TestSparseSeriesAnalytics(object): - def setUp(self): + def setup_method(self, method): arr, index = _test_data1() self.bseries = SparseSeries(arr, index=index, kind='block', name='bseries') @@ -1326,12 +1444,12 @@ def test_cumsum(self): tm.assert_sp_series_equal(result, expected) result = self.zbseries.cumsum() - expected = self.zbseries.to_dense().cumsum() + expected = self.zbseries.to_dense().cumsum().to_sparse() tm.assert_series_equal(result, expected) axis = 1 # Series is 1-D, so only axis = 0 is valid. msg = "No axis named {axis}".format(axis=axis) - with tm.assertRaisesRegexp(ValueError, msg): + with pytest.raises(ValueError, match=msg): self.bseries.cumsum(axis=axis) def test_numpy_cumsum(self): @@ -1340,23 +1458,78 @@ def test_numpy_cumsum(self): tm.assert_sp_series_equal(result, expected) result = np.cumsum(self.zbseries) - expected = self.zbseries.to_dense().cumsum() + expected = self.zbseries.to_dense().cumsum().to_sparse() tm.assert_series_equal(result, expected) msg = "the 'dtype' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - self.bseries, dtype=np.int64) + with pytest.raises(ValueError, match=msg): + np.cumsum(self.bseries, dtype=np.int64) msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - self.zbseries, out=result) + with pytest.raises(ValueError, match=msg): + np.cumsum(self.zbseries, out=result) def test_numpy_func_call(self): # no exception should be raised even though # numpy passes in 'axis=None' or `axis=-1' funcs = ['sum', 'cumsum', 'var', 'mean', 'prod', 'cumprod', 'std', 'argsort', - 'argmin', 'argmax', 'min', 'max'] + 'min', 'max'] for func in funcs: for series in ('bseries', 'zbseries'): getattr(np, func)(getattr(self, series)) + + def test_deprecated_numpy_func_call(self): + # NOTE: These should be add to the 'test_numpy_func_call' test above + # once the behavior of argmin/argmax is corrected. + funcs = ['argmin', 'argmax'] + for func in funcs: + for series in ('bseries', 'zbseries'): + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + getattr(np, func)(getattr(self, series)) + + with tm.assert_produces_warning(FutureWarning, + check_stacklevel=False): + getattr(getattr(self, series), func)() + + def test_deprecated_reindex_axis(self): + # https://github.com/pandas-dev/pandas/issues/17833 + with tm.assert_produces_warning(FutureWarning) as m: + self.bseries.reindex_axis([0, 1, 2]) + assert 'reindex' in str(m[0].message) + + +@pytest.mark.parametrize( + 'datetime_type', (np.datetime64, + pd.Timestamp, + lambda x: datetime.strptime(x, '%Y-%m-%d'))) +def test_constructor_dict_datetime64_index(datetime_type): + # GH 9456 + dates = ['1984-02-19', '1988-11-06', '1989-12-03', '1990-03-15'] + values = [42544017.198965244, 1234565, 40512335.181958228, -1] + + result = SparseSeries(dict(zip(map(datetime_type, dates), values))) + expected = SparseSeries(values, map(pd.Timestamp, dates)) + + tm.assert_sp_series_equal(result, expected) + + +def test_to_sparse(): + # https://github.com/pandas-dev/pandas/issues/22389 + arr = pd.SparseArray([1, 2, None, 3]) + result = pd.Series(arr).to_sparse() + assert len(result) == 4 + tm.assert_sp_array_equal(result.values, arr, check_kind=False) + + +def test_constructor_mismatched_raises(): + msg = "Length of passed values is 2, index implies 3" + with pytest.raises(ValueError, match=msg): + SparseSeries([1, 2], index=[1, 2, 3]) + + +def test_block_deprecated(): + s = SparseSeries([1]) + with tm.assert_produces_warning(FutureWarning): + s.block diff --git a/pandas/tests/sparse/test_array.py b/pandas/tests/sparse/test_array.py deleted file mode 100644 index 15531cecfe79b..0000000000000 --- a/pandas/tests/sparse/test_array.py +++ /dev/null @@ -1,812 +0,0 @@ -from pandas.compat import range -import re -import operator -import warnings - -from numpy import nan -import numpy as np - -from pandas import _np_version_under1p8 -from pandas.sparse.api import SparseArray, SparseSeries -from pandas.sparse.libsparse import IntIndex -from pandas.util.testing import assert_almost_equal, assertRaisesRegexp -import pandas.util.testing as tm - - -class TestSparseArray(tm.TestCase): - - def setUp(self): - self.arr_data = np.array([nan, nan, 1, 2, 3, nan, 4, 5, nan, 6]) - self.arr = SparseArray(self.arr_data) - self.zarr = SparseArray([0, 0, 1, 2, 3, 0, 4, 5, 0, 6], fill_value=0) - - def test_constructor_dtype(self): - arr = SparseArray([np.nan, 1, 2, np.nan]) - self.assertEqual(arr.dtype, np.float64) - self.assertTrue(np.isnan(arr.fill_value)) - - arr = SparseArray([np.nan, 1, 2, np.nan], fill_value=0) - self.assertEqual(arr.dtype, np.float64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray([0, 1, 2, 4], dtype=np.float64) - self.assertEqual(arr.dtype, np.float64) - self.assertTrue(np.isnan(arr.fill_value)) - - arr = SparseArray([0, 1, 2, 4], dtype=np.int64) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray([0, 1, 2, 4], fill_value=0, dtype=np.int64) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray([0, 1, 2, 4], dtype=None) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray([0, 1, 2, 4], fill_value=0, dtype=None) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - def test_constructor_object_dtype(self): - # GH 11856 - arr = SparseArray(['A', 'A', np.nan, 'B'], dtype=np.object) - self.assertEqual(arr.dtype, np.object) - self.assertTrue(np.isnan(arr.fill_value)) - - arr = SparseArray(['A', 'A', np.nan, 'B'], dtype=np.object, - fill_value='A') - self.assertEqual(arr.dtype, np.object) - self.assertEqual(arr.fill_value, 'A') - - def test_constructor_spindex_dtype(self): - arr = SparseArray(data=[1, 2], sparse_index=IntIndex(4, [1, 2])) - tm.assert_sp_array_equal(arr, SparseArray([np.nan, 1, 2, np.nan])) - self.assertEqual(arr.dtype, np.float64) - self.assertTrue(np.isnan(arr.fill_value)) - - arr = SparseArray(data=[1, 2, 3], - sparse_index=IntIndex(4, [1, 2, 3]), - dtype=np.int64, fill_value=0) - exp = SparseArray([0, 1, 2, 3], dtype=np.int64, fill_value=0) - tm.assert_sp_array_equal(arr, exp) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray(data=[1, 2], sparse_index=IntIndex(4, [1, 2]), - fill_value=0, dtype=np.int64) - exp = SparseArray([0, 1, 2, 0], fill_value=0, dtype=np.int64) - tm.assert_sp_array_equal(arr, exp) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray(data=[1, 2, 3], - sparse_index=IntIndex(4, [1, 2, 3]), - dtype=None, fill_value=0) - exp = SparseArray([0, 1, 2, 3], dtype=None) - tm.assert_sp_array_equal(arr, exp) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - # scalar input - arr = SparseArray(data=1, sparse_index=IntIndex(1, [0]), dtype=None) - exp = SparseArray([1], dtype=None) - tm.assert_sp_array_equal(arr, exp) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - arr = SparseArray(data=[1, 2], sparse_index=IntIndex(4, [1, 2]), - fill_value=0, dtype=None) - exp = SparseArray([0, 1, 2, 0], fill_value=0, dtype=None) - tm.assert_sp_array_equal(arr, exp) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - def test_sparseseries_roundtrip(self): - # GH 13999 - for kind in ['integer', 'block']: - for fill in [1, np.nan, 0]: - arr = SparseArray([np.nan, 1, np.nan, 2, 3], kind=kind, - fill_value=fill) - res = SparseArray(SparseSeries(arr)) - tm.assert_sp_array_equal(arr, res) - - arr = SparseArray([0, 0, 0, 1, 1, 2], dtype=np.int64, - kind=kind, fill_value=fill) - res = SparseArray(SparseSeries(arr), dtype=np.int64) - tm.assert_sp_array_equal(arr, res) - - res = SparseArray(SparseSeries(arr)) - tm.assert_sp_array_equal(arr, res) - - for fill in [True, False, np.nan]: - arr = SparseArray([True, False, True, True], dtype=np.bool, - kind=kind, fill_value=fill) - res = SparseArray(SparseSeries(arr)) - tm.assert_sp_array_equal(arr, res) - - res = SparseArray(SparseSeries(arr)) - tm.assert_sp_array_equal(arr, res) - - def test_get_item(self): - - self.assertTrue(np.isnan(self.arr[1])) - self.assertEqual(self.arr[2], 1) - self.assertEqual(self.arr[7], 5) - - self.assertEqual(self.zarr[0], 0) - self.assertEqual(self.zarr[2], 1) - self.assertEqual(self.zarr[7], 5) - - errmsg = re.compile("bounds") - assertRaisesRegexp(IndexError, errmsg, lambda: self.arr[11]) - assertRaisesRegexp(IndexError, errmsg, lambda: self.arr[-11]) - self.assertEqual(self.arr[-1], self.arr[len(self.arr) - 1]) - - def test_take(self): - self.assertTrue(np.isnan(self.arr.take(0))) - self.assertTrue(np.isscalar(self.arr.take(2))) - - # np.take in < 1.8 doesn't support scalar indexing - if not _np_version_under1p8: - self.assertEqual(self.arr.take(2), np.take(self.arr_data, 2)) - self.assertEqual(self.arr.take(6), np.take(self.arr_data, 6)) - - exp = SparseArray(np.take(self.arr_data, [2, 3])) - tm.assert_sp_array_equal(self.arr.take([2, 3]), exp) - - exp = SparseArray(np.take(self.arr_data, [0, 1, 2])) - tm.assert_sp_array_equal(self.arr.take([0, 1, 2]), exp) - - def test_take_fill_value(self): - data = np.array([1, np.nan, 0, 3, 0]) - sparse = SparseArray(data, fill_value=0) - - exp = SparseArray(np.take(data, [0]), fill_value=0) - tm.assert_sp_array_equal(sparse.take([0]), exp) - - exp = SparseArray(np.take(data, [1, 3, 4]), fill_value=0) - tm.assert_sp_array_equal(sparse.take([1, 3, 4]), exp) - - def test_take_negative(self): - exp = SparseArray(np.take(self.arr_data, [-1])) - tm.assert_sp_array_equal(self.arr.take([-1]), exp) - - exp = SparseArray(np.take(self.arr_data, [-4, -3, -2])) - tm.assert_sp_array_equal(self.arr.take([-4, -3, -2]), exp) - - def test_bad_take(self): - assertRaisesRegexp(IndexError, "bounds", lambda: self.arr.take(11)) - self.assertRaises(IndexError, lambda: self.arr.take(-11)) - - def test_take_invalid_kwargs(self): - msg = r"take\(\) got an unexpected keyword argument 'foo'" - tm.assertRaisesRegexp(TypeError, msg, self.arr.take, - [2, 3], foo=2) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, self.arr.take, - [2, 3], out=self.arr) - - msg = "the 'mode' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, self.arr.take, - [2, 3], mode='clip') - - def test_take_filling(self): - # similar tests as GH 12631 - sparse = SparseArray([np.nan, np.nan, 1, np.nan, 4]) - result = sparse.take(np.array([1, 0, -1])) - expected = SparseArray([np.nan, np.nan, 4]) - tm.assert_sp_array_equal(result, expected) - - # fill_value - result = sparse.take(np.array([1, 0, -1]), fill_value=True) - expected = SparseArray([np.nan, np.nan, np.nan]) - tm.assert_sp_array_equal(result, expected) - - # allow_fill=False - result = sparse.take(np.array([1, 0, -1]), - allow_fill=False, fill_value=True) - expected = SparseArray([np.nan, np.nan, 4]) - tm.assert_sp_array_equal(result, expected) - - msg = ('When allow_fill=True and fill_value is not None, ' - 'all indices must be >= -1') - with tm.assertRaisesRegexp(ValueError, msg): - sparse.take(np.array([1, 0, -2]), fill_value=True) - with tm.assertRaisesRegexp(ValueError, msg): - sparse.take(np.array([1, 0, -5]), fill_value=True) - - with tm.assertRaises(IndexError): - sparse.take(np.array([1, -6])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5]), fill_value=True) - - def test_take_filling_fill_value(self): - # same tests as GH 12631 - sparse = SparseArray([np.nan, 0, 1, 0, 4], fill_value=0) - result = sparse.take(np.array([1, 0, -1])) - expected = SparseArray([0, np.nan, 4], fill_value=0) - tm.assert_sp_array_equal(result, expected) - - # fill_value - result = sparse.take(np.array([1, 0, -1]), fill_value=True) - expected = SparseArray([0, np.nan, 0], fill_value=0) - tm.assert_sp_array_equal(result, expected) - - # allow_fill=False - result = sparse.take(np.array([1, 0, -1]), - allow_fill=False, fill_value=True) - expected = SparseArray([0, np.nan, 4], fill_value=0) - tm.assert_sp_array_equal(result, expected) - - msg = ('When allow_fill=True and fill_value is not None, ' - 'all indices must be >= -1') - with tm.assertRaisesRegexp(ValueError, msg): - sparse.take(np.array([1, 0, -2]), fill_value=True) - with tm.assertRaisesRegexp(ValueError, msg): - sparse.take(np.array([1, 0, -5]), fill_value=True) - - with tm.assertRaises(IndexError): - sparse.take(np.array([1, -6])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5]), fill_value=True) - - def test_take_filling_all_nan(self): - sparse = SparseArray([np.nan, np.nan, np.nan, np.nan, np.nan]) - result = sparse.take(np.array([1, 0, -1])) - expected = SparseArray([np.nan, np.nan, np.nan]) - tm.assert_sp_array_equal(result, expected) - - result = sparse.take(np.array([1, 0, -1]), fill_value=True) - expected = SparseArray([np.nan, np.nan, np.nan]) - tm.assert_sp_array_equal(result, expected) - - with tm.assertRaises(IndexError): - sparse.take(np.array([1, -6])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5])) - with tm.assertRaises(IndexError): - sparse.take(np.array([1, 5]), fill_value=True) - - def test_set_item(self): - def setitem(): - self.arr[5] = 3 - - def setslice(): - self.arr[1:5] = 2 - - assertRaisesRegexp(TypeError, "item assignment", setitem) - assertRaisesRegexp(TypeError, "item assignment", setslice) - - def test_constructor_from_too_large_array(self): - assertRaisesRegexp(TypeError, "expected dimension <= 1 data", - SparseArray, np.arange(10).reshape((2, 5))) - - def test_constructor_from_sparse(self): - res = SparseArray(self.zarr) - self.assertEqual(res.fill_value, 0) - assert_almost_equal(res.sp_values, self.zarr.sp_values) - - def test_constructor_copy(self): - cp = SparseArray(self.arr, copy=True) - cp.sp_values[:3] = 0 - self.assertFalse((self.arr.sp_values[:3] == 0).any()) - - not_copy = SparseArray(self.arr) - not_copy.sp_values[:3] = 0 - self.assertTrue((self.arr.sp_values[:3] == 0).all()) - - def test_constructor_bool(self): - # GH 10648 - data = np.array([False, False, True, True, False, False]) - arr = SparseArray(data, fill_value=False, dtype=bool) - - self.assertEqual(arr.dtype, bool) - tm.assert_numpy_array_equal(arr.sp_values, np.array([True, True])) - tm.assert_numpy_array_equal(arr.sp_values, np.asarray(arr)) - tm.assert_numpy_array_equal(arr.sp_index.indices, - np.array([2, 3], np.int32)) - - for dense in [arr.to_dense(), arr.values]: - self.assertEqual(dense.dtype, bool) - tm.assert_numpy_array_equal(dense, data) - - def test_constructor_bool_fill_value(self): - arr = SparseArray([True, False, True], dtype=None) - self.assertEqual(arr.dtype, np.bool) - self.assertFalse(arr.fill_value) - - arr = SparseArray([True, False, True], dtype=np.bool) - self.assertEqual(arr.dtype, np.bool) - self.assertFalse(arr.fill_value) - - arr = SparseArray([True, False, True], dtype=np.bool, fill_value=True) - self.assertEqual(arr.dtype, np.bool) - self.assertTrue(arr.fill_value) - - def test_constructor_float32(self): - # GH 10648 - data = np.array([1., np.nan, 3], dtype=np.float32) - arr = SparseArray(data, dtype=np.float32) - - self.assertEqual(arr.dtype, np.float32) - tm.assert_numpy_array_equal(arr.sp_values, - np.array([1, 3], dtype=np.float32)) - tm.assert_numpy_array_equal(arr.sp_values, np.asarray(arr)) - tm.assert_numpy_array_equal(arr.sp_index.indices, - np.array([0, 2], dtype=np.int32)) - - for dense in [arr.to_dense(), arr.values]: - self.assertEqual(dense.dtype, np.float32) - self.assert_numpy_array_equal(dense, data) - - def test_astype(self): - res = self.arr.astype('f8') - res.sp_values[:3] = 27 - self.assertFalse((self.arr.sp_values[:3] == 27).any()) - - msg = "unable to coerce current fill_value nan to int64 dtype" - with tm.assertRaisesRegexp(ValueError, msg): - self.arr.astype('i8') - - arr = SparseArray([0, np.nan, 0, 1]) - with tm.assertRaisesRegexp(ValueError, msg): - arr.astype('i8') - - arr = SparseArray([0, np.nan, 0, 1], fill_value=0) - msg = 'Cannot convert non-finite values \\(NA or inf\\) to integer' - with tm.assertRaisesRegexp(ValueError, msg): - arr.astype('i8') - - def test_astype_all(self): - vals = np.array([1, 2, 3]) - arr = SparseArray(vals, fill_value=1) - - types = [np.float64, np.float32, np.int64, - np.int32, np.int16, np.int8] - for typ in types: - res = arr.astype(typ) - self.assertEqual(res.dtype, typ) - self.assertEqual(res.sp_values.dtype, typ) - - tm.assert_numpy_array_equal(res.values, vals.astype(typ)) - - def test_set_fill_value(self): - arr = SparseArray([1., np.nan, 2.], fill_value=np.nan) - arr.fill_value = 2 - self.assertEqual(arr.fill_value, 2) - - arr = SparseArray([1, 0, 2], fill_value=0, dtype=np.int64) - arr.fill_value = 2 - self.assertEqual(arr.fill_value, 2) - - # coerces to int - msg = "unable to set fill_value 3\\.1 to int64 dtype" - with tm.assertRaisesRegexp(ValueError, msg): - arr.fill_value = 3.1 - - msg = "unable to set fill_value nan to int64 dtype" - with tm.assertRaisesRegexp(ValueError, msg): - arr.fill_value = np.nan - - arr = SparseArray([True, False, True], fill_value=False, dtype=np.bool) - arr.fill_value = True - self.assertTrue(arr.fill_value) - - # coerces to bool - msg = "unable to set fill_value 0 to bool dtype" - with tm.assertRaisesRegexp(ValueError, msg): - arr.fill_value = 0 - - msg = "unable to set fill_value nan to bool dtype" - with tm.assertRaisesRegexp(ValueError, msg): - arr.fill_value = np.nan - - # invalid - msg = "fill_value must be a scalar" - for val in [[1, 2, 3], np.array([1, 2]), (1, 2, 3)]: - with tm.assertRaisesRegexp(ValueError, msg): - arr.fill_value = val - - def test_copy_shallow(self): - arr2 = self.arr.copy(deep=False) - - def _get_base(values): - base = values.base - while base.base is not None: - base = base.base - return base - - assert (_get_base(arr2) is _get_base(self.arr)) - - def test_values_asarray(self): - assert_almost_equal(self.arr.values, self.arr_data) - assert_almost_equal(self.arr.to_dense(), self.arr_data) - assert_almost_equal(self.arr.sp_values, np.asarray(self.arr)) - - def test_to_dense(self): - vals = np.array([1, np.nan, np.nan, 3, np.nan]) - res = SparseArray(vals).to_dense() - tm.assert_numpy_array_equal(res, vals) - - res = SparseArray(vals, fill_value=0).to_dense() - tm.assert_numpy_array_equal(res, vals) - - vals = np.array([1, np.nan, 0, 3, 0]) - res = SparseArray(vals).to_dense() - tm.assert_numpy_array_equal(res, vals) - - res = SparseArray(vals, fill_value=0).to_dense() - tm.assert_numpy_array_equal(res, vals) - - vals = np.array([np.nan, np.nan, np.nan, np.nan, np.nan]) - res = SparseArray(vals).to_dense() - tm.assert_numpy_array_equal(res, vals) - - res = SparseArray(vals, fill_value=0).to_dense() - tm.assert_numpy_array_equal(res, vals) - - # see gh-14647 - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - SparseArray(vals).to_dense(fill=2) - - def test_getitem(self): - def _checkit(i): - assert_almost_equal(self.arr[i], self.arr.values[i]) - - for i in range(len(self.arr)): - _checkit(i) - _checkit(-i) - - def test_getslice(self): - result = self.arr[:-3] - exp = SparseArray(self.arr.values[:-3]) - tm.assert_sp_array_equal(result, exp) - - result = self.arr[-4:] - exp = SparseArray(self.arr.values[-4:]) - tm.assert_sp_array_equal(result, exp) - - # two corner cases from Series - result = self.arr[-12:] - exp = SparseArray(self.arr) - tm.assert_sp_array_equal(result, exp) - - result = self.arr[:-12] - exp = SparseArray(self.arr.values[:0]) - tm.assert_sp_array_equal(result, exp) - - def test_getslice_tuple(self): - dense = np.array([np.nan, 0, 3, 4, 0, 5, np.nan, np.nan, 0]) - - sparse = SparseArray(dense) - res = sparse[4:, ] - exp = SparseArray(dense[4:, ]) - tm.assert_sp_array_equal(res, exp) - - sparse = SparseArray(dense, fill_value=0) - res = sparse[4:, ] - exp = SparseArray(dense[4:, ], fill_value=0) - tm.assert_sp_array_equal(res, exp) - - with tm.assertRaises(IndexError): - sparse[4:, :] - - with tm.assertRaises(IndexError): - # check numpy compat - dense[4:, :] - - def test_binary_operators(self): - data1 = np.random.randn(20) - data2 = np.random.randn(20) - data1[::2] = np.nan - data2[::3] = np.nan - - arr1 = SparseArray(data1) - arr2 = SparseArray(data2) - - data1[::2] = 3 - data2[::3] = 3 - farr1 = SparseArray(data1, fill_value=3) - farr2 = SparseArray(data2, fill_value=3) - - def _check_op(op, first, second): - res = op(first, second) - exp = SparseArray(op(first.values, second.values), - fill_value=first.fill_value) - tm.assertIsInstance(res, SparseArray) - assert_almost_equal(res.values, exp.values) - - res2 = op(first, second.values) - tm.assertIsInstance(res2, SparseArray) - tm.assert_sp_array_equal(res, res2) - - res3 = op(first.values, second) - tm.assertIsInstance(res3, SparseArray) - tm.assert_sp_array_equal(res, res3) - - res4 = op(first, 4) - tm.assertIsInstance(res4, SparseArray) - - # ignore this if the actual op raises (e.g. pow) - try: - exp = op(first.values, 4) - exp_fv = op(first.fill_value, 4) - assert_almost_equal(res4.fill_value, exp_fv) - assert_almost_equal(res4.values, exp) - except ValueError: - pass - - def _check_inplace_op(op): - tmp = arr1.copy() - self.assertRaises(NotImplementedError, op, tmp, arr2) - - with np.errstate(all='ignore'): - bin_ops = [operator.add, operator.sub, operator.mul, - operator.truediv, operator.floordiv, operator.pow] - for op in bin_ops: - _check_op(op, arr1, arr2) - _check_op(op, farr1, farr2) - - inplace_ops = ['iadd', 'isub', 'imul', 'itruediv', 'ifloordiv', - 'ipow'] - for op in inplace_ops: - _check_inplace_op(getattr(operator, op)) - - def test_pickle(self): - def _check_roundtrip(obj): - unpickled = self.round_trip_pickle(obj) - tm.assert_sp_array_equal(unpickled, obj) - - _check_roundtrip(self.arr) - _check_roundtrip(self.zarr) - - def test_generator_warnings(self): - sp_arr = SparseArray([1, 2, 3]) - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings(action='always', - category=DeprecationWarning) - warnings.filterwarnings(action='always', - category=PendingDeprecationWarning) - for _ in sp_arr: - pass - assert len(w) == 0 - - def test_fillna(self): - s = SparseArray([1, np.nan, np.nan, 3, np.nan]) - res = s.fillna(-1) - exp = SparseArray([1, -1, -1, 3, -1], fill_value=-1, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - s = SparseArray([1, np.nan, np.nan, 3, np.nan], fill_value=0) - res = s.fillna(-1) - exp = SparseArray([1, -1, -1, 3, -1], fill_value=0, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - s = SparseArray([1, np.nan, 0, 3, 0]) - res = s.fillna(-1) - exp = SparseArray([1, -1, 0, 3, 0], fill_value=-1, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - s = SparseArray([1, np.nan, 0, 3, 0], fill_value=0) - res = s.fillna(-1) - exp = SparseArray([1, -1, 0, 3, 0], fill_value=0, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - s = SparseArray([np.nan, np.nan, np.nan, np.nan]) - res = s.fillna(-1) - exp = SparseArray([-1, -1, -1, -1], fill_value=-1, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - s = SparseArray([np.nan, np.nan, np.nan, np.nan], fill_value=0) - res = s.fillna(-1) - exp = SparseArray([-1, -1, -1, -1], fill_value=0, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - # float dtype's fill_value is np.nan, replaced by -1 - s = SparseArray([0., 0., 0., 0.]) - res = s.fillna(-1) - exp = SparseArray([0., 0., 0., 0.], fill_value=-1) - tm.assert_sp_array_equal(res, exp) - - # int dtype shouldn't have missing. No changes. - s = SparseArray([0, 0, 0, 0]) - self.assertEqual(s.dtype, np.int64) - self.assertEqual(s.fill_value, 0) - res = s.fillna(-1) - tm.assert_sp_array_equal(res, s) - - s = SparseArray([0, 0, 0, 0], fill_value=0) - self.assertEqual(s.dtype, np.int64) - self.assertEqual(s.fill_value, 0) - res = s.fillna(-1) - exp = SparseArray([0, 0, 0, 0], fill_value=0) - tm.assert_sp_array_equal(res, exp) - - # fill_value can be nan if there is no missing hole. - # only fill_value will be changed - s = SparseArray([0, 0, 0, 0], fill_value=np.nan) - self.assertEqual(s.dtype, np.int64) - self.assertTrue(np.isnan(s.fill_value)) - res = s.fillna(-1) - exp = SparseArray([0, 0, 0, 0], fill_value=-1) - tm.assert_sp_array_equal(res, exp) - - def test_fillna_overlap(self): - s = SparseArray([1, np.nan, np.nan, 3, np.nan]) - # filling with existing value doesn't replace existing value with - # fill_value, i.e. existing 3 remains in sp_values - res = s.fillna(3) - exp = np.array([1, 3, 3, 3, 3], dtype=np.float64) - tm.assert_numpy_array_equal(res.to_dense(), exp) - - s = SparseArray([1, np.nan, np.nan, 3, np.nan], fill_value=0) - res = s.fillna(3) - exp = SparseArray([1, 3, 3, 3, 3], fill_value=0, dtype=np.float64) - tm.assert_sp_array_equal(res, exp) - - -class TestSparseArrayAnalytics(tm.TestCase): - - def test_sum(self): - data = np.arange(10).astype(float) - out = SparseArray(data).sum() - self.assertEqual(out, 45.0) - - data[5] = np.nan - out = SparseArray(data, fill_value=2).sum() - self.assertEqual(out, 40.0) - - out = SparseArray(data, fill_value=np.nan).sum() - self.assertEqual(out, 40.0) - - def test_numpy_sum(self): - data = np.arange(10).astype(float) - out = np.sum(SparseArray(data)) - self.assertEqual(out, 45.0) - - data[5] = np.nan - out = np.sum(SparseArray(data, fill_value=2)) - self.assertEqual(out, 40.0) - - out = np.sum(SparseArray(data, fill_value=np.nan)) - self.assertEqual(out, 40.0) - - msg = "the 'dtype' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.sum, - SparseArray(data), dtype=np.int64) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.sum, - SparseArray(data), out=out) - - def test_cumsum(self): - non_null_data = np.array([1, 2, 3, 4, 5], dtype=float) - non_null_expected = SparseArray(non_null_data.cumsum()) - - null_data = np.array([1, 2, np.nan, 4, 5], dtype=float) - null_expected = SparseArray(np.array([1.0, 3.0, np.nan, 7.0, 12.0])) - - for data, expected in [ - (null_data, null_expected), - (non_null_data, non_null_expected) - ]: - out = SparseArray(data).cumsum() - tm.assert_sp_array_equal(out, expected) - - out = SparseArray(data, fill_value=np.nan).cumsum() - tm.assert_sp_array_equal(out, expected) - - out = SparseArray(data, fill_value=2).cumsum() - tm.assert_sp_array_equal(out, expected) - - axis = 1 # SparseArray currently 1-D, so only axis = 0 is valid. - msg = "axis\\(={axis}\\) out of bounds".format(axis=axis) - with tm.assertRaisesRegexp(ValueError, msg): - SparseArray(data).cumsum(axis=axis) - - def test_numpy_cumsum(self): - non_null_data = np.array([1, 2, 3, 4, 5], dtype=float) - non_null_expected = SparseArray(non_null_data.cumsum()) - - null_data = np.array([1, 2, np.nan, 4, 5], dtype=float) - null_expected = SparseArray(np.array([1.0, 3.0, np.nan, 7.0, 12.0])) - - for data, expected in [ - (null_data, null_expected), - (non_null_data, non_null_expected) - ]: - out = np.cumsum(SparseArray(data)) - tm.assert_sp_array_equal(out, expected) - - out = np.cumsum(SparseArray(data, fill_value=np.nan)) - tm.assert_sp_array_equal(out, expected) - - out = np.cumsum(SparseArray(data, fill_value=2)) - tm.assert_sp_array_equal(out, expected) - - msg = "the 'dtype' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - SparseArray(data), dtype=np.int64) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - SparseArray(data), out=out) - - def test_mean(self): - data = np.arange(10).astype(float) - out = SparseArray(data).mean() - self.assertEqual(out, 4.5) - - data[5] = np.nan - out = SparseArray(data).mean() - self.assertEqual(out, 40.0 / 9) - - def test_numpy_mean(self): - data = np.arange(10).astype(float) - out = np.mean(SparseArray(data)) - self.assertEqual(out, 4.5) - - data[5] = np.nan - out = np.mean(SparseArray(data)) - self.assertEqual(out, 40.0 / 9) - - msg = "the 'dtype' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.mean, - SparseArray(data), dtype=np.int64) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.mean, - SparseArray(data), out=out) - - def test_ufunc(self): - # GH 13853 make sure ufunc is applied to fill_value - sparse = SparseArray([1, np.nan, 2, np.nan, -2]) - result = SparseArray([1, np.nan, 2, np.nan, 2]) - tm.assert_sp_array_equal(abs(sparse), result) - tm.assert_sp_array_equal(np.abs(sparse), result) - - sparse = SparseArray([1, -1, 2, -2], fill_value=1) - result = SparseArray([1, 2, 2], sparse_index=sparse.sp_index, - fill_value=1) - tm.assert_sp_array_equal(abs(sparse), result) - tm.assert_sp_array_equal(np.abs(sparse), result) - - sparse = SparseArray([1, -1, 2, -2], fill_value=-1) - result = SparseArray([1, 2, 2], sparse_index=sparse.sp_index, - fill_value=1) - tm.assert_sp_array_equal(abs(sparse), result) - tm.assert_sp_array_equal(np.abs(sparse), result) - - sparse = SparseArray([1, np.nan, 2, np.nan, -2]) - result = SparseArray(np.sin([1, np.nan, 2, np.nan, -2])) - tm.assert_sp_array_equal(np.sin(sparse), result) - - sparse = SparseArray([1, -1, 2, -2], fill_value=1) - result = SparseArray(np.sin([1, -1, 2, -2]), fill_value=np.sin(1)) - tm.assert_sp_array_equal(np.sin(sparse), result) - - sparse = SparseArray([1, -1, 0, -2], fill_value=0) - result = SparseArray(np.sin([1, -1, 0, -2]), fill_value=np.sin(0)) - tm.assert_sp_array_equal(np.sin(sparse), result) - - def test_ufunc_args(self): - # GH 13853 make sure ufunc is applied to fill_value, including its arg - sparse = SparseArray([1, np.nan, 2, np.nan, -2]) - result = SparseArray([2, np.nan, 3, np.nan, -1]) - tm.assert_sp_array_equal(np.add(sparse, 1), result) - - sparse = SparseArray([1, -1, 2, -2], fill_value=1) - result = SparseArray([2, 0, 3, -1], fill_value=2) - tm.assert_sp_array_equal(np.add(sparse, 1), result) - - sparse = SparseArray([1, -1, 0, -2], fill_value=0) - result = SparseArray([2, 0, 1, -1], fill_value=1) - tm.assert_sp_array_equal(np.add(sparse, 1), result) diff --git a/pandas/tests/sparse/test_combine_concat.py b/pandas/tests/sparse/test_combine_concat.py index 81655daec6164..97d5aaca82778 100644 --- a/pandas/tests/sparse/test_combine_concat.py +++ b/pandas/tests/sparse/test_combine_concat.py @@ -1,32 +1,66 @@ # pylint: disable-msg=E1101,W0612 +import itertools import numpy as np +import pytest + +from pandas.errors import PerformanceWarning + import pandas as pd import pandas.util.testing as tm -class TestSparseSeriesConcat(tm.TestCase): +class TestSparseArrayConcat(object): + @pytest.mark.parametrize('kind', ['integer', 'block']) + def test_basic(self, kind): + a = pd.SparseArray([1, 0, 0, 2], kind=kind) + b = pd.SparseArray([1, 0, 2, 2], kind=kind) - def test_concat(self): + result = pd.SparseArray._concat_same_type([a, b]) + # Can't make any assertions about the sparse index itself + # since we aren't don't merge sparse blocs across arrays + # in to_concat + expected = np.array([1, 2, 1, 2, 2], dtype='int64') + tm.assert_numpy_array_equal(result.sp_values, expected) + assert result.kind == kind + + @pytest.mark.parametrize('kind', ['integer', 'block']) + def test_uses_first_kind(self, kind): + other = 'integer' if kind == 'block' else 'block' + a = pd.SparseArray([1, 0, 0, 2], kind=kind) + b = pd.SparseArray([1, 0, 2, 2], kind=other) + + result = pd.SparseArray._concat_same_type([a, b]) + expected = np.array([1, 2, 1, 2, 2], dtype='int64') + tm.assert_numpy_array_equal(result.sp_values, expected) + assert result.kind == kind + + +class TestSparseSeriesConcat(object): + + @pytest.mark.parametrize('kind', [ + 'integer', + 'block', + ]) + def test_concat(self, kind): val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) val2 = np.array([3, np.nan, 4, 0, 0]) - for kind in ['integer', 'block']: - sparse1 = pd.SparseSeries(val1, name='x', kind=kind) - sparse2 = pd.SparseSeries(val2, name='y', kind=kind) + sparse1 = pd.SparseSeries(val1, name='x', kind=kind) + sparse2 = pd.SparseSeries(val2, name='y', kind=kind) - res = pd.concat([sparse1, sparse2]) - exp = pd.concat([pd.Series(val1), pd.Series(val2)]) - exp = pd.SparseSeries(exp, kind=kind) - tm.assert_sp_series_equal(res, exp) + res = pd.concat([sparse1, sparse2]) + exp = pd.concat([pd.Series(val1), pd.Series(val2)]) + exp = pd.SparseSeries(exp, kind=kind) + tm.assert_sp_series_equal(res, exp, consolidate_block_indices=True) - sparse1 = pd.SparseSeries(val1, fill_value=0, name='x', kind=kind) - sparse2 = pd.SparseSeries(val2, fill_value=0, name='y', kind=kind) + sparse1 = pd.SparseSeries(val1, fill_value=0, name='x', kind=kind) + sparse2 = pd.SparseSeries(val2, fill_value=0, name='y', kind=kind) - res = pd.concat([sparse1, sparse2]) - exp = pd.concat([pd.Series(val1), pd.Series(val2)]) - exp = pd.SparseSeries(exp, fill_value=0, kind=kind) - tm.assert_sp_series_equal(res, exp) + res = pd.concat([sparse1, sparse2]) + exp = pd.concat([pd.Series(val1), pd.Series(val2)]) + exp = pd.SparseSeries(exp, fill_value=0, kind=kind) + tm.assert_sp_series_equal(res, exp, consolidate_block_indices=True) def test_concat_axis1(self): val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) @@ -39,7 +73,7 @@ def test_concat_axis1(self): exp = pd.concat([pd.Series(val1, name='x'), pd.Series(val2, name='y')], axis=1) exp = pd.SparseDataFrame(exp) - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) def test_concat_different_fill(self): val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) @@ -49,12 +83,16 @@ def test_concat_different_fill(self): sparse1 = pd.SparseSeries(val1, name='x', kind=kind) sparse2 = pd.SparseSeries(val2, name='y', kind=kind, fill_value=0) - res = pd.concat([sparse1, sparse2]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse1, sparse2]) + exp = pd.concat([pd.Series(val1), pd.Series(val2)]) exp = pd.SparseSeries(exp, kind=kind) tm.assert_sp_series_equal(res, exp) - res = pd.concat([sparse2, sparse1]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse2, sparse1]) + exp = pd.concat([pd.Series(val2), pd.Series(val1)]) exp = pd.SparseSeries(exp, kind=kind, fill_value=0) tm.assert_sp_series_equal(res, exp) @@ -69,7 +107,7 @@ def test_concat_axis1_different_fill(self): res = pd.concat([sparse1, sparse2], axis=1) exp = pd.concat([pd.Series(val1, name='x'), pd.Series(val2, name='y')], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) def test_concat_different_kind(self): @@ -77,54 +115,71 @@ def test_concat_different_kind(self): val2 = np.array([3, np.nan, 4, 0, 0]) sparse1 = pd.SparseSeries(val1, name='x', kind='integer') - sparse2 = pd.SparseSeries(val2, name='y', kind='block', fill_value=0) + sparse2 = pd.SparseSeries(val2, name='y', kind='block') res = pd.concat([sparse1, sparse2]) exp = pd.concat([pd.Series(val1), pd.Series(val2)]) - exp = pd.SparseSeries(exp, kind='integer') + exp = pd.SparseSeries(exp, kind=sparse1.kind) tm.assert_sp_series_equal(res, exp) res = pd.concat([sparse2, sparse1]) exp = pd.concat([pd.Series(val2), pd.Series(val1)]) - exp = pd.SparseSeries(exp, kind='block', fill_value=0) - tm.assert_sp_series_equal(res, exp) - - def test_concat_sparse_dense(self): + exp = pd.SparseSeries(exp, kind=sparse2.kind) + tm.assert_sp_series_equal(res, exp, consolidate_block_indices=True) + + @pytest.mark.parametrize('kind', [ + 'integer', + 'block', + ]) + def test_concat_sparse_dense(self, kind): # use first input's fill_value val1 = np.array([1, 2, np.nan, np.nan, 0, np.nan]) val2 = np.array([3, np.nan, 4, 0, 0]) - for kind in ['integer', 'block']: - sparse = pd.SparseSeries(val1, name='x', kind=kind) - dense = pd.Series(val2, name='y') - - res = pd.concat([sparse, dense]) - exp = pd.concat([pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind) - tm.assert_sp_series_equal(res, exp) - - res = pd.concat([dense, sparse, dense]) - exp = pd.concat([dense, pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind) - tm.assert_sp_series_equal(res, exp) - - sparse = pd.SparseSeries(val1, name='x', kind=kind, fill_value=0) - dense = pd.Series(val2, name='y') - - res = pd.concat([sparse, dense]) - exp = pd.concat([pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind, fill_value=0) - tm.assert_sp_series_equal(res, exp) - - res = pd.concat([dense, sparse, dense]) - exp = pd.concat([dense, pd.Series(val1), dense]) - exp = pd.SparseSeries(exp, kind=kind, fill_value=0) - tm.assert_sp_series_equal(res, exp) - + sparse = pd.SparseSeries(val1, name='x', kind=kind) + dense = pd.Series(val2, name='y') -class TestSparseDataFrameConcat(tm.TestCase): + res = pd.concat([sparse, dense]) + exp = pd.SparseSeries(pd.concat([pd.Series(val1), dense]), kind=kind) + tm.assert_sp_series_equal(res, exp) - def setUp(self): + res = pd.concat([dense, sparse, dense]) + exp = pd.concat([dense, pd.Series(val1), dense]) + # XXX: changed from SparseSeries to Series[sparse] + exp = pd.Series( + pd.SparseArray(exp, kind=kind), + index=exp.index, + name=exp.name, + ) + tm.assert_series_equal(res, exp) + + sparse = pd.SparseSeries(val1, name='x', kind=kind, fill_value=0) + dense = pd.Series(val2, name='y') + + res = pd.concat([sparse, dense]) + # XXX: changed from SparseSeries to Series[sparse] + exp = pd.concat([pd.Series(val1), dense]) + exp = pd.Series( + pd.SparseArray(exp, kind=kind, fill_value=0), + index=exp.index, + name=exp.name, + ) + tm.assert_series_equal(res, exp) + + res = pd.concat([dense, sparse, dense]) + exp = pd.concat([dense, pd.Series(val1), dense]) + # XXX: changed from SparseSeries to Series[sparse] + exp = pd.Series( + pd.SparseArray(exp, kind=kind, fill_value=0), + index=exp.index, + name=exp.name, + ) + tm.assert_series_equal(res, exp) + + +class TestSparseDataFrameConcat(object): + + def setup_method(self, method): self.dense1 = pd.DataFrame({'A': [0., 1., 2., np.nan], 'B': [0., 0., 0., 0.], @@ -148,19 +203,19 @@ def test_concat(self): res = pd.concat([sparse, sparse]) exp = pd.concat([self.dense1, self.dense1]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse2, sparse2]) exp = pd.concat([self.dense2, self.dense2]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse, sparse2]) exp = pd.concat([self.dense1, self.dense2]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse2, sparse]) exp = pd.concat([self.dense2, self.dense1]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) # fill_value = 0 sparse = self.dense1.to_sparse(fill_value=0) @@ -169,77 +224,106 @@ def test_concat(self): res = pd.concat([sparse, sparse]) exp = pd.concat([self.dense1, self.dense1]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse2, sparse2]) exp = pd.concat([self.dense2, self.dense2]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse, sparse2]) exp = pd.concat([self.dense1, self.dense2]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) res = pd.concat([sparse2, sparse]) exp = pd.concat([self.dense2, self.dense1]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) def test_concat_different_fill_value(self): # 1st fill_value will be used sparse = self.dense1.to_sparse() sparse2 = self.dense2.to_sparse(fill_value=0) - res = pd.concat([sparse, sparse2]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse, sparse2]) exp = pd.concat([self.dense1, self.dense2]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) - res = pd.concat([sparse2, sparse]) + with tm.assert_produces_warning(PerformanceWarning): + res = pd.concat([sparse2, sparse]) exp = pd.concat([self.dense2, self.dense1]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True) + + def test_concat_different_columns_sort_warns(self): + sparse = self.dense1.to_sparse() + sparse3 = self.dense3.to_sparse() + + with tm.assert_produces_warning(FutureWarning): + res = pd.concat([sparse, sparse3]) + with tm.assert_produces_warning(FutureWarning): + exp = pd.concat([self.dense1, self.dense3]) + + exp = exp.to_sparse() + tm.assert_sp_frame_equal(res, exp, check_kind=False) def test_concat_different_columns(self): # fill_value = np.nan sparse = self.dense1.to_sparse() sparse3 = self.dense3.to_sparse() - res = pd.concat([sparse, sparse3]) - exp = pd.concat([self.dense1, self.dense3]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + res = pd.concat([sparse, sparse3], sort=True) + exp = pd.concat([self.dense1, self.dense3], sort=True).to_sparse() + tm.assert_sp_frame_equal(res, exp, check_kind=False) - res = pd.concat([sparse3, sparse]) - exp = pd.concat([self.dense3, self.dense1]).to_sparse() + res = pd.concat([sparse3, sparse], sort=True) + exp = pd.concat([self.dense3, self.dense1], sort=True).to_sparse() exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, check_kind=False) + + def test_concat_bug(self): + from pandas.core.sparse.api import SparseDtype + x = pd.SparseDataFrame({"A": pd.SparseArray([np.nan, np.nan], + fill_value=0)}) + y = pd.SparseDataFrame({"B": []}) + res = pd.concat([x, y], sort=False)[['A']] + exp = pd.DataFrame({"A": pd.SparseArray([np.nan, np.nan], + dtype=SparseDtype(float, 0))}) + tm.assert_frame_equal(res, exp) - # fill_value = 0 + def test_concat_different_columns_buggy(self): sparse = self.dense1.to_sparse(fill_value=0) sparse3 = self.dense3.to_sparse(fill_value=0) - res = pd.concat([sparse, sparse3]) - exp = pd.concat([self.dense1, self.dense3]).to_sparse(fill_value=0) + res = pd.concat([sparse, sparse3], sort=True) + exp = (pd.concat([self.dense1, self.dense3], sort=True) + .to_sparse(fill_value=0)) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) - res = pd.concat([sparse3, sparse]) - exp = pd.concat([self.dense3, self.dense1]).to_sparse(fill_value=0) + tm.assert_sp_frame_equal(res, exp, check_kind=False, + consolidate_block_indices=True) + + res = pd.concat([sparse3, sparse], sort=True) + exp = (pd.concat([self.dense3, self.dense1], sort=True) + .to_sparse(fill_value=0)) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, check_kind=False, + consolidate_block_indices=True) # different fill values sparse = self.dense1.to_sparse() sparse3 = self.dense3.to_sparse(fill_value=0) # each columns keeps its fill_value, thus compare in dense - res = pd.concat([sparse, sparse3]) - exp = pd.concat([self.dense1, self.dense3]) - self.assertIsInstance(res, pd.SparseDataFrame) + res = pd.concat([sparse, sparse3], sort=True) + exp = pd.concat([self.dense1, self.dense3], sort=True) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) - res = pd.concat([sparse3, sparse]) - exp = pd.concat([self.dense3, self.dense1]) - self.assertIsInstance(res, pd.SparseDataFrame) + res = pd.concat([sparse3, sparse], sort=True) + exp = pd.concat([self.dense3, self.dense1], sort=True) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) def test_concat_series(self): @@ -250,11 +334,11 @@ def test_concat_series(self): for col in ['A', 'D']: res = pd.concat([sparse, sparse2[col]]) exp = pd.concat([self.dense1, self.dense2[col]]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, check_kind=False) res = pd.concat([sparse2[col], sparse]) exp = pd.concat([self.dense2[col], self.dense1]).to_sparse() - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, check_kind=False) # fill_value = 0 sparse = self.dense1.to_sparse(fill_value=0) @@ -265,13 +349,16 @@ def test_concat_series(self): exp = pd.concat([self.dense1, self.dense2[col]]).to_sparse(fill_value=0) exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, check_kind=False, + consolidate_block_indices=True) res = pd.concat([sparse2[col], sparse]) exp = pd.concat([self.dense2[col], self.dense1]).to_sparse(fill_value=0) + exp['C'] = res['C'] exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) + tm.assert_sp_frame_equal(res, exp, consolidate_block_indices=True, + check_kind=False) def test_concat_axis1(self): # fill_value = np.nan @@ -309,45 +396,67 @@ def test_concat_axis1(self): # each columns keeps its fill_value, thus compare in dense res = pd.concat([sparse, sparse3], axis=1) exp = pd.concat([self.dense1, self.dense3], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) res = pd.concat([sparse3, sparse], axis=1) exp = pd.concat([self.dense3, self.dense1], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), exp) - - def test_concat_sparse_dense(self): - sparse = self.dense1.to_sparse() - - res = pd.concat([sparse, self.dense2]) - exp = pd.concat([self.dense1, self.dense2]) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), exp) - - res = pd.concat([self.dense2, sparse]) - exp = pd.concat([self.dense2, self.dense1]) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), exp) - - sparse = self.dense1.to_sparse(fill_value=0) - - res = pd.concat([sparse, self.dense2]) - exp = pd.concat([self.dense1, self.dense2]) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), exp) - - res = pd.concat([self.dense2, sparse]) - exp = pd.concat([self.dense2, self.dense1]) - self.assertIsInstance(res, pd.SparseDataFrame) + assert isinstance(res, pd.SparseDataFrame) tm.assert_frame_equal(res.to_dense(), exp) - res = pd.concat([self.dense3, sparse], axis=1) - exp = pd.concat([self.dense3, self.dense1], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res, exp) - - res = pd.concat([sparse, self.dense3], axis=1) - exp = pd.concat([self.dense1, self.dense3], axis=1) - self.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res, exp) + @pytest.mark.parametrize('fill_value,sparse_idx,dense_idx', + itertools.product([None, 0, 1, np.nan], + [0, 1], + [1, 0])) + def test_concat_sparse_dense_rows(self, fill_value, sparse_idx, dense_idx): + frames = [self.dense1, self.dense2] + sparse_frame = [frames[dense_idx], + frames[sparse_idx].to_sparse(fill_value=fill_value)] + dense_frame = [frames[dense_idx], frames[sparse_idx]] + + # This will try both directions sparse + dense and dense + sparse + for _ in range(2): + res = pd.concat(sparse_frame) + exp = pd.concat(dense_frame) + + assert isinstance(res, pd.SparseDataFrame) + tm.assert_frame_equal(res.to_dense(), exp) + + sparse_frame = sparse_frame[::-1] + dense_frame = dense_frame[::-1] + + @pytest.mark.parametrize('fill_value,sparse_idx,dense_idx', + itertools.product([None, 0, 1, np.nan], + [0, 1], + [1, 0])) + @pytest.mark.xfail(reason="The iloc fails and I can't make expected", + strict=False) + def test_concat_sparse_dense_cols(self, fill_value, sparse_idx, dense_idx): + # See GH16874, GH18914 and #18686 for why this should be a DataFrame + from pandas.core.dtypes.common import is_sparse + + frames = [self.dense1, self.dense3] + + sparse_frame = [frames[dense_idx], + frames[sparse_idx].to_sparse(fill_value=fill_value)] + dense_frame = [frames[dense_idx], frames[sparse_idx]] + + # This will try both directions sparse + dense and dense + sparse + for _ in range(2): + res = pd.concat(sparse_frame, axis=1) + exp = pd.concat(dense_frame, axis=1) + cols = [i for (i, x) in enumerate(res.dtypes) if is_sparse(x)] + + for col in cols: + exp.iloc[:, col] = exp.iloc[:, col].astype("Sparse") + + for column in frames[dense_idx].columns: + if dense_idx == sparse_idx: + tm.assert_frame_equal(res[column], exp[column]) + else: + tm.assert_series_equal(res[column], exp[column]) + + tm.assert_frame_equal(res, exp) + + sparse_frame = sparse_frame[::-1] + dense_frame = dense_frame[::-1] diff --git a/pandas/tests/sparse/test_format.py b/pandas/tests/sparse/test_format.py index ba870a2c33801..63018f9525b1f 100644 --- a/pandas/tests/sparse/test_format.py +++ b/pandas/tests/sparse/test_format.py @@ -2,18 +2,17 @@ from __future__ import print_function import numpy as np -import pandas as pd -import pandas.util.testing as tm -from pandas.compat import (is_platform_windows, - is_platform_32bit) -from pandas.core.config import option_context +from pandas.compat import is_platform_32bit, is_platform_windows +import pandas as pd +from pandas.core.config import option_context +import pandas.util.testing as tm use_32bit_repr = is_platform_windows() or is_platform_32bit() -class TestSparseSeriesFormatting(tm.TestCase): +class TestSparseSeriesFormatting(object): @property def dtype_format_for_platform(self): @@ -24,19 +23,23 @@ def test_sparse_max_row(self): result = repr(s) dfm = self.dtype_format_for_platform exp = ("0 1.0\n1 NaN\n2 NaN\n3 3.0\n" - "4 NaN\ndtype: float64\nBlockIndex\n" + "4 NaN\ndtype: Sparse[float64, nan]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dfm)) - self.assertEqual(result, exp) + assert result == exp + + def test_sparsea_max_row_truncated(self): + s = pd.Series([1, np.nan, np.nan, 3, np.nan]).to_sparse() + dfm = self.dtype_format_for_platform with option_context("display.max_rows", 3): # GH 10560 result = repr(s) exp = ("0 1.0\n ... \n4 NaN\n" - "dtype: float64\nBlockIndex\n" + "Length: 5, dtype: Sparse[float64, nan]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dfm)) - self.assertEqual(result, exp) + assert result == exp def test_sparse_mi_max_row(self): idx = pd.MultiIndex.from_tuples([('A', 0), ('A', 1), ('B', 0), @@ -47,19 +50,20 @@ def test_sparse_mi_max_row(self): dfm = self.dtype_format_for_platform exp = ("A 0 1.0\n 1 NaN\nB 0 NaN\n" "C 0 3.0\n 1 NaN\n 2 NaN\n" - "dtype: float64\nBlockIndex\n" + "dtype: Sparse[float64, nan]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dfm)) - self.assertEqual(result, exp) + assert result == exp - with option_context("display.max_rows", 3): + with option_context("display.max_rows", 3, + "display.show_dimensions", False): # GH 13144 result = repr(s) exp = ("A 0 1.0\n ... \nC 2 NaN\n" - "dtype: float64\nBlockIndex\n" + "dtype: Sparse[float64, nan]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dfm)) - self.assertEqual(result, exp) + assert result == exp def test_sparse_bool(self): # GH 13110 @@ -69,18 +73,18 @@ def test_sparse_bool(self): dtype = '' if use_32bit_repr else ', dtype=int32' exp = ("0 True\n1 False\n2 False\n" "3 True\n4 False\n5 False\n" - "dtype: bool\nBlockIndex\n" + "dtype: Sparse[bool, False]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dtype)) - self.assertEqual(result, exp) + assert result == exp with option_context("display.max_rows", 3): result = repr(s) exp = ("0 True\n ... \n5 False\n" - "dtype: bool\nBlockIndex\n" + "Length: 6, dtype: Sparse[bool, False]\nBlockIndex\n" "Block locations: array([0, 3]{0})\n" "Block lengths: array([1, 1]{0})".format(dtype)) - self.assertEqual(result, exp) + assert result == exp def test_sparse_int(self): # GH 13110 @@ -89,21 +93,22 @@ def test_sparse_int(self): result = repr(s) dtype = '' if use_32bit_repr else ', dtype=int32' exp = ("0 0\n1 1\n2 0\n3 0\n4 1\n" - "5 0\ndtype: int64\nBlockIndex\n" + "5 0\ndtype: Sparse[int64, False]\nBlockIndex\n" "Block locations: array([1, 4]{0})\n" "Block lengths: array([1, 1]{0})".format(dtype)) - self.assertEqual(result, exp) + assert result == exp - with option_context("display.max_rows", 3): + with option_context("display.max_rows", 3, + "display.show_dimensions", False): result = repr(s) exp = ("0 0\n ..\n5 0\n" - "dtype: int64\nBlockIndex\n" + "dtype: Sparse[int64, False]\nBlockIndex\n" "Block locations: array([1, 4]{0})\n" "Block lengths: array([1, 1]{0})".format(dtype)) - self.assertEqual(result, exp) + assert result == exp -class TestSparseDataFrameFormatting(tm.TestCase): +class TestSparseDataFrameFormatting(object): def test_sparse_frame(self): # GH 13110 @@ -112,10 +117,10 @@ def test_sparse_frame(self): 'C': [0, 0, 3, 0, 5], 'D': [np.nan, np.nan, np.nan, 1, 2]}) sparse = df.to_sparse() - self.assertEqual(repr(sparse), repr(df)) + assert repr(sparse) == repr(df) with option_context("display.max_rows", 3): - self.assertEqual(repr(sparse), repr(df)) + assert repr(sparse) == repr(df) def test_sparse_repr_after_set(self): # GH 15488 diff --git a/pandas/tests/sparse/test_frame.py b/pandas/tests/sparse/test_frame.py deleted file mode 100644 index ae1a1e35f1859..0000000000000 --- a/pandas/tests/sparse/test_frame.py +++ /dev/null @@ -1,1319 +0,0 @@ -# pylint: disable-msg=E1101,W0612 - -import operator - -import pytest - -from numpy import nan -import numpy as np -import pandas as pd - -from pandas import Series, DataFrame, bdate_range, Panel -from pandas.types.common import (is_bool_dtype, - is_float_dtype, - is_object_dtype, - is_float) -from pandas.tseries.index import DatetimeIndex -from pandas.tseries.offsets import BDay -import pandas.util.testing as tm -from pandas.compat import lrange -from pandas import compat -import pandas.sparse.frame as spf - -from pandas.sparse.libsparse import BlockIndex, IntIndex -from pandas.sparse.api import SparseSeries, SparseDataFrame, SparseArray -from pandas.tests.frame.test_misc_api import SharedWithSparse - -from pandas.tests.sparse.common import spmatrix # noqa: F401 - - -class TestSparseDataFrame(tm.TestCase, SharedWithSparse): - klass = SparseDataFrame - - def setUp(self): - self.data = {'A': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], - 'B': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], - 'C': np.arange(10, dtype=np.float64), - 'D': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]} - - self.dates = bdate_range('1/1/2011', periods=10) - - self.orig = pd.DataFrame(self.data, index=self.dates) - self.iorig = pd.DataFrame(self.data, index=self.dates) - - self.frame = SparseDataFrame(self.data, index=self.dates) - self.iframe = SparseDataFrame(self.data, index=self.dates, - default_kind='integer') - - values = self.frame.values.copy() - values[np.isnan(values)] = 0 - - self.zorig = pd.DataFrame(values, columns=['A', 'B', 'C', 'D'], - index=self.dates) - self.zframe = SparseDataFrame(values, columns=['A', 'B', 'C', 'D'], - default_fill_value=0, index=self.dates) - - values = self.frame.values.copy() - values[np.isnan(values)] = 2 - - self.fill_orig = pd.DataFrame(values, columns=['A', 'B', 'C', 'D'], - index=self.dates) - self.fill_frame = SparseDataFrame(values, columns=['A', 'B', 'C', 'D'], - default_fill_value=2, - index=self.dates) - - self.empty = SparseDataFrame() - - def test_fill_value_when_combine_const(self): - # GH12723 - dat = np.array([0, 1, np.nan, 3, 4, 5], dtype='float') - df = SparseDataFrame({'foo': dat}, index=range(6)) - - exp = df.fillna(0).add(2) - res = df.add(2, fill_value=0) - tm.assert_sp_frame_equal(res, exp) - - def test_as_matrix(self): - empty = self.empty.as_matrix() - self.assertEqual(empty.shape, (0, 0)) - - no_cols = SparseDataFrame(index=np.arange(10)) - mat = no_cols.as_matrix() - self.assertEqual(mat.shape, (10, 0)) - - no_index = SparseDataFrame(columns=np.arange(10)) - mat = no_index.as_matrix() - self.assertEqual(mat.shape, (0, 10)) - - def test_copy(self): - cp = self.frame.copy() - tm.assertIsInstance(cp, SparseDataFrame) - tm.assert_sp_frame_equal(cp, self.frame) - - # as of v0.15.0 - # this is now identical (but not is_a ) - self.assertTrue(cp.index.identical(self.frame.index)) - - def test_constructor(self): - for col, series in compat.iteritems(self.frame): - tm.assertIsInstance(series, SparseSeries) - - tm.assertIsInstance(self.iframe['A'].sp_index, IntIndex) - - # constructed zframe from matrix above - self.assertEqual(self.zframe['A'].fill_value, 0) - tm.assert_numpy_array_equal(pd.SparseArray([1., 2., 3., 4., 5., 6.]), - self.zframe['A'].values) - tm.assert_numpy_array_equal(np.array([0., 0., 0., 0., 1., 2., - 3., 4., 5., 6.]), - self.zframe['A'].to_dense().values) - - # construct no data - sdf = SparseDataFrame(columns=np.arange(10), index=np.arange(10)) - for col, series in compat.iteritems(sdf): - tm.assertIsInstance(series, SparseSeries) - - # construct from nested dict - data = {} - for c, s in compat.iteritems(self.frame): - data[c] = s.to_dict() - - sdf = SparseDataFrame(data) - tm.assert_sp_frame_equal(sdf, self.frame) - - # TODO: test data is copied from inputs - - # init dict with different index - idx = self.frame.index[:5] - cons = SparseDataFrame( - self.frame, index=idx, columns=self.frame.columns, - default_fill_value=self.frame.default_fill_value, - default_kind=self.frame.default_kind, copy=True) - reindexed = self.frame.reindex(idx) - - tm.assert_sp_frame_equal(cons, reindexed, exact_indices=False) - - # assert level parameter breaks reindex - with tm.assertRaises(TypeError): - self.frame.reindex(idx, level=0) - - repr(self.frame) - - def test_constructor_ndarray(self): - # no index or columns - sp = SparseDataFrame(self.frame.values) - - # 1d - sp = SparseDataFrame(self.data['A'], index=self.dates, columns=['A']) - tm.assert_sp_frame_equal(sp, self.frame.reindex(columns=['A'])) - - # raise on level argument - self.assertRaises(TypeError, self.frame.reindex, columns=['A'], - level=1) - - # wrong length index / columns - with tm.assertRaisesRegexp(ValueError, "^Index length"): - SparseDataFrame(self.frame.values, index=self.frame.index[:-1]) - - with tm.assertRaisesRegexp(ValueError, "^Column length"): - SparseDataFrame(self.frame.values, columns=self.frame.columns[:-1]) - - # GH 9272 - def test_constructor_empty(self): - sp = SparseDataFrame() - self.assertEqual(len(sp.index), 0) - self.assertEqual(len(sp.columns), 0) - - def test_constructor_dataframe(self): - dense = self.frame.to_dense() - sp = SparseDataFrame(dense) - tm.assert_sp_frame_equal(sp, self.frame) - - def test_constructor_convert_index_once(self): - arr = np.array([1.5, 2.5, 3.5]) - sdf = SparseDataFrame(columns=lrange(4), index=arr) - self.assertTrue(sdf[0].index is sdf[1].index) - - def test_constructor_from_series(self): - - # GH 2873 - x = Series(np.random.randn(10000), name='a') - x = x.to_sparse(fill_value=0) - tm.assertIsInstance(x, SparseSeries) - df = SparseDataFrame(x) - tm.assertIsInstance(df, SparseDataFrame) - - x = Series(np.random.randn(10000), name='a') - y = Series(np.random.randn(10000), name='b') - x2 = x.astype(float) - x2.loc[:9998] = np.NaN - # TODO: x_sparse is unused...fix - x_sparse = x2.to_sparse(fill_value=np.NaN) # noqa - - # Currently fails too with weird ufunc error - # df1 = SparseDataFrame([x_sparse, y]) - - y.loc[:9998] = 0 - # TODO: y_sparse is unsused...fix - y_sparse = y.to_sparse(fill_value=0) # noqa - # without sparse value raises error - # df2 = SparseDataFrame([x2_sparse, y]) - - def test_constructor_preserve_attr(self): - # GH 13866 - arr = pd.SparseArray([1, 0, 3, 0], dtype=np.int64, fill_value=0) - self.assertEqual(arr.dtype, np.int64) - self.assertEqual(arr.fill_value, 0) - - df = pd.SparseDataFrame({'x': arr}) - self.assertEqual(df['x'].dtype, np.int64) - self.assertEqual(df['x'].fill_value, 0) - - s = pd.SparseSeries(arr, name='x') - self.assertEqual(s.dtype, np.int64) - self.assertEqual(s.fill_value, 0) - - df = pd.SparseDataFrame(s) - self.assertEqual(df['x'].dtype, np.int64) - self.assertEqual(df['x'].fill_value, 0) - - df = pd.SparseDataFrame({'x': s}) - self.assertEqual(df['x'].dtype, np.int64) - self.assertEqual(df['x'].fill_value, 0) - - def test_constructor_nan_dataframe(self): - # GH 10079 - trains = np.arange(100) - tresholds = [10, 20, 30, 40, 50, 60] - tuples = [(i, j) for i in trains for j in tresholds] - index = pd.MultiIndex.from_tuples(tuples, - names=['trains', 'tresholds']) - matrix = np.empty((len(index), len(trains))) - matrix.fill(np.nan) - df = pd.DataFrame(matrix, index=index, columns=trains, dtype=float) - result = df.to_sparse() - expected = pd.SparseDataFrame(matrix, index=index, columns=trains, - dtype=float) - tm.assert_sp_frame_equal(result, expected) - - def test_type_coercion_at_construction(self): - # GH 15682 - result = pd.SparseDataFrame( - {'a': [1, 0, 0], 'b': [0, 1, 0], 'c': [0, 0, 1]}, dtype='uint8', - default_fill_value=0) - expected = pd.SparseDataFrame( - {'a': pd.SparseSeries([1, 0, 0], dtype='uint8'), - 'b': pd.SparseSeries([0, 1, 0], dtype='uint8'), - 'c': pd.SparseSeries([0, 0, 1], dtype='uint8')}, - default_fill_value=0) - tm.assert_sp_frame_equal(result, expected) - - def test_dtypes(self): - df = DataFrame(np.random.randn(10000, 4)) - df.loc[:9998] = np.nan - sdf = df.to_sparse() - - result = sdf.get_dtype_counts() - expected = Series({'float64': 4}) - tm.assert_series_equal(result, expected) - - def test_shape(self): - # GH 10452 - self.assertEqual(self.frame.shape, (10, 4)) - self.assertEqual(self.iframe.shape, (10, 4)) - self.assertEqual(self.zframe.shape, (10, 4)) - self.assertEqual(self.fill_frame.shape, (10, 4)) - - def test_str(self): - df = DataFrame(np.random.randn(10000, 4)) - df.loc[:9998] = np.nan - - sdf = df.to_sparse() - str(sdf) - - def test_array_interface(self): - res = np.sqrt(self.frame) - dres = np.sqrt(self.frame.to_dense()) - tm.assert_frame_equal(res.to_dense(), dres) - - def test_pickle(self): - - def _test_roundtrip(frame, orig): - result = self.round_trip_pickle(frame) - tm.assert_sp_frame_equal(frame, result) - tm.assert_frame_equal(result.to_dense(), orig, check_dtype=False) - - _test_roundtrip(SparseDataFrame(), DataFrame()) - self._check_all(_test_roundtrip) - - def test_dense_to_sparse(self): - df = DataFrame({'A': [nan, nan, nan, 1, 2], - 'B': [1, 2, nan, nan, nan]}) - sdf = df.to_sparse() - tm.assertIsInstance(sdf, SparseDataFrame) - self.assertTrue(np.isnan(sdf.default_fill_value)) - tm.assertIsInstance(sdf['A'].sp_index, BlockIndex) - tm.assert_frame_equal(sdf.to_dense(), df) - - sdf = df.to_sparse(kind='integer') - tm.assertIsInstance(sdf['A'].sp_index, IntIndex) - - df = DataFrame({'A': [0, 0, 0, 1, 2], - 'B': [1, 2, 0, 0, 0]}, dtype=float) - sdf = df.to_sparse(fill_value=0) - self.assertEqual(sdf.default_fill_value, 0) - tm.assert_frame_equal(sdf.to_dense(), df) - - def test_density(self): - df = SparseSeries([nan, nan, nan, 0, 1, 2, 3, 4, 5, 6]) - self.assertEqual(df.density, 0.7) - - df = SparseDataFrame({'A': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], - 'B': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], - 'C': np.arange(10), - 'D': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]}) - - self.assertEqual(df.density, 0.75) - - def test_sparse_to_dense(self): - pass - - def test_sparse_series_ops(self): - self._check_frame_ops(self.frame) - - def test_sparse_series_ops_i(self): - self._check_frame_ops(self.iframe) - - def test_sparse_series_ops_z(self): - self._check_frame_ops(self.zframe) - - def test_sparse_series_ops_fill(self): - self._check_frame_ops(self.fill_frame) - - def _check_frame_ops(self, frame): - - def _compare_to_dense(a, b, da, db, op): - sparse_result = op(a, b) - dense_result = op(da, db) - - fill = sparse_result.default_fill_value - dense_result = dense_result.to_sparse(fill_value=fill) - tm.assert_sp_frame_equal(sparse_result, dense_result, - exact_indices=False) - - if isinstance(a, DataFrame) and isinstance(db, DataFrame): - mixed_result = op(a, db) - tm.assertIsInstance(mixed_result, SparseDataFrame) - tm.assert_sp_frame_equal(mixed_result, sparse_result, - exact_indices=False) - - opnames = ['add', 'sub', 'mul', 'truediv', 'floordiv'] - ops = [getattr(operator, name) for name in opnames] - - fidx = frame.index - - # time series operations - - series = [frame['A'], frame['B'], frame['C'], frame['D'], - frame['A'].reindex(fidx[:7]), frame['A'].reindex(fidx[::2]), - SparseSeries( - [], index=[])] - - for op in opnames: - _compare_to_dense(frame, frame[::2], frame.to_dense(), - frame[::2].to_dense(), getattr(operator, op)) - - # 2304, no auto-broadcasting - for i, s in enumerate(series): - f = lambda a, b: getattr(a, op)(b, axis='index') - _compare_to_dense(frame, s, frame.to_dense(), s.to_dense(), f) - - # rops are not implemented - # _compare_to_dense(s, frame, s.to_dense(), - # frame.to_dense(), f) - - # cross-sectional operations - series = [frame.xs(fidx[0]), frame.xs(fidx[3]), frame.xs(fidx[5]), - frame.xs(fidx[7]), frame.xs(fidx[5])[:2]] - - for op in ops: - for s in series: - _compare_to_dense(frame, s, frame.to_dense(), s, op) - _compare_to_dense(s, frame, s, frame.to_dense(), op) - - # it works! - result = self.frame + self.frame.loc[:, ['A', 'B']] # noqa - - def test_op_corners(self): - empty = self.empty + self.empty - self.assertTrue(empty.empty) - - foo = self.frame + self.empty - tm.assertIsInstance(foo.index, DatetimeIndex) - tm.assert_frame_equal(foo, self.frame * np.nan) - - foo = self.empty + self.frame - tm.assert_frame_equal(foo, self.frame * np.nan) - - def test_scalar_ops(self): - pass - - def test_getitem(self): - # 1585 select multiple columns - sdf = SparseDataFrame(index=[0, 1, 2], columns=['a', 'b', 'c']) - - result = sdf[['a', 'b']] - exp = sdf.reindex(columns=['a', 'b']) - tm.assert_sp_frame_equal(result, exp) - - self.assertRaises(Exception, sdf.__getitem__, ['a', 'd']) - - def test_iloc(self): - - # 2227 - result = self.frame.iloc[:, 0] - self.assertTrue(isinstance(result, SparseSeries)) - tm.assert_sp_series_equal(result, self.frame['A']) - - # preserve sparse index type. #2251 - data = {'A': [0, 1]} - iframe = SparseDataFrame(data, default_kind='integer') - self.assertEqual(type(iframe['A'].sp_index), - type(iframe.iloc[:, 0].sp_index)) - - def test_set_value(self): - - # ok as the index gets conver to object - frame = self.frame.copy() - res = frame.set_value('foobar', 'B', 1.5) - self.assertEqual(res.index.dtype, 'object') - - res = self.frame - res.index = res.index.astype(object) - - res = self.frame.set_value('foobar', 'B', 1.5) - self.assertIsNot(res, self.frame) - self.assertEqual(res.index[-1], 'foobar') - self.assertEqual(res.get_value('foobar', 'B'), 1.5) - - res2 = res.set_value('foobar', 'qux', 1.5) - self.assertIsNot(res2, res) - self.assert_index_equal(res2.columns, - pd.Index(list(self.frame.columns) + ['qux'])) - self.assertEqual(res2.get_value('foobar', 'qux'), 1.5) - - def test_fancy_index_misc(self): - # axis = 0 - sliced = self.frame.iloc[-2:, :] - expected = self.frame.reindex(index=self.frame.index[-2:]) - tm.assert_sp_frame_equal(sliced, expected) - - # axis = 1 - sliced = self.frame.iloc[:, -2:] - expected = self.frame.reindex(columns=self.frame.columns[-2:]) - tm.assert_sp_frame_equal(sliced, expected) - - def test_getitem_overload(self): - # slicing - sl = self.frame[:20] - tm.assert_sp_frame_equal(sl, self.frame.reindex(self.frame.index[:20])) - - # boolean indexing - d = self.frame.index[5] - indexer = self.frame.index > d - - subindex = self.frame.index[indexer] - subframe = self.frame[indexer] - - self.assert_index_equal(subindex, subframe.index) - self.assertRaises(Exception, self.frame.__getitem__, indexer[:-1]) - - def test_setitem(self): - - def _check_frame(frame, orig): - N = len(frame) - - # insert SparseSeries - frame['E'] = frame['A'] - tm.assertIsInstance(frame['E'], SparseSeries) - tm.assert_sp_series_equal(frame['E'], frame['A'], - check_names=False) - - # insert SparseSeries differently-indexed - to_insert = frame['A'][::2] - frame['E'] = to_insert - expected = to_insert.to_dense().reindex(frame.index) - result = frame['E'].to_dense() - tm.assert_series_equal(result, expected, check_names=False) - self.assertEqual(result.name, 'E') - - # insert Series - frame['F'] = frame['A'].to_dense() - tm.assertIsInstance(frame['F'], SparseSeries) - tm.assert_sp_series_equal(frame['F'], frame['A'], - check_names=False) - - # insert Series differently-indexed - to_insert = frame['A'].to_dense()[::2] - frame['G'] = to_insert - expected = to_insert.reindex(frame.index) - expected.name = 'G' - tm.assert_series_equal(frame['G'].to_dense(), expected) - - # insert ndarray - frame['H'] = np.random.randn(N) - tm.assertIsInstance(frame['H'], SparseSeries) - - to_sparsify = np.random.randn(N) - to_sparsify[N // 2:] = frame.default_fill_value - frame['I'] = to_sparsify - self.assertEqual(len(frame['I'].sp_values), N // 2) - - # insert ndarray wrong size - self.assertRaises(Exception, frame.__setitem__, 'foo', - np.random.randn(N - 1)) - - # scalar value - frame['J'] = 5 - self.assertEqual(len(frame['J'].sp_values), N) - self.assertTrue((frame['J'].sp_values == 5).all()) - - frame['K'] = frame.default_fill_value - self.assertEqual(len(frame['K'].sp_values), 0) - - self._check_all(_check_frame) - - def test_setitem_corner(self): - self.frame['a'] = self.frame['B'] - tm.assert_sp_series_equal(self.frame['a'], self.frame['B'], - check_names=False) - - def test_setitem_array(self): - arr = self.frame['B'] - - self.frame['E'] = arr - tm.assert_sp_series_equal(self.frame['E'], self.frame['B'], - check_names=False) - - self.frame['F'] = arr[:-1] - index = self.frame.index[:-1] - tm.assert_sp_series_equal(self.frame['E'].reindex(index), - self.frame['F'].reindex(index), - check_names=False) - - def test_delitem(self): - A = self.frame['A'] - C = self.frame['C'] - - del self.frame['B'] - self.assertNotIn('B', self.frame) - tm.assert_sp_series_equal(self.frame['A'], A) - tm.assert_sp_series_equal(self.frame['C'], C) - - del self.frame['D'] - self.assertNotIn('D', self.frame) - - del self.frame['A'] - self.assertNotIn('A', self.frame) - - def test_set_columns(self): - self.frame.columns = self.frame.columns - self.assertRaises(Exception, setattr, self.frame, 'columns', - self.frame.columns[:-1]) - - def test_set_index(self): - self.frame.index = self.frame.index - self.assertRaises(Exception, setattr, self.frame, 'index', - self.frame.index[:-1]) - - def test_append(self): - a = self.frame[:5] - b = self.frame[5:] - - appended = a.append(b) - tm.assert_sp_frame_equal(appended, self.frame, exact_indices=False) - - a = self.frame.iloc[:5, :3] - b = self.frame.iloc[5:] - appended = a.append(b) - tm.assert_sp_frame_equal(appended.iloc[:, :3], self.frame.iloc[:, :3], - exact_indices=False) - - def test_apply(self): - applied = self.frame.apply(np.sqrt) - tm.assertIsInstance(applied, SparseDataFrame) - tm.assert_almost_equal(applied.values, np.sqrt(self.frame.values)) - - applied = self.fill_frame.apply(np.sqrt) - self.assertEqual(applied['A'].fill_value, np.sqrt(2)) - - # agg / broadcast - broadcasted = self.frame.apply(np.sum, broadcast=True) - tm.assertIsInstance(broadcasted, SparseDataFrame) - - exp = self.frame.to_dense().apply(np.sum, broadcast=True) - tm.assert_frame_equal(broadcasted.to_dense(), exp) - - self.assertIs(self.empty.apply(np.sqrt), self.empty) - - from pandas.core import nanops - applied = self.frame.apply(np.sum) - tm.assert_series_equal(applied, - self.frame.to_dense().apply(nanops.nansum)) - - def test_apply_nonuq(self): - orig = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], - index=['a', 'a', 'c']) - sparse = orig.to_sparse() - res = sparse.apply(lambda s: s[0], axis=1) - exp = orig.apply(lambda s: s[0], axis=1) - # dtype must be kept - self.assertEqual(res.dtype, np.int64) - # ToDo: apply must return subclassed dtype - self.assertIsInstance(res, pd.Series) - tm.assert_series_equal(res.to_dense(), exp) - - # df.T breaks - sparse = orig.T.to_sparse() - res = sparse.apply(lambda s: s[0], axis=0) # noqa - exp = orig.T.apply(lambda s: s[0], axis=0) - # TODO: no non-unique columns supported in sparse yet - # tm.assert_series_equal(res.to_dense(), exp) - - def test_applymap(self): - # just test that it works - result = self.frame.applymap(lambda x: x * 2) - tm.assertIsInstance(result, SparseDataFrame) - - def test_astype(self): - sparse = pd.SparseDataFrame({'A': SparseArray([1, 2, 3, 4], - dtype=np.int64), - 'B': SparseArray([4, 5, 6, 7], - dtype=np.int64)}) - self.assertEqual(sparse['A'].dtype, np.int64) - self.assertEqual(sparse['B'].dtype, np.int64) - - res = sparse.astype(np.float64) - exp = pd.SparseDataFrame({'A': SparseArray([1., 2., 3., 4.], - fill_value=0.), - 'B': SparseArray([4., 5., 6., 7.], - fill_value=0.)}, - default_fill_value=np.nan) - tm.assert_sp_frame_equal(res, exp) - self.assertEqual(res['A'].dtype, np.float64) - self.assertEqual(res['B'].dtype, np.float64) - - sparse = pd.SparseDataFrame({'A': SparseArray([0, 2, 0, 4], - dtype=np.int64), - 'B': SparseArray([0, 5, 0, 7], - dtype=np.int64)}, - default_fill_value=0) - self.assertEqual(sparse['A'].dtype, np.int64) - self.assertEqual(sparse['B'].dtype, np.int64) - - res = sparse.astype(np.float64) - exp = pd.SparseDataFrame({'A': SparseArray([0., 2., 0., 4.], - fill_value=0.), - 'B': SparseArray([0., 5., 0., 7.], - fill_value=0.)}, - default_fill_value=0.) - tm.assert_sp_frame_equal(res, exp) - self.assertEqual(res['A'].dtype, np.float64) - self.assertEqual(res['B'].dtype, np.float64) - - def test_astype_bool(self): - sparse = pd.SparseDataFrame({'A': SparseArray([0, 2, 0, 4], - fill_value=0, - dtype=np.int64), - 'B': SparseArray([0, 5, 0, 7], - fill_value=0, - dtype=np.int64)}, - default_fill_value=0) - self.assertEqual(sparse['A'].dtype, np.int64) - self.assertEqual(sparse['B'].dtype, np.int64) - - res = sparse.astype(bool) - exp = pd.SparseDataFrame({'A': SparseArray([False, True, False, True], - dtype=np.bool, - fill_value=False), - 'B': SparseArray([False, True, False, True], - dtype=np.bool, - fill_value=False)}, - default_fill_value=False) - tm.assert_sp_frame_equal(res, exp) - self.assertEqual(res['A'].dtype, np.bool) - self.assertEqual(res['B'].dtype, np.bool) - - def test_fillna(self): - df = self.zframe.reindex(lrange(5)) - dense = self.zorig.reindex(lrange(5)) - - result = df.fillna(0) - expected = dense.fillna(0) - tm.assert_sp_frame_equal(result, expected.to_sparse(fill_value=0), - exact_indices=False) - tm.assert_frame_equal(result.to_dense(), expected) - - result = df.copy() - result.fillna(0, inplace=True) - expected = dense.fillna(0) - - tm.assert_sp_frame_equal(result, expected.to_sparse(fill_value=0), - exact_indices=False) - tm.assert_frame_equal(result.to_dense(), expected) - - result = df.copy() - result = df['A'] - result.fillna(0, inplace=True) - - expected = dense['A'].fillna(0) - # this changes internal SparseArray repr - # tm.assert_sp_series_equal(result, expected.to_sparse(fill_value=0)) - tm.assert_series_equal(result.to_dense(), expected) - - def test_fillna_fill_value(self): - df = pd.DataFrame({'A': [1, 0, 0], 'B': [np.nan, np.nan, 4]}) - - sparse = pd.SparseDataFrame(df) - tm.assert_frame_equal(sparse.fillna(-1).to_dense(), - df.fillna(-1), check_dtype=False) - - sparse = pd.SparseDataFrame(df, default_fill_value=0) - tm.assert_frame_equal(sparse.fillna(-1).to_dense(), - df.fillna(-1), check_dtype=False) - - def test_sparse_frame_pad_backfill_limit(self): - index = np.arange(10) - df = DataFrame(np.random.randn(10, 4), index=index) - sdf = df.to_sparse() - - result = sdf[:2].reindex(index, method='pad', limit=5) - - expected = sdf[:2].reindex(index).fillna(method='pad') - expected = expected.to_dense() - expected.values[-3:] = np.nan - expected = expected.to_sparse() - tm.assert_frame_equal(result, expected) - - result = sdf[-2:].reindex(index, method='backfill', limit=5) - - expected = sdf[-2:].reindex(index).fillna(method='backfill') - expected = expected.to_dense() - expected.values[:3] = np.nan - expected = expected.to_sparse() - tm.assert_frame_equal(result, expected) - - def test_sparse_frame_fillna_limit(self): - index = np.arange(10) - df = DataFrame(np.random.randn(10, 4), index=index) - sdf = df.to_sparse() - - result = sdf[:2].reindex(index) - result = result.fillna(method='pad', limit=5) - - expected = sdf[:2].reindex(index).fillna(method='pad') - expected = expected.to_dense() - expected.values[-3:] = np.nan - expected = expected.to_sparse() - tm.assert_frame_equal(result, expected) - - result = sdf[-2:].reindex(index) - result = result.fillna(method='backfill', limit=5) - - expected = sdf[-2:].reindex(index).fillna(method='backfill') - expected = expected.to_dense() - expected.values[:3] = np.nan - expected = expected.to_sparse() - tm.assert_frame_equal(result, expected) - - def test_rename(self): - result = self.frame.rename(index=str) - expected = SparseDataFrame(self.data, index=self.dates.strftime( - "%Y-%m-%d %H:%M:%S")) - tm.assert_sp_frame_equal(result, expected) - - result = self.frame.rename(columns=lambda x: '%s%d' % (x, len(x))) - data = {'A1': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], - 'B1': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], - 'C1': np.arange(10, dtype=np.float64), - 'D1': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]} - expected = SparseDataFrame(data, index=self.dates) - tm.assert_sp_frame_equal(result, expected) - - def test_corr(self): - res = self.frame.corr() - tm.assert_frame_equal(res, self.frame.to_dense().corr()) - - def test_describe(self): - self.frame['foo'] = np.nan - self.frame.get_dtype_counts() - str(self.frame) - desc = self.frame.describe() # noqa - - def test_join(self): - left = self.frame.loc[:, ['A', 'B']] - right = self.frame.loc[:, ['C', 'D']] - joined = left.join(right) - tm.assert_sp_frame_equal(joined, self.frame, exact_indices=False) - - right = self.frame.loc[:, ['B', 'D']] - self.assertRaises(Exception, left.join, right) - - with tm.assertRaisesRegexp(ValueError, - 'Other Series must have a name'): - self.frame.join(Series( - np.random.randn(len(self.frame)), index=self.frame.index)) - - def test_reindex(self): - - def _check_frame(frame): - index = frame.index - sidx = index[::2] - sidx2 = index[:5] # noqa - - sparse_result = frame.reindex(sidx) - dense_result = frame.to_dense().reindex(sidx) - tm.assert_frame_equal(sparse_result.to_dense(), dense_result) - - tm.assert_frame_equal(frame.reindex(list(sidx)).to_dense(), - dense_result) - - sparse_result2 = sparse_result.reindex(index) - dense_result2 = dense_result.reindex(index) - tm.assert_frame_equal(sparse_result2.to_dense(), dense_result2) - - # propagate CORRECT fill value - tm.assert_almost_equal(sparse_result.default_fill_value, - frame.default_fill_value) - tm.assert_almost_equal(sparse_result['A'].fill_value, - frame['A'].fill_value) - - # length zero - length_zero = frame.reindex([]) - self.assertEqual(len(length_zero), 0) - self.assertEqual(len(length_zero.columns), len(frame.columns)) - self.assertEqual(len(length_zero['A']), 0) - - # frame being reindexed has length zero - length_n = length_zero.reindex(index) - self.assertEqual(len(length_n), len(frame)) - self.assertEqual(len(length_n.columns), len(frame.columns)) - self.assertEqual(len(length_n['A']), len(frame)) - - # reindex columns - reindexed = frame.reindex(columns=['A', 'B', 'Z']) - self.assertEqual(len(reindexed.columns), 3) - tm.assert_almost_equal(reindexed['Z'].fill_value, - frame.default_fill_value) - self.assertTrue(np.isnan(reindexed['Z'].sp_values).all()) - - _check_frame(self.frame) - _check_frame(self.iframe) - _check_frame(self.zframe) - _check_frame(self.fill_frame) - - # with copy=False - reindexed = self.frame.reindex(self.frame.index, copy=False) - reindexed['F'] = reindexed['A'] - self.assertIn('F', self.frame) - - reindexed = self.frame.reindex(self.frame.index) - reindexed['G'] = reindexed['A'] - self.assertNotIn('G', self.frame) - - def test_reindex_fill_value(self): - rng = bdate_range('20110110', periods=20) - - result = self.zframe.reindex(rng, fill_value=0) - exp = self.zorig.reindex(rng, fill_value=0) - exp = exp.to_sparse(self.zframe.default_fill_value) - tm.assert_sp_frame_equal(result, exp) - - def test_reindex_method(self): - - sparse = SparseDataFrame(data=[[11., 12., 14.], - [21., 22., 24.], - [41., 42., 44.]], - index=[1, 2, 4], - columns=[1, 2, 4], - dtype=float) - - # Over indices - - # default method - result = sparse.reindex(index=range(6)) - expected = SparseDataFrame(data=[[nan, nan, nan], - [11., 12., 14.], - [21., 22., 24.], - [nan, nan, nan], - [41., 42., 44.], - [nan, nan, nan]], - index=range(6), - columns=[1, 2, 4], - dtype=float) - tm.assert_sp_frame_equal(result, expected) - - # method='bfill' - result = sparse.reindex(index=range(6), method='bfill') - expected = SparseDataFrame(data=[[11., 12., 14.], - [11., 12., 14.], - [21., 22., 24.], - [41., 42., 44.], - [41., 42., 44.], - [nan, nan, nan]], - index=range(6), - columns=[1, 2, 4], - dtype=float) - tm.assert_sp_frame_equal(result, expected) - - # method='ffill' - result = sparse.reindex(index=range(6), method='ffill') - expected = SparseDataFrame(data=[[nan, nan, nan], - [11., 12., 14.], - [21., 22., 24.], - [21., 22., 24.], - [41., 42., 44.], - [41., 42., 44.]], - index=range(6), - columns=[1, 2, 4], - dtype=float) - tm.assert_sp_frame_equal(result, expected) - - # Over columns - - # default method - result = sparse.reindex(columns=range(6)) - expected = SparseDataFrame(data=[[nan, 11., 12., nan, 14., nan], - [nan, 21., 22., nan, 24., nan], - [nan, 41., 42., nan, 44., nan]], - index=[1, 2, 4], - columns=range(6), - dtype=float) - tm.assert_sp_frame_equal(result, expected) - - # method='bfill' - with tm.assertRaises(NotImplementedError): - sparse.reindex(columns=range(6), method='bfill') - - # method='ffill' - with tm.assertRaises(NotImplementedError): - sparse.reindex(columns=range(6), method='ffill') - - def test_take(self): - result = self.frame.take([1, 0, 2], axis=1) - expected = self.frame.reindex(columns=['B', 'A', 'C']) - tm.assert_sp_frame_equal(result, expected) - - def test_to_dense(self): - def _check(frame, orig): - dense_dm = frame.to_dense() - tm.assert_frame_equal(frame, dense_dm) - tm.assert_frame_equal(dense_dm, orig, check_dtype=False) - - self._check_all(_check) - - def test_stack_sparse_frame(self): - def _check(frame): - dense_frame = frame.to_dense() # noqa - - wp = Panel.from_dict({'foo': frame}) - from_dense_lp = wp.to_frame() - - from_sparse_lp = spf.stack_sparse_frame(frame) - - self.assert_numpy_array_equal(from_dense_lp.values, - from_sparse_lp.values) - - _check(self.frame) - _check(self.iframe) - - # for now - self.assertRaises(Exception, _check, self.zframe) - self.assertRaises(Exception, _check, self.fill_frame) - - def test_transpose(self): - - def _check(frame, orig): - transposed = frame.T - untransposed = transposed.T - tm.assert_sp_frame_equal(frame, untransposed) - - tm.assert_frame_equal(frame.T.to_dense(), orig.T) - tm.assert_frame_equal(frame.T.T.to_dense(), orig.T.T) - tm.assert_sp_frame_equal(frame, frame.T.T, exact_indices=False) - - self._check_all(_check) - - def test_shift(self): - - def _check(frame, orig): - shifted = frame.shift(0) - exp = orig.shift(0) - tm.assert_frame_equal(shifted.to_dense(), exp) - - shifted = frame.shift(1) - exp = orig.shift(1) - tm.assert_frame_equal(shifted, exp) - - shifted = frame.shift(-2) - exp = orig.shift(-2) - tm.assert_frame_equal(shifted, exp) - - shifted = frame.shift(2, freq='B') - exp = orig.shift(2, freq='B') - exp = exp.to_sparse(frame.default_fill_value) - tm.assert_frame_equal(shifted, exp) - - shifted = frame.shift(2, freq=BDay()) - exp = orig.shift(2, freq=BDay()) - exp = exp.to_sparse(frame.default_fill_value) - tm.assert_frame_equal(shifted, exp) - - self._check_all(_check) - - def test_count(self): - dense_result = self.frame.to_dense().count() - - result = self.frame.count() - tm.assert_series_equal(result, dense_result) - - result = self.frame.count(axis=None) - tm.assert_series_equal(result, dense_result) - - result = self.frame.count(axis=0) - tm.assert_series_equal(result, dense_result) - - result = self.frame.count(axis=1) - dense_result = self.frame.to_dense().count(axis=1) - - # win32 don't check dtype - tm.assert_series_equal(result, dense_result, check_dtype=False) - - def _check_all(self, check_func): - check_func(self.frame, self.orig) - check_func(self.iframe, self.iorig) - check_func(self.zframe, self.zorig) - check_func(self.fill_frame, self.fill_orig) - - def test_numpy_transpose(self): - sdf = SparseDataFrame([1, 2, 3], index=[1, 2, 3], columns=['a']) - result = np.transpose(np.transpose(sdf)) - tm.assert_sp_frame_equal(result, sdf) - - msg = "the 'axes' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.transpose, sdf, axes=1) - - def test_combine_first(self): - df = self.frame - - result = df[::2].combine_first(df) - result2 = df[::2].combine_first(df.to_dense()) - - expected = df[::2].to_dense().combine_first(df.to_dense()) - expected = expected.to_sparse(fill_value=df.default_fill_value) - - tm.assert_sp_frame_equal(result, result2) - tm.assert_sp_frame_equal(result, expected) - - def test_combine_add(self): - df = self.frame.to_dense() - df2 = df.copy() - df2['C'][:3] = np.nan - df['A'][:3] = 5.7 - - result = df.to_sparse().add(df2.to_sparse(), fill_value=0) - expected = df.add(df2, fill_value=0).to_sparse() - tm.assert_sp_frame_equal(result, expected) - - def test_isin(self): - sparse_df = DataFrame({'flag': [1., 0., 1.]}).to_sparse(fill_value=0.) - xp = sparse_df[sparse_df.flag == 1.] - rs = sparse_df[sparse_df.flag.isin([1.])] - tm.assert_frame_equal(xp, rs) - - def test_sparse_pow_issue(self): - # 2220 - df = SparseDataFrame({'A': [1.1, 3.3], 'B': [2.5, -3.9]}) - - # note : no error without nan - df = SparseDataFrame({'A': [nan, 0, 1]}) - - # note that 2 ** df works fine, also df ** 1 - result = 1 ** df - - r1 = result.take([0], 1)['A'] - r2 = result['A'] - - self.assertEqual(len(r2.sp_values), len(r1.sp_values)) - - def test_as_blocks(self): - df = SparseDataFrame({'A': [1.1, 3.3], 'B': [nan, -3.9]}, - dtype='float64') - - df_blocks = df.blocks - self.assertEqual(list(df_blocks.keys()), ['float64']) - tm.assert_frame_equal(df_blocks['float64'], df) - - def test_nan_columnname(self): - # GH 8822 - nan_colname = DataFrame(Series(1.0, index=[0]), columns=[nan]) - nan_colname_sparse = nan_colname.to_sparse() - self.assertTrue(np.isnan(nan_colname_sparse.columns[0])) - - def test_isnull(self): - # GH 8276 - df = pd.SparseDataFrame({'A': [np.nan, np.nan, 1, 2, np.nan], - 'B': [0, np.nan, np.nan, 2, np.nan]}) - - res = df.isnull() - exp = pd.SparseDataFrame({'A': [True, True, False, False, True], - 'B': [False, True, True, False, True]}, - default_fill_value=True) - exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) - - # if fill_value is not nan, True can be included in sp_values - df = pd.SparseDataFrame({'A': [0, 0, 1, 2, np.nan], - 'B': [0, np.nan, 0, 2, np.nan]}, - default_fill_value=0.) - res = df.isnull() - tm.assertIsInstance(res, pd.SparseDataFrame) - exp = pd.DataFrame({'A': [False, False, False, False, True], - 'B': [False, True, False, False, True]}) - tm.assert_frame_equal(res.to_dense(), exp) - - def test_isnotnull(self): - # GH 8276 - df = pd.SparseDataFrame({'A': [np.nan, np.nan, 1, 2, np.nan], - 'B': [0, np.nan, np.nan, 2, np.nan]}) - - res = df.isnotnull() - exp = pd.SparseDataFrame({'A': [False, False, True, True, False], - 'B': [True, False, False, True, False]}, - default_fill_value=False) - exp._default_fill_value = np.nan - tm.assert_sp_frame_equal(res, exp) - - # if fill_value is not nan, True can be included in sp_values - df = pd.SparseDataFrame({'A': [0, 0, 1, 2, np.nan], - 'B': [0, np.nan, 0, 2, np.nan]}, - default_fill_value=0.) - res = df.isnotnull() - tm.assertIsInstance(res, pd.SparseDataFrame) - exp = pd.DataFrame({'A': [True, True, True, True, False], - 'B': [True, False, True, True, False]}) - tm.assert_frame_equal(res.to_dense(), exp) - - -@pytest.mark.parametrize('index', [None, list('ab')]) # noqa: F811 -@pytest.mark.parametrize('columns', [None, list('cd')]) -@pytest.mark.parametrize('fill_value', [None, 0, np.nan]) -@pytest.mark.parametrize('dtype', [bool, int, float, np.uint16]) -def test_from_to_scipy(spmatrix, index, columns, fill_value, dtype): - # GH 4343 - tm.skip_if_no_package('scipy') - - # Make one ndarray and from it one sparse matrix, both to be used for - # constructing frames and comparing results - arr = np.eye(2, dtype=dtype) - try: - spm = spmatrix(arr) - assert spm.dtype == arr.dtype - except (TypeError, AssertionError): - # If conversion to sparse fails for this spmatrix type and arr.dtype, - # then the combination is not currently supported in NumPy, so we - # can just skip testing it thoroughly - return - - sdf = pd.SparseDataFrame(spm, index=index, columns=columns, - default_fill_value=fill_value) - - # Expected result construction is kind of tricky for all - # dtype-fill_value combinations; easiest to cast to something generic - # and except later on - rarr = arr.astype(object) - rarr[arr == 0] = np.nan - expected = pd.SparseDataFrame(rarr, index=index, columns=columns).fillna( - fill_value if fill_value is not None else np.nan) - - # Assert frame is as expected - sdf_obj = sdf.astype(object) - tm.assert_sp_frame_equal(sdf_obj, expected) - tm.assert_frame_equal(sdf_obj.to_dense(), expected.to_dense()) - - # Assert spmatrices equal - tm.assert_equal(dict(sdf.to_coo().todok()), dict(spm.todok())) - - # Ensure dtype is preserved if possible - was_upcast = ((fill_value is None or is_float(fill_value)) and - not is_object_dtype(dtype) and - not is_float_dtype(dtype)) - res_dtype = (bool if is_bool_dtype(dtype) else - float if was_upcast else - dtype) - tm.assert_contains_all(sdf.dtypes, {np.dtype(res_dtype)}) - tm.assert_equal(sdf.to_coo().dtype, res_dtype) - - # However, adding a str column results in an upcast to object - sdf['strings'] = np.arange(len(sdf)).astype(str) - tm.assert_equal(sdf.to_coo().dtype, np.object_) - - -@pytest.mark.parametrize('fill_value', [None, 0, np.nan]) # noqa: F811 -def test_from_to_scipy_object(spmatrix, fill_value): - # GH 4343 - dtype = object - columns = list('cd') - index = list('ab') - tm.skip_if_no_package('scipy', max_version='0.19.0') - - # Make one ndarray and from it one sparse matrix, both to be used for - # constructing frames and comparing results - arr = np.eye(2, dtype=dtype) - try: - spm = spmatrix(arr) - assert spm.dtype == arr.dtype - except (TypeError, AssertionError): - # If conversion to sparse fails for this spmatrix type and arr.dtype, - # then the combination is not currently supported in NumPy, so we - # can just skip testing it thoroughly - return - - sdf = pd.SparseDataFrame(spm, index=index, columns=columns, - default_fill_value=fill_value) - - # Expected result construction is kind of tricky for all - # dtype-fill_value combinations; easiest to cast to something generic - # and except later on - rarr = arr.astype(object) - rarr[arr == 0] = np.nan - expected = pd.SparseDataFrame(rarr, index=index, columns=columns).fillna( - fill_value if fill_value is not None else np.nan) - - # Assert frame is as expected - sdf_obj = sdf.astype(object) - tm.assert_sp_frame_equal(sdf_obj, expected) - tm.assert_frame_equal(sdf_obj.to_dense(), expected.to_dense()) - - # Assert spmatrices equal - tm.assert_equal(dict(sdf.to_coo().todok()), dict(spm.todok())) - - # Ensure dtype is preserved if possible - res_dtype = object - tm.assert_contains_all(sdf.dtypes, {np.dtype(res_dtype)}) - tm.assert_equal(sdf.to_coo().dtype, res_dtype) - - -class TestSparseDataFrameArithmetic(tm.TestCase): - - def test_numeric_op_scalar(self): - df = pd.DataFrame({'A': [nan, nan, 0, 1, ], - 'B': [0, 1, 2, nan], - 'C': [1., 2., 3., 4.], - 'D': [nan, nan, nan, nan]}) - sparse = df.to_sparse() - - tm.assert_sp_frame_equal(sparse + 1, (df + 1).to_sparse()) - - def test_comparison_op_scalar(self): - # GH 13001 - df = pd.DataFrame({'A': [nan, nan, 0, 1, ], - 'B': [0, 1, 2, nan], - 'C': [1., 2., 3., 4.], - 'D': [nan, nan, nan, nan]}) - sparse = df.to_sparse() - - # comparison changes internal repr, compare with dense - res = sparse > 1 - tm.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), df > 1) - - res = sparse != 0 - tm.assertIsInstance(res, pd.SparseDataFrame) - tm.assert_frame_equal(res.to_dense(), df != 0) - - -class TestSparseDataFrameAnalytics(tm.TestCase): - def setUp(self): - self.data = {'A': [nan, nan, nan, 0, 1, 2, 3, 4, 5, 6], - 'B': [0, 1, 2, nan, nan, nan, 3, 4, 5, 6], - 'C': np.arange(10, dtype=float), - 'D': [0, 1, 2, 3, 4, 5, nan, nan, nan, nan]} - - self.dates = bdate_range('1/1/2011', periods=10) - - self.frame = SparseDataFrame(self.data, index=self.dates) - - def test_cumsum(self): - expected = SparseDataFrame(self.frame.to_dense().cumsum()) - - result = self.frame.cumsum() - tm.assert_sp_frame_equal(result, expected) - - result = self.frame.cumsum(axis=None) - tm.assert_sp_frame_equal(result, expected) - - result = self.frame.cumsum(axis=0) - tm.assert_sp_frame_equal(result, expected) - - def test_numpy_cumsum(self): - result = np.cumsum(self.frame) - expected = SparseDataFrame(self.frame.to_dense().cumsum()) - tm.assert_sp_frame_equal(result, expected) - - msg = "the 'dtype' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - self.frame, dtype=np.int64) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.cumsum, - self.frame, out=result) - - def test_numpy_func_call(self): - # no exception should be raised even though - # numpy passes in 'axis=None' or `axis=-1' - funcs = ['sum', 'cumsum', 'var', - 'mean', 'prod', 'cumprod', - 'std', 'min', 'max'] - for func in funcs: - getattr(np, func)(self.frame) diff --git a/pandas/tests/sparse/test_groupby.py b/pandas/tests/sparse/test_groupby.py index 23bea94a2aef8..d0ff2a02c4046 100644 --- a/pandas/tests/sparse/test_groupby.py +++ b/pandas/tests/sparse/test_groupby.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- import numpy as np +import pytest + import pandas as pd import pandas.util.testing as tm -class TestSparseGroupBy(tm.TestCase): +class TestSparseGroupBy(object): - def setUp(self): + def setup_method(self, method): self.dense = pd.DataFrame({'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'], 'B': ['one', 'one', 'two', 'three', @@ -22,23 +24,47 @@ def test_first_last_nth(self): sparse_grouped = self.sparse.groupby('A') dense_grouped = self.dense.groupby('A') - tm.assert_frame_equal(sparse_grouped.first(), - dense_grouped.first()) - tm.assert_frame_equal(sparse_grouped.last(), - dense_grouped.last()) - tm.assert_frame_equal(sparse_grouped.nth(1), - dense_grouped.nth(1)) + sparse_grouped_first = sparse_grouped.first() + sparse_grouped_last = sparse_grouped.last() + sparse_grouped_nth = sparse_grouped.nth(1) + + dense_grouped_first = dense_grouped.first().to_sparse() + dense_grouped_last = dense_grouped.last().to_sparse() + dense_grouped_nth = dense_grouped.nth(1).to_sparse() + + # TODO: shouldn't these all be spares or not? + tm.assert_frame_equal(sparse_grouped_first, + dense_grouped_first) + tm.assert_frame_equal(sparse_grouped_last, + dense_grouped_last) + tm.assert_frame_equal(sparse_grouped_nth, + dense_grouped_nth) def test_aggfuncs(self): sparse_grouped = self.sparse.groupby('A') dense_grouped = self.dense.groupby('A') - tm.assert_frame_equal(sparse_grouped.mean(), - dense_grouped.mean()) + result = sparse_grouped.mean().to_sparse() + expected = dense_grouped.mean().to_sparse() + + tm.assert_frame_equal(result, expected) # ToDo: sparse sum includes str column # tm.assert_frame_equal(sparse_grouped.sum(), # dense_grouped.sum()) - tm.assert_frame_equal(sparse_grouped.count(), - dense_grouped.count()) + result = sparse_grouped.count().to_sparse() + expected = dense_grouped.count().to_sparse() + + tm.assert_frame_equal(result, expected) + + +@pytest.mark.parametrize("fill_value", [0, np.nan]) +def test_groupby_includes_fill_value(fill_value): + # https://github.com/pandas-dev/pandas/issues/5078 + df = pd.DataFrame({'a': [fill_value, 1, fill_value, fill_value], + 'b': [fill_value, 1, fill_value, fill_value]}) + sdf = df.to_sparse(fill_value=fill_value) + result = sdf.groupby('a').sum() + expected = df.groupby('a').sum().to_sparse(fill_value=fill_value) + tm.assert_frame_equal(result, expected, check_index_type=False) diff --git a/pandas/tests/sparse/test_indexing.py b/pandas/tests/sparse/test_indexing.py index 1a0782c0a3db9..6d8c6f13cd32b 100644 --- a/pandas/tests/sparse/test_indexing.py +++ b/pandas/tests/sparse/test_indexing.py @@ -1,14 +1,16 @@ # pylint: disable-msg=E1101,W0612 -import pytest # noqa import numpy as np +import pytest + import pandas as pd +from pandas.core.sparse.api import SparseDtype import pandas.util.testing as tm -class TestSparseSeriesIndexing(tm.TestCase): +class TestSparseSeriesIndexing(object): - def setUp(self): + def setup_method(self, method): self.orig = pd.Series([1, np.nan, np.nan, 3, np.nan]) self.sparse = self.orig.to_sparse() @@ -16,9 +18,9 @@ def test_getitem(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse[0], 1) - self.assertTrue(np.isnan(sparse[1])) - self.assertEqual(sparse[3], 3) + assert sparse[0] == 1 + assert np.isnan(sparse[1]) + assert sparse[3] == 3 result = sparse[[1, 3, 4]] exp = orig[[1, 3, 4]].to_sparse() @@ -53,23 +55,23 @@ def test_getitem_int_dtype(self): res = s[::2] exp = pd.SparseSeries([0, 2, 4, 6], index=[0, 2, 4, 6], name='xxx') tm.assert_sp_series_equal(res, exp) - self.assertEqual(res.dtype, np.int64) + assert res.dtype == SparseDtype(np.int64) s = pd.SparseSeries([0, 1, 2, 3, 4, 5, 6], fill_value=0, name='xxx') res = s[::2] exp = pd.SparseSeries([0, 2, 4, 6], index=[0, 2, 4, 6], fill_value=0, name='xxx') tm.assert_sp_series_equal(res, exp) - self.assertEqual(res.dtype, np.int64) + assert res.dtype == SparseDtype(np.int64) def test_getitem_fill_value(self): orig = pd.Series([1, np.nan, 0, 3, 0]) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse[0], 1) - self.assertTrue(np.isnan(sparse[1])) - self.assertEqual(sparse[2], 0) - self.assertEqual(sparse[3], 3) + assert sparse[0] == 1 + assert np.isnan(sparse[1]) + assert sparse[2] == 0 + assert sparse[3] == 3 result = sparse[[1, 3, 4]] exp = orig[[1, 3, 4]].to_sparse(fill_value=0) @@ -113,19 +115,19 @@ def test_loc(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse.loc[0], 1) - self.assertTrue(np.isnan(sparse.loc[1])) + assert sparse.loc[0] == 1 + assert np.isnan(sparse.loc[1]) result = sparse.loc[[1, 3, 4]] exp = orig.loc[[1, 3, 4]].to_sparse() tm.assert_sp_series_equal(result, exp) # exceeds the bounds - result = sparse.loc[[1, 3, 4, 5]] - exp = orig.loc[[1, 3, 4, 5]].to_sparse() + result = sparse.reindex([1, 3, 4, 5]) + exp = orig.reindex([1, 3, 4, 5]).to_sparse() tm.assert_sp_series_equal(result, exp) # padded with NaN - self.assertTrue(np.isnan(result[-1])) + assert np.isnan(result[-1]) # dense array result = sparse.loc[orig % 2 == 1] @@ -145,8 +147,8 @@ def test_loc_index(self): orig = pd.Series([1, np.nan, np.nan, 3, np.nan], index=list('ABCDE')) sparse = orig.to_sparse() - self.assertEqual(sparse.loc['A'], 1) - self.assertTrue(np.isnan(sparse.loc['B'])) + assert sparse.loc['A'] == 1 + assert np.isnan(sparse.loc['B']) result = sparse.loc[['A', 'C', 'D']] exp = orig.loc[['A', 'C', 'D']].to_sparse() @@ -170,8 +172,8 @@ def test_loc_index_fill_value(self): orig = pd.Series([1, np.nan, 0, 3, 0], index=list('ABCDE')) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse.loc['A'], 1) - self.assertTrue(np.isnan(sparse.loc['B'])) + assert sparse.loc['A'] == 1 + assert np.isnan(sparse.loc['B']) result = sparse.loc[['A', 'C', 'D']] exp = orig.loc[['A', 'C', 'D']].to_sparse(fill_value=0) @@ -209,8 +211,8 @@ def test_iloc(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse.iloc[3], 3) - self.assertTrue(np.isnan(sparse.iloc[2])) + assert sparse.iloc[3] == 3 + assert np.isnan(sparse.iloc[2]) result = sparse.iloc[[1, 3, 4]] exp = orig.iloc[[1, 3, 4]].to_sparse() @@ -220,16 +222,16 @@ def test_iloc(self): exp = orig.iloc[[1, -2, -4]].to_sparse() tm.assert_sp_series_equal(result, exp) - with tm.assertRaises(IndexError): + with pytest.raises(IndexError): sparse.iloc[[1, 3, 5]] def test_iloc_fill_value(self): orig = pd.Series([1, np.nan, 0, 3, 0]) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse.iloc[3], 3) - self.assertTrue(np.isnan(sparse.iloc[1])) - self.assertEqual(sparse.iloc[4], 0) + assert sparse.iloc[3] == 3 + assert np.isnan(sparse.iloc[1]) + assert sparse.iloc[4] == 0 result = sparse.iloc[[1, 3, 4]] exp = orig.iloc[[1, 3, 4]].to_sparse(fill_value=0) @@ -249,74 +251,74 @@ def test_iloc_slice_fill_value(self): def test_at(self): orig = pd.Series([1, np.nan, np.nan, 3, np.nan]) sparse = orig.to_sparse() - self.assertEqual(sparse.at[0], orig.at[0]) - self.assertTrue(np.isnan(sparse.at[1])) - self.assertTrue(np.isnan(sparse.at[2])) - self.assertEqual(sparse.at[3], orig.at[3]) - self.assertTrue(np.isnan(sparse.at[4])) + assert sparse.at[0] == orig.at[0] + assert np.isnan(sparse.at[1]) + assert np.isnan(sparse.at[2]) + assert sparse.at[3] == orig.at[3] + assert np.isnan(sparse.at[4]) orig = pd.Series([1, np.nan, np.nan, 3, np.nan], index=list('abcde')) sparse = orig.to_sparse() - self.assertEqual(sparse.at['a'], orig.at['a']) - self.assertTrue(np.isnan(sparse.at['b'])) - self.assertTrue(np.isnan(sparse.at['c'])) - self.assertEqual(sparse.at['d'], orig.at['d']) - self.assertTrue(np.isnan(sparse.at['e'])) + assert sparse.at['a'] == orig.at['a'] + assert np.isnan(sparse.at['b']) + assert np.isnan(sparse.at['c']) + assert sparse.at['d'] == orig.at['d'] + assert np.isnan(sparse.at['e']) def test_at_fill_value(self): orig = pd.Series([1, np.nan, 0, 3, 0], index=list('abcde')) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse.at['a'], orig.at['a']) - self.assertTrue(np.isnan(sparse.at['b'])) - self.assertEqual(sparse.at['c'], orig.at['c']) - self.assertEqual(sparse.at['d'], orig.at['d']) - self.assertEqual(sparse.at['e'], orig.at['e']) + assert sparse.at['a'] == orig.at['a'] + assert np.isnan(sparse.at['b']) + assert sparse.at['c'] == orig.at['c'] + assert sparse.at['d'] == orig.at['d'] + assert sparse.at['e'] == orig.at['e'] def test_iat(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse.iat[0], orig.iat[0]) - self.assertTrue(np.isnan(sparse.iat[1])) - self.assertTrue(np.isnan(sparse.iat[2])) - self.assertEqual(sparse.iat[3], orig.iat[3]) - self.assertTrue(np.isnan(sparse.iat[4])) + assert sparse.iat[0] == orig.iat[0] + assert np.isnan(sparse.iat[1]) + assert np.isnan(sparse.iat[2]) + assert sparse.iat[3] == orig.iat[3] + assert np.isnan(sparse.iat[4]) - self.assertTrue(np.isnan(sparse.iat[-1])) - self.assertEqual(sparse.iat[-5], orig.iat[-5]) + assert np.isnan(sparse.iat[-1]) + assert sparse.iat[-5] == orig.iat[-5] def test_iat_fill_value(self): orig = pd.Series([1, np.nan, 0, 3, 0]) sparse = orig.to_sparse() - self.assertEqual(sparse.iat[0], orig.iat[0]) - self.assertTrue(np.isnan(sparse.iat[1])) - self.assertEqual(sparse.iat[2], orig.iat[2]) - self.assertEqual(sparse.iat[3], orig.iat[3]) - self.assertEqual(sparse.iat[4], orig.iat[4]) + assert sparse.iat[0] == orig.iat[0] + assert np.isnan(sparse.iat[1]) + assert sparse.iat[2] == orig.iat[2] + assert sparse.iat[3] == orig.iat[3] + assert sparse.iat[4] == orig.iat[4] - self.assertEqual(sparse.iat[-1], orig.iat[-1]) - self.assertEqual(sparse.iat[-5], orig.iat[-5]) + assert sparse.iat[-1] == orig.iat[-1] + assert sparse.iat[-5] == orig.iat[-5] def test_get(self): s = pd.SparseSeries([1, np.nan, np.nan, 3, np.nan]) - self.assertEqual(s.get(0), 1) - self.assertTrue(np.isnan(s.get(1))) - self.assertIsNone(s.get(5)) + assert s.get(0) == 1 + assert np.isnan(s.get(1)) + assert s.get(5) is None s = pd.SparseSeries([1, np.nan, 0, 3, 0], index=list('ABCDE')) - self.assertEqual(s.get('A'), 1) - self.assertTrue(np.isnan(s.get('B'))) - self.assertEqual(s.get('C'), 0) - self.assertIsNone(s.get('XX')) + assert s.get('A') == 1 + assert np.isnan(s.get('B')) + assert s.get('C') == 0 + assert s.get('XX') is None s = pd.SparseSeries([1, np.nan, 0, 3, 0], index=list('ABCDE'), fill_value=0) - self.assertEqual(s.get('A'), 1) - self.assertTrue(np.isnan(s.get('B'))) - self.assertEqual(s.get('C'), 0) - self.assertIsNone(s.get('XX')) + assert s.get('A') == 1 + assert np.isnan(s.get('B')) + assert s.get('C') == 0 + assert s.get('XX') is None def test_take(self): orig = pd.Series([1, np.nan, np.nan, 3, np.nan], @@ -393,6 +395,10 @@ def test_fill_value_reindex(self): index=list('ABCDE')) sparse = orig.to_sparse(fill_value=0) + def test_fill_value_reindex_coerces_float_int(self): + orig = pd.Series([1, np.nan, 0, 3, 0], index=list('ABCDE')) + sparse = orig.to_sparse(fill_value=0) + res = sparse.reindex(['A', 'E', 'C', 'D']) exp = orig.reindex(['A', 'E', 'C', 'D']).to_sparse(fill_value=0) tm.assert_sp_series_equal(res, exp) @@ -414,39 +420,45 @@ def test_reindex_nearest(self): expected = pd.Series([0, 1, np.nan, 2], target).to_sparse() tm.assert_sp_series_equal(expected, actual) - def tests_indexing_with_sparse(self): - # GH 13985 + actual = s.reindex(target, method='nearest', + tolerance=[0.3, 0.01, 0.4, 3]) + expected = pd.Series([0, np.nan, np.nan, 2], target).to_sparse() + tm.assert_sp_series_equal(expected, actual) - for kind in ['integer', 'block']: - for fill in [True, False, np.nan]: - arr = pd.SparseArray([1, 2, 3], kind=kind) - indexer = pd.SparseArray([True, False, True], fill_value=fill, - dtype=bool) + @pytest.mark.parametrize("kind", ["integer", "block"]) + @pytest.mark.parametrize("fill", [True, False, np.nan]) + def tests_indexing_with_sparse(self, kind, fill): + # see gh-13985 + arr = pd.SparseArray([1, 2, 3], kind=kind) + indexer = pd.SparseArray([True, False, True], + fill_value=fill, + dtype=bool) - tm.assert_sp_array_equal(pd.SparseArray([1, 3], kind=kind), - arr[indexer]) + expected = arr[indexer] + result = pd.SparseArray([1, 3], kind=kind) + tm.assert_sp_array_equal(result, expected) - s = pd.SparseSeries(arr, index=['a', 'b', 'c'], - dtype=np.float64) - exp = pd.SparseSeries([1, 3], index=['a', 'c'], - dtype=np.float64, kind=kind) - tm.assert_sp_series_equal(s[indexer], exp) - tm.assert_sp_series_equal(s.loc[indexer], exp) - tm.assert_sp_series_equal(s.iloc[indexer], exp) + s = pd.SparseSeries(arr, index=["a", "b", "c"], dtype=np.float64) + expected = pd.SparseSeries([1, 3], index=["a", "c"], kind=kind, + dtype=SparseDtype(np.float64, s.fill_value)) - indexer = pd.SparseSeries(indexer, index=['a', 'b', 'c']) - tm.assert_sp_series_equal(s[indexer], exp) - tm.assert_sp_series_equal(s.loc[indexer], exp) + tm.assert_sp_series_equal(s[indexer], expected) + tm.assert_sp_series_equal(s.loc[indexer], expected) + tm.assert_sp_series_equal(s.iloc[indexer], expected) - msg = ("iLocation based boolean indexing cannot use an " - "indexable as a mask") - with tm.assertRaisesRegexp(ValueError, msg): - s.iloc[indexer] + indexer = pd.SparseSeries(indexer, index=["a", "b", "c"]) + tm.assert_sp_series_equal(s[indexer], expected) + tm.assert_sp_series_equal(s.loc[indexer], expected) + + msg = ("iLocation based boolean indexing cannot " + "use an indexable as a mask") + with pytest.raises(ValueError, match=msg): + s.iloc[indexer] class TestSparseSeriesMultiIndexing(TestSparseSeriesIndexing): - def setUp(self): + def setup_method(self, method): # Mi with duplicated values idx = pd.MultiIndex.from_tuples([('A', 0), ('A', 1), ('B', 0), ('C', 0), ('C', 1)]) @@ -457,9 +469,9 @@ def test_getitem_multi(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse[0], orig[0]) - self.assertTrue(np.isnan(sparse[1])) - self.assertEqual(sparse[3], orig[3]) + assert sparse[0] == orig[0] + assert np.isnan(sparse[1]) + assert sparse[3] == orig[3] tm.assert_sp_series_equal(sparse['A'], orig['A'].to_sparse()) tm.assert_sp_series_equal(sparse['B'], orig['B'].to_sparse()) @@ -486,9 +498,9 @@ def test_getitem_multi_tuple(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse['C', 0], orig['C', 0]) - self.assertTrue(np.isnan(sparse['A', 1])) - self.assertTrue(np.isnan(sparse['B', 0])) + assert sparse['C', 0] == orig['C', 0] + assert np.isnan(sparse['A', 1]) + assert np.isnan(sparse['B', 0]) def test_getitems_slice_multi(self): orig = self.orig @@ -544,9 +556,9 @@ def test_loc_multi_tuple(self): orig = self.orig sparse = self.sparse - self.assertEqual(sparse.loc['C', 0], orig.loc['C', 0]) - self.assertTrue(np.isnan(sparse.loc['A', 1])) - self.assertTrue(np.isnan(sparse.loc['B', 0])) + assert sparse.loc['C', 0] == orig.loc['C', 0] + assert np.isnan(sparse.loc['A', 1]) + assert np.isnan(sparse.loc['B', 0]) def test_loc_slice(self): orig = self.orig @@ -578,7 +590,7 @@ def test_reindex(self): exp = orig.reindex(['A'], level=0).to_sparse() tm.assert_sp_series_equal(res, exp) - with tm.assertRaises(TypeError): + with pytest.raises(TypeError): # Incomplete keys are not accepted for reindexing: sparse.reindex(['A', 'C']) @@ -586,10 +598,10 @@ def test_reindex(self): res = sparse.reindex(sparse.index, copy=True) exp = orig.reindex(orig.index, copy=True).to_sparse() tm.assert_sp_series_equal(res, exp) - self.assertIsNot(sparse, res) + assert sparse is not res -class TestSparseDataFrameIndexing(tm.TestCase): +class TestSparseDataFrameIndexing(object): def test_getitem(self): orig = pd.DataFrame([[1, np.nan, np.nan], @@ -618,6 +630,10 @@ def test_getitem_fill_value(self): columns=list('xyz')) sparse = orig.to_sparse(fill_value=0) + result = sparse[['z']] + expected = orig[['z']].to_sparse(fill_value=0) + tm.assert_sp_frame_equal(result, expected, check_fill_value=False) + tm.assert_sp_series_equal(sparse['y'], orig['y'].to_sparse(fill_value=0)) @@ -645,16 +661,21 @@ def test_loc(self): columns=list('xyz')) sparse = orig.to_sparse() - self.assertEqual(sparse.loc[0, 'x'], 1) - self.assertTrue(np.isnan(sparse.loc[1, 'z'])) - self.assertEqual(sparse.loc[2, 'z'], 4) - - tm.assert_sp_series_equal(sparse.loc[0], orig.loc[0].to_sparse()) - tm.assert_sp_series_equal(sparse.loc[1], orig.loc[1].to_sparse()) + assert sparse.loc[0, 'x'] == 1 + assert np.isnan(sparse.loc[1, 'z']) + assert sparse.loc[2, 'z'] == 4 + + # have to specify `kind='integer'`, since we construct a + # new SparseArray here, and the default sparse type is + # integer there, but block in SparseSeries + tm.assert_sp_series_equal(sparse.loc[0], + orig.loc[0].to_sparse(kind='integer')) + tm.assert_sp_series_equal(sparse.loc[1], + orig.loc[1].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc[2, :], - orig.loc[2, :].to_sparse()) + orig.loc[2, :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc[2, :], - orig.loc[2, :].to_sparse()) + orig.loc[2, :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc[:, 'y'], orig.loc[:, 'y'].to_sparse()) tm.assert_sp_series_equal(sparse.loc[:, 'y'], @@ -677,8 +698,8 @@ def test_loc(self): tm.assert_sp_frame_equal(result, exp) # exceeds the bounds - result = sparse.loc[[1, 3, 4, 5]] - exp = orig.loc[[1, 3, 4, 5]].to_sparse() + result = sparse.reindex([1, 3, 4, 5]) + exp = orig.reindex([1, 3, 4, 5]).to_sparse() tm.assert_sp_frame_equal(result, exp) # dense array @@ -702,16 +723,18 @@ def test_loc_index(self): index=list('abc'), columns=list('xyz')) sparse = orig.to_sparse() - self.assertEqual(sparse.loc['a', 'x'], 1) - self.assertTrue(np.isnan(sparse.loc['b', 'z'])) - self.assertEqual(sparse.loc['c', 'z'], 4) + assert sparse.loc['a', 'x'] == 1 + assert np.isnan(sparse.loc['b', 'z']) + assert sparse.loc['c', 'z'] == 4 - tm.assert_sp_series_equal(sparse.loc['a'], orig.loc['a'].to_sparse()) - tm.assert_sp_series_equal(sparse.loc['b'], orig.loc['b'].to_sparse()) + tm.assert_sp_series_equal(sparse.loc['a'], + orig.loc['a'].to_sparse(kind='integer')) + tm.assert_sp_series_equal(sparse.loc['b'], + orig.loc['b'].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc['b', :], - orig.loc['b', :].to_sparse()) + orig.loc['b', :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc['b', :], - orig.loc['b', :].to_sparse()) + orig.loc['b', :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.loc[:, 'z'], orig.loc[:, 'z'].to_sparse()) @@ -762,15 +785,17 @@ def test_iloc(self): [np.nan, np.nan, 4]]) sparse = orig.to_sparse() - self.assertEqual(sparse.iloc[1, 1], 3) - self.assertTrue(np.isnan(sparse.iloc[2, 0])) + assert sparse.iloc[1, 1] == 3 + assert np.isnan(sparse.iloc[2, 0]) - tm.assert_sp_series_equal(sparse.iloc[0], orig.loc[0].to_sparse()) - tm.assert_sp_series_equal(sparse.iloc[1], orig.loc[1].to_sparse()) + tm.assert_sp_series_equal(sparse.iloc[0], + orig.loc[0].to_sparse(kind='integer')) + tm.assert_sp_series_equal(sparse.iloc[1], + orig.loc[1].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.iloc[2, :], - orig.iloc[2, :].to_sparse()) + orig.iloc[2, :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.iloc[2, :], - orig.iloc[2, :].to_sparse()) + orig.iloc[2, :].to_sparse(kind='integer')) tm.assert_sp_series_equal(sparse.iloc[:, 1], orig.iloc[:, 1].to_sparse()) tm.assert_sp_series_equal(sparse.iloc[:, 1], @@ -792,7 +817,7 @@ def test_iloc(self): exp = orig.iloc[[2], [1, 0]].to_sparse() tm.assert_sp_frame_equal(result, exp) - with tm.assertRaises(IndexError): + with pytest.raises(IndexError): sparse.iloc[[1, 3, 5]] def test_iloc_slice(self): @@ -810,10 +835,10 @@ def test_at(self): [0, np.nan, 5]], index=list('ABCD'), columns=list('xyz')) sparse = orig.to_sparse() - self.assertEqual(sparse.at['A', 'x'], orig.at['A', 'x']) - self.assertTrue(np.isnan(sparse.at['B', 'z'])) - self.assertTrue(np.isnan(sparse.at['C', 'y'])) - self.assertEqual(sparse.at['D', 'x'], orig.at['D', 'x']) + assert sparse.at['A', 'x'] == orig.at['A', 'x'] + assert np.isnan(sparse.at['B', 'z']) + assert np.isnan(sparse.at['C', 'y']) + assert sparse.at['D', 'x'] == orig.at['D', 'x'] def test_at_fill_value(self): orig = pd.DataFrame([[1, np.nan, 0], @@ -822,10 +847,10 @@ def test_at_fill_value(self): [0, np.nan, 5]], index=list('ABCD'), columns=list('xyz')) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse.at['A', 'x'], orig.at['A', 'x']) - self.assertTrue(np.isnan(sparse.at['B', 'z'])) - self.assertTrue(np.isnan(sparse.at['C', 'y'])) - self.assertEqual(sparse.at['D', 'x'], orig.at['D', 'x']) + assert sparse.at['A', 'x'] == orig.at['A', 'x'] + assert np.isnan(sparse.at['B', 'z']) + assert np.isnan(sparse.at['C', 'y']) + assert sparse.at['D', 'x'] == orig.at['D', 'x'] def test_iat(self): orig = pd.DataFrame([[1, np.nan, 0], @@ -834,13 +859,13 @@ def test_iat(self): [0, np.nan, 5]], index=list('ABCD'), columns=list('xyz')) sparse = orig.to_sparse() - self.assertEqual(sparse.iat[0, 0], orig.iat[0, 0]) - self.assertTrue(np.isnan(sparse.iat[1, 2])) - self.assertTrue(np.isnan(sparse.iat[2, 1])) - self.assertEqual(sparse.iat[2, 0], orig.iat[2, 0]) + assert sparse.iat[0, 0] == orig.iat[0, 0] + assert np.isnan(sparse.iat[1, 2]) + assert np.isnan(sparse.iat[2, 1]) + assert sparse.iat[2, 0] == orig.iat[2, 0] - self.assertTrue(np.isnan(sparse.iat[-1, -2])) - self.assertEqual(sparse.iat[-1, -1], orig.iat[-1, -1]) + assert np.isnan(sparse.iat[-1, -2]) + assert sparse.iat[-1, -1] == orig.iat[-1, -1] def test_iat_fill_value(self): orig = pd.DataFrame([[1, np.nan, 0], @@ -849,13 +874,13 @@ def test_iat_fill_value(self): [0, np.nan, 5]], index=list('ABCD'), columns=list('xyz')) sparse = orig.to_sparse(fill_value=0) - self.assertEqual(sparse.iat[0, 0], orig.iat[0, 0]) - self.assertTrue(np.isnan(sparse.iat[1, 2])) - self.assertTrue(np.isnan(sparse.iat[2, 1])) - self.assertEqual(sparse.iat[2, 0], orig.iat[2, 0]) + assert sparse.iat[0, 0] == orig.iat[0, 0] + assert np.isnan(sparse.iat[1, 2]) + assert np.isnan(sparse.iat[2, 1]) + assert sparse.iat[2, 0] == orig.iat[2, 0] - self.assertTrue(np.isnan(sparse.iat[-1, -2])) - self.assertEqual(sparse.iat[-1, -1], orig.iat[-1, -1]) + assert np.isnan(sparse.iat[-1, -2]) + assert sparse.iat[-1, -1] == orig.iat[-1, -1] def test_take(self): orig = pd.DataFrame([[1, np.nan, 0], @@ -944,7 +969,8 @@ def test_reindex_fill_value(self): [0, 0, 0], [0, 0, 0], [0, 0, 0]], - index=list('ABCD'), columns=list('xyz')) + index=list('ABCD'), columns=list('xyz'), + dtype=np.int) sparse = orig.to_sparse(fill_value=0) res = sparse.reindex(['A', 'C', 'B']) @@ -952,9 +978,9 @@ def test_reindex_fill_value(self): tm.assert_sp_frame_equal(res, exp) -class TestMultitype(tm.TestCase): +class TestMultitype(object): - def setUp(self): + def setup_method(self, method): self.cols = ['string', 'int', 'float', 'object'] self.string_series = pd.SparseSeries(['a', 'b', 'c']) @@ -972,7 +998,7 @@ def setUp(self): def test_frame_basic_dtypes(self): for _, row in self.sdf.iterrows(): - self.assertEqual(row.dtype, object) + assert row.dtype == SparseDtype(object) tm.assert_sp_series_equal(self.sdf['string'], self.string_series, check_names=False) tm.assert_sp_series_equal(self.sdf['int'], self.int_series, @@ -1014,13 +1040,14 @@ def test_frame_indexing_multiple(self): def test_series_indexing_single(self): for i, idx in enumerate(self.cols): - self.assertEqual(self.ss.iloc[i], self.ss[idx]) - self.assertEqual(type(self.ss.iloc[i]), - type(self.ss[idx])) - self.assertEqual(self.ss['string'], 'a') - self.assertEqual(self.ss['int'], 1) - self.assertEqual(self.ss['float'], 1.1) - self.assertEqual(self.ss['object'], []) + assert self.ss.iloc[i] == self.ss[idx] + tm.assert_class_equal(self.ss.iloc[i], self.ss[idx], + obj="series index") + + assert self.ss['string'] == 'a' + assert self.ss['int'] == 1 + assert self.ss['float'] == 1.1 + assert self.ss['object'] == [] def test_series_indexing_multiple(self): tm.assert_sp_series_equal(self.ss.loc[['string', 'int']], diff --git a/pandas/tests/sparse/test_list.py b/pandas/tests/sparse/test_list.py deleted file mode 100644 index 8511cd5997368..0000000000000 --- a/pandas/tests/sparse/test_list.py +++ /dev/null @@ -1,112 +0,0 @@ -from pandas.compat import range -import unittest - -from numpy import nan -import numpy as np - -from pandas.sparse.api import SparseList, SparseArray -import pandas.util.testing as tm - - -class TestSparseList(unittest.TestCase): - - def setUp(self): - self.na_data = np.array([nan, nan, 1, 2, 3, nan, 4, 5, nan, 6]) - self.zero_data = np.array([0, 0, 1, 2, 3, 0, 4, 5, 0, 6]) - - def test_deprecation(self): - # see gh-13784 - with tm.assert_produces_warning(FutureWarning): - SparseList() - - def test_constructor(self): - with tm.assert_produces_warning(FutureWarning): - lst1 = SparseList(self.na_data[:5]) - with tm.assert_produces_warning(FutureWarning): - exp = SparseList() - - exp.append(self.na_data[:5]) - tm.assert_sp_list_equal(lst1, exp) - - def test_len(self): - with tm.assert_produces_warning(FutureWarning): - arr = self.na_data - splist = SparseList() - splist.append(arr[:5]) - self.assertEqual(len(splist), 5) - splist.append(arr[5]) - self.assertEqual(len(splist), 6) - splist.append(arr[6:]) - self.assertEqual(len(splist), 10) - - def test_append_na(self): - with tm.assert_produces_warning(FutureWarning): - arr = self.na_data - splist = SparseList() - splist.append(arr[:5]) - splist.append(arr[5]) - splist.append(arr[6:]) - - sparr = splist.to_array() - tm.assert_sp_array_equal(sparr, SparseArray(arr)) - - def test_append_zero(self): - with tm.assert_produces_warning(FutureWarning): - arr = self.zero_data - splist = SparseList(fill_value=0) - splist.append(arr[:5]) - splist.append(arr[5]) - splist.append(arr[6:]) - - # list always produces int64, but SA constructor - # is platform dtype aware - sparr = splist.to_array() - exp = SparseArray(arr, fill_value=0) - tm.assert_sp_array_equal(sparr, exp, check_dtype=False) - - def test_consolidate(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - arr = self.na_data - exp_sparr = SparseArray(arr) - - splist = SparseList() - splist.append(arr[:5]) - splist.append(arr[5]) - splist.append(arr[6:]) - - consol = splist.consolidate(inplace=False) - self.assertEqual(consol.nchunks, 1) - self.assertEqual(splist.nchunks, 3) - tm.assert_sp_array_equal(consol.to_array(), exp_sparr) - - splist.consolidate() - self.assertEqual(splist.nchunks, 1) - tm.assert_sp_array_equal(splist.to_array(), exp_sparr) - - def test_copy(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - arr = self.na_data - exp_sparr = SparseArray(arr) - - splist = SparseList() - splist.append(arr[:5]) - splist.append(arr[5]) - - cp = splist.copy() - cp.append(arr[6:]) - self.assertEqual(splist.nchunks, 2) - tm.assert_sp_array_equal(cp.to_array(), exp_sparr) - - def test_getitem(self): - with tm.assert_produces_warning(FutureWarning): - arr = self.na_data - splist = SparseList() - splist.append(arr[:5]) - splist.append(arr[5]) - splist.append(arr[6:]) - - for i in range(len(arr)): - tm.assert_almost_equal(splist[i], arr[i]) - tm.assert_almost_equal(splist[-i], arr[-i]) diff --git a/pandas/tests/sparse/test_pivot.py b/pandas/tests/sparse/test_pivot.py index 4ff9f20093c67..af7de43ec0f8a 100644 --- a/pandas/tests/sparse/test_pivot.py +++ b/pandas/tests/sparse/test_pivot.py @@ -1,11 +1,12 @@ import numpy as np + import pandas as pd import pandas.util.testing as tm -class TestPivotTable(tm.TestCase): +class TestPivotTable(object): - def setUp(self): + def setup_method(self, method): self.dense = pd.DataFrame({'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'], 'B': ['one', 'one', 'two', 'three', @@ -47,4 +48,5 @@ def test_pivot_table_multi(self): values=['D', 'E']) res_dense = pd.pivot_table(self.dense, index='A', columns='B', values=['D', 'E']) + res_dense = res_dense.apply(lambda x: x.astype("Sparse[float64]")) tm.assert_frame_equal(res_sparse, res_dense) diff --git a/pandas/tests/sparse/test_reshape.py b/pandas/tests/sparse/test_reshape.py new file mode 100644 index 0000000000000..6830e40ce6533 --- /dev/null +++ b/pandas/tests/sparse/test_reshape.py @@ -0,0 +1,42 @@ +import numpy as np +import pytest + +import pandas as pd +import pandas.util.testing as tm + + +@pytest.fixture +def sparse_df(): + return pd.SparseDataFrame({0: {0: 1}, 1: {1: 1}, 2: {2: 1}}) # eye + + +@pytest.fixture +def multi_index3(): + return pd.MultiIndex.from_tuples([(0, 0), (1, 1), (2, 2)]) + + +def test_sparse_frame_stack(sparse_df, multi_index3): + ss = sparse_df.stack() + expected = pd.SparseSeries(np.ones(3), index=multi_index3) + tm.assert_sp_series_equal(ss, expected) + + +def test_sparse_frame_unstack(sparse_df): + mi = pd.MultiIndex.from_tuples([(0, 0), (1, 0), (1, 2)]) + sparse_df.index = mi + arr = np.array([[1, np.nan, np.nan], + [np.nan, 1, np.nan], + [np.nan, np.nan, 1]]) + unstacked_df = pd.DataFrame(arr, index=mi).unstack() + unstacked_sdf = sparse_df.unstack() + + tm.assert_numpy_array_equal(unstacked_df.values, unstacked_sdf.values) + + +def test_sparse_series_unstack(sparse_df, multi_index3): + frame = pd.SparseSeries(np.ones(3), index=multi_index3).unstack() + + arr = np.array([1, np.nan, np.nan]) + arrays = {i: pd.SparseArray(np.roll(arr, i)) for i in range(3)} + expected = pd.DataFrame(arrays) + tm.assert_frame_equal(frame, expected) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index ac3a42c3cf122..3f75c508d22f9 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -1,26 +1,34 @@ # -*- coding: utf-8 -*- -from pandas.compat import range -import numpy as np -from numpy.random import RandomState -from numpy import nan from datetime import datetime from itertools import permutations -from pandas import Series, Categorical, CategoricalIndex, Index -import pandas as pd +import struct -from pandas import compat -from pandas._libs import (groupby as libgroupby, algos as libalgos, - hashtable) -from pandas._libs.hashtable import unique_label_indices -from pandas.compat import lrange +import numpy as np +from numpy import nan +from numpy.random import RandomState +import pytest + +from pandas._libs import ( + algos as libalgos, groupby as libgroupby, hashtable as ht) +from pandas.compat import PY2, lrange, range +from pandas.compat.numpy import np_array_datetime64_compat +import pandas.util._test_decorators as td + +from pandas.core.dtypes.dtypes import CategoricalDtype as CDT + +import pandas as pd +from pandas import ( + Categorical, CategoricalIndex, DatetimeIndex, Index, IntervalIndex, Series, + Timestamp, compat) import pandas.core.algorithms as algos +from pandas.core.arrays import DatetimeArray +import pandas.core.common as com import pandas.util.testing as tm -from pandas.compat.numpy import np_array_datetime64_compat from pandas.util.testing import assert_almost_equal -class TestMatch(tm.TestCase): +class TestMatch(object): def test_ints(self): values = np.array([0, 2, 1]) @@ -28,16 +36,16 @@ def test_ints(self): result = algos.match(to_match, values) expected = np.array([0, 2, 1, 1, 0, 2, -1, 0], dtype=np.int64) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = Series(algos.match(to_match, values, np.nan)) expected = Series(np.array([0, 2, 1, 1, 0, 2, np.nan, 0])) tm.assert_series_equal(result, expected) - s = pd.Series(np.arange(5), dtype=np.float32) + s = Series(np.arange(5), dtype=np.float32) result = algos.match(s, [2, 4]) expected = np.array([-1, -1, 0, -1, 1], dtype=np.int64) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = Series(algos.match(s, [2, 4], np.nan)) expected = Series(np.array([np.nan, np.nan, 0, np.nan, 1])) @@ -49,140 +57,54 @@ def test_strings(self): result = algos.match(to_match, values) expected = np.array([1, 0, -1, 0, 1, 2, -1], dtype=np.int64) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = Series(algos.match(to_match, values, np.nan)) expected = Series(np.array([1, 0, np.nan, 0, 1, 2, np.nan])) tm.assert_series_equal(result, expected) -class TestSafeSort(tm.TestCase): - - def test_basic_sort(self): - values = [3, 1, 2, 0, 4] - result = algos.safe_sort(values) - expected = np.array([0, 1, 2, 3, 4]) - tm.assert_numpy_array_equal(result, expected) - - values = list("baaacb") - result = algos.safe_sort(values) - expected = np.array(list("aaabbc")) - tm.assert_numpy_array_equal(result, expected) - - values = [] - result = algos.safe_sort(values) - expected = np.array([]) - tm.assert_numpy_array_equal(result, expected) - - def test_labels(self): - values = [3, 1, 2, 0, 4] - expected = np.array([0, 1, 2, 3, 4]) - - labels = [0, 1, 1, 2, 3, 0, -1, 4] - result, result_labels = algos.safe_sort(values, labels) - expected_labels = np.array([3, 1, 1, 2, 0, 3, -1, 4], dtype=np.intp) - tm.assert_numpy_array_equal(result, expected) - tm.assert_numpy_array_equal(result_labels, expected_labels) - - # na_sentinel - labels = [0, 1, 1, 2, 3, 0, 99, 4] - result, result_labels = algos.safe_sort(values, labels, - na_sentinel=99) - expected_labels = np.array([3, 1, 1, 2, 0, 3, 99, 4], dtype=np.intp) - tm.assert_numpy_array_equal(result, expected) - tm.assert_numpy_array_equal(result_labels, expected_labels) - - # out of bound indices - labels = [0, 101, 102, 2, 3, 0, 99, 4] - result, result_labels = algos.safe_sort(values, labels) - expected_labels = np.array([3, -1, -1, 2, 0, 3, -1, 4], dtype=np.intp) - tm.assert_numpy_array_equal(result, expected) - tm.assert_numpy_array_equal(result_labels, expected_labels) - - labels = [] - result, result_labels = algos.safe_sort(values, labels) - expected_labels = np.array([], dtype=np.intp) - tm.assert_numpy_array_equal(result, expected) - tm.assert_numpy_array_equal(result_labels, expected_labels) - - def test_mixed_integer(self): - values = np.array(['b', 1, 0, 'a', 0, 'b'], dtype=object) - result = algos.safe_sort(values) - expected = np.array([0, 0, 1, 'a', 'b', 'b'], dtype=object) - tm.assert_numpy_array_equal(result, expected) - - values = np.array(['b', 1, 0, 'a'], dtype=object) - labels = [0, 1, 2, 3, 0, -1, 1] - result, result_labels = algos.safe_sort(values, labels) - expected = np.array([0, 1, 'a', 'b'], dtype=object) - expected_labels = np.array([3, 1, 0, 2, 3, -1, 1], dtype=np.intp) - tm.assert_numpy_array_equal(result, expected) - tm.assert_numpy_array_equal(result_labels, expected_labels) - - def test_unsortable(self): - # GH 13714 - arr = np.array([1, 2, datetime.now(), 0, 3], dtype=object) - if compat.PY2 and not pd._np_version_under1p10: - # RuntimeWarning: tp_compare didn't return -1 or -2 for exception - with tm.assert_produces_warning(RuntimeWarning): - tm.assertRaises(TypeError, algos.safe_sort, arr) - else: - tm.assertRaises(TypeError, algos.safe_sort, arr) - - def test_exceptions(self): - with tm.assertRaisesRegexp(TypeError, - "Only list-like objects are allowed"): - algos.safe_sort(values=1) - - with tm.assertRaisesRegexp(TypeError, - "Only list-like objects or None"): - algos.safe_sort(values=[0, 1, 2], labels=1) - - with tm.assertRaisesRegexp(ValueError, "values should be unique"): - algos.safe_sort(values=[0, 1, 2, 1], labels=[0, 1]) - - -class TestFactorize(tm.TestCase): +class TestFactorize(object): def test_basic(self): labels, uniques = algos.factorize(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) - self.assert_numpy_array_equal( + tm.assert_numpy_array_equal( uniques, np.array(['a', 'b', 'c'], dtype=object)) labels, uniques = algos.factorize(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'], sort=True) exp = np.array([0, 1, 1, 0, 0, 2, 2, 2], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) + tm.assert_numpy_array_equal(labels, exp) exp = np.array(['a', 'b', 'c'], dtype=object) - self.assert_numpy_array_equal(uniques, exp) + tm.assert_numpy_array_equal(uniques, exp) labels, uniques = algos.factorize(list(reversed(range(5)))) exp = np.array([0, 1, 2, 3, 4], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) + tm.assert_numpy_array_equal(labels, exp) exp = np.array([4, 3, 2, 1, 0], dtype=np.int64) - self.assert_numpy_array_equal(uniques, exp) + tm.assert_numpy_array_equal(uniques, exp) labels, uniques = algos.factorize(list(reversed(range(5))), sort=True) exp = np.array([4, 3, 2, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) + tm.assert_numpy_array_equal(labels, exp) exp = np.array([0, 1, 2, 3, 4], dtype=np.int64) - self.assert_numpy_array_equal(uniques, exp) + tm.assert_numpy_array_equal(uniques, exp) labels, uniques = algos.factorize(list(reversed(np.arange(5.)))) exp = np.array([0, 1, 2, 3, 4], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) + tm.assert_numpy_array_equal(labels, exp) exp = np.array([4., 3., 2., 1., 0.], dtype=np.float64) - self.assert_numpy_array_equal(uniques, exp) + tm.assert_numpy_array_equal(uniques, exp) labels, uniques = algos.factorize(list(reversed(np.arange(5.))), sort=True) exp = np.array([4, 3, 2, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) + tm.assert_numpy_array_equal(labels, exp) exp = np.array([0., 1., 2., 3., 4.], dtype=np.float64) - self.assert_numpy_array_equal(uniques, exp) + tm.assert_numpy_array_equal(uniques, exp) def test_mixed(self): @@ -191,34 +113,34 @@ def test_mixed(self): labels, uniques = algos.factorize(x) exp = np.array([0, 0, -1, 1, 2, 3], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - exp = pd.Index(['A', 'B', 3.14, np.inf]) + tm.assert_numpy_array_equal(labels, exp) + exp = Index(['A', 'B', 3.14, np.inf]) tm.assert_index_equal(uniques, exp) labels, uniques = algos.factorize(x, sort=True) exp = np.array([2, 2, -1, 3, 0, 1], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - exp = pd.Index([3.14, np.inf, 'A', 'B']) + tm.assert_numpy_array_equal(labels, exp) + exp = Index([3.14, np.inf, 'A', 'B']) tm.assert_index_equal(uniques, exp) def test_datelike(self): # M8 - v1 = pd.Timestamp('20130101 09:00:00.00004') - v2 = pd.Timestamp('20130101') + v1 = Timestamp('20130101 09:00:00.00004') + v2 = Timestamp('20130101') x = Series([v1, v1, v1, v2, v2, v1]) labels, uniques = algos.factorize(x) exp = np.array([0, 0, 0, 1, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - exp = pd.DatetimeIndex([v1, v2]) - self.assert_index_equal(uniques, exp) + tm.assert_numpy_array_equal(labels, exp) + exp = DatetimeIndex([v1, v2]) + tm.assert_index_equal(uniques, exp) labels, uniques = algos.factorize(x, sort=True) exp = np.array([1, 1, 1, 0, 0, 1], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - exp = pd.DatetimeIndex([v2, v1]) - self.assert_index_equal(uniques, exp) + tm.assert_numpy_array_equal(labels, exp) + exp = DatetimeIndex([v2, v1]) + tm.assert_index_equal(uniques, exp) # period v1 = pd.Period('201302', freq='M') @@ -228,13 +150,13 @@ def test_datelike(self): # periods are not 'sorted' as they are converted back into an index labels, uniques = algos.factorize(x) exp = np.array([0, 0, 0, 1, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - self.assert_index_equal(uniques, pd.PeriodIndex([v1, v2])) + tm.assert_numpy_array_equal(labels, exp) + tm.assert_index_equal(uniques, pd.PeriodIndex([v1, v2])) labels, uniques = algos.factorize(x, sort=True) exp = np.array([0, 0, 0, 1, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - self.assert_index_equal(uniques, pd.PeriodIndex([v1, v2])) + tm.assert_numpy_array_equal(labels, exp) + tm.assert_index_equal(uniques, pd.PeriodIndex([v1, v2])) # GH 5986 v1 = pd.to_timedelta('1 day 1 min') @@ -242,26 +164,26 @@ def test_datelike(self): x = Series([v1, v2, v1, v1, v2, v2, v1]) labels, uniques = algos.factorize(x) exp = np.array([0, 1, 0, 0, 1, 1, 0], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - self.assert_index_equal(uniques, pd.to_timedelta([v1, v2])) + tm.assert_numpy_array_equal(labels, exp) + tm.assert_index_equal(uniques, pd.to_timedelta([v1, v2])) labels, uniques = algos.factorize(x, sort=True) exp = np.array([1, 0, 1, 1, 0, 0, 1], dtype=np.intp) - self.assert_numpy_array_equal(labels, exp) - self.assert_index_equal(uniques, pd.to_timedelta([v2, v1])) + tm.assert_numpy_array_equal(labels, exp) + tm.assert_index_equal(uniques, pd.to_timedelta([v2, v1])) def test_factorize_nan(self): # nan should map to na_sentinel, not reverse_indexer[na_sentinel] # rizer.factorize should not raise an exception if na_sentinel indexes # outside of reverse_indexer key = np.array([1, 2, 1, np.nan], dtype='O') - rizer = hashtable.Factorizer(len(key)) + rizer = ht.Factorizer(len(key)) for na_sentinel in (-1, 20): ids = rizer.factorize(key, sort=True, na_sentinel=na_sentinel) expected = np.array([0, 1, 0, na_sentinel], dtype='int32') - self.assertEqual(len(set(key)), len(set(expected))) - self.assertTrue(np.array_equal( - pd.isnull(key), expected == na_sentinel)) + assert len(set(key)) == len(set(expected)) + tm.assert_numpy_array_equal(pd.isna(key), + expected == na_sentinel) # nan still maps to na_sentinel when sort=False key = np.array([0, np.nan, 1], dtype='O') @@ -271,51 +193,153 @@ def test_factorize_nan(self): ids = rizer.factorize(key, sort=False, na_sentinel=na_sentinel) # noqa expected = np.array([2, -1, 0], dtype='int32') - self.assertEqual(len(set(key)), len(set(expected))) - self.assertTrue( - np.array_equal(pd.isnull(key), expected == na_sentinel)) - + assert len(set(key)) == len(set(expected)) + tm.assert_numpy_array_equal(pd.isna(key), expected == na_sentinel) + + @pytest.mark.parametrize("data,expected_label,expected_level", [ + ( + [(1, 1), (1, 2), (0, 0), (1, 2), 'nonsense'], + [0, 1, 2, 1, 3], + [(1, 1), (1, 2), (0, 0), 'nonsense'] + ), + ( + [(1, 1), (1, 2), (0, 0), (1, 2), (1, 2, 3)], + [0, 1, 2, 1, 3], + [(1, 1), (1, 2), (0, 0), (1, 2, 3)] + ), + ( + [(1, 1), (1, 2), (0, 0), (1, 2)], + [0, 1, 2, 1], + [(1, 1), (1, 2), (0, 0)] + ) + ]) + def test_factorize_tuple_list(self, data, expected_label, expected_level): + # GH9454 + result = pd.factorize(data) + + tm.assert_numpy_array_equal(result[0], + np.array(expected_label, dtype=np.intp)) + + expected_level_array = com.asarray_tuplesafe(expected_level, + dtype=object) + tm.assert_numpy_array_equal(result[1], expected_level_array) + + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_complex_sorting(self): # gh 12666 - check no segfault - # Test not valid numpy versions older than 1.11 - if pd._np_version_under1p11: - self.skipTest("Test valid only for numpy 1.11+") - x17 = np.array([complex(i) for i in range(17)], dtype=object) - self.assertRaises(TypeError, algos.factorize, x17[::-1], sort=True) + msg = (r"'(<|>)' not supported between instances of 'complex' and" + r" 'complex'|" + r"unorderable types: complex\(\) > complex\(\)") + with pytest.raises(TypeError, match=msg): + algos.factorize(x17[::-1], sort=True) - def test_uint64_factorize(self): - data = np.array([2**63, 1, 2**63], dtype=np.uint64) + def test_float64_factorize(self, writable): + data = np.array([1.0, 1e8, 1.0, 1e-8, 1e8, 1.0], dtype=np.float64) + data.setflags(write=writable) + exp_labels = np.array([0, 1, 0, 2, 1, 0], dtype=np.intp) + exp_uniques = np.array([1.0, 1e8, 1e-8], dtype=np.float64) + + labels, uniques = algos.factorize(data) + tm.assert_numpy_array_equal(labels, exp_labels) + tm.assert_numpy_array_equal(uniques, exp_uniques) + + def test_uint64_factorize(self, writable): + data = np.array([2**64 - 1, 1, 2**64 - 1], dtype=np.uint64) + data.setflags(write=writable) exp_labels = np.array([0, 1, 0], dtype=np.intp) - exp_uniques = np.array([2**63, 1], dtype=np.uint64) + exp_uniques = np.array([2**64 - 1, 1], dtype=np.uint64) labels, uniques = algos.factorize(data) tm.assert_numpy_array_equal(labels, exp_labels) tm.assert_numpy_array_equal(uniques, exp_uniques) - data = np.array([2**63, -1, 2**63], dtype=object) + def test_int64_factorize(self, writable): + data = np.array([2**63 - 1, -2**63, 2**63 - 1], dtype=np.int64) + data.setflags(write=writable) exp_labels = np.array([0, 1, 0], dtype=np.intp) - exp_uniques = np.array([2**63, -1], dtype=object) + exp_uniques = np.array([2**63 - 1, -2**63], dtype=np.int64) labels, uniques = algos.factorize(data) tm.assert_numpy_array_equal(labels, exp_labels) tm.assert_numpy_array_equal(uniques, exp_uniques) + def test_string_factorize(self, writable): + data = np.array(['a', 'c', 'a', 'b', 'c'], + dtype=object) + data.setflags(write=writable) + exp_labels = np.array([0, 1, 0, 2, 1], dtype=np.intp) + exp_uniques = np.array(['a', 'c', 'b'], dtype=object) -class TestUnique(tm.TestCase): + labels, uniques = algos.factorize(data) + tm.assert_numpy_array_equal(labels, exp_labels) + tm.assert_numpy_array_equal(uniques, exp_uniques) + + def test_object_factorize(self, writable): + data = np.array(['a', 'c', None, np.nan, 'a', 'b', pd.NaT, 'c'], + dtype=object) + data.setflags(write=writable) + exp_labels = np.array([0, 1, -1, -1, 0, 2, -1, 1], dtype=np.intp) + exp_uniques = np.array(['a', 'c', 'b'], dtype=object) + + labels, uniques = algos.factorize(data) + tm.assert_numpy_array_equal(labels, exp_labels) + tm.assert_numpy_array_equal(uniques, exp_uniques) + + def test_deprecate_order(self): + # gh 19727 - check warning is raised for deprecated keyword, order. + # Test not valid once order keyword is removed. + data = np.array([2**63, 1, 2**63], dtype=np.uint64) + with tm.assert_produces_warning(expected_warning=FutureWarning): + algos.factorize(data, order=True) + with tm.assert_produces_warning(False): + algos.factorize(data) + + @pytest.mark.parametrize('data', [ + np.array([0, 1, 0], dtype='u8'), + np.array([-2**63, 1, -2**63], dtype='i8'), + np.array(['__nan__', 'foo', '__nan__'], dtype='object'), + ]) + def test_parametrized_factorize_na_value_default(self, data): + # arrays that include the NA default for that type, but isn't used. + l, u = algos.factorize(data) + expected_uniques = data[[0, 1]] + expected_labels = np.array([0, 1, 0], dtype=np.intp) + tm.assert_numpy_array_equal(l, expected_labels) + tm.assert_numpy_array_equal(u, expected_uniques) + + @pytest.mark.parametrize('data, na_value', [ + (np.array([0, 1, 0, 2], dtype='u8'), 0), + (np.array([1, 0, 1, 2], dtype='u8'), 1), + (np.array([-2**63, 1, -2**63, 0], dtype='i8'), -2**63), + (np.array([1, -2**63, 1, 0], dtype='i8'), 1), + (np.array(['a', '', 'a', 'b'], dtype=object), 'a'), + (np.array([(), ('a', 1), (), ('a', 2)], dtype=object), ()), + (np.array([('a', 1), (), ('a', 1), ('a', 2)], dtype=object), + ('a', 1)), + ]) + def test_parametrized_factorize_na_value(self, data, na_value): + l, u = algos._factorize_array(data, na_value=na_value) + expected_uniques = data[[1, 3]] + expected_labels = np.array([-1, 0, -1, 1], dtype=np.intp) + tm.assert_numpy_array_equal(l, expected_labels) + tm.assert_numpy_array_equal(u, expected_uniques) + + +class TestUnique(object): def test_ints(self): arr = np.random.randint(0, 100, size=50) result = algos.unique(arr) - tm.assertIsInstance(result, np.ndarray) + assert isinstance(result, np.ndarray) def test_objects(self): arr = np.random.randint(0, 100, size=50).astype('O') result = algos.unique(arr) - tm.assertIsInstance(result, np.ndarray) + assert isinstance(result, np.ndarray) def test_object_refcount_bug(self): lst = ['A', 'B', 'C', 'D', 'E'] @@ -343,22 +367,22 @@ def test_datetime64_dtype_array_returned(self): '2015-01-01T00:00:00.000000000+0000'], dtype='M8[ns]') - dt_index = pd.to_datetime(['2015-01-03T00:00:00.000000000+0000', - '2015-01-01T00:00:00.000000000+0000', - '2015-01-01T00:00:00.000000000+0000']) + dt_index = pd.to_datetime(['2015-01-03T00:00:00.000000000', + '2015-01-01T00:00:00.000000000', + '2015-01-01T00:00:00.000000000']) result = algos.unique(dt_index) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype - s = pd.Series(dt_index) + s = Series(dt_index) result = algos.unique(s) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype arr = s.values result = algos.unique(arr) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype def test_timedelta64_dtype_array_returned(self): # GH 9431 @@ -367,31 +391,217 @@ def test_timedelta64_dtype_array_returned(self): td_index = pd.to_timedelta([31200, 45678, 31200, 10000, 45678]) result = algos.unique(td_index) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype - s = pd.Series(td_index) + s = Series(td_index) result = algos.unique(s) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype arr = s.values result = algos.unique(arr) tm.assert_numpy_array_equal(result, expected) - self.assertEqual(result.dtype, expected.dtype) + assert result.dtype == expected.dtype def test_uint64_overflow(self): - s = pd.Series([1, 2, 2**63, 2**63], dtype=np.uint64) + s = Series([1, 2, 2**63, 2**63], dtype=np.uint64) exp = np.array([1, 2, 2**63], dtype=np.uint64) tm.assert_numpy_array_equal(algos.unique(s), exp) + def test_nan_in_object_array(self): + duplicated_items = ['a', np.nan, 'c', 'c'] + result = pd.unique(duplicated_items) + expected = np.array(['a', np.nan, 'c'], dtype=object) + tm.assert_numpy_array_equal(result, expected) + + def test_categorical(self): + + # we are expecting to return in the order + # of appearance + expected = Categorical(list('bac'), categories=list('bac')) + + # we are expecting to return in the order + # of the categories + expected_o = Categorical( + list('bac'), categories=list('abc'), ordered=True) + + # GH 15939 + c = Categorical(list('baabc')) + result = c.unique() + tm.assert_categorical_equal(result, expected) + + result = algos.unique(c) + tm.assert_categorical_equal(result, expected) + + c = Categorical(list('baabc'), ordered=True) + result = c.unique() + tm.assert_categorical_equal(result, expected_o) + + result = algos.unique(c) + tm.assert_categorical_equal(result, expected_o) + + # Series of categorical dtype + s = Series(Categorical(list('baabc')), name='foo') + result = s.unique() + tm.assert_categorical_equal(result, expected) + + result = pd.unique(s) + tm.assert_categorical_equal(result, expected) + + # CI -> return CI + ci = CategoricalIndex(Categorical(list('baabc'), + categories=list('bac'))) + expected = CategoricalIndex(expected) + result = ci.unique() + tm.assert_index_equal(result, expected) + + result = pd.unique(ci) + tm.assert_index_equal(result, expected) + + def test_datetime64tz_aware(self): + # GH 15939 + + result = Series( + Index([Timestamp('20160101', tz='US/Eastern'), + Timestamp('20160101', tz='US/Eastern')])).unique() + expected = DatetimeArray._from_sequence(np.array([ + Timestamp('2016-01-01 00:00:00-0500', tz="US/Eastern") + ])) + tm.assert_extension_array_equal(result, expected) + + result = Index([Timestamp('20160101', tz='US/Eastern'), + Timestamp('20160101', tz='US/Eastern')]).unique() + expected = DatetimeIndex(['2016-01-01 00:00:00'], + dtype='datetime64[ns, US/Eastern]', freq=None) + tm.assert_index_equal(result, expected) + + result = pd.unique( + Series(Index([Timestamp('20160101', tz='US/Eastern'), + Timestamp('20160101', tz='US/Eastern')]))) + expected = DatetimeArray._from_sequence(np.array([ + Timestamp('2016-01-01', tz="US/Eastern"), + ])) + tm.assert_extension_array_equal(result, expected) + + result = pd.unique(Index([Timestamp('20160101', tz='US/Eastern'), + Timestamp('20160101', tz='US/Eastern')])) + expected = DatetimeIndex(['2016-01-01 00:00:00'], + dtype='datetime64[ns, US/Eastern]', freq=None) + tm.assert_index_equal(result, expected) + + def test_order_of_appearance(self): + # 9346 + # light testing of guarantee of order of appearance + # these also are the doc-examples + result = pd.unique(Series([2, 1, 3, 3])) + tm.assert_numpy_array_equal(result, + np.array([2, 1, 3], dtype='int64')) + + result = pd.unique(Series([2] + [1] * 5)) + tm.assert_numpy_array_equal(result, + np.array([2, 1], dtype='int64')) + + result = pd.unique(Series([Timestamp('20160101'), + Timestamp('20160101')])) + expected = np.array(['2016-01-01T00:00:00.000000000'], + dtype='datetime64[ns]') + tm.assert_numpy_array_equal(result, expected) + + result = pd.unique(Index( + [Timestamp('20160101', tz='US/Eastern'), + Timestamp('20160101', tz='US/Eastern')])) + expected = DatetimeIndex(['2016-01-01 00:00:00'], + dtype='datetime64[ns, US/Eastern]', + freq=None) + tm.assert_index_equal(result, expected) -class TestIsin(tm.TestCase): + result = pd.unique(list('aabc')) + expected = np.array(['a', 'b', 'c'], dtype=object) + tm.assert_numpy_array_equal(result, expected) + + result = pd.unique(Series(Categorical(list('aabc')))) + expected = Categorical(list('abc')) + tm.assert_categorical_equal(result, expected) + + @pytest.mark.parametrize("arg ,expected", [ + (('1', '1', '2'), np.array(['1', '2'], dtype=object)), + (('foo',), np.array(['foo'], dtype=object)) + ]) + def test_tuple_with_strings(self, arg, expected): + # see GH 17108 + result = pd.unique(arg) + tm.assert_numpy_array_equal(result, expected) + + def test_obj_none_preservation(self): + # GH 20866 + arr = np.array(['foo', None], dtype=object) + result = pd.unique(arr) + expected = np.array(['foo', None], dtype=object) + + tm.assert_numpy_array_equal(result, expected, strict_nan=True) + + def test_signed_zero(self): + # GH 21866 + a = np.array([-0.0, 0.0]) + result = pd.unique(a) + expected = np.array([-0.0]) # 0.0 and -0.0 are equivalent + tm.assert_numpy_array_equal(result, expected) + + def test_different_nans(self): + # GH 21866 + # create different nans from bit-patterns: + NAN1 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000000))[0] + NAN2 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000001))[0] + assert NAN1 != NAN1 + assert NAN2 != NAN2 + a = np.array([NAN1, NAN2]) # NAN1 and NAN2 are equivalent + result = pd.unique(a) + expected = np.array([np.nan]) + tm.assert_numpy_array_equal(result, expected) + + def test_first_nan_kept(self): + # GH 22295 + # create different nans from bit-patterns: + bits_for_nan1 = 0xfff8000000000001 + bits_for_nan2 = 0x7ff8000000000001 + NAN1 = struct.unpack("d", struct.pack("=Q", bits_for_nan1))[0] + NAN2 = struct.unpack("d", struct.pack("=Q", bits_for_nan2))[0] + assert NAN1 != NAN1 + assert NAN2 != NAN2 + for el_type in [np.float64, np.object]: + a = np.array([NAN1, NAN2], dtype=el_type) + result = pd.unique(a) + assert result.size == 1 + # use bit patterns to identify which nan was kept: + result_nan_bits = struct.unpack("=Q", + struct.pack("d", result[0]))[0] + assert result_nan_bits == bits_for_nan1 + + def test_do_not_mangle_na_values(self, unique_nulls_fixture, + unique_nulls_fixture2): + # GH 22295 + if unique_nulls_fixture is unique_nulls_fixture2: + return # skip it, values not unique + a = np.array([unique_nulls_fixture, + unique_nulls_fixture2], dtype=np.object) + result = pd.unique(a) + assert result.size == 2 + assert a[0] is unique_nulls_fixture + assert a[1] is unique_nulls_fixture2 + + +class TestIsin(object): def test_invalid(self): - self.assertRaises(TypeError, lambda: algos.isin(1, 1)) - self.assertRaises(TypeError, lambda: algos.isin(1, [1])) - self.assertRaises(TypeError, lambda: algos.isin([1], 1)) + msg = (r"only list-like objects are allowed to be passed to isin\(\)," + r" you passed a \[int\]") + with pytest.raises(TypeError, match=msg): + algos.isin(1, 1) + with pytest.raises(TypeError, match=msg): + algos.isin(1, [1]) + with pytest.raises(TypeError, match=msg): + algos.isin([1], 1) def test_basic(self): @@ -403,15 +613,15 @@ def test_basic(self): expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) - result = algos.isin(pd.Series([1, 2]), [1]) + result = algos.isin(Series([1, 2]), [1]) expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) - result = algos.isin(pd.Series([1, 2]), pd.Series([1])) + result = algos.isin(Series([1, 2]), Series([1])) expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) - result = algos.isin(pd.Series([1, 2]), set([1])) + result = algos.isin(Series([1, 2]), {1}) expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) @@ -419,11 +629,11 @@ def test_basic(self): expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) - result = algos.isin(pd.Series(['a', 'b']), pd.Series(['a'])) + result = algos.isin(Series(['a', 'b']), Series(['a'])) expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) - result = algos.isin(pd.Series(['a', 'b']), set(['a'])) + result = algos.isin(Series(['a', 'b']), {'a'}) expected = np.array([True, False]) tm.assert_numpy_array_equal(result, expected) @@ -468,47 +678,160 @@ def test_large(self): expected[1] = True tm.assert_numpy_array_equal(result, expected) + def test_categorical_from_codes(self): + # GH 16639 + vals = np.array([0, 1, 2, 0]) + cats = ['a', 'b', 'c'] + Sd = Series(Categorical(1).from_codes(vals, cats)) + St = Series(Categorical(1).from_codes(np.array([0, 1]), cats)) + expected = np.array([True, True, False, True]) + result = algos.isin(Sd, St) + tm.assert_numpy_array_equal(expected, result) + + def test_same_nan_is_in(self): + # GH 22160 + # nan is special, because from " a is b" doesn't follow "a == b" + # at least, isin() should follow python's "np.nan in [nan] == True" + # casting to -> np.float64 -> another float-object somewher on + # the way could lead jepardize this behavior + comps = [np.nan] # could be casted to float64 + values = [np.nan] + expected = np.array([True]) + result = algos.isin(comps, values) + tm.assert_numpy_array_equal(expected, result) + + def test_same_object_is_in(self): + # GH 22160 + # there could be special treatment for nans + # the user however could define a custom class + # with similar behavior, then we at least should + # fall back to usual python's behavior: "a in [a] == True" + class LikeNan(object): + def __eq__(self): + return False + + def __hash__(self): + return 0 + + a, b = LikeNan(), LikeNan() + # same object -> True + tm.assert_numpy_array_equal(algos.isin([a], [a]), np.array([True])) + # different objects -> False + tm.assert_numpy_array_equal(algos.isin([a], [b]), np.array([False])) + + def test_different_nans(self): + # GH 22160 + # all nans are handled as equivalent + + comps = [float('nan')] + values = [float('nan')] + assert comps[0] is not values[0] # different nan-objects + + # as list of python-objects: + result = algos.isin(comps, values) + tm.assert_numpy_array_equal(np.array([True]), result) + + # as object-array: + result = algos.isin(np.asarray(comps, dtype=np.object), + np.asarray(values, dtype=np.object)) + tm.assert_numpy_array_equal(np.array([True]), result) + + # as float64-array: + result = algos.isin(np.asarray(comps, dtype=np.float64), + np.asarray(values, dtype=np.float64)) + tm.assert_numpy_array_equal(np.array([True]), result) + + def test_no_cast(self): + # GH 22160 + # ensure 42 is not casted to a string + comps = ['ss', 42] + values = ['42'] + expected = np.array([False, False]) + result = algos.isin(comps, values) + tm.assert_numpy_array_equal(expected, result) + + @pytest.mark.parametrize("empty", [[], Series(), np.array([])]) + def test_empty(self, empty): + # see gh-16991 + vals = Index(["a", "b"]) + expected = np.array([False, False]) + + result = algos.isin(vals, empty) + tm.assert_numpy_array_equal(expected, result) + + def test_different_nan_objects(self): + # GH 22119 + comps = np.array(['nan', np.nan * 1j, float('nan')], dtype=np.object) + vals = np.array([float('nan')], dtype=np.object) + expected = np.array([False, False, True]) + result = algos.isin(comps, vals) + tm.assert_numpy_array_equal(expected, result) + + def test_different_nans_as_float64(self): + # GH 21866 + # create different nans from bit-patterns, + # these nans will land in different buckets in the hash-table + # if no special care is taken + NAN1 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000000))[0] + NAN2 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000001))[0] + assert NAN1 != NAN1 + assert NAN2 != NAN2 + + # check that NAN1 and NAN2 are equivalent: + arr = np.array([NAN1, NAN2], dtype=np.float64) + lookup1 = np.array([NAN1], dtype=np.float64) + result = algos.isin(arr, lookup1) + expected = np.array([True, True]) + tm.assert_numpy_array_equal(result, expected) + + lookup2 = np.array([NAN2], dtype=np.float64) + result = algos.isin(arr, lookup2) + expected = np.array([True, True]) + tm.assert_numpy_array_equal(result, expected) + -class TestValueCounts(tm.TestCase): +class TestValueCounts(object): def test_value_counts(self): np.random.seed(1234) - from pandas.tools.tile import cut + from pandas.core.reshape.tile import cut arr = np.random.randn(4) factor = cut(arr, 4) - tm.assertIsInstance(factor, Categorical) + # assert isinstance(factor, n) result = algos.value_counts(factor) - cats = ['(-1.194, -0.535]', '(-0.535, 0.121]', '(0.121, 0.777]', - '(0.777, 1.433]'] - expected_index = CategoricalIndex(cats, cats, ordered=True) - expected = Series([1, 1, 1, 1], index=expected_index) + breaks = [-1.194, -0.535, 0.121, 0.777, 1.433] + index = IntervalIndex.from_breaks(breaks).astype(CDT(ordered=True)) + expected = Series([1, 1, 1, 1], index=index) tm.assert_series_equal(result.sort_index(), expected.sort_index()) def test_value_counts_bins(self): s = [1, 2, 3, 4] result = algos.value_counts(s, bins=1) - self.assertEqual(result.tolist(), [4]) - self.assertEqual(result.index[0], 0.997) + expected = Series([4], + index=IntervalIndex.from_tuples([(0.996, 4.0)])) + tm.assert_series_equal(result, expected) result = algos.value_counts(s, bins=2, sort=False) - self.assertEqual(result.tolist(), [2, 2]) - self.assertEqual(result.index[0], 0.997) - self.assertEqual(result.index[1], 2.5) + expected = Series([2, 2], + index=IntervalIndex.from_tuples([(0.996, 2.5), + (2.5, 4.0)])) + tm.assert_series_equal(result, expected) def test_value_counts_dtypes(self): result = algos.value_counts([1, 1.]) - self.assertEqual(len(result), 1) + assert len(result) == 1 result = algos.value_counts([1, 1.], bins=1) - self.assertEqual(len(result), 1) + assert len(result) == 1 result = algos.value_counts(Series([1, 1., '1'])) # object - self.assertEqual(len(result), 2) + assert len(result) == 2 - self.assertRaises(TypeError, lambda s: algos.value_counts(s, bins=1), - ['1', 1]) + msg = "bins argument only works with numeric data" + with pytest.raises(TypeError, match=msg): + algos.value_counts(['1', 1], bins=1) def test_value_counts_nat(self): td = Series([np.timedelta64(10000), pd.NaT], dtype='timedelta64[ns]') @@ -517,36 +840,36 @@ def test_value_counts_nat(self): for s in [td, dt]: vc = algos.value_counts(s) vc_with_na = algos.value_counts(s, dropna=False) - self.assertEqual(len(vc), 1) - self.assertEqual(len(vc_with_na), 2) + assert len(vc) == 1 + assert len(vc_with_na) == 2 - exp_dt = pd.Series({pd.Timestamp('2014-01-01 00:00:00'): 1}) + exp_dt = Series({Timestamp('2014-01-01 00:00:00'): 1}) tm.assert_series_equal(algos.value_counts(dt), exp_dt) # TODO same for (timedelta) def test_value_counts_datetime_outofbounds(self): # GH 13663 - s = pd.Series([datetime(3000, 1, 1), datetime(5000, 1, 1), - datetime(5000, 1, 1), datetime(6000, 1, 1), - datetime(3000, 1, 1), datetime(3000, 1, 1)]) + s = Series([datetime(3000, 1, 1), datetime(5000, 1, 1), + datetime(5000, 1, 1), datetime(6000, 1, 1), + datetime(3000, 1, 1), datetime(3000, 1, 1)]) res = s.value_counts() - exp_index = pd.Index([datetime(3000, 1, 1), datetime(5000, 1, 1), - datetime(6000, 1, 1)], dtype=object) - exp = pd.Series([3, 2, 1], index=exp_index) + exp_index = Index([datetime(3000, 1, 1), datetime(5000, 1, 1), + datetime(6000, 1, 1)], dtype=object) + exp = Series([3, 2, 1], index=exp_index) tm.assert_series_equal(res, exp) # GH 12424 - res = pd.to_datetime(pd.Series(['2362-01-01', np.nan]), + res = pd.to_datetime(Series(['2362-01-01', np.nan]), errors='ignore') - exp = pd.Series(['2362-01-01', np.nan], dtype=object) + exp = Series(['2362-01-01', np.nan], dtype=object) tm.assert_series_equal(res, exp) def test_categorical(self): - s = Series(pd.Categorical(list('aaabbc'))) + s = Series(Categorical(list('aaabbc'))) result = s.value_counts() - expected = pd.Series([3, 2, 1], - index=pd.CategoricalIndex(['a', 'b', 'c'])) + expected = Series([3, 2, 1], index=CategoricalIndex(['a', 'b', 'c'])) + tm.assert_series_equal(result, expected, check_index_type=True) # preserve order? @@ -556,38 +879,38 @@ def test_categorical(self): tm.assert_series_equal(result, expected, check_index_type=True) def test_categorical_nans(self): - s = Series(pd.Categorical(list('aaaaabbbcc'))) # 4,3,2,1 (nan) + s = Series(Categorical(list('aaaaabbbcc'))) # 4,3,2,1 (nan) s.iloc[1] = np.nan result = s.value_counts() - expected = pd.Series([4, 3, 2], index=pd.CategoricalIndex( + expected = Series([4, 3, 2], index=CategoricalIndex( ['a', 'b', 'c'], categories=['a', 'b', 'c'])) tm.assert_series_equal(result, expected, check_index_type=True) result = s.value_counts(dropna=False) - expected = pd.Series([ + expected = Series([ 4, 3, 2, 1 - ], index=pd.CategoricalIndex(['a', 'b', 'c', np.nan])) + ], index=CategoricalIndex(['a', 'b', 'c', np.nan])) tm.assert_series_equal(result, expected, check_index_type=True) # out of order - s = Series(pd.Categorical( + s = Series(Categorical( list('aaaaabbbcc'), ordered=True, categories=['b', 'a', 'c'])) s.iloc[1] = np.nan result = s.value_counts() - expected = pd.Series([4, 3, 2], index=pd.CategoricalIndex( + expected = Series([4, 3, 2], index=CategoricalIndex( ['a', 'b', 'c'], categories=['b', 'a', 'c'], ordered=True)) tm.assert_series_equal(result, expected, check_index_type=True) result = s.value_counts(dropna=False) - expected = pd.Series([4, 3, 2, 1], index=pd.CategoricalIndex( + expected = Series([4, 3, 2, 1], index=CategoricalIndex( ['a', 'b', 'c', np.nan], categories=['b', 'a', 'c'], ordered=True)) tm.assert_series_equal(result, expected, check_index_type=True) def test_categorical_zeroes(self): # keep the `d` category with 0 - s = Series(pd.Categorical( + s = Series(Categorical( list('bbbaac'), categories=list('abcd'), ordered=True)) result = s.value_counts() - expected = Series([3, 2, 1, 0], index=pd.Categorical( + expected = Series([3, 2, 1, 0], index=Categorical( ['b', 'a', 'c', 'd'], categories=list('abcd'), ordered=True)) tm.assert_series_equal(result, expected, check_index_type=True) @@ -595,34 +918,34 @@ def test_dropna(self): # https://github.com/pandas-dev/pandas/issues/9443#issuecomment-73719328 tm.assert_series_equal( - pd.Series([True, True, False]).value_counts(dropna=True), - pd.Series([2, 1], index=[True, False])) + Series([True, True, False]).value_counts(dropna=True), + Series([2, 1], index=[True, False])) tm.assert_series_equal( - pd.Series([True, True, False]).value_counts(dropna=False), - pd.Series([2, 1], index=[True, False])) + Series([True, True, False]).value_counts(dropna=False), + Series([2, 1], index=[True, False])) tm.assert_series_equal( - pd.Series([True, True, False, None]).value_counts(dropna=True), - pd.Series([2, 1], index=[True, False])) + Series([True, True, False, None]).value_counts(dropna=True), + Series([2, 1], index=[True, False])) tm.assert_series_equal( - pd.Series([True, True, False, None]).value_counts(dropna=False), - pd.Series([2, 1, 1], index=[True, False, np.nan])) + Series([True, True, False, None]).value_counts(dropna=False), + Series([2, 1, 1], index=[True, False, np.nan])) tm.assert_series_equal( - pd.Series([10.3, 5., 5.]).value_counts(dropna=True), - pd.Series([2, 1], index=[5., 10.3])) + Series([10.3, 5., 5.]).value_counts(dropna=True), + Series([2, 1], index=[5., 10.3])) tm.assert_series_equal( - pd.Series([10.3, 5., 5.]).value_counts(dropna=False), - pd.Series([2, 1], index=[5., 10.3])) + Series([10.3, 5., 5.]).value_counts(dropna=False), + Series([2, 1], index=[5., 10.3])) tm.assert_series_equal( - pd.Series([10.3, 5., 5., None]).value_counts(dropna=True), - pd.Series([2, 1], index=[5., 10.3])) + Series([10.3, 5., 5., None]).value_counts(dropna=True), + Series([2, 1], index=[5., 10.3])) # 32-bit linux has a different ordering if not compat.is_platform_32bit(): - tm.assert_series_equal( - pd.Series([10.3, 5., 5., None]).value_counts(dropna=False), - pd.Series([2, 1, 1], index=[5., 10.3, np.nan])) + result = Series([10.3, 5., 5., None]).value_counts(dropna=False) + expected = Series([2, 1, 1], index=[5., 10.3, np.nan]) + tm.assert_series_equal(result, expected) def test_value_counts_normalized(self): # GH12558 @@ -656,7 +979,7 @@ def test_value_counts_uint64(self): tm.assert_series_equal(result, expected) -class TestDuplicated(tm.TestCase): +class TestDuplicated(object): def test_duplicated_with_nas(self): keys = np.array([0, 1, np.nan, 0, 2, np.nan], dtype=object) @@ -696,55 +1019,55 @@ def test_duplicated_with_nas(self): expected = np.array(trues + trues) tm.assert_numpy_array_equal(result, expected) - def test_numeric_object_likes(self): - cases = [np.array([1, 2, 1, 5, 3, - 2, 4, 1, 5, 6]), - np.array([1.1, 2.2, 1.1, np.nan, 3.3, - 2.2, 4.4, 1.1, np.nan, 6.6]), - np.array([1 + 1j, 2 + 2j, 1 + 1j, 5 + 5j, 3 + 3j, - 2 + 2j, 4 + 4j, 1 + 1j, 5 + 5j, 6 + 6j]), - np.array(['a', 'b', 'a', 'e', 'c', - 'b', 'd', 'a', 'e', 'f'], dtype=object), - np.array([1, 2**63, 1, 3**5, 10, - 2**63, 39, 1, 3**5, 7], dtype=np.uint64)] - + @pytest.mark.parametrize('case', [ + np.array([1, 2, 1, 5, 3, + 2, 4, 1, 5, 6]), + np.array([1.1, 2.2, 1.1, np.nan, 3.3, + 2.2, 4.4, 1.1, np.nan, 6.6]), + np.array([1 + 1j, 2 + 2j, 1 + 1j, 5 + 5j, 3 + 3j, + 2 + 2j, 4 + 4j, 1 + 1j, 5 + 5j, 6 + 6j]), + np.array(['a', 'b', 'a', 'e', 'c', + 'b', 'd', 'a', 'e', 'f'], dtype=object), + np.array([1, 2**63, 1, 3**5, 10, 2**63, 39, 1, 3**5, 7], + dtype=np.uint64), + ]) + def test_numeric_object_likes(self, case): exp_first = np.array([False, False, True, False, False, True, False, True, True, False]) exp_last = np.array([True, True, True, True, False, False, False, False, False, False]) exp_false = exp_first | exp_last - for case in cases: - res_first = algos.duplicated(case, keep='first') - tm.assert_numpy_array_equal(res_first, exp_first) + res_first = algos.duplicated(case, keep='first') + tm.assert_numpy_array_equal(res_first, exp_first) - res_last = algos.duplicated(case, keep='last') - tm.assert_numpy_array_equal(res_last, exp_last) + res_last = algos.duplicated(case, keep='last') + tm.assert_numpy_array_equal(res_last, exp_last) - res_false = algos.duplicated(case, keep=False) - tm.assert_numpy_array_equal(res_false, exp_false) + res_false = algos.duplicated(case, keep=False) + tm.assert_numpy_array_equal(res_false, exp_false) - # index - for idx in [pd.Index(case), pd.Index(case, dtype='category')]: - res_first = idx.duplicated(keep='first') - tm.assert_numpy_array_equal(res_first, exp_first) + # index + for idx in [Index(case), Index(case, dtype='category')]: + res_first = idx.duplicated(keep='first') + tm.assert_numpy_array_equal(res_first, exp_first) - res_last = idx.duplicated(keep='last') - tm.assert_numpy_array_equal(res_last, exp_last) + res_last = idx.duplicated(keep='last') + tm.assert_numpy_array_equal(res_last, exp_last) - res_false = idx.duplicated(keep=False) - tm.assert_numpy_array_equal(res_false, exp_false) + res_false = idx.duplicated(keep=False) + tm.assert_numpy_array_equal(res_false, exp_false) - # series - for s in [pd.Series(case), pd.Series(case, dtype='category')]: - res_first = s.duplicated(keep='first') - tm.assert_series_equal(res_first, pd.Series(exp_first)) + # series + for s in [Series(case), Series(case, dtype='category')]: + res_first = s.duplicated(keep='first') + tm.assert_series_equal(res_first, Series(exp_first)) - res_last = s.duplicated(keep='last') - tm.assert_series_equal(res_last, pd.Series(exp_last)) + res_last = s.duplicated(keep='last') + tm.assert_series_equal(res_last, Series(exp_last)) - res_false = s.duplicated(keep=False) - tm.assert_series_equal(res_false, pd.Series(exp_false)) + res_false = s.duplicated(keep=False) + tm.assert_series_equal(res_false, Series(exp_false)) def test_datetime_likes(self): @@ -753,8 +1076,8 @@ def test_datetime_likes(self): td = ['1 days', '2 days', '1 days', 'NaT', '3 days', '2 days', '4 days', '1 days', 'NaT', '6 days'] - cases = [np.array([pd.Timestamp(d) for d in dt]), - np.array([pd.Timestamp(d, tz='US/Eastern') for d in dt]), + cases = [np.array([Timestamp(d) for d in dt]), + np.array([Timestamp(d, tz='US/Eastern') for d in dt]), np.array([pd.Period(d, freq='D') for d in dt]), np.array([np.datetime64(d) for d in dt]), np.array([pd.Timedelta(d) for d in td])] @@ -776,8 +1099,8 @@ def test_datetime_likes(self): tm.assert_numpy_array_equal(res_false, exp_false) # index - for idx in [pd.Index(case), pd.Index(case, dtype='category'), - pd.Index(case, dtype=object)]: + for idx in [Index(case), Index(case, dtype='category'), + Index(case, dtype=object)]: res_first = idx.duplicated(keep='first') tm.assert_numpy_array_equal(res_first, exp_first) @@ -788,24 +1111,40 @@ def test_datetime_likes(self): tm.assert_numpy_array_equal(res_false, exp_false) # series - for s in [pd.Series(case), pd.Series(case, dtype='category'), - pd.Series(case, dtype=object)]: + for s in [Series(case), Series(case, dtype='category'), + Series(case, dtype=object)]: res_first = s.duplicated(keep='first') - tm.assert_series_equal(res_first, pd.Series(exp_first)) + tm.assert_series_equal(res_first, Series(exp_first)) res_last = s.duplicated(keep='last') - tm.assert_series_equal(res_last, pd.Series(exp_last)) + tm.assert_series_equal(res_last, Series(exp_last)) res_false = s.duplicated(keep=False) - tm.assert_series_equal(res_false, pd.Series(exp_false)) + tm.assert_series_equal(res_false, Series(exp_false)) def test_unique_index(self): - cases = [pd.Index([1, 2, 3]), pd.RangeIndex(0, 3)] + cases = [Index([1, 2, 3]), pd.RangeIndex(0, 3)] for case in cases: - self.assertTrue(case.is_unique) + assert case.is_unique is True tm.assert_numpy_array_equal(case.duplicated(), np.array([False, False, False])) + @pytest.mark.parametrize('arr, unique', [ + ([(0, 0), (0, 1), (1, 0), (1, 1), (0, 0), (0, 1), (1, 0), (1, 1)], + [(0, 0), (0, 1), (1, 0), (1, 1)]), + ([('b', 'c'), ('a', 'b'), ('a', 'b'), ('b', 'c')], + [('b', 'c'), ('a', 'b')]), + ([('a', 1), ('b', 2), ('a', 3), ('a', 1)], + [('a', 1), ('b', 2), ('a', 3)]), + ]) + def test_unique_tuples(self, arr, unique): + # https://github.com/pandas-dev/pandas/issues/16519 + expected = np.empty(len(unique), dtype=object) + expected[:] = unique + + result = pd.unique(arr) + tm.assert_numpy_array_equal(result, expected) + class GroupVarTestMixin(object): @@ -823,7 +1162,7 @@ def test_group_var_generic_1d(self): expected_counts = counts + 3 self.algo(out, counts, values, labels) - self.assertTrue(np.allclose(out, expected_out, self.rtol)) + assert np.allclose(out, expected_out, self.rtol) tm.assert_numpy_array_equal(counts, expected_counts) def test_group_var_generic_1d_flat_labels(self): @@ -839,7 +1178,7 @@ def test_group_var_generic_1d_flat_labels(self): self.algo(out, counts, values, labels) - self.assertTrue(np.allclose(out, expected_out, self.rtol)) + assert np.allclose(out, expected_out, self.rtol) tm.assert_numpy_array_equal(counts, expected_counts) def test_group_var_generic_2d_all_finite(self): @@ -854,7 +1193,7 @@ def test_group_var_generic_2d_all_finite(self): expected_counts = counts + 2 self.algo(out, counts, values, labels) - self.assertTrue(np.allclose(out, expected_out, self.rtol)) + assert np.allclose(out, expected_out, self.rtol) tm.assert_numpy_array_equal(counts, expected_counts) def test_group_var_generic_2d_some_nan(self): @@ -886,15 +1225,15 @@ def test_group_var_constant(self): self.algo(out, counts, values, labels) - self.assertEqual(counts[0], 3) - self.assertTrue(out[0, 0] >= 0) + assert counts[0] == 3 + assert out[0, 0] >= 0 tm.assert_almost_equal(out[0, 0], 0.0) -class TestGroupVarFloat64(tm.TestCase, GroupVarTestMixin): +class TestGroupVarFloat64(GroupVarTestMixin): __test__ = True - algo = libgroupby.group_var_float64 + algo = staticmethod(libgroupby.group_var_float64) dtype = np.float64 rtol = 1e-5 @@ -910,62 +1249,185 @@ def test_group_var_large_inputs(self): self.algo(out, counts, values, labels) - self.assertEqual(counts[0], 10 ** 6) + assert counts[0] == 10 ** 6 tm.assert_almost_equal(out[0, 0], 1.0 / 12, check_less_precise=True) -class TestGroupVarFloat32(tm.TestCase, GroupVarTestMixin): +class TestGroupVarFloat32(GroupVarTestMixin): __test__ = True - algo = libgroupby.group_var_float32 + algo = staticmethod(libgroupby.group_var_float32) dtype = np.float32 rtol = 1e-2 -class TestHashTable(tm.TestCase): +class TestHashTable(object): - def test_lookup_nan(self): + def test_lookup_nan(self, writable): xs = np.array([2.718, 3.14, np.nan, -7, 5, 2, 3]) - m = hashtable.Float64HashTable() + # GH 21688 ensure we can deal with readonly memory views + xs.setflags(write=writable) + m = ht.Float64HashTable() m.map_locations(xs) - self.assert_numpy_array_equal(m.lookup(xs), - np.arange(len(xs), dtype=np.int64)) - - def test_lookup_overflow(self): + tm.assert_numpy_array_equal(m.lookup(xs), np.arange(len(xs), + dtype=np.int64)) + + def test_add_signed_zeros(self): + # GH 21866 inconsistent hash-function for float64 + # default hash-function would lead to different hash-buckets + # for 0.0 and -0.0 if there are more than 2^30 hash-buckets + # but this would mean 16GB + N = 4 # 12 * 10**8 would trigger the error, if you have enough memory + m = ht.Float64HashTable(N) + m.set_item(0.0, 0) + m.set_item(-0.0, 0) + assert len(m) == 1 # 0.0 and -0.0 are equivalent + + def test_add_different_nans(self): + # GH 21866 inconsistent hash-function for float64 + # create different nans from bit-patterns: + NAN1 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000000))[0] + NAN2 = struct.unpack("d", struct.pack("=Q", 0x7ff8000000000001))[0] + assert NAN1 != NAN1 + assert NAN2 != NAN2 + # default hash function would lead to different hash-buckets + # for NAN1 and NAN2 even if there are only 4 buckets: + m = ht.Float64HashTable() + m.set_item(NAN1, 0) + m.set_item(NAN2, 0) + assert len(m) == 1 # NAN1 and NAN2 are equivalent + + def test_lookup_overflow(self, writable): xs = np.array([1, 2, 2**63], dtype=np.uint64) - m = hashtable.UInt64HashTable() + # GH 21688 ensure we can deal with readonly memory views + xs.setflags(write=writable) + m = ht.UInt64HashTable() m.map_locations(xs) - self.assert_numpy_array_equal(m.lookup(xs), - np.arange(len(xs), dtype=np.int64)) + tm.assert_numpy_array_equal(m.lookup(xs), np.arange(len(xs), + dtype=np.int64)) def test_get_unique(self): - s = pd.Series([1, 2, 2**63, 2**63], dtype=np.uint64) + s = Series([1, 2, 2**63, 2**63], dtype=np.uint64) exp = np.array([1, 2, 2**63], dtype=np.uint64) - self.assert_numpy_array_equal(s.unique(), exp) - - def test_vector_resize(self): + tm.assert_numpy_array_equal(s.unique(), exp) + + @pytest.mark.parametrize('nvals', [0, 10]) # resizing to 0 is special case + @pytest.mark.parametrize('htable, uniques, dtype, safely_resizes', [ + (ht.PyObjectHashTable, ht.ObjectVector, 'object', False), + (ht.StringHashTable, ht.ObjectVector, 'object', True), + (ht.Float64HashTable, ht.Float64Vector, 'float64', False), + (ht.Int64HashTable, ht.Int64Vector, 'int64', False), + (ht.UInt64HashTable, ht.UInt64Vector, 'uint64', False)]) + def test_vector_resize(self, writable, htable, uniques, dtype, + safely_resizes, nvals): # Test for memory errors after internal vector - # reallocations (pull request #7157) - - def _test_vector_resize(htable, uniques, dtype, nvals): - vals = np.array(np.random.randn(1000), dtype=dtype) - # get_labels appends to the vector - htable.get_labels(vals[:nvals], uniques, 0, -1) - # to_array resizes the vector - uniques.to_array() + # reallocations (GH 7157) + vals = np.array(np.random.randn(1000), dtype=dtype) + + # GH 21688 ensures we can deal with read-only memory views + vals.setflags(write=writable) + + # initialise instances; cannot initialise in parametrization, + # as otherwise external views would be held on the array (which is + # one of the things this test is checking) + htable = htable() + uniques = uniques() + + # get_labels may append to uniques + htable.get_labels(vals[:nvals], uniques, 0, -1) + # to_array() sets an external_view_exists flag on uniques. + tmp = uniques.to_array() + oldshape = tmp.shape + + # subsequent get_labels() calls can no longer append to it + # (except for StringHashTables + ObjectVector) + if safely_resizes: htable.get_labels(vals, uniques, 0, -1) - - test_cases = [ - (hashtable.PyObjectHashTable, hashtable.ObjectVector, 'object'), - (hashtable.StringHashTable, hashtable.ObjectVector, 'object'), - (hashtable.Float64HashTable, hashtable.Float64Vector, 'float64'), - (hashtable.Int64HashTable, hashtable.Int64Vector, 'int64'), - (hashtable.UInt64HashTable, hashtable.UInt64Vector, 'uint64')] - - for (tbl, vect, dtype) in test_cases: - # resizing to empty is a special case - _test_vector_resize(tbl(), vect(), dtype, 0) - _test_vector_resize(tbl(), vect(), dtype, 10) + else: + with pytest.raises(ValueError, match='external reference.*'): + htable.get_labels(vals, uniques, 0, -1) + + uniques.to_array() # should not raise here + assert tmp.shape == oldshape + + @pytest.mark.parametrize('htable, tm_dtype', [ + (ht.PyObjectHashTable, 'String'), + (ht.StringHashTable, 'String'), + (ht.Float64HashTable, 'Float'), + (ht.Int64HashTable, 'Int'), + (ht.UInt64HashTable, 'UInt')]) + def test_hashtable_unique(self, htable, tm_dtype, writable): + # output of maker has guaranteed unique elements + maker = getattr(tm, 'make' + tm_dtype + 'Index') + s = Series(maker(1000)) + if htable == ht.Float64HashTable: + # add NaN for float column + s.loc[500] = np.nan + elif htable == ht.PyObjectHashTable: + # use different NaN types for object column + s.loc[500:502] = [np.nan, None, pd.NaT] + + # create duplicated selection + s_duplicated = s.sample(frac=3, replace=True).reset_index(drop=True) + s_duplicated.values.setflags(write=writable) + + # drop_duplicates has own cython code (hash_table_func_helper.pxi) + # and is tested separately; keeps first occurrence like ht.unique() + expected_unique = s_duplicated.drop_duplicates(keep='first').values + result_unique = htable().unique(s_duplicated.values) + tm.assert_numpy_array_equal(result_unique, expected_unique) + + # test return_inverse=True + # reconstruction can only succeed if the inverse is correct + result_unique, result_inverse = htable().unique(s_duplicated.values, + return_inverse=True) + tm.assert_numpy_array_equal(result_unique, expected_unique) + reconstr = result_unique[result_inverse] + tm.assert_numpy_array_equal(reconstr, s_duplicated.values) + + @pytest.mark.parametrize('htable, tm_dtype', [ + (ht.PyObjectHashTable, 'String'), + (ht.StringHashTable, 'String'), + (ht.Float64HashTable, 'Float'), + (ht.Int64HashTable, 'Int'), + (ht.UInt64HashTable, 'UInt')]) + def test_hashtable_factorize(self, htable, tm_dtype, writable): + # output of maker has guaranteed unique elements + maker = getattr(tm, 'make' + tm_dtype + 'Index') + s = Series(maker(1000)) + if htable == ht.Float64HashTable: + # add NaN for float column + s.loc[500] = np.nan + elif htable == ht.PyObjectHashTable: + # use different NaN types for object column + s.loc[500:502] = [np.nan, None, pd.NaT] + + # create duplicated selection + s_duplicated = s.sample(frac=3, replace=True).reset_index(drop=True) + s_duplicated.values.setflags(write=writable) + na_mask = s_duplicated.isna().values + + result_unique, result_inverse = htable().factorize(s_duplicated.values) + + # drop_duplicates has own cython code (hash_table_func_helper.pxi) + # and is tested separately; keeps first occurrence like ht.factorize() + # since factorize removes all NaNs, we do the same here + expected_unique = s_duplicated.dropna().drop_duplicates().values + tm.assert_numpy_array_equal(result_unique, expected_unique) + + # reconstruction can only succeed if the inverse is correct. Since + # factorize removes the NaNs, those have to be excluded here as well + result_reconstruct = result_unique[result_inverse[~na_mask]] + expected_reconstruct = s_duplicated.dropna().values + tm.assert_numpy_array_equal(result_reconstruct, expected_reconstruct) + + @pytest.mark.parametrize('hashtable', [ + ht.PyObjectHashTable, ht.StringHashTable, + ht.Float64HashTable, ht.Int64HashTable, ht.UInt64HashTable]) + def test_hashtable_large_sizehint(self, hashtable): + # GH 22729 + size_hint = np.iinfo(np.uint32).max + 1 + tbl = hashtable(size_hint=size_hint) # noqa def test_quantile(): @@ -980,23 +1442,23 @@ def test_unique_label_indices(): a = np.random.randint(1, 1 << 10, 1 << 15).astype('i8') - left = unique_label_indices(a) + left = ht.unique_label_indices(a) right = np.unique(a, return_index=True)[1] tm.assert_numpy_array_equal(left, right, check_dtype=False) a[np.random.choice(len(a), 10)] = -1 - left = unique_label_indices(a) + left = ht.unique_label_indices(a) right = np.unique(a, return_index=True)[1][1:] tm.assert_numpy_array_equal(left, right, check_dtype=False) -class TestRank(tm.TestCase): +class TestRank(object): + @td.skip_if_no_scipy def test_scipy_compat(self): - tm._skip_if_no_scipy() from scipy.stats import rankdata def _check(arr): @@ -1029,30 +1491,41 @@ def test_too_many_ndims(self): arr = np.array([[[1, 2, 3], [4, 5, 6], [7, 8, 9]]]) msg = "Array with ndim > 2 are not supported" - with tm.assertRaisesRegexp(TypeError, msg): + with pytest.raises(TypeError, match=msg): algos.rank(arr) + @pytest.mark.single + @pytest.mark.high_memory + @pytest.mark.parametrize('values', [ + np.arange(2**24 + 1), + np.arange(2**25 + 2).reshape(2**24 + 1, 2)], + ids=['1d', '2d']) + def test_pct_max_many_rows(self, values): + # GH 18271 + result = algos.rank(values, pct=True).max() + assert result == 1 + def test_pad_backfill_object_segfault(): old = np.array([], dtype='O') new = np.array([datetime(2010, 12, 31)], dtype='O') - result = libalgos.pad_object(old, new) + result = libalgos.pad["object"](old, new) expected = np.array([-1], dtype=np.int64) - assert (np.array_equal(result, expected)) + tm.assert_numpy_array_equal(result, expected) - result = libalgos.pad_object(new, old) + result = libalgos.pad["object"](new, old) expected = np.array([], dtype=np.int64) - assert (np.array_equal(result, expected)) + tm.assert_numpy_array_equal(result, expected) - result = libalgos.backfill_object(old, new) + result = libalgos.backfill["object"](old, new) expected = np.array([-1], dtype=np.int64) - assert (np.array_equal(result, expected)) + tm.assert_numpy_array_equal(result, expected) - result = libalgos.backfill_object(new, old) + result = libalgos.backfill["object"](new, old) expected = np.array([], dtype=np.int64) - assert (np.array_equal(result, expected)) + tm.assert_numpy_array_equal(result, expected) def test_arrmap(): @@ -1061,7 +1534,7 @@ def test_arrmap(): assert (result.dtype == np.bool_) -class TestTseriesUtil(tm.TestCase): +class TestTseriesUtil(object): def test_combineFunc(self): pass @@ -1069,7 +1542,7 @@ def test_combineFunc(self): def test_reindex(self): pass - def test_isnull(self): + def test_isna(self): pass def test_groupby(self): @@ -1082,36 +1555,36 @@ def test_backfill(self): old = Index([1, 5, 10]) new = Index(lrange(12)) - filler = libalgos.backfill_int64(old.values, new.values) + filler = libalgos.backfill["int64_t"](old.values, new.values) expect_filler = np.array([0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, -1], dtype=np.int64) - self.assert_numpy_array_equal(filler, expect_filler) + tm.assert_numpy_array_equal(filler, expect_filler) # corner case old = Index([1, 4]) new = Index(lrange(5, 10)) - filler = libalgos.backfill_int64(old.values, new.values) + filler = libalgos.backfill["int64_t"](old.values, new.values) expect_filler = np.array([-1, -1, -1, -1, -1], dtype=np.int64) - self.assert_numpy_array_equal(filler, expect_filler) + tm.assert_numpy_array_equal(filler, expect_filler) def test_pad(self): old = Index([1, 5, 10]) new = Index(lrange(12)) - filler = libalgos.pad_int64(old.values, new.values) + filler = libalgos.pad["int64_t"](old.values, new.values) expect_filler = np.array([-1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2], dtype=np.int64) - self.assert_numpy_array_equal(filler, expect_filler) + tm.assert_numpy_array_equal(filler, expect_filler) # corner case old = Index([5, 10]) new = Index(lrange(5)) - filler = libalgos.pad_int64(old.values, new.values) + filler = libalgos.pad["int64_t"](old.values, new.values) expect_filler = np.array([-1, -1, -1, -1, -1], dtype=np.int64) - self.assert_numpy_array_equal(filler, expect_filler) + tm.assert_numpy_array_equal(filler, expect_filler) def test_is_lexsorted(): @@ -1127,7 +1600,7 @@ def test_is_lexsorted(): 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype='int64'), np.array([30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 30, 29, 28, @@ -1139,19 +1612,10 @@ def test_is_lexsorted(): 7, 6, 5, 4, 3, 2, 1, 0, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, - 4, 3, 2, 1, 0])] + 4, 3, 2, 1, 0], dtype='int64')] assert (not libalgos.is_lexsorted(failure)) -# def test_get_group_index(): -# a = np.array([0, 1, 2, 0, 2, 1, 0, 0], dtype=np.int64) -# b = np.array([1, 0, 3, 2, 0, 2, 3, 0], dtype=np.int64) -# expected = np.array([1, 4, 11, 2, 8, 6, 3, 0], dtype=np.int64) - -# result = lib.get_group_index([a, b], (3, 4)) - -# assert(np.array_equal(result, expected)) - def test_groupsort_indexer(): a = np.random.randint(0, 1000, 100).astype(np.int64) @@ -1160,14 +1624,22 @@ def test_groupsort_indexer(): result = libalgos.groupsort_indexer(a, 1000)[0] # need to use a stable sort + # np.argsort returns int, groupsort_indexer + # always returns int64 expected = np.argsort(a, kind='mergesort') - assert (np.array_equal(result, expected)) + expected = expected.astype(np.int64) + + tm.assert_numpy_array_equal(result, expected) # compare with lexsort + # np.lexsort returns int, groupsort_indexer + # always returns int64 key = a * 1000 + b result = libalgos.groupsort_indexer(key, 1000000)[0] expected = np.lexsort((b, a)) - assert (np.array_equal(result, expected)) + expected = expected.astype(np.int64) + + tm.assert_numpy_array_equal(result, expected) def test_infinity_sort(): @@ -1185,11 +1657,15 @@ def test_infinity_sort(): assert all(Inf > x or x is Inf for x in ref_nums) assert Inf >= Inf and Inf == Inf assert not Inf < Inf and not Inf > Inf + assert libalgos.Infinity() == libalgos.Infinity() + assert not libalgos.Infinity() != libalgos.Infinity() assert all(NegInf <= x for x in ref_nums) assert all(NegInf < x or x is NegInf for x in ref_nums) assert NegInf <= NegInf and NegInf == NegInf assert not NegInf < NegInf and not NegInf > NegInf + assert libalgos.NegInfinity() == libalgos.NegInfinity() + assert not libalgos.NegInfinity() != libalgos.NegInfinity() for perm in permutations(ref_nums): assert sorted(perm) == ref_nums @@ -1199,6 +1675,25 @@ def test_infinity_sort(): np.array([libalgos.NegInfinity()] * 32).argsort() +def test_infinity_against_nan(): + Inf = libalgos.Infinity() + NegInf = libalgos.NegInfinity() + + assert not Inf > np.nan + assert not Inf >= np.nan + assert not Inf < np.nan + assert not Inf <= np.nan + assert not Inf == np.nan + assert Inf != np.nan + + assert not NegInf > np.nan + assert not NegInf >= np.nan + assert not NegInf < np.nan + assert not NegInf <= np.nan + assert not NegInf == np.nan + assert NegInf != np.nan + + def test_ensure_platform_int(): arr = np.arange(100, dtype=np.intp) @@ -1212,27 +1707,27 @@ def test_int64_add_overflow(): m = np.iinfo(np.int64).max n = np.iinfo(np.int64).min - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, m]), m) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, m]), np.array([m, m])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([n, n]), n) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([n, n]), np.array([n, n])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, n]), np.array([n, n])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), arr_mask=np.array([False, True])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), b_mask=np.array([False, True])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), arr_mask=np.array([False, True]), b_mask=np.array([False, True])) - with tm.assertRaisesRegexp(OverflowError, msg): + with pytest.raises(OverflowError, match=msg): with tm.assert_produces_warning(RuntimeWarning): algos.checked_add_with_arr(np.array([m, m]), np.array([np.nan, m])) @@ -1240,29 +1735,23 @@ def test_int64_add_overflow(): # Check that the nan boolean arrays override whether or not # the addition overflows. We don't check the result but just # the fact that an OverflowError is not raised. - with tm.assertRaises(AssertionError): - with tm.assertRaisesRegexp(OverflowError, msg): - algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), - arr_mask=np.array([True, True])) - with tm.assertRaises(AssertionError): - with tm.assertRaisesRegexp(OverflowError, msg): - algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), - b_mask=np.array([True, True])) - with tm.assertRaises(AssertionError): - with tm.assertRaisesRegexp(OverflowError, msg): - algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), - arr_mask=np.array([True, False]), - b_mask=np.array([False, True])) - - -class TestMode(tm.TestCase): + algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), + arr_mask=np.array([True, True])) + algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), + b_mask=np.array([True, True])) + algos.checked_add_with_arr(np.array([m, m]), np.array([m, m]), + arr_mask=np.array([True, False]), + b_mask=np.array([False, True])) + + +class TestMode(object): def test_no_mode(self): exp = Series([], dtype=np.float64) tm.assert_series_equal(algos.mode([]), exp) - # GH 15714 def test_mode_single(self): + # GH 15714 exp_single = [1] data_single = [1] @@ -1356,16 +1845,19 @@ def test_uint64_overflow(self): def test_categorical(self): c = Categorical([1, 2]) - exp = Series([1, 2], dtype=np.int64) - tm.assert_series_equal(algos.mode(c), exp) + exp = c + tm.assert_categorical_equal(algos.mode(c), exp) + tm.assert_categorical_equal(c.mode(), exp) c = Categorical([1, 'a', 'a']) - exp = Series(['a'], dtype=object) - tm.assert_series_equal(algos.mode(c), exp) + exp = Categorical(['a'], categories=[1, 'a']) + tm.assert_categorical_equal(algos.mode(c), exp) + tm.assert_categorical_equal(c.mode(), exp) c = Categorical([1, 1, 2, 3, 3]) - exp = Series([1, 3], dtype=np.int64) - tm.assert_series_equal(algos.mode(c), exp) + exp = Categorical([1, 3], categories=[1, 2, 3]) + tm.assert_categorical_equal(algos.mode(c), exp) + tm.assert_categorical_equal(c.mode(), exp) def test_index(self): idx = Index([1, 2, 3]) diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py index 032e3a186b84a..ac365eb87d1bc 100644 --- a/pandas/tests/test_base.py +++ b/pandas/tests/test_base.py @@ -1,24 +1,32 @@ # -*- coding: utf-8 -*- from __future__ import print_function +from datetime import datetime, timedelta import re import sys -from datetime import datetime, timedelta -import pytest + import numpy as np +import pytest -import pandas as pd +from pandas._libs.tslib import iNaT import pandas.compat as compat -from pandas.types.common import (is_object_dtype, is_datetimetz, - needs_i8_conversion) -import pandas.util.testing as tm -from pandas import (Series, Index, DatetimeIndex, TimedeltaIndex, PeriodIndex, - Timedelta) -from pandas.compat import StringIO +from pandas.compat import PYPY, StringIO, long from pandas.compat.numpy import np_array_datetime64_compat -from pandas.core.base import PandasDelegate, NoNewAttributesMixin -from pandas.tseries.base import DatetimeIndexOpsMixin -from pandas._libs.tslib import iNaT + +from pandas.core.dtypes.common import ( + is_datetime64_dtype, is_datetime64tz_dtype, is_object_dtype, + is_timedelta64_dtype, needs_i8_conversion) +from pandas.core.dtypes.dtypes import DatetimeTZDtype + +import pandas as pd +from pandas import ( + CategoricalIndex, DataFrame, DatetimeIndex, Index, Interval, IntervalIndex, + Panel, PeriodIndex, Series, Timedelta, TimedeltaIndex, Timestamp) +from pandas.core.accessor import PandasDelegate +from pandas.core.arrays import DatetimeArray, PandasArray, TimedeltaArray +from pandas.core.base import NoNewAttributesMixin, PandasObject +from pandas.core.indexes.datetimelike import DatetimeIndexOpsMixin +import pandas.util.testing as tm class CheckStringMixin(object): @@ -44,9 +52,10 @@ class CheckImmutable(object): mutable_regex = re.compile('does not support mutable operations') def check_mutable_error(self, *args, **kwargs): - # pass whatever functions you normally would to assertRaises (after the - # Exception kind) - tm.assertRaisesRegexp(TypeError, self.mutable_regex, *args, **kwargs) + # Pass whatever function you normally would to pytest.raises + # (after the Exception kind). + with pytest.raises(TypeError): + self.mutable_regex(*args, **kwargs) def test_no_mutable_funcs(self): def setitem(): @@ -69,6 +78,7 @@ def delslice(): self.check_mutable_error(delslice) mutable_methods = getattr(self, "mutable_methods", []) + for meth in mutable_methods: self.check_mutable_error(getattr(self.container, meth)) @@ -79,11 +89,11 @@ def test_slicing_maintains_type(self): def check_result(self, result, expected, klass=None): klass = klass or self.klass - self.assertIsInstance(result, klass) - self.assertEqual(result, expected) + assert isinstance(result, klass) + assert result == expected -class TestPandasDelegate(tm.TestCase): +class TestPandasDelegate(object): class Delegator(object): _properties = ['foo'] @@ -101,17 +111,18 @@ def bar(self, *args, **kwargs): """ a test bar method """ pass - class Delegate(PandasDelegate): + class Delegate(PandasDelegate, PandasObject): def __init__(self, obj): self.obj = obj - def setUp(self): + def setup_method(self, method): pass - def test_invalida_delgation(self): + def test_invalid_delegation(self): # these show that in order for the delegation to work - # the _delegate_* methods need to be overriden to not raise a TypeError + # the _delegate_* methods need to be overridden to not raise + # a TypeError self.Delegate._add_delegate_accessors( delegate=self.Delegator, @@ -126,21 +137,16 @@ def test_invalida_delgation(self): delegate = self.Delegate(self.Delegator()) - def f(): + with pytest.raises(TypeError): delegate.foo - self.assertRaises(TypeError, f) - - def f(): + with pytest.raises(TypeError): delegate.foo = 5 - self.assertRaises(TypeError, f) - - def f(): + with pytest.raises(TypeError): delegate.foo() - self.assertRaises(TypeError, f) - + @pytest.mark.skipif(PYPY, reason="not relevant for PyPy") def test_memory_usage(self): # Delegate does not implement memory_usage. # Check that we fall back to in-built `__sizeof__` @@ -149,7 +155,7 @@ def test_memory_usage(self): sys.getsizeof(delegate) -class Ops(tm.TestCase): +class Ops(object): def _allow_na_ops(self, obj): """Whether to skip test cases including NaN""" @@ -159,7 +165,7 @@ def _allow_na_ops(self, obj): return False return True - def setUp(self): + def setup_method(self, method): self.bool_index = tm.makeBoolIndex(10, name='a') self.int_index = tm.makeIntIndex(10, name='a') self.float_index = tm.makeFloatIndex(10, name='a') @@ -171,19 +177,20 @@ def setUp(self): self.unicode_index = tm.makeUnicodeIndex(10, name='a') arr = np.random.randn(10) + self.bool_series = Series(arr, index=self.bool_index, name='a') self.int_series = Series(arr, index=self.int_index, name='a') self.float_series = Series(arr, index=self.float_index, name='a') self.dt_series = Series(arr, index=self.dt_index, name='a') self.dt_tz_series = self.dt_tz_index.to_series(keep_tz=True) self.period_series = Series(arr, index=self.period_index, name='a') self.string_series = Series(arr, index=self.string_index, name='a') + self.unicode_series = Series(arr, index=self.unicode_index, name='a') types = ['bool', 'int', 'float', 'dt', 'dt_tz', 'period', 'string', 'unicode'] - fmts = ["{0}_{1}".format(t, f) - for t in types for f in ['index', 'series']] - self.objs = [getattr(self, f) - for f in fmts if getattr(self, f, None) is not None] + self.indexes = [getattr(self, '{}_index'.format(t)) for t in types] + self.series = [getattr(self, '{}_series'.format(t)) for t in types] + self.objs = self.indexes + self.series def check_ops_properties(self, props, filter=None, ignore_failures=False): for op in props: @@ -214,9 +221,9 @@ def check_ops_properties(self, props, filter=None, ignore_failures=False): tm.assert_index_equal(result, expected) elif isinstance(result, np.ndarray) and isinstance(expected, np.ndarray): - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) else: - self.assertEqual(result, expected) + assert result == expected # freq raises AttributeError on an Int64Index because its not # defined we mostly care about Series here anyhow @@ -225,14 +232,15 @@ def check_ops_properties(self, props, filter=None, ignore_failures=False): # an object that is datetimelike will raise a TypeError, # otherwise an AttributeError + err = AttributeError if issubclass(type(o), DatetimeIndexOpsMixin): - self.assertRaises(TypeError, lambda: getattr(o, op)) - else: - self.assertRaises(AttributeError, - lambda: getattr(o, op)) + err = TypeError + + with pytest.raises(err): + getattr(o, op) - def test_binary_ops_docs(self): - from pandas import DataFrame, Panel + @pytest.mark.parametrize('klass', [Series, DataFrame, Panel]) + def test_binary_ops_docs(self, klass): op_map = {'add': '+', 'sub': '-', 'mul': '*', @@ -240,28 +248,24 @@ def test_binary_ops_docs(self): 'pow': '**', 'truediv': '/', 'floordiv': '//'} - for op_name in ['add', 'sub', 'mul', 'mod', 'pow', 'truediv', - 'floordiv']: - for klass in [Series, DataFrame, Panel]: - operand1 = klass.__name__.lower() - operand2 = 'other' - op = op_map[op_name] - expected_str = ' '.join([operand1, op, operand2]) - self.assertTrue(expected_str in getattr(klass, - op_name).__doc__) - - # reverse version of the binary ops - expected_str = ' '.join([operand2, op, operand1]) - self.assertTrue(expected_str in getattr(klass, 'r' + - op_name).__doc__) + for op_name in op_map: + operand1 = klass.__name__.lower() + operand2 = 'other' + op = op_map[op_name] + expected_str = ' '.join([operand1, op, operand2]) + assert expected_str in getattr(klass, op_name).__doc__ + + # reverse version of the binary ops + expected_str = ' '.join([operand2, op, operand1]) + assert expected_str in getattr(klass, 'r' + op_name).__doc__ class TestIndexOps(Ops): - def setUp(self): - super(TestIndexOps, self).setUp() - self.is_valid_objs = [o for o in self.objs if o._allow_index_ops] - self.not_valid_objs = [o for o in self.objs if not o._allow_index_ops] + def setup_method(self, method): + super(TestIndexOps, self).setup_method(method) + self.is_valid_objs = self.objs + self.not_valid_objs = [] def test_none_comparison(self): @@ -274,116 +278,69 @@ def test_none_comparison(self): # noinspection PyComparisonWithNone result = o == None # noqa - self.assertFalse(result.iat[0]) - self.assertFalse(result.iat[1]) + assert not result.iat[0] + assert not result.iat[1] # noinspection PyComparisonWithNone result = o != None # noqa - self.assertTrue(result.iat[0]) - self.assertTrue(result.iat[1]) + assert result.iat[0] + assert result.iat[1] result = None == o # noqa - self.assertFalse(result.iat[0]) - self.assertFalse(result.iat[1]) - - # this fails for numpy < 1.9 - # and oddly for *some* platforms - # result = None != o # noqa - # self.assertTrue(result.iat[0]) - # self.assertTrue(result.iat[1]) - - result = None > o - self.assertFalse(result.iat[0]) - self.assertFalse(result.iat[1]) + assert not result.iat[0] + assert not result.iat[1] + + result = None != o # noqa + assert result.iat[0] + assert result.iat[1] + + if (is_datetime64_dtype(o) or is_datetime64tz_dtype(o)): + # Following DatetimeIndex (and Timestamp) convention, + # inequality comparisons with Series[datetime64] raise + with pytest.raises(TypeError): + None > o + with pytest.raises(TypeError): + o > None + else: + result = None > o + assert not result.iat[0] + assert not result.iat[1] - result = o < None - self.assertFalse(result.iat[0]) - self.assertFalse(result.iat[1]) + result = o < None + assert not result.iat[0] + assert not result.iat[1] def test_ndarray_compat_properties(self): for o in self.objs: + # Check that we work. + for p in ['shape', 'dtype', 'T', 'nbytes']: + assert getattr(o, p, None) is not None - # check that we work - for p in ['shape', 'dtype', 'flags', 'T', 'strides', 'itemsize', - 'nbytes']: - self.assertIsNotNone(getattr(o, p, None)) - self.assertTrue(hasattr(o, 'base')) + # deprecated properties + for p in ['flags', 'strides', 'itemsize']: + with tm.assert_produces_warning(FutureWarning): + assert getattr(o, p, None) is not None - # if we have a datetimelike dtype then needs a view to work + with tm.assert_produces_warning(FutureWarning): + assert hasattr(o, 'base') + + # If we have a datetime-like dtype then needs a view to work # but the user is responsible for that try: - self.assertIsNotNone(o.data) + with tm.assert_produces_warning(FutureWarning): + assert o.data is not None except ValueError: pass - self.assertRaises(ValueError, o.item) # len > 1 - self.assertEqual(o.ndim, 1) - self.assertEqual(o.size, len(o)) + with pytest.raises(ValueError): + o.item() # len > 1 - self.assertEqual(Index([1]).item(), 1) - self.assertEqual(Series([1]).item(), 1) + assert o.ndim == 1 + assert o.size == len(o) - def test_ops(self): - for op in ['max', 'min']: - for o in self.objs: - result = getattr(o, op)() - if not isinstance(o, PeriodIndex): - expected = getattr(o.values, op)() - else: - expected = pd.Period(ordinal=getattr(o._values, op)(), - freq=o.freq) - try: - self.assertEqual(result, expected) - except TypeError: - # comparing tz-aware series with np.array results in - # TypeError - expected = expected.astype('M8[ns]').astype('int64') - self.assertEqual(result.value, expected) - - def test_nanops(self): - # GH 7261 - for op in ['max', 'min']: - for klass in [Index, Series]: - - obj = klass([np.nan, 2.0]) - self.assertEqual(getattr(obj, op)(), 2.0) - - obj = klass([np.nan]) - self.assertTrue(pd.isnull(getattr(obj, op)())) - - obj = klass([]) - self.assertTrue(pd.isnull(getattr(obj, op)())) - - obj = klass([pd.NaT, datetime(2011, 11, 1)]) - # check DatetimeIndex monotonic path - self.assertEqual(getattr(obj, op)(), datetime(2011, 11, 1)) - - obj = klass([pd.NaT, datetime(2011, 11, 1), pd.NaT]) - # check DatetimeIndex non-monotonic path - self.assertEqual(getattr(obj, op)(), datetime(2011, 11, 1)) - - # argmin/max - obj = Index(np.arange(5, dtype='int64')) - self.assertEqual(obj.argmin(), 0) - self.assertEqual(obj.argmax(), 4) - - obj = Index([np.nan, 1, np.nan, 2]) - self.assertEqual(obj.argmin(), 1) - self.assertEqual(obj.argmax(), 3) - - obj = Index([np.nan]) - self.assertEqual(obj.argmin(), -1) - self.assertEqual(obj.argmax(), -1) - - obj = Index([pd.NaT, datetime(2011, 11, 1), datetime(2011, 11, 2), - pd.NaT]) - self.assertEqual(obj.argmin(), 1) - self.assertEqual(obj.argmax(), 2) - - obj = Index([pd.NaT]) - self.assertEqual(obj.argmin(), -1) - self.assertEqual(obj.argmax(), -1) + assert Index([1]).item() == 1 + assert Series([1]).item() == 1 def test_value_counts_unique_nunique(self): for orig in self.objs: @@ -400,315 +357,316 @@ def test_value_counts_unique_nunique(self): if isinstance(o, Index) and o.is_boolean(): continue elif isinstance(o, Index): - expected_index = pd.Index(o[::-1]) + expected_index = Index(o[::-1]) expected_index.name = None o = o.repeat(range(1, len(o) + 1)) o.name = 'a' else: - expected_index = pd.Index(values[::-1]) + expected_index = Index(values[::-1]) idx = o.index.repeat(range(1, len(o) + 1)) - rep = np.repeat(values, range(1, len(o) + 1)) + # take-based repeat + indices = np.repeat(np.arange(len(o)), range(1, len(o) + 1)) + rep = values.take(indices) o = klass(rep, index=idx, name='a') # check values has the same dtype as the original - self.assertEqual(o.dtype, orig.dtype) + assert o.dtype == orig.dtype expected_s = Series(range(10, 0, -1), index=expected_index, dtype='int64', name='a') result = o.value_counts() tm.assert_series_equal(result, expected_s) - self.assertTrue(result.index.name is None) - self.assertEqual(result.name, 'a') + assert result.index.name is None + assert result.name == 'a' result = o.unique() if isinstance(o, Index): - self.assertTrue(isinstance(result, o.__class__)) - self.assert_index_equal(result, orig) - elif is_datetimetz(o): + assert isinstance(result, o.__class__) + tm.assert_index_equal(result, orig) + elif is_datetime64tz_dtype(o): # datetimetz Series returns array of Timestamp - self.assertEqual(result[0], orig[0]) + assert result[0] == orig[0] for r in result: - self.assertIsInstance(r, pd.Timestamp) - tm.assert_numpy_array_equal(result, - orig._values.asobject.values) + assert isinstance(r, Timestamp) + + tm.assert_numpy_array_equal( + result.astype(object), + orig._values.astype(object)) else: tm.assert_numpy_array_equal(result, orig.values) - self.assertEqual(o.nunique(), len(np.unique(o.values))) - - def test_value_counts_unique_nunique_null(self): - - for null_obj in [np.nan, None]: - for orig in self.objs: - o = orig.copy() - klass = type(o) - values = o._values - - if not self._allow_na_ops(o): - continue - - # special assign to the numpy array - if is_datetimetz(o): - if isinstance(o, DatetimeIndex): - v = o.asi8 - v[0:2] = iNaT - values = o._shallow_copy(v) - else: - o = o.copy() - o[0:2] = iNaT - values = o._values - - elif needs_i8_conversion(o): - values[0:2] = iNaT - values = o._shallow_copy(values) - else: - values[0:2] = null_obj - # check values has the same dtype as the original + assert o.nunique() == len(np.unique(o.values)) - self.assertEqual(values.dtype, o.dtype) + @pytest.mark.parametrize('null_obj', [np.nan, None]) + def test_value_counts_unique_nunique_null(self, null_obj): - # create repeated values, 'n'th element is repeated by n+1 - # times - if isinstance(o, (DatetimeIndex, PeriodIndex)): - expected_index = o.copy() - expected_index.name = None + for orig in self.objs: + o = orig.copy() + klass = type(o) + values = o._ndarray_values - # attach name to klass - o = klass(values.repeat(range(1, len(o) + 1))) - o.name = 'a' - else: - if is_datetimetz(o): - expected_index = orig._values._shallow_copy(values) - else: - expected_index = pd.Index(values) - expected_index.name = None - o = o.repeat(range(1, len(o) + 1)) - o.name = 'a' + if not self._allow_na_ops(o): + continue - # check values has the same dtype as the original - self.assertEqual(o.dtype, orig.dtype) - # check values correctly have NaN - nanloc = np.zeros(len(o), dtype=np.bool) - nanloc[:3] = True - if isinstance(o, Index): - self.assert_numpy_array_equal(pd.isnull(o), nanloc) + # special assign to the numpy array + if is_datetime64tz_dtype(o): + if isinstance(o, DatetimeIndex): + v = o.asi8 + v[0:2] = iNaT + values = o._shallow_copy(v) else: - exp = pd.Series(nanloc, o.index, name='a') - self.assert_series_equal(pd.isnull(o), exp) - - expected_s_na = Series(list(range(10, 2, -1)) + [3], - index=expected_index[9:0:-1], - dtype='int64', name='a') - expected_s = Series(list(range(10, 2, -1)), - index=expected_index[9:1:-1], - dtype='int64', name='a') - - result_s_na = o.value_counts(dropna=False) - tm.assert_series_equal(result_s_na, expected_s_na) - self.assertTrue(result_s_na.index.name is None) - self.assertEqual(result_s_na.name, 'a') - result_s = o.value_counts() - tm.assert_series_equal(o.value_counts(), expected_s) - self.assertTrue(result_s.index.name is None) - self.assertEqual(result_s.name, 'a') - - result = o.unique() - if isinstance(o, Index): - tm.assert_index_equal(result, - Index(values[1:], name='a')) - elif is_datetimetz(o): - # unable to compare NaT / nan - tm.assert_numpy_array_equal(result[1:], - values[2:].asobject.values) - self.assertIs(result[0], pd.NaT) - else: - tm.assert_numpy_array_equal(result[1:], values[2:]) - - self.assertTrue(pd.isnull(result[0])) - self.assertEqual(result.dtype, orig.dtype) + o = o.copy() + o[0:2] = iNaT + values = o._values - self.assertEqual(o.nunique(), 8) - self.assertEqual(o.nunique(dropna=False), 9) - - def test_value_counts_inferred(self): - klasses = [Index, Series] - for klass in klasses: - s_values = ['a', 'b', 'b', 'b', 'b', 'c', 'd', 'd', 'a', 'a'] - s = klass(s_values) - expected = Series([4, 3, 2, 1], index=['b', 'a', 'd', 'c']) - tm.assert_series_equal(s.value_counts(), expected) - - if isinstance(s, Index): - exp = Index(np.unique(np.array(s_values, dtype=np.object_))) - tm.assert_index_equal(s.unique(), exp) - else: - exp = np.unique(np.array(s_values, dtype=np.object_)) - tm.assert_numpy_array_equal(s.unique(), exp) - - self.assertEqual(s.nunique(), 4) - # don't sort, have to sort after the fact as not sorting is - # platform-dep - hist = s.value_counts(sort=False).sort_values() - expected = Series([3, 1, 4, 2], index=list('acbd')).sort_values() - tm.assert_series_equal(hist, expected) - - # sort ascending - hist = s.value_counts(ascending=True) - expected = Series([1, 2, 3, 4], index=list('cdab')) - tm.assert_series_equal(hist, expected) - - # relative histogram. - hist = s.value_counts(normalize=True) - expected = Series([.4, .3, .2, .1], index=['b', 'a', 'd', 'c']) - tm.assert_series_equal(hist, expected) - - def test_value_counts_bins(self): - klasses = [Index, Series] - for klass in klasses: - s_values = ['a', 'b', 'b', 'b', 'b', 'c', 'd', 'd', 'a', 'a'] - s = klass(s_values) - - # bins - self.assertRaises(TypeError, - lambda bins: s.value_counts(bins=bins), 1) - - s1 = Series([1, 1, 2, 3]) - res1 = s1.value_counts(bins=1) - exp1 = Series({0.998: 4}) - tm.assert_series_equal(res1, exp1) - res1n = s1.value_counts(bins=1, normalize=True) - exp1n = Series({0.998: 1.0}) - tm.assert_series_equal(res1n, exp1n) - - if isinstance(s1, Index): - tm.assert_index_equal(s1.unique(), Index([1, 2, 3])) - else: - exp = np.array([1, 2, 3], dtype=np.int64) - tm.assert_numpy_array_equal(s1.unique(), exp) - - self.assertEqual(s1.nunique(), 3) - - res4 = s1.value_counts(bins=4) - exp4 = Series({0.998: 2, - 1.5: 1, - 2.0: 0, - 2.5: 1}, index=[0.998, 2.5, 1.5, 2.0]) - tm.assert_series_equal(res4, exp4) - res4n = s1.value_counts(bins=4, normalize=True) - exp4n = Series( - {0.998: 0.5, - 1.5: 0.25, - 2.0: 0.0, - 2.5: 0.25}, index=[0.998, 2.5, 1.5, 2.0]) - tm.assert_series_equal(res4n, exp4n) - - # handle NA's properly - s_values = ['a', 'b', 'b', 'b', np.nan, np.nan, - 'd', 'd', 'a', 'a', 'b'] - s = klass(s_values) - expected = Series([4, 3, 2], index=['b', 'a', 'd']) - tm.assert_series_equal(s.value_counts(), expected) - - if isinstance(s, Index): - exp = Index(['a', 'b', np.nan, 'd']) - tm.assert_index_equal(s.unique(), exp) + elif needs_i8_conversion(o): + values[0:2] = iNaT + values = o._shallow_copy(values) else: - exp = np.array(['a', 'b', np.nan, 'd'], dtype=object) - tm.assert_numpy_array_equal(s.unique(), exp) - self.assertEqual(s.nunique(), 3) - - s = klass({}) - expected = Series([], dtype=np.int64) - tm.assert_series_equal(s.value_counts(), expected, - check_index_type=False) - # returned dtype differs depending on original - if isinstance(s, Index): - self.assert_index_equal(s.unique(), Index([]), - exact=False) - else: - self.assert_numpy_array_equal(s.unique(), np.array([]), - check_dtype=False) - - self.assertEqual(s.nunique(), 0) - - def test_value_counts_datetime64(self): - klasses = [Index, Series] - for klass in klasses: - # GH 3002, datetime64[ns] - # don't test names though - txt = "\n".join(['xxyyzz20100101PIE', 'xxyyzz20100101GUM', - 'xxyyzz20100101EGG', 'xxyyww20090101EGG', - 'foofoo20080909PIE', 'foofoo20080909GUM']) - f = StringIO(txt) - df = pd.read_fwf(f, widths=[6, 8, 3], - names=["person_id", "dt", "food"], - parse_dates=["dt"]) - - s = klass(df['dt'].copy()) - s.name = None - - idx = pd.to_datetime(['2010-01-01 00:00:00Z', - '2008-09-09 00:00:00Z', - '2009-01-01 00:00:00X']) - expected_s = Series([3, 2, 1], index=idx) - tm.assert_series_equal(s.value_counts(), expected_s) - - expected = np_array_datetime64_compat(['2010-01-01 00:00:00Z', - '2009-01-01 00:00:00Z', - '2008-09-09 00:00:00Z'], - dtype='datetime64[ns]') - if isinstance(s, Index): - tm.assert_index_equal(s.unique(), DatetimeIndex(expected)) - else: - tm.assert_numpy_array_equal(s.unique(), expected) - - self.assertEqual(s.nunique(), 3) - - # with NaT - s = df['dt'].copy() - s = klass([v for v in s.values] + [pd.NaT]) - - result = s.value_counts() - self.assertEqual(result.index.dtype, 'datetime64[ns]') - tm.assert_series_equal(result, expected_s) + values[0:2] = null_obj + # check values has the same dtype as the original - result = s.value_counts(dropna=False) - expected_s[pd.NaT] = 1 - tm.assert_series_equal(result, expected_s) + assert values.dtype == o.dtype - unique = s.unique() - self.assertEqual(unique.dtype, 'datetime64[ns]') + # create repeated values, 'n'th element is repeated by n+1 + # times + if isinstance(o, (DatetimeIndex, PeriodIndex)): + expected_index = o.copy() + expected_index.name = None - # numpy_array_equal cannot compare pd.NaT - if isinstance(s, Index): - exp_idx = DatetimeIndex(expected.tolist() + [pd.NaT]) - tm.assert_index_equal(unique, exp_idx) + # attach name to klass + o = klass(values.repeat(range(1, len(o) + 1))) + o.name = 'a' else: - tm.assert_numpy_array_equal(unique[:3], expected) - self.assertTrue(pd.isnull(unique[3])) - - self.assertEqual(s.nunique(), 3) - self.assertEqual(s.nunique(dropna=False), 4) + if isinstance(o, DatetimeIndex): + expected_index = orig._values._shallow_copy(values) + else: + expected_index = Index(values) + expected_index.name = None + o = o.repeat(range(1, len(o) + 1)) + o.name = 'a' - # timedelta64[ns] - td = df.dt - df.dt + timedelta(1) - td = klass(td, name='dt') + # check values has the same dtype as the original + assert o.dtype == orig.dtype + # check values correctly have NaN + nanloc = np.zeros(len(o), dtype=np.bool) + nanloc[:3] = True + if isinstance(o, Index): + tm.assert_numpy_array_equal(pd.isna(o), nanloc) + else: + exp = Series(nanloc, o.index, name='a') + tm.assert_series_equal(pd.isna(o), exp) + + expected_s_na = Series(list(range(10, 2, -1)) + [3], + index=expected_index[9:0:-1], + dtype='int64', name='a') + expected_s = Series(list(range(10, 2, -1)), + index=expected_index[9:1:-1], + dtype='int64', name='a') - result = td.value_counts() - expected_s = Series([6], index=[Timedelta('1day')], name='dt') - tm.assert_series_equal(result, expected_s) + result_s_na = o.value_counts(dropna=False) + tm.assert_series_equal(result_s_na, expected_s_na) + assert result_s_na.index.name is None + assert result_s_na.name == 'a' + result_s = o.value_counts() + tm.assert_series_equal(o.value_counts(), expected_s) + assert result_s.index.name is None + assert result_s.name == 'a' - expected = TimedeltaIndex(['1 days'], name='dt') - if isinstance(td, Index): - tm.assert_index_equal(td.unique(), expected) + result = o.unique() + if isinstance(o, Index): + tm.assert_index_equal(result, + Index(values[1:], name='a')) + elif is_datetime64tz_dtype(o): + # unable to compare NaT / nan + tm.assert_extension_array_equal(result[1:], values[2:]) + assert result[0] is pd.NaT else: - tm.assert_numpy_array_equal(td.unique(), expected.values) - - td2 = timedelta(1) + (df.dt - df.dt) - td2 = klass(td2, name='dt') - result2 = td2.value_counts() - tm.assert_series_equal(result2, expected_s) + tm.assert_numpy_array_equal(result[1:], values[2:]) + + assert pd.isna(result[0]) + assert result.dtype == orig.dtype + + assert o.nunique() == 8 + assert o.nunique(dropna=False) == 9 + + @pytest.mark.parametrize('klass', [Index, Series]) + def test_value_counts_inferred(self, klass): + s_values = ['a', 'b', 'b', 'b', 'b', 'c', 'd', 'd', 'a', 'a'] + s = klass(s_values) + expected = Series([4, 3, 2, 1], index=['b', 'a', 'd', 'c']) + tm.assert_series_equal(s.value_counts(), expected) + + if isinstance(s, Index): + exp = Index(np.unique(np.array(s_values, dtype=np.object_))) + tm.assert_index_equal(s.unique(), exp) + else: + exp = np.unique(np.array(s_values, dtype=np.object_)) + tm.assert_numpy_array_equal(s.unique(), exp) + + assert s.nunique() == 4 + # don't sort, have to sort after the fact as not sorting is + # platform-dep + hist = s.value_counts(sort=False).sort_values() + expected = Series([3, 1, 4, 2], index=list('acbd')).sort_values() + tm.assert_series_equal(hist, expected) + + # sort ascending + hist = s.value_counts(ascending=True) + expected = Series([1, 2, 3, 4], index=list('cdab')) + tm.assert_series_equal(hist, expected) + + # relative histogram. + hist = s.value_counts(normalize=True) + expected = Series([.4, .3, .2, .1], index=['b', 'a', 'd', 'c']) + tm.assert_series_equal(hist, expected) + + @pytest.mark.parametrize('klass', [Index, Series]) + def test_value_counts_bins(self, klass): + s_values = ['a', 'b', 'b', 'b', 'b', 'c', 'd', 'd', 'a', 'a'] + s = klass(s_values) + + # bins + with pytest.raises(TypeError): + s.value_counts(bins=1) + + s1 = Series([1, 1, 2, 3]) + res1 = s1.value_counts(bins=1) + exp1 = Series({Interval(0.997, 3.0): 4}) + tm.assert_series_equal(res1, exp1) + res1n = s1.value_counts(bins=1, normalize=True) + exp1n = Series({Interval(0.997, 3.0): 1.0}) + tm.assert_series_equal(res1n, exp1n) + + if isinstance(s1, Index): + tm.assert_index_equal(s1.unique(), Index([1, 2, 3])) + else: + exp = np.array([1, 2, 3], dtype=np.int64) + tm.assert_numpy_array_equal(s1.unique(), exp) + + assert s1.nunique() == 3 + + # these return the same + res4 = s1.value_counts(bins=4, dropna=True) + intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0]) + exp4 = Series([2, 1, 1, 0], index=intervals.take([0, 3, 1, 2])) + tm.assert_series_equal(res4, exp4) + + res4 = s1.value_counts(bins=4, dropna=False) + intervals = IntervalIndex.from_breaks([0.997, 1.5, 2.0, 2.5, 3.0]) + exp4 = Series([2, 1, 1, 0], index=intervals.take([0, 3, 1, 2])) + tm.assert_series_equal(res4, exp4) + + res4n = s1.value_counts(bins=4, normalize=True) + exp4n = Series([0.5, 0.25, 0.25, 0], + index=intervals.take([0, 3, 1, 2])) + tm.assert_series_equal(res4n, exp4n) + + # handle NA's properly + s_values = ['a', 'b', 'b', 'b', np.nan, np.nan, + 'd', 'd', 'a', 'a', 'b'] + s = klass(s_values) + expected = Series([4, 3, 2], index=['b', 'a', 'd']) + tm.assert_series_equal(s.value_counts(), expected) + + if isinstance(s, Index): + exp = Index(['a', 'b', np.nan, 'd']) + tm.assert_index_equal(s.unique(), exp) + else: + exp = np.array(['a', 'b', np.nan, 'd'], dtype=object) + tm.assert_numpy_array_equal(s.unique(), exp) + assert s.nunique() == 3 + + s = klass({}) + expected = Series([], dtype=np.int64) + tm.assert_series_equal(s.value_counts(), expected, + check_index_type=False) + # returned dtype differs depending on original + if isinstance(s, Index): + tm.assert_index_equal(s.unique(), Index([]), exact=False) + else: + tm.assert_numpy_array_equal(s.unique(), np.array([]), + check_dtype=False) + + assert s.nunique() == 0 + + @pytest.mark.parametrize('klass', [Index, Series]) + def test_value_counts_datetime64(self, klass): + + # GH 3002, datetime64[ns] + # don't test names though + txt = "\n".join(['xxyyzz20100101PIE', 'xxyyzz20100101GUM', + 'xxyyzz20100101EGG', 'xxyyww20090101EGG', + 'foofoo20080909PIE', 'foofoo20080909GUM']) + f = StringIO(txt) + df = pd.read_fwf(f, widths=[6, 8, 3], + names=["person_id", "dt", "food"], + parse_dates=["dt"]) + + s = klass(df['dt'].copy()) + s.name = None + idx = pd.to_datetime(['2010-01-01 00:00:00', + '2008-09-09 00:00:00', + '2009-01-01 00:00:00']) + expected_s = Series([3, 2, 1], index=idx) + tm.assert_series_equal(s.value_counts(), expected_s) + + expected = np_array_datetime64_compat(['2010-01-01 00:00:00', + '2009-01-01 00:00:00', + '2008-09-09 00:00:00'], + dtype='datetime64[ns]') + if isinstance(s, Index): + tm.assert_index_equal(s.unique(), DatetimeIndex(expected)) + else: + tm.assert_numpy_array_equal(s.unique(), expected) + + assert s.nunique() == 3 + + # with NaT + s = df['dt'].copy() + s = klass([v for v in s.values] + [pd.NaT]) + + result = s.value_counts() + assert result.index.dtype == 'datetime64[ns]' + tm.assert_series_equal(result, expected_s) + + result = s.value_counts(dropna=False) + expected_s[pd.NaT] = 1 + tm.assert_series_equal(result, expected_s) + + unique = s.unique() + assert unique.dtype == 'datetime64[ns]' + + # numpy_array_equal cannot compare pd.NaT + if isinstance(s, Index): + exp_idx = DatetimeIndex(expected.tolist() + [pd.NaT]) + tm.assert_index_equal(unique, exp_idx) + else: + tm.assert_numpy_array_equal(unique[:3], expected) + assert pd.isna(unique[3]) + + assert s.nunique() == 3 + assert s.nunique(dropna=False) == 4 + + # timedelta64[ns] + td = df.dt - df.dt + timedelta(1) + td = klass(td, name='dt') + + result = td.value_counts() + expected_s = Series([6], index=[Timedelta('1day')], name='dt') + tm.assert_series_equal(result, expected_s) + + expected = TimedeltaIndex(['1 days'], name='dt') + if isinstance(td, Index): + tm.assert_index_equal(td.unique(), expected) + else: + tm.assert_numpy_array_equal(td.unique(), expected.values) + + td2 = timedelta(1) + (df.dt - df.dt) + td2 = klass(td2, name='dt') + result2 = td2.value_counts() + tm.assert_series_equal(result2, expected_s) def test_factorize(self): for orig in self.objs: @@ -723,14 +681,14 @@ def test_factorize(self): exp_uniques = o labels, uniques = o.factorize() - self.assert_numpy_array_equal(labels, exp_arr) + tm.assert_numpy_array_equal(labels, exp_arr) if isinstance(o, Series): - self.assert_index_equal(uniques, Index(orig), - check_names=False) + tm.assert_index_equal(uniques, Index(orig), + check_names=False) else: # factorize explicitly resets name - self.assert_index_equal(uniques, exp_uniques, - check_names=False) + tm.assert_index_equal(uniques, exp_uniques, + check_names=False) def test_factorize_repeated(self): for orig in self.objs: @@ -753,24 +711,24 @@ def test_factorize_repeated(self): dtype=np.intp) labels, uniques = n.factorize(sort=True) - self.assert_numpy_array_equal(labels, exp_arr) + tm.assert_numpy_array_equal(labels, exp_arr) if isinstance(o, Series): - self.assert_index_equal(uniques, Index(orig).sort_values(), - check_names=False) + tm.assert_index_equal(uniques, Index(orig).sort_values(), + check_names=False) else: - self.assert_index_equal(uniques, o, check_names=False) + tm.assert_index_equal(uniques, o, check_names=False) exp_arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4], np.intp) labels, uniques = n.factorize(sort=False) - self.assert_numpy_array_equal(labels, exp_arr) + tm.assert_numpy_array_equal(labels, exp_arr) if isinstance(o, Series): expected = Index(o.iloc[5:10].append(o.iloc[:5])) - self.assert_index_equal(uniques, expected, check_names=False) + tm.assert_index_equal(uniques, expected, check_names=False) else: expected = o[5:10].append(o[:5]) - self.assert_index_equal(uniques, expected, check_names=False) + tm.assert_index_equal(uniques, expected, check_names=False) def test_duplicated_drop_duplicates_index(self): # GH 4060 @@ -788,13 +746,13 @@ def test_duplicated_drop_duplicates_index(self): expected = np.array([False] * len(original), dtype=bool) duplicated = original.duplicated() tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool result = original.drop_duplicates() tm.assert_index_equal(result, original) - self.assertFalse(result is original) + assert result is not original # has_duplicates - self.assertFalse(original.has_duplicates) + assert not original.has_duplicates # create repeated values, 3rd and 5th values are duplicated idx = original[list(range(len(original))) + [5, 3]] @@ -802,7 +760,7 @@ def test_duplicated_drop_duplicates_index(self): dtype=bool) duplicated = idx.duplicated() tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool tm.assert_index_equal(idx.drop_duplicates(), original) base = [False] * len(idx) @@ -812,7 +770,7 @@ def test_duplicated_drop_duplicates_index(self): duplicated = idx.duplicated(keep='last') tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool result = idx.drop_duplicates(keep='last') tm.assert_index_equal(result, idx[~expected]) @@ -823,13 +781,13 @@ def test_duplicated_drop_duplicates_index(self): duplicated = idx.duplicated(keep=False) tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool result = idx.drop_duplicates(keep=False) tm.assert_index_equal(result, idx[~expected]) - with tm.assertRaisesRegexp( - TypeError, r"drop_duplicates\(\) got an unexpected " - "keyword argument"): + with pytest.raises(TypeError, + match=(r"drop_duplicates\(\) got an " + r"unexpected keyword argument")): idx.drop_duplicates(inplace=True) else: @@ -838,7 +796,7 @@ def test_duplicated_drop_duplicates_index(self): tm.assert_series_equal(original.duplicated(), expected) result = original.drop_duplicates() tm.assert_series_equal(result, original) - self.assertFalse(result is original) + assert result is not original idx = original.index[list(range(len(original))) + [5, 3]] values = original._values[list(range(len(original))) + [5, 3]] @@ -898,11 +856,11 @@ def test_fillna(self): # values will not be changed result = o.fillna(o.astype(object).values[0]) if isinstance(o, Index): - self.assert_index_equal(o, result) + tm.assert_index_equal(o, result) else: - self.assert_series_equal(o, result) + tm.assert_series_equal(o, result) # check shallow_copied - self.assertFalse(o is result) + assert o is not result for null_obj in [np.nan, None]: for orig in self.objs: @@ -928,16 +886,17 @@ def test_fillna(self): o = klass(values) # check values has the same dtype as the original - self.assertEqual(o.dtype, orig.dtype) + assert o.dtype == orig.dtype result = o.fillna(fill_value) if isinstance(o, Index): - self.assert_index_equal(result, expected) + tm.assert_index_equal(result, expected) else: - self.assert_series_equal(result, expected) + tm.assert_series_equal(result, expected) # check shallow_copied - self.assertFalse(o is result) + assert o is not result + @pytest.mark.skipif(PYPY, reason="not relevant for PyPy") def test_memory_usage(self): for o in self.objs: res = o.memory_usage() @@ -946,83 +905,447 @@ def test_memory_usage(self): if (is_object_dtype(o) or (isinstance(o, Series) and is_object_dtype(o.index))): # if there are objects, only deep will pick them up - self.assertTrue(res_deep > res) + assert res_deep > res else: - self.assertEqual(res, res_deep) + assert res == res_deep if isinstance(o, Series): - self.assertEqual( - (o.memory_usage(index=False) + - o.index.memory_usage()), - o.memory_usage(index=True) - ) + assert ((o.memory_usage(index=False) + + o.index.memory_usage()) == + o.memory_usage(index=True)) # sys.getsizeof will call the .memory_usage with # deep=True, and add on some GC overhead diff = res_deep - sys.getsizeof(o) - self.assertTrue(abs(diff) < 100) + assert abs(diff) < 100 def test_searchsorted(self): # See gh-12238 for o in self.objs: index = np.searchsorted(o, max(o)) - self.assertTrue(0 <= index <= len(o)) + assert 0 <= index <= len(o) index = np.searchsorted(o, max(o), sorter=range(len(o))) - self.assertTrue(0 <= index <= len(o)) + assert 0 <= index <= len(o) def test_validate_bool_args(self): invalid_values = [1, "True", [1, 2, 3], 5.0] for value in invalid_values: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): self.int_series.drop_duplicates(inplace=value) + def test_getitem(self): + for i in self.indexes: + s = pd.Series(i) + + assert i[0] == s.iloc[0] + assert i[5] == s.iloc[5] + assert i[-1] == s.iloc[-1] + + assert i[-1] == i[9] + + with pytest.raises(IndexError): + i[20] + with pytest.raises(IndexError): + s.iloc[20] + + @pytest.mark.parametrize('indexer_klass', [list, pd.Index]) + @pytest.mark.parametrize('indexer', [[True] * 10, [False] * 10, + [True, False, True, True, False, + False, True, True, False, True]]) + def test_bool_indexing(self, indexer_klass, indexer): + # GH 22533 + for idx in self.indexes: + exp_idx = [i for i in range(len(indexer)) if indexer[i]] + tm.assert_index_equal(idx[indexer_klass(indexer)], idx[exp_idx]) + s = pd.Series(idx) + tm.assert_series_equal(s[indexer_klass(indexer)], s.iloc[exp_idx]) + class TestTranspose(Ops): errmsg = "the 'axes' parameter is not supported" def test_transpose(self): for obj in self.objs: - if isinstance(obj, Index): - tm.assert_index_equal(obj.transpose(), obj) - else: - tm.assert_series_equal(obj.transpose(), obj) + tm.assert_equal(obj.transpose(), obj) def test_transpose_non_default_axes(self): for obj in self.objs: - tm.assertRaisesRegexp(ValueError, self.errmsg, - obj.transpose, 1) - tm.assertRaisesRegexp(ValueError, self.errmsg, - obj.transpose, axes=1) + with pytest.raises(ValueError, match=self.errmsg): + obj.transpose(1) + with pytest.raises(ValueError, match=self.errmsg): + obj.transpose(axes=1) def test_numpy_transpose(self): for obj in self.objs: - if isinstance(obj, Index): - tm.assert_index_equal(np.transpose(obj), obj) - else: - tm.assert_series_equal(np.transpose(obj), obj) + tm.assert_equal(np.transpose(obj), obj) - tm.assertRaisesRegexp(ValueError, self.errmsg, - np.transpose, obj, axes=1) + with pytest.raises(ValueError, match=self.errmsg): + np.transpose(obj, axes=1) -class TestNoNewAttributesMixin(tm.TestCase): +class TestNoNewAttributesMixin(object): def test_mixin(self): class T(NoNewAttributesMixin): pass t = T() - self.assertFalse(hasattr(t, "__frozen")) + assert not hasattr(t, "__frozen") + t.a = "test" - self.assertEqual(t.a, "test") + assert t.a == "test" + t._freeze() - # self.assertTrue("__frozen" not in dir(t)) - self.assertIs(getattr(t, "__frozen"), True) + assert "__frozen" in dir(t) + assert getattr(t, "__frozen") - def f(): + with pytest.raises(AttributeError): t.b = "test" - self.assertRaises(AttributeError, f) - self.assertFalse(hasattr(t, "b")) + assert not hasattr(t, "b") + + +class TestToIterable(object): + # test that we convert an iterable to python types + + dtypes = [ + ('int8', (int, long)), + ('int16', (int, long)), + ('int32', (int, long)), + ('int64', (int, long)), + ('uint8', (int, long)), + ('uint16', (int, long)), + ('uint32', (int, long)), + ('uint64', (int, long)), + ('float16', float), + ('float32', float), + ('float64', float), + ('datetime64[ns]', Timestamp), + ('datetime64[ns, US/Eastern]', Timestamp), + ('timedelta64[ns]', Timedelta)] + + @pytest.mark.parametrize( + 'dtype, rdtype', dtypes) + @pytest.mark.parametrize( + 'method', + [ + lambda x: x.tolist(), + lambda x: x.to_list(), + lambda x: list(x), + lambda x: list(x.__iter__()), + ], ids=['tolist', 'to_list', 'list', 'iter']) + @pytest.mark.parametrize('typ', [Series, Index]) + @pytest.mark.filterwarnings("ignore:\\n Passing:FutureWarning") + # TODO(GH-24559): Remove the filterwarnings + def test_iterable(self, typ, method, dtype, rdtype): + # gh-10904 + # gh-13258 + # coerce iteration to underlying python / pandas types + s = typ([1], dtype=dtype) + result = method(s)[0] + assert isinstance(result, rdtype) + + @pytest.mark.parametrize( + 'dtype, rdtype, obj', + [ + ('object', object, 'a'), + ('object', (int, long), 1), + ('category', object, 'a'), + ('category', (int, long), 1)]) + @pytest.mark.parametrize( + 'method', + [ + lambda x: x.tolist(), + lambda x: x.to_list(), + lambda x: list(x), + lambda x: list(x.__iter__()), + ], ids=['tolist', 'to_list', 'list', 'iter']) + @pytest.mark.parametrize('typ', [Series, Index]) + def test_iterable_object_and_category(self, typ, method, + dtype, rdtype, obj): + # gh-10904 + # gh-13258 + # coerce iteration to underlying python / pandas types + s = typ([obj], dtype=dtype) + result = method(s)[0] + assert isinstance(result, rdtype) + + @pytest.mark.parametrize( + 'dtype, rdtype', dtypes) + def test_iterable_items(self, dtype, rdtype): + # gh-13258 + # test items / iteritems yields the correct boxed scalars + # this only applies to series + s = Series([1], dtype=dtype) + _, result = list(s.items())[0] + assert isinstance(result, rdtype) + + _, result = list(s.iteritems())[0] + assert isinstance(result, rdtype) + + @pytest.mark.parametrize( + 'dtype, rdtype', + dtypes + [ + ('object', (int, long)), + ('category', (int, long))]) + @pytest.mark.parametrize('typ', [Series, Index]) + @pytest.mark.filterwarnings("ignore:\\n Passing:FutureWarning") + # TODO(GH-24559): Remove the filterwarnings + def test_iterable_map(self, typ, dtype, rdtype): + # gh-13236 + # coerce iteration to underlying python / pandas types + s = typ([1], dtype=dtype) + result = s.map(type)[0] + if not isinstance(rdtype, tuple): + rdtype = tuple([rdtype]) + assert result in rdtype + + @pytest.mark.parametrize( + 'method', + [ + lambda x: x.tolist(), + lambda x: x.to_list(), + lambda x: list(x), + lambda x: list(x.__iter__()), + ], ids=['tolist', 'to_list', 'list', 'iter']) + def test_categorial_datetimelike(self, method): + i = CategoricalIndex([Timestamp('1999-12-31'), + Timestamp('2000-12-31')]) + + result = method(i)[0] + assert isinstance(result, Timestamp) + + def test_iter_box(self): + vals = [Timestamp('2011-01-01'), Timestamp('2011-01-02')] + s = Series(vals) + assert s.dtype == 'datetime64[ns]' + for res, exp in zip(s, vals): + assert isinstance(res, Timestamp) + assert res.tz is None + assert res == exp + + vals = [Timestamp('2011-01-01', tz='US/Eastern'), + Timestamp('2011-01-02', tz='US/Eastern')] + s = Series(vals) + + assert s.dtype == 'datetime64[ns, US/Eastern]' + for res, exp in zip(s, vals): + assert isinstance(res, Timestamp) + assert res.tz == exp.tz + assert res == exp + + # timedelta + vals = [Timedelta('1 days'), Timedelta('2 days')] + s = Series(vals) + assert s.dtype == 'timedelta64[ns]' + for res, exp in zip(s, vals): + assert isinstance(res, Timedelta) + assert res == exp + + # period + vals = [pd.Period('2011-01-01', freq='M'), + pd.Period('2011-01-02', freq='M')] + s = Series(vals) + assert s.dtype == 'Period[M]' + for res, exp in zip(s, vals): + assert isinstance(res, pd.Period) + assert res.freq == 'M' + assert res == exp + + +@pytest.mark.parametrize('array, expected_type, dtype', [ + (np.array([0, 1], dtype=np.int64), np.ndarray, 'int64'), + (np.array(['a', 'b']), np.ndarray, 'object'), + (pd.Categorical(['a', 'b']), pd.Categorical, 'category'), + (pd.DatetimeIndex(['2017', '2018'], tz="US/Central"), DatetimeArray, + 'datetime64[ns, US/Central]'), + + (pd.PeriodIndex([2018, 2019], freq='A'), pd.core.arrays.PeriodArray, + pd.core.dtypes.dtypes.PeriodDtype("A-DEC")), + (pd.IntervalIndex.from_breaks([0, 1, 2]), pd.core.arrays.IntervalArray, + 'interval'), + + # This test is currently failing for datetime64[ns] and timedelta64[ns]. + # The NumPy type system is sufficient for representing these types, so + # we just use NumPy for Series / DataFrame columns of these types (so + # we get consolidation and so on). + # However, DatetimeIndex and TimedeltaIndex use the DateLikeArray + # abstraction to for code reuse. + # At the moment, we've judged that allowing this test to fail is more + # practical that overriding Series._values to special case + # Series[M8[ns]] and Series[m8[ns]] to return a DateLikeArray. + pytest.param( + pd.DatetimeIndex(['2017', '2018']), np.ndarray, 'datetime64[ns]', + marks=[pytest.mark.xfail(reason="datetime _values", strict=True)] + ), + pytest.param( + pd.TimedeltaIndex([10**10]), np.ndarray, 'm8[ns]', + marks=[pytest.mark.xfail(reason="timedelta _values", strict=True)] + ), + +]) +def test_values_consistent(array, expected_type, dtype): + l_values = pd.Series(array)._values + r_values = pd.Index(array)._values + assert type(l_values) is expected_type + assert type(l_values) is type(r_values) + + tm.assert_equal(l_values, r_values) + + +@pytest.mark.parametrize('array, expected', [ + (np.array([0, 1], dtype=np.int64), np.array([0, 1], dtype=np.int64)), + (np.array(['0', '1']), np.array(['0', '1'], dtype=object)), + (pd.Categorical(['a', 'a']), np.array([0, 0], dtype='int8')), + (pd.DatetimeIndex(['2017-01-01T00:00:00']), + np.array(['2017-01-01T00:00:00'], dtype='M8[ns]')), + (pd.DatetimeIndex(['2017-01-01T00:00:00'], tz="US/Eastern"), + np.array(['2017-01-01T05:00:00'], dtype='M8[ns]')), + (pd.TimedeltaIndex([10**10]), np.array([10**10], dtype='m8[ns]')), + (pd.PeriodIndex(['2017', '2018'], freq='D'), + np.array([17167, 17532], dtype=np.int64)), +]) +def test_ndarray_values(array, expected): + l_values = pd.Series(array)._ndarray_values + r_values = pd.Index(array)._ndarray_values + tm.assert_numpy_array_equal(l_values, r_values) + tm.assert_numpy_array_equal(l_values, expected) + + +@pytest.mark.parametrize("arr", [ + np.array([1, 2, 3]), +]) +def test_numpy_array(arr): + ser = pd.Series(arr) + result = ser.array + expected = PandasArray(arr) + tm.assert_extension_array_equal(result, expected) + + +def test_numpy_array_all_dtypes(any_numpy_dtype): + ser = pd.Series(dtype=any_numpy_dtype) + result = ser.array + if is_datetime64_dtype(any_numpy_dtype): + assert isinstance(result, DatetimeArray) + elif is_timedelta64_dtype(any_numpy_dtype): + assert isinstance(result, TimedeltaArray) + else: + assert isinstance(result, PandasArray) + + +@pytest.mark.parametrize("array, attr", [ + (pd.Categorical(['a', 'b']), '_codes'), + (pd.core.arrays.period_array(['2000', '2001'], freq='D'), '_data'), + (pd.core.arrays.integer_array([0, np.nan]), '_data'), + (pd.core.arrays.IntervalArray.from_breaks([0, 1]), '_left'), + (pd.SparseArray([0, 1]), '_sparse_values'), + (DatetimeArray(np.array([1, 2], dtype="datetime64[ns]")), "_data"), + # tz-aware Datetime + (DatetimeArray(np.array(['2000-01-01T12:00:00', + '2000-01-02T12:00:00'], + dtype='M8[ns]'), + dtype=DatetimeTZDtype(tz="US/Central")), + '_data'), +]) +@pytest.mark.parametrize('box', [pd.Series, pd.Index]) +def test_array(array, attr, box): + if array.dtype.name in ('Int64', 'Sparse[int64, 0]') and box is pd.Index: + pytest.skip("No index type for {}".format(array.dtype)) + result = box(array, copy=False).array + + if attr: + array = getattr(array, attr) + result = getattr(result, attr) + + assert result is array + + +def test_array_multiindex_raises(): + idx = pd.MultiIndex.from_product([['A'], ['a', 'b']]) + with pytest.raises(ValueError, match='MultiIndex'): + idx.array + + +@pytest.mark.parametrize('array, expected', [ + (np.array([1, 2], dtype=np.int64), np.array([1, 2], dtype=np.int64)), + (pd.Categorical(['a', 'b']), np.array(['a', 'b'], dtype=object)), + (pd.core.arrays.period_array(['2000', '2001'], freq='D'), + np.array([pd.Period('2000', freq="D"), pd.Period('2001', freq='D')])), + (pd.core.arrays.integer_array([0, np.nan]), + np.array([0, np.nan], dtype=object)), + (pd.core.arrays.IntervalArray.from_breaks([0, 1, 2]), + np.array([pd.Interval(0, 1), pd.Interval(1, 2)], dtype=object)), + (pd.SparseArray([0, 1]), np.array([0, 1], dtype=np.int64)), + + # tz-naive datetime + (DatetimeArray(np.array(['2000', '2001'], dtype='M8[ns]')), + np.array(['2000', '2001'], dtype='M8[ns]')), + + # tz-aware stays tz`-aware + (DatetimeArray(np.array(['2000-01-01T06:00:00', + '2000-01-02T06:00:00'], + dtype='M8[ns]'), + dtype=DatetimeTZDtype(tz='US/Central')), + np.array([pd.Timestamp('2000-01-01', tz='US/Central'), + pd.Timestamp('2000-01-02', tz='US/Central')])), + + # Timedelta + (TimedeltaArray(np.array([0, 3600000000000], dtype='i8'), freq='H'), + np.array([0, 3600000000000], dtype='m8[ns]')), +]) +@pytest.mark.parametrize('box', [pd.Series, pd.Index]) +def test_to_numpy(array, expected, box): + thing = box(array) + + if array.dtype.name in ('Int64', 'Sparse[int64, 0]') and box is pd.Index: + pytest.skip("No index type for {}".format(array.dtype)) + + result = thing.to_numpy() + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize("as_series", [True, False]) +@pytest.mark.parametrize("arr", [ + np.array([1, 2, 3], dtype="int64"), + np.array(['a', 'b', 'c'], dtype=object), +]) +def test_to_numpy_copy(arr, as_series): + obj = pd.Index(arr, copy=False) + if as_series: + obj = pd.Series(obj.values, copy=False) + + # no copy by default + result = obj.to_numpy() + assert np.shares_memory(arr, result) is True + + result = obj.to_numpy(copy=False) + assert np.shares_memory(arr, result) is True + + # copy=True + result = obj.to_numpy(copy=True) + assert np.shares_memory(arr, result) is False + + +@pytest.mark.parametrize("as_series", [True, False]) +def test_to_numpy_dtype(as_series): + tz = "US/Eastern" + obj = pd.DatetimeIndex(['2000', '2001'], tz=tz) + if as_series: + obj = pd.Series(obj) + + # preserve tz by default + result = obj.to_numpy() + expected = np.array([pd.Timestamp('2000', tz=tz), + pd.Timestamp('2001', tz=tz)], + dtype=object) + tm.assert_numpy_array_equal(result, expected) + + result = obj.to_numpy(dtype="object") + tm.assert_numpy_array_equal(result, expected) + + result = obj.to_numpy(dtype="M8[ns]") + expected = np.array(['2000-01-01T05', '2001-01-01T05'], + dtype='M8[ns]') + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/test_categorical.py b/pandas/tests/test_categorical.py deleted file mode 100644 index ea2697ec19df3..0000000000000 --- a/pandas/tests/test_categorical.py +++ /dev/null @@ -1,4427 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=E1101,E1103,W0232 - -import pytest -import sys -from datetime import datetime -from distutils.version import LooseVersion - -import numpy as np - -from pandas.types.dtypes import CategoricalDtype -from pandas.types.common import (is_categorical_dtype, - is_object_dtype, - is_float_dtype, - is_integer_dtype) - -import pandas as pd -import pandas.compat as compat -import pandas.util.testing as tm -from pandas import (Categorical, Index, Series, DataFrame, - Timestamp, CategoricalIndex, isnull, - date_range, DatetimeIndex, - period_range, PeriodIndex, - timedelta_range, TimedeltaIndex, NaT) -from pandas.compat import range, lrange, u, PY3 -from pandas.core.config import option_context - -# GH 12066 -# flake8: noqa - - -class TestCategorical(tm.TestCase): - - def setUp(self): - self.factor = Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'], - ordered=True) - - def test_getitem(self): - self.assertEqual(self.factor[0], 'a') - self.assertEqual(self.factor[-1], 'c') - - subf = self.factor[[0, 1, 2]] - tm.assert_numpy_array_equal(subf._codes, - np.array([0, 1, 1], dtype=np.int8)) - - subf = self.factor[np.asarray(self.factor) == 'c'] - tm.assert_numpy_array_equal(subf._codes, - np.array([2, 2, 2], dtype=np.int8)) - - def test_getitem_listlike(self): - - # GH 9469 - # properly coerce the input indexers - np.random.seed(1) - c = Categorical(np.random.randint(0, 5, size=150000).astype(np.int8)) - result = c.codes[np.array([100000]).astype(np.int64)] - expected = c[np.array([100000]).astype(np.int64)].codes - self.assert_numpy_array_equal(result, expected) - - def test_getitem_category_type(self): - # GH 14580 - # test iloc() on Series with Categorical data - - s = pd.Series([1, 2, 3]).astype('category') - - # get slice - result = s.iloc[0:2] - expected = pd.Series([1, 2]).astype('category', categories=[1, 2, 3]) - tm.assert_series_equal(result, expected) - - # get list of indexes - result = s.iloc[[0, 1]] - expected = pd.Series([1, 2]).astype('category', categories=[1, 2, 3]) - tm.assert_series_equal(result, expected) - - # get boolean array - result = s.iloc[[True, False, False]] - expected = pd.Series([1]).astype('category', categories=[1, 2, 3]) - tm.assert_series_equal(result, expected) - - def test_setitem(self): - - # int/positional - c = self.factor.copy() - c[0] = 'b' - self.assertEqual(c[0], 'b') - c[-1] = 'a' - self.assertEqual(c[-1], 'a') - - # boolean - c = self.factor.copy() - indexer = np.zeros(len(c), dtype='bool') - indexer[0] = True - indexer[-1] = True - c[indexer] = 'c' - expected = Categorical(['c', 'b', 'b', 'a', 'a', 'c', 'c', 'c'], - ordered=True) - - self.assert_categorical_equal(c, expected) - - def test_setitem_listlike(self): - - # GH 9469 - # properly coerce the input indexers - np.random.seed(1) - c = Categorical(np.random.randint(0, 5, size=150000).astype( - np.int8)).add_categories([-1000]) - indexer = np.array([100000]).astype(np.int64) - c[indexer] = -1000 - - # we are asserting the code result here - # which maps to the -1000 category - result = c.codes[np.array([100000]).astype(np.int64)] - self.assertEqual(result, np.array([5], dtype='int8')) - - def test_constructor_unsortable(self): - - # it works! - arr = np.array([1, 2, 3, datetime.now()], dtype='O') - factor = Categorical(arr, ordered=False) - self.assertFalse(factor.ordered) - - # this however will raise as cannot be sorted - self.assertRaises( - TypeError, lambda: Categorical(arr, ordered=True)) - - def test_is_equal_dtype(self): - - # test dtype comparisons between cats - - c1 = Categorical(list('aabca'), categories=list('abc'), ordered=False) - c2 = Categorical(list('aabca'), categories=list('cab'), ordered=False) - c3 = Categorical(list('aabca'), categories=list('cab'), ordered=True) - self.assertTrue(c1.is_dtype_equal(c1)) - self.assertTrue(c2.is_dtype_equal(c2)) - self.assertTrue(c3.is_dtype_equal(c3)) - self.assertFalse(c1.is_dtype_equal(c2)) - self.assertFalse(c1.is_dtype_equal(c3)) - self.assertFalse(c1.is_dtype_equal(Index(list('aabca')))) - self.assertFalse(c1.is_dtype_equal(c1.astype(object))) - self.assertTrue(c1.is_dtype_equal(CategoricalIndex(c1))) - self.assertFalse(c1.is_dtype_equal( - CategoricalIndex(c1, categories=list('cab')))) - self.assertFalse(c1.is_dtype_equal(CategoricalIndex(c1, ordered=True))) - - def test_constructor(self): - - exp_arr = np.array(["a", "b", "c", "a", "b", "c"], dtype=np.object_) - c1 = Categorical(exp_arr) - self.assert_numpy_array_equal(c1.__array__(), exp_arr) - c2 = Categorical(exp_arr, categories=["a", "b", "c"]) - self.assert_numpy_array_equal(c2.__array__(), exp_arr) - c2 = Categorical(exp_arr, categories=["c", "b", "a"]) - self.assert_numpy_array_equal(c2.__array__(), exp_arr) - - # categories must be unique - def f(): - Categorical([1, 2], [1, 2, 2]) - - self.assertRaises(ValueError, f) - - def f(): - Categorical(["a", "b"], ["a", "b", "b"]) - - self.assertRaises(ValueError, f) - - # The default should be unordered - c1 = Categorical(["a", "b", "c", "a"]) - self.assertFalse(c1.ordered) - - # Categorical as input - c1 = Categorical(["a", "b", "c", "a"]) - c2 = Categorical(c1) - tm.assert_categorical_equal(c1, c2) - - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "b", "c", "d"]) - c2 = Categorical(c1) - tm.assert_categorical_equal(c1, c2) - - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "c", "b"]) - c2 = Categorical(c1) - tm.assert_categorical_equal(c1, c2) - - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "c", "b"]) - c2 = Categorical(c1, categories=["a", "b", "c"]) - self.assert_numpy_array_equal(c1.__array__(), c2.__array__()) - self.assert_index_equal(c2.categories, Index(["a", "b", "c"])) - - # Series of dtype category - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "b", "c", "d"]) - c2 = Categorical(Series(c1)) - tm.assert_categorical_equal(c1, c2) - - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "c", "b"]) - c2 = Categorical(Series(c1)) - tm.assert_categorical_equal(c1, c2) - - # Series - c1 = Categorical(["a", "b", "c", "a"]) - c2 = Categorical(Series(["a", "b", "c", "a"])) - tm.assert_categorical_equal(c1, c2) - - c1 = Categorical(["a", "b", "c", "a"], categories=["a", "b", "c", "d"]) - c2 = Categorical(Series(["a", "b", "c", "a"]), - categories=["a", "b", "c", "d"]) - tm.assert_categorical_equal(c1, c2) - - # This should result in integer categories, not float! - cat = pd.Categorical([1, 2, 3, np.nan], categories=[1, 2, 3]) - self.assertTrue(is_integer_dtype(cat.categories)) - - # https://github.com/pandas-dev/pandas/issues/3678 - cat = pd.Categorical([np.nan, 1, 2, 3]) - self.assertTrue(is_integer_dtype(cat.categories)) - - # this should result in floats - cat = pd.Categorical([np.nan, 1, 2., 3]) - self.assertTrue(is_float_dtype(cat.categories)) - - cat = pd.Categorical([np.nan, 1., 2., 3.]) - self.assertTrue(is_float_dtype(cat.categories)) - - # This doesn't work -> this would probably need some kind of "remember - # the original type" feature to try to cast the array interface result - # to... - - # vals = np.asarray(cat[cat.notnull()]) - # self.assertTrue(is_integer_dtype(vals)) - - # corner cases - cat = pd.Categorical([1]) - self.assertTrue(len(cat.categories) == 1) - self.assertTrue(cat.categories[0] == 1) - self.assertTrue(len(cat.codes) == 1) - self.assertTrue(cat.codes[0] == 0) - - cat = pd.Categorical(["a"]) - self.assertTrue(len(cat.categories) == 1) - self.assertTrue(cat.categories[0] == "a") - self.assertTrue(len(cat.codes) == 1) - self.assertTrue(cat.codes[0] == 0) - - # Scalars should be converted to lists - cat = pd.Categorical(1) - self.assertTrue(len(cat.categories) == 1) - self.assertTrue(cat.categories[0] == 1) - self.assertTrue(len(cat.codes) == 1) - self.assertTrue(cat.codes[0] == 0) - - cat = pd.Categorical([1], categories=1) - self.assertTrue(len(cat.categories) == 1) - self.assertTrue(cat.categories[0] == 1) - self.assertTrue(len(cat.codes) == 1) - self.assertTrue(cat.codes[0] == 0) - - # Catch old style constructor useage: two arrays, codes + categories - # We can only catch two cases: - # - when the first is an integer dtype and the second is not - # - when the resulting codes are all -1/NaN - with tm.assert_produces_warning(RuntimeWarning): - c_old = Categorical([0, 1, 2, 0, 1, 2], - categories=["a", "b", "c"]) # noqa - - with tm.assert_produces_warning(RuntimeWarning): - c_old = Categorical([0, 1, 2, 0, 1, 2], # noqa - categories=[3, 4, 5]) - - # the next one are from the old docs, but unfortunately these don't - # trigger :-( - with tm.assert_produces_warning(None): - c_old2 = Categorical([0, 1, 2, 0, 1, 2], [1, 2, 3]) # noqa - cat = Categorical([1, 2], categories=[1, 2, 3]) - - # this is a legitimate constructor - with tm.assert_produces_warning(None): - c = Categorical(np.array([], dtype='int64'), # noqa - categories=[3, 2, 1], ordered=True) - - def test_constructor_with_null(self): - - # Cannot have NaN in categories - with pytest.raises(ValueError): - pd.Categorical([np.nan, "a", "b", "c"], - categories=[np.nan, "a", "b", "c"]) - - with pytest.raises(ValueError): - pd.Categorical([None, "a", "b", "c"], - categories=[None, "a", "b", "c"]) - - with pytest.raises(ValueError): - pd.Categorical(DatetimeIndex(['nat', '20160101']), - categories=[NaT, Timestamp('20160101')]) - - - def test_constructor_with_index(self): - ci = CategoricalIndex(list('aabbca'), categories=list('cab')) - tm.assert_categorical_equal(ci.values, Categorical(ci)) - - ci = CategoricalIndex(list('aabbca'), categories=list('cab')) - tm.assert_categorical_equal(ci.values, - Categorical(ci.astype(object), - categories=ci.categories)) - - def test_constructor_with_generator(self): - # This was raising an Error in isnull(single_val).any() because isnull - # returned a scalar for a generator - xrange = range - - exp = Categorical([0, 1, 2]) - cat = Categorical((x for x in [0, 1, 2])) - tm.assert_categorical_equal(cat, exp) - cat = Categorical(xrange(3)) - tm.assert_categorical_equal(cat, exp) - - # This uses xrange internally - from pandas.core.index import MultiIndex - MultiIndex.from_product([range(5), ['a', 'b', 'c']]) - - # check that categories accept generators and sequences - cat = pd.Categorical([0, 1, 2], categories=(x for x in [0, 1, 2])) - tm.assert_categorical_equal(cat, exp) - cat = pd.Categorical([0, 1, 2], categories=xrange(3)) - tm.assert_categorical_equal(cat, exp) - - def test_constructor_with_datetimelike(self): - - # 12077 - # constructor wwth a datetimelike and NaT - - for dtl in [pd.date_range('1995-01-01 00:00:00', - periods=5, freq='s'), - pd.date_range('1995-01-01 00:00:00', - periods=5, freq='s', tz='US/Eastern'), - pd.timedelta_range('1 day', periods=5, freq='s')]: - - s = Series(dtl) - c = Categorical(s) - expected = type(dtl)(s) - expected.freq = None - tm.assert_index_equal(c.categories, expected) - self.assert_numpy_array_equal(c.codes, np.arange(5, dtype='int8')) - - # with NaT - s2 = s.copy() - s2.iloc[-1] = pd.NaT - c = Categorical(s2) - expected = type(dtl)(s2.dropna()) - expected.freq = None - tm.assert_index_equal(c.categories, expected) - - exp = np.array([0, 1, 2, 3, -1], dtype=np.int8) - self.assert_numpy_array_equal(c.codes, exp) - - result = repr(c) - self.assertTrue('NaT' in result) - - def test_constructor_from_index_series_datetimetz(self): - idx = pd.date_range('2015-01-01 10:00', freq='D', periods=3, - tz='US/Eastern') - result = pd.Categorical(idx) - tm.assert_index_equal(result.categories, idx) - - result = pd.Categorical(pd.Series(idx)) - tm.assert_index_equal(result.categories, idx) - - def test_constructor_from_index_series_timedelta(self): - idx = pd.timedelta_range('1 days', freq='D', periods=3) - result = pd.Categorical(idx) - tm.assert_index_equal(result.categories, idx) - - result = pd.Categorical(pd.Series(idx)) - tm.assert_index_equal(result.categories, idx) - - def test_constructor_from_index_series_period(self): - idx = pd.period_range('2015-01-01', freq='D', periods=3) - result = pd.Categorical(idx) - tm.assert_index_equal(result.categories, idx) - - result = pd.Categorical(pd.Series(idx)) - tm.assert_index_equal(result.categories, idx) - - def test_constructor_invariant(self): - # GH 14190 - vals = [ - np.array([1., 1.2, 1.8, np.nan]), - np.array([1, 2, 3], dtype='int64'), - ['a', 'b', 'c', np.nan], - [pd.Period('2014-01'), pd.Period('2014-02'), pd.NaT], - [pd.Timestamp('2014-01-01'), pd.Timestamp('2014-01-02'), pd.NaT], - [pd.Timestamp('2014-01-01', tz='US/Eastern'), - pd.Timestamp('2014-01-02', tz='US/Eastern'), pd.NaT], - ] - for val in vals: - c = Categorical(val) - c2 = Categorical(c) - tm.assert_categorical_equal(c, c2) - - def test_from_codes(self): - - # too few categories - def f(): - Categorical.from_codes([1, 2], [1, 2]) - - self.assertRaises(ValueError, f) - - # no int codes - def f(): - Categorical.from_codes(["a"], [1, 2]) - - self.assertRaises(ValueError, f) - - # no unique categories - def f(): - Categorical.from_codes([0, 1, 2], ["a", "a", "b"]) - - self.assertRaises(ValueError, f) - - # NaN categories included - def f(): - Categorical.from_codes([0, 1, 2], ["a", "b", np.nan]) - - self.assertRaises(ValueError, f) - - # too negative - def f(): - Categorical.from_codes([-2, 1, 2], ["a", "b", "c"]) - - self.assertRaises(ValueError, f) - - exp = Categorical(["a", "b", "c"], ordered=False) - res = Categorical.from_codes([0, 1, 2], ["a", "b", "c"]) - tm.assert_categorical_equal(exp, res) - - # Not available in earlier numpy versions - if hasattr(np.random, "choice"): - codes = np.random.choice([0, 1], 5, p=[0.9, 0.1]) - pd.Categorical.from_codes(codes, categories=["train", "test"]) - - def test_validate_ordered(self): - # see gh-14058 - exp_msg = "'ordered' must either be 'True' or 'False'" - exp_err = TypeError - - # This should be a boolean. - ordered = np.array([0, 1, 2]) - - with tm.assertRaisesRegexp(exp_err, exp_msg): - Categorical([1, 2, 3], ordered=ordered) - - with tm.assertRaisesRegexp(exp_err, exp_msg): - Categorical.from_codes([0, 0, 1], categories=['a', 'b', 'c'], - ordered=ordered) - - def test_comparisons(self): - - result = self.factor[self.factor == 'a'] - expected = self.factor[np.asarray(self.factor) == 'a'] - tm.assert_categorical_equal(result, expected) - - result = self.factor[self.factor != 'a'] - expected = self.factor[np.asarray(self.factor) != 'a'] - tm.assert_categorical_equal(result, expected) - - result = self.factor[self.factor < 'c'] - expected = self.factor[np.asarray(self.factor) < 'c'] - tm.assert_categorical_equal(result, expected) - - result = self.factor[self.factor > 'a'] - expected = self.factor[np.asarray(self.factor) > 'a'] - tm.assert_categorical_equal(result, expected) - - result = self.factor[self.factor >= 'b'] - expected = self.factor[np.asarray(self.factor) >= 'b'] - tm.assert_categorical_equal(result, expected) - - result = self.factor[self.factor <= 'b'] - expected = self.factor[np.asarray(self.factor) <= 'b'] - tm.assert_categorical_equal(result, expected) - - n = len(self.factor) - - other = self.factor[np.random.permutation(n)] - result = self.factor == other - expected = np.asarray(self.factor) == np.asarray(other) - self.assert_numpy_array_equal(result, expected) - - result = self.factor == 'd' - expected = np.repeat(False, len(self.factor)) - self.assert_numpy_array_equal(result, expected) - - # comparisons with categoricals - cat_rev = pd.Categorical(["a", "b", "c"], categories=["c", "b", "a"], - ordered=True) - cat_rev_base = pd.Categorical( - ["b", "b", "b"], categories=["c", "b", "a"], ordered=True) - cat = pd.Categorical(["a", "b", "c"], ordered=True) - cat_base = pd.Categorical(["b", "b", "b"], categories=cat.categories, - ordered=True) - - # comparisons need to take categories ordering into account - res_rev = cat_rev > cat_rev_base - exp_rev = np.array([True, False, False]) - self.assert_numpy_array_equal(res_rev, exp_rev) - - res_rev = cat_rev < cat_rev_base - exp_rev = np.array([False, False, True]) - self.assert_numpy_array_equal(res_rev, exp_rev) - - res = cat > cat_base - exp = np.array([False, False, True]) - self.assert_numpy_array_equal(res, exp) - - # Only categories with same categories can be compared - def f(): - cat > cat_rev - - self.assertRaises(TypeError, f) - - cat_rev_base2 = pd.Categorical( - ["b", "b", "b"], categories=["c", "b", "a", "d"]) - - def f(): - cat_rev > cat_rev_base2 - - self.assertRaises(TypeError, f) - - # Only categories with same ordering information can be compared - cat_unorderd = cat.set_ordered(False) - self.assertFalse((cat > cat).any()) - - def f(): - cat > cat_unorderd - - self.assertRaises(TypeError, f) - - # comparison (in both directions) with Series will raise - s = Series(["b", "b", "b"]) - self.assertRaises(TypeError, lambda: cat > s) - self.assertRaises(TypeError, lambda: cat_rev > s) - self.assertRaises(TypeError, lambda: s < cat) - self.assertRaises(TypeError, lambda: s < cat_rev) - - # comparison with numpy.array will raise in both direction, but only on - # newer numpy versions - a = np.array(["b", "b", "b"]) - self.assertRaises(TypeError, lambda: cat > a) - self.assertRaises(TypeError, lambda: cat_rev > a) - - # The following work via '__array_priority__ = 1000' - # works only on numpy >= 1.7.1 - if LooseVersion(np.__version__) > "1.7.1": - self.assertRaises(TypeError, lambda: a < cat) - self.assertRaises(TypeError, lambda: a < cat_rev) - - # Make sure that unequal comparison take the categories order in - # account - cat_rev = pd.Categorical( - list("abc"), categories=list("cba"), ordered=True) - exp = np.array([True, False, False]) - res = cat_rev > "b" - self.assert_numpy_array_equal(res, exp) - - def test_argsort(self): - c = Categorical([5, 3, 1, 4, 2], ordered=True) - - expected = np.array([2, 4, 1, 3, 0]) - tm.assert_numpy_array_equal(c.argsort(ascending=True), expected, - check_dtype=False) - - expected = expected[::-1] - tm.assert_numpy_array_equal(c.argsort(ascending=False), expected, - check_dtype=False) - - def test_numpy_argsort(self): - c = Categorical([5, 3, 1, 4, 2], ordered=True) - - expected = np.array([2, 4, 1, 3, 0]) - tm.assert_numpy_array_equal(np.argsort(c), expected, - check_dtype=False) - - msg = "the 'kind' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.argsort, - c, kind='mergesort') - - msg = "the 'axis' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.argsort, - c, axis=0) - - msg = "the 'order' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.argsort, - c, order='C') - - def test_na_flags_int_categories(self): - # #1457 - - categories = lrange(10) - labels = np.random.randint(0, 10, 20) - labels[::5] = -1 - - cat = Categorical(labels, categories, fastpath=True) - repr(cat) - - self.assert_numpy_array_equal(isnull(cat), labels == -1) - - def test_categories_none(self): - factor = Categorical(['a', 'b', 'b', 'a', - 'a', 'c', 'c', 'c'], ordered=True) - tm.assert_categorical_equal(factor, self.factor) - - def test_describe(self): - # string type - desc = self.factor.describe() - self.assertTrue(self.factor.ordered) - exp_index = pd.CategoricalIndex(['a', 'b', 'c'], name='categories', - ordered=self.factor.ordered) - expected = DataFrame({'counts': [3, 2, 3], - 'freqs': [3 / 8., 2 / 8., 3 / 8.]}, - index=exp_index) - tm.assert_frame_equal(desc, expected) - - # check unused categories - cat = self.factor.copy() - cat.set_categories(["a", "b", "c", "d"], inplace=True) - desc = cat.describe() - - exp_index = pd.CategoricalIndex(['a', 'b', 'c', 'd'], - ordered=self.factor.ordered, - name='categories') - expected = DataFrame({'counts': [3, 2, 3, 0], - 'freqs': [3 / 8., 2 / 8., 3 / 8., 0]}, - index=exp_index) - tm.assert_frame_equal(desc, expected) - - # check an integer one - cat = Categorical([1, 2, 3, 1, 2, 3, 3, 2, 1, 1, 1]) - desc = cat.describe() - exp_index = pd.CategoricalIndex([1, 2, 3], ordered=cat.ordered, - name='categories') - expected = DataFrame({'counts': [5, 3, 3], - 'freqs': [5 / 11., 3 / 11., 3 / 11.]}, - index=exp_index) - tm.assert_frame_equal(desc, expected) - - # https://github.com/pandas-dev/pandas/issues/3678 - # describe should work with NaN - cat = pd.Categorical([np.nan, 1, 2, 2]) - desc = cat.describe() - expected = DataFrame({'counts': [1, 2, 1], - 'freqs': [1 / 4., 2 / 4., 1 / 4.]}, - index=pd.CategoricalIndex([1, 2, np.nan], - categories=[1, 2], - name='categories')) - tm.assert_frame_equal(desc, expected) - - def test_print(self): - expected = ["[a, b, b, a, a, c, c, c]", - "Categories (3, object): [a < b < c]"] - expected = "\n".join(expected) - actual = repr(self.factor) - self.assertEqual(actual, expected) - - def test_big_print(self): - factor = Categorical([0, 1, 2, 0, 1, 2] * 100, ['a', 'b', 'c'], - fastpath=True) - expected = ["[a, b, c, a, b, ..., b, c, a, b, c]", "Length: 600", - "Categories (3, object): [a, b, c]"] - expected = "\n".join(expected) - - actual = repr(factor) - - self.assertEqual(actual, expected) - - def test_empty_print(self): - factor = Categorical([], ["a", "b", "c"]) - expected = ("[], Categories (3, object): [a, b, c]") - # hack because array_repr changed in numpy > 1.6.x - actual = repr(factor) - self.assertEqual(actual, expected) - - self.assertEqual(expected, actual) - factor = Categorical([], ["a", "b", "c"], ordered=True) - expected = ("[], Categories (3, object): [a < b < c]") - actual = repr(factor) - self.assertEqual(expected, actual) - - factor = Categorical([], []) - expected = ("[], Categories (0, object): []") - self.assertEqual(expected, repr(factor)) - - def test_print_none_width(self): - # GH10087 - a = pd.Series(pd.Categorical([1, 2, 3, 4])) - exp = u("0 1\n1 2\n2 3\n3 4\n" + - "dtype: category\nCategories (4, int64): [1, 2, 3, 4]") - - with option_context("display.width", None): - self.assertEqual(exp, repr(a)) - - def test_unicode_print(self): - if PY3: - _rep = repr - else: - _rep = unicode # noqa - - c = pd.Categorical(['aaaaa', 'bb', 'cccc'] * 20) - expected = u"""\ -[aaaaa, bb, cccc, aaaaa, bb, ..., bb, cccc, aaaaa, bb, cccc] -Length: 60 -Categories (3, object): [aaaaa, bb, cccc]""" - - self.assertEqual(_rep(c), expected) - - c = pd.Categorical([u'ああああ', u'いいいいい', u'ううううううう'] - * 20) - expected = u"""\ -[ああああ, いいいいい, ううううううう, ああああ, いいいいい, ..., いいいいい, ううううううう, ああああ, いいいいい, ううううううう] -Length: 60 -Categories (3, object): [ああああ, いいいいい, ううううううう]""" # noqa - - self.assertEqual(_rep(c), expected) - - # unicode option should not affect to Categorical, as it doesn't care - # the repr width - with option_context('display.unicode.east_asian_width', True): - - c = pd.Categorical([u'ああああ', u'いいいいい', u'ううううううう'] - * 20) - expected = u"""[ああああ, いいいいい, ううううううう, ああああ, いいいいい, ..., いいいいい, ううううううう, ああああ, いいいいい, ううううううう] -Length: 60 -Categories (3, object): [ああああ, いいいいい, ううううううう]""" # noqa - - self.assertEqual(_rep(c), expected) - - def test_periodindex(self): - idx1 = PeriodIndex(['2014-01', '2014-01', '2014-02', '2014-02', - '2014-03', '2014-03'], freq='M') - - cat1 = Categorical(idx1) - str(cat1) - exp_arr = np.array([0, 0, 1, 1, 2, 2], dtype=np.int8) - exp_idx = PeriodIndex(['2014-01', '2014-02', '2014-03'], freq='M') - self.assert_numpy_array_equal(cat1._codes, exp_arr) - self.assert_index_equal(cat1.categories, exp_idx) - - idx2 = PeriodIndex(['2014-03', '2014-03', '2014-02', '2014-01', - '2014-03', '2014-01'], freq='M') - cat2 = Categorical(idx2, ordered=True) - str(cat2) - exp_arr = np.array([2, 2, 1, 0, 2, 0], dtype=np.int8) - exp_idx2 = PeriodIndex(['2014-01', '2014-02', '2014-03'], freq='M') - self.assert_numpy_array_equal(cat2._codes, exp_arr) - self.assert_index_equal(cat2.categories, exp_idx2) - - idx3 = PeriodIndex(['2013-12', '2013-11', '2013-10', '2013-09', - '2013-08', '2013-07', '2013-05'], freq='M') - cat3 = Categorical(idx3, ordered=True) - exp_arr = np.array([6, 5, 4, 3, 2, 1, 0], dtype=np.int8) - exp_idx = PeriodIndex(['2013-05', '2013-07', '2013-08', '2013-09', - '2013-10', '2013-11', '2013-12'], freq='M') - self.assert_numpy_array_equal(cat3._codes, exp_arr) - self.assert_index_equal(cat3.categories, exp_idx) - - def test_categories_assigments(self): - s = pd.Categorical(["a", "b", "c", "a"]) - exp = np.array([1, 2, 3, 1], dtype=np.int64) - s.categories = [1, 2, 3] - self.assert_numpy_array_equal(s.__array__(), exp) - self.assert_index_equal(s.categories, Index([1, 2, 3])) - - # lengthen - def f(): - s.categories = [1, 2, 3, 4] - - self.assertRaises(ValueError, f) - - # shorten - def f(): - s.categories = [1, 2] - - self.assertRaises(ValueError, f) - - def test_construction_with_ordered(self): - # GH 9347, 9190 - cat = Categorical([0, 1, 2]) - self.assertFalse(cat.ordered) - cat = Categorical([0, 1, 2], ordered=False) - self.assertFalse(cat.ordered) - cat = Categorical([0, 1, 2], ordered=True) - self.assertTrue(cat.ordered) - - def test_ordered_api(self): - # GH 9347 - cat1 = pd.Categorical(["a", "c", "b"], ordered=False) - self.assert_index_equal(cat1.categories, Index(['a', 'b', 'c'])) - self.assertFalse(cat1.ordered) - - cat2 = pd.Categorical(["a", "c", "b"], categories=['b', 'c', 'a'], - ordered=False) - self.assert_index_equal(cat2.categories, Index(['b', 'c', 'a'])) - self.assertFalse(cat2.ordered) - - cat3 = pd.Categorical(["a", "c", "b"], ordered=True) - self.assert_index_equal(cat3.categories, Index(['a', 'b', 'c'])) - self.assertTrue(cat3.ordered) - - cat4 = pd.Categorical(["a", "c", "b"], categories=['b', 'c', 'a'], - ordered=True) - self.assert_index_equal(cat4.categories, Index(['b', 'c', 'a'])) - self.assertTrue(cat4.ordered) - - def test_set_ordered(self): - - cat = Categorical(["a", "b", "c", "a"], ordered=True) - cat2 = cat.as_unordered() - self.assertFalse(cat2.ordered) - cat2 = cat.as_ordered() - self.assertTrue(cat2.ordered) - cat2.as_unordered(inplace=True) - self.assertFalse(cat2.ordered) - cat2.as_ordered(inplace=True) - self.assertTrue(cat2.ordered) - - self.assertTrue(cat2.set_ordered(True).ordered) - self.assertFalse(cat2.set_ordered(False).ordered) - cat2.set_ordered(True, inplace=True) - self.assertTrue(cat2.ordered) - cat2.set_ordered(False, inplace=True) - self.assertFalse(cat2.ordered) - - # removed in 0.19.0 - msg = "can\'t set attribute" - with tm.assertRaisesRegexp(AttributeError, msg): - cat.ordered = True - with tm.assertRaisesRegexp(AttributeError, msg): - cat.ordered = False - - def test_set_categories(self): - cat = Categorical(["a", "b", "c", "a"], ordered=True) - exp_categories = Index(["c", "b", "a"]) - exp_values = np.array(["a", "b", "c", "a"], dtype=np.object_) - - res = cat.set_categories(["c", "b", "a"], inplace=True) - self.assert_index_equal(cat.categories, exp_categories) - self.assert_numpy_array_equal(cat.__array__(), exp_values) - self.assertIsNone(res) - - res = cat.set_categories(["a", "b", "c"]) - # cat must be the same as before - self.assert_index_equal(cat.categories, exp_categories) - self.assert_numpy_array_equal(cat.__array__(), exp_values) - # only res is changed - exp_categories_back = Index(["a", "b", "c"]) - self.assert_index_equal(res.categories, exp_categories_back) - self.assert_numpy_array_equal(res.__array__(), exp_values) - - # not all "old" included in "new" -> all not included ones are now - # np.nan - cat = Categorical(["a", "b", "c", "a"], ordered=True) - res = cat.set_categories(["a"]) - self.assert_numpy_array_equal(res.codes, - np.array([0, -1, -1, 0], dtype=np.int8)) - - # still not all "old" in "new" - res = cat.set_categories(["a", "b", "d"]) - self.assert_numpy_array_equal(res.codes, - np.array([0, 1, -1, 0], dtype=np.int8)) - self.assert_index_equal(res.categories, Index(["a", "b", "d"])) - - # all "old" included in "new" - cat = cat.set_categories(["a", "b", "c", "d"]) - exp_categories = Index(["a", "b", "c", "d"]) - self.assert_index_equal(cat.categories, exp_categories) - - # internals... - c = Categorical([1, 2, 3, 4, 1], categories=[1, 2, 3, 4], ordered=True) - self.assert_numpy_array_equal(c._codes, - np.array([0, 1, 2, 3, 0], dtype=np.int8)) - self.assert_index_equal(c.categories, Index([1, 2, 3, 4])) - - exp = np.array([1, 2, 3, 4, 1], dtype=np.int64) - self.assert_numpy_array_equal(c.get_values(), exp) - - # all "pointers" to '4' must be changed from 3 to 0,... - c = c.set_categories([4, 3, 2, 1]) - - # positions are changed - self.assert_numpy_array_equal(c._codes, - np.array([3, 2, 1, 0, 3], dtype=np.int8)) - - # categories are now in new order - self.assert_index_equal(c.categories, Index([4, 3, 2, 1])) - - # output is the same - exp = np.array([1, 2, 3, 4, 1], dtype=np.int64) - self.assert_numpy_array_equal(c.get_values(), exp) - self.assertTrue(c.min(), 4) - self.assertTrue(c.max(), 1) - - # set_categories should set the ordering if specified - c2 = c.set_categories([4, 3, 2, 1], ordered=False) - self.assertFalse(c2.ordered) - self.assert_numpy_array_equal(c.get_values(), c2.get_values()) - - # set_categories should pass thru the ordering - c2 = c.set_ordered(False).set_categories([4, 3, 2, 1]) - self.assertFalse(c2.ordered) - self.assert_numpy_array_equal(c.get_values(), c2.get_values()) - - def test_rename_categories(self): - cat = pd.Categorical(["a", "b", "c", "a"]) - - # inplace=False: the old one must not be changed - res = cat.rename_categories([1, 2, 3]) - self.assert_numpy_array_equal(res.__array__(), - np.array([1, 2, 3, 1], dtype=np.int64)) - self.assert_index_equal(res.categories, Index([1, 2, 3])) - - exp_cat = np.array(["a", "b", "c", "a"], dtype=np.object_) - self.assert_numpy_array_equal(cat.__array__(), exp_cat) - - exp_cat = Index(["a", "b", "c"]) - self.assert_index_equal(cat.categories, exp_cat) - res = cat.rename_categories([1, 2, 3], inplace=True) - - # and now inplace - self.assertIsNone(res) - self.assert_numpy_array_equal(cat.__array__(), - np.array([1, 2, 3, 1], dtype=np.int64)) - self.assert_index_equal(cat.categories, Index([1, 2, 3])) - - # lengthen - def f(): - cat.rename_categories([1, 2, 3, 4]) - - self.assertRaises(ValueError, f) - - # shorten - def f(): - cat.rename_categories([1, 2]) - - self.assertRaises(ValueError, f) - - def test_reorder_categories(self): - cat = Categorical(["a", "b", "c", "a"], ordered=True) - old = cat.copy() - new = Categorical(["a", "b", "c", "a"], categories=["c", "b", "a"], - ordered=True) - - # first inplace == False - res = cat.reorder_categories(["c", "b", "a"]) - # cat must be the same as before - self.assert_categorical_equal(cat, old) - # only res is changed - self.assert_categorical_equal(res, new) - - # inplace == True - res = cat.reorder_categories(["c", "b", "a"], inplace=True) - self.assertIsNone(res) - self.assert_categorical_equal(cat, new) - - # not all "old" included in "new" - cat = Categorical(["a", "b", "c", "a"], ordered=True) - - def f(): - cat.reorder_categories(["a"]) - - self.assertRaises(ValueError, f) - - # still not all "old" in "new" - def f(): - cat.reorder_categories(["a", "b", "d"]) - - self.assertRaises(ValueError, f) - - # all "old" included in "new", but too long - def f(): - cat.reorder_categories(["a", "b", "c", "d"]) - - self.assertRaises(ValueError, f) - - def test_add_categories(self): - cat = Categorical(["a", "b", "c", "a"], ordered=True) - old = cat.copy() - new = Categorical(["a", "b", "c", "a"], - categories=["a", "b", "c", "d"], ordered=True) - - # first inplace == False - res = cat.add_categories("d") - self.assert_categorical_equal(cat, old) - self.assert_categorical_equal(res, new) - - res = cat.add_categories(["d"]) - self.assert_categorical_equal(cat, old) - self.assert_categorical_equal(res, new) - - # inplace == True - res = cat.add_categories("d", inplace=True) - self.assert_categorical_equal(cat, new) - self.assertIsNone(res) - - # new is in old categories - def f(): - cat.add_categories(["d"]) - - self.assertRaises(ValueError, f) - - # GH 9927 - cat = Categorical(list("abc"), ordered=True) - expected = Categorical( - list("abc"), categories=list("abcde"), ordered=True) - # test with Series, np.array, index, list - res = cat.add_categories(Series(["d", "e"])) - self.assert_categorical_equal(res, expected) - res = cat.add_categories(np.array(["d", "e"])) - self.assert_categorical_equal(res, expected) - res = cat.add_categories(Index(["d", "e"])) - self.assert_categorical_equal(res, expected) - res = cat.add_categories(["d", "e"]) - self.assert_categorical_equal(res, expected) - - def test_remove_categories(self): - cat = Categorical(["a", "b", "c", "a"], ordered=True) - old = cat.copy() - new = Categorical(["a", "b", np.nan, "a"], categories=["a", "b"], - ordered=True) - - # first inplace == False - res = cat.remove_categories("c") - self.assert_categorical_equal(cat, old) - self.assert_categorical_equal(res, new) - - res = cat.remove_categories(["c"]) - self.assert_categorical_equal(cat, old) - self.assert_categorical_equal(res, new) - - # inplace == True - res = cat.remove_categories("c", inplace=True) - self.assert_categorical_equal(cat, new) - self.assertIsNone(res) - - # removal is not in categories - def f(): - cat.remove_categories(["c"]) - - self.assertRaises(ValueError, f) - - def test_remove_unused_categories(self): - c = Categorical(["a", "b", "c", "d", "a"], - categories=["a", "b", "c", "d", "e"]) - exp_categories_all = Index(["a", "b", "c", "d", "e"]) - exp_categories_dropped = Index(["a", "b", "c", "d"]) - - self.assert_index_equal(c.categories, exp_categories_all) - - res = c.remove_unused_categories() - self.assert_index_equal(res.categories, exp_categories_dropped) - self.assert_index_equal(c.categories, exp_categories_all) - - res = c.remove_unused_categories(inplace=True) - self.assert_index_equal(c.categories, exp_categories_dropped) - self.assertIsNone(res) - - # with NaN values (GH11599) - c = Categorical(["a", "b", "c", np.nan], - categories=["a", "b", "c", "d", "e"]) - res = c.remove_unused_categories() - self.assert_index_equal(res.categories, - Index(np.array(["a", "b", "c"]))) - exp_codes = np.array([0, 1, 2, -1], dtype=np.int8) - self.assert_numpy_array_equal(res.codes, exp_codes) - self.assert_index_equal(c.categories, exp_categories_all) - - val = ['F', np.nan, 'D', 'B', 'D', 'F', np.nan] - cat = pd.Categorical(values=val, categories=list('ABCDEFG')) - out = cat.remove_unused_categories() - self.assert_index_equal(out.categories, Index(['B', 'D', 'F'])) - exp_codes = np.array([2, -1, 1, 0, 1, 2, -1], dtype=np.int8) - self.assert_numpy_array_equal(out.codes, exp_codes) - self.assertEqual(out.get_values().tolist(), val) - - alpha = list('abcdefghijklmnopqrstuvwxyz') - val = np.random.choice(alpha[::2], 10000).astype('object') - val[np.random.choice(len(val), 100)] = np.nan - - cat = pd.Categorical(values=val, categories=alpha) - out = cat.remove_unused_categories() - self.assertEqual(out.get_values().tolist(), val.tolist()) - - def test_nan_handling(self): - - # Nans are represented as -1 in codes - c = Categorical(["a", "b", np.nan, "a"]) - self.assert_index_equal(c.categories, Index(["a", "b"])) - self.assert_numpy_array_equal(c._codes, - np.array([0, 1, -1, 0], dtype=np.int8)) - c[1] = np.nan - self.assert_index_equal(c.categories, Index(["a", "b"])) - self.assert_numpy_array_equal(c._codes, - np.array([0, -1, -1, 0], dtype=np.int8)) - - # Adding nan to categories should make assigned nan point to the - # category! - c = Categorical(["a", "b", np.nan, "a"]) - self.assert_index_equal(c.categories, Index(["a", "b"])) - self.assert_numpy_array_equal(c._codes, - np.array([0, 1, -1, 0], dtype=np.int8)) - - def test_isnull(self): - exp = np.array([False, False, True]) - c = Categorical(["a", "b", np.nan]) - res = c.isnull() - - self.assert_numpy_array_equal(res, exp) - - def test_codes_immutable(self): - - # Codes should be read only - c = Categorical(["a", "b", "c", "a", np.nan]) - exp = np.array([0, 1, 2, 0, -1], dtype='int8') - self.assert_numpy_array_equal(c.codes, exp) - - # Assignments to codes should raise - def f(): - c.codes = np.array([0, 1, 2, 0, 1], dtype='int8') - - self.assertRaises(ValueError, f) - - # changes in the codes array should raise - # np 1.6.1 raises RuntimeError rather than ValueError - codes = c.codes - - def f(): - codes[4] = 1 - - self.assertRaises(ValueError, f) - - # But even after getting the codes, the original array should still be - # writeable! - c[4] = "a" - exp = np.array([0, 1, 2, 0, 0], dtype='int8') - self.assert_numpy_array_equal(c.codes, exp) - c._codes[4] = 2 - exp = np.array([0, 1, 2, 0, 2], dtype='int8') - self.assert_numpy_array_equal(c.codes, exp) - - def test_min_max(self): - - # unordered cats have no min/max - cat = Categorical(["a", "b", "c", "d"], ordered=False) - self.assertRaises(TypeError, lambda: cat.min()) - self.assertRaises(TypeError, lambda: cat.max()) - cat = Categorical(["a", "b", "c", "d"], ordered=True) - _min = cat.min() - _max = cat.max() - self.assertEqual(_min, "a") - self.assertEqual(_max, "d") - cat = Categorical(["a", "b", "c", "d"], - categories=['d', 'c', 'b', 'a'], ordered=True) - _min = cat.min() - _max = cat.max() - self.assertEqual(_min, "d") - self.assertEqual(_max, "a") - cat = Categorical([np.nan, "b", "c", np.nan], - categories=['d', 'c', 'b', 'a'], ordered=True) - _min = cat.min() - _max = cat.max() - self.assertTrue(np.isnan(_min)) - self.assertEqual(_max, "b") - - _min = cat.min(numeric_only=True) - self.assertEqual(_min, "c") - _max = cat.max(numeric_only=True) - self.assertEqual(_max, "b") - - cat = Categorical([np.nan, 1, 2, np.nan], categories=[5, 4, 3, 2, 1], - ordered=True) - _min = cat.min() - _max = cat.max() - self.assertTrue(np.isnan(_min)) - self.assertEqual(_max, 1) - - _min = cat.min(numeric_only=True) - self.assertEqual(_min, 2) - _max = cat.max(numeric_only=True) - self.assertEqual(_max, 1) - - def test_unique(self): - # categories are reordered based on value when ordered=False - cat = Categorical(["a", "b"]) - exp = Index(["a", "b"]) - res = cat.unique() - self.assert_index_equal(res.categories, exp) - self.assert_categorical_equal(res, cat) - - cat = Categorical(["a", "b", "a", "a"], categories=["a", "b", "c"]) - res = cat.unique() - self.assert_index_equal(res.categories, exp) - tm.assert_categorical_equal(res, Categorical(exp)) - - cat = Categorical(["c", "a", "b", "a", "a"], - categories=["a", "b", "c"]) - exp = Index(["c", "a", "b"]) - res = cat.unique() - self.assert_index_equal(res.categories, exp) - exp_cat = Categorical(exp, categories=['c', 'a', 'b']) - tm.assert_categorical_equal(res, exp_cat) - - # nan must be removed - cat = Categorical(["b", np.nan, "b", np.nan, "a"], - categories=["a", "b", "c"]) - res = cat.unique() - exp = Index(["b", "a"]) - self.assert_index_equal(res.categories, exp) - exp_cat = Categorical(["b", np.nan, "a"], categories=["b", "a"]) - tm.assert_categorical_equal(res, exp_cat) - - def test_unique_ordered(self): - # keep categories order when ordered=True - cat = Categorical(['b', 'a', 'b'], categories=['a', 'b'], ordered=True) - res = cat.unique() - exp_cat = Categorical(['b', 'a'], categories=['a', 'b'], ordered=True) - tm.assert_categorical_equal(res, exp_cat) - - cat = Categorical(['c', 'b', 'a', 'a'], categories=['a', 'b', 'c'], - ordered=True) - res = cat.unique() - exp_cat = Categorical(['c', 'b', 'a'], categories=['a', 'b', 'c'], - ordered=True) - tm.assert_categorical_equal(res, exp_cat) - - cat = Categorical(['b', 'a', 'a'], categories=['a', 'b', 'c'], - ordered=True) - res = cat.unique() - exp_cat = Categorical(['b', 'a'], categories=['a', 'b'], ordered=True) - tm.assert_categorical_equal(res, exp_cat) - - cat = Categorical(['b', 'b', np.nan, 'a'], categories=['a', 'b', 'c'], - ordered=True) - res = cat.unique() - exp_cat = Categorical(['b', np.nan, 'a'], categories=['a', 'b'], - ordered=True) - tm.assert_categorical_equal(res, exp_cat) - - def test_unique_index_series(self): - c = Categorical([3, 1, 2, 2, 1], categories=[3, 2, 1]) - # Categorical.unique sorts categories by appearance order - # if ordered=False - exp = Categorical([3, 1, 2], categories=[3, 1, 2]) - tm.assert_categorical_equal(c.unique(), exp) - - tm.assert_index_equal(Index(c).unique(), Index(exp)) - tm.assert_categorical_equal(pd.Series(c).unique(), exp) - - c = Categorical([1, 1, 2, 2], categories=[3, 2, 1]) - exp = Categorical([1, 2], categories=[1, 2]) - tm.assert_categorical_equal(c.unique(), exp) - tm.assert_index_equal(Index(c).unique(), Index(exp)) - tm.assert_categorical_equal(pd.Series(c).unique(), exp) - - c = Categorical([3, 1, 2, 2, 1], categories=[3, 2, 1], ordered=True) - # Categorical.unique keeps categories order if ordered=True - exp = Categorical([3, 1, 2], categories=[3, 2, 1], ordered=True) - tm.assert_categorical_equal(c.unique(), exp) - - tm.assert_index_equal(Index(c).unique(), Index(exp)) - tm.assert_categorical_equal(pd.Series(c).unique(), exp) - - def test_mode(self): - s = Categorical([1, 1, 2, 4, 5, 5, 5], categories=[5, 4, 3, 2, 1], - ordered=True) - res = s.mode() - exp = Categorical([5], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - s = Categorical([1, 1, 1, 4, 5, 5, 5], categories=[5, 4, 3, 2, 1], - ordered=True) - res = s.mode() - exp = Categorical([5, 1], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - s = Categorical([1, 2, 3, 4, 5], categories=[5, 4, 3, 2, 1], - ordered=True) - res = s.mode() - exp = Categorical([5, 4, 3, 2, 1], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - # NaN should not become the mode! - s = Categorical([np.nan, np.nan, np.nan, 4, 5], - categories=[5, 4, 3, 2, 1], ordered=True) - res = s.mode() - exp = Categorical([5, 4], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - s = Categorical([np.nan, np.nan, np.nan, 4, 5, 4], - categories=[5, 4, 3, 2, 1], ordered=True) - res = s.mode() - exp = Categorical([4], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - s = Categorical([np.nan, np.nan, 4, 5, 4], categories=[5, 4, 3, 2, 1], - ordered=True) - res = s.mode() - exp = Categorical([4], categories=[5, 4, 3, 2, 1], ordered=True) - tm.assert_categorical_equal(res, exp) - - def test_sort_values(self): - - # unordered cats are sortable - cat = Categorical(["a", "b", "b", "a"], ordered=False) - cat.sort_values() - - cat = Categorical(["a", "c", "b", "d"], ordered=True) - - # sort_values - res = cat.sort_values() - exp = np.array(["a", "b", "c", "d"], dtype=object) - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, cat.categories) - - cat = Categorical(["a", "c", "b", "d"], - categories=["a", "b", "c", "d"], ordered=True) - res = cat.sort_values() - exp = np.array(["a", "b", "c", "d"], dtype=object) - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, cat.categories) - - res = cat.sort_values(ascending=False) - exp = np.array(["d", "c", "b", "a"], dtype=object) - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, cat.categories) - - # sort (inplace order) - cat1 = cat.copy() - cat1.sort_values(inplace=True) - exp = np.array(["a", "b", "c", "d"], dtype=object) - self.assert_numpy_array_equal(cat1.__array__(), exp) - self.assert_index_equal(res.categories, cat.categories) - - # reverse - cat = Categorical(["a", "c", "c", "b", "d"], ordered=True) - res = cat.sort_values(ascending=False) - exp_val = np.array(["d", "c", "c", "b", "a"], dtype=object) - exp_categories = Index(["a", "b", "c", "d"]) - self.assert_numpy_array_equal(res.__array__(), exp_val) - self.assert_index_equal(res.categories, exp_categories) - - def test_sort_values_na_position(self): - # see gh-12882 - cat = Categorical([5, 2, np.nan, 2, np.nan], ordered=True) - exp_categories = Index([2, 5]) - - exp = np.array([2.0, 2.0, 5.0, np.nan, np.nan]) - res = cat.sort_values() # default arguments - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, exp_categories) - - exp = np.array([np.nan, np.nan, 2.0, 2.0, 5.0]) - res = cat.sort_values(ascending=True, na_position='first') - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, exp_categories) - - exp = np.array([np.nan, np.nan, 5.0, 2.0, 2.0]) - res = cat.sort_values(ascending=False, na_position='first') - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, exp_categories) - - exp = np.array([2.0, 2.0, 5.0, np.nan, np.nan]) - res = cat.sort_values(ascending=True, na_position='last') - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, exp_categories) - - exp = np.array([5.0, 2.0, 2.0, np.nan, np.nan]) - res = cat.sort_values(ascending=False, na_position='last') - self.assert_numpy_array_equal(res.__array__(), exp) - self.assert_index_equal(res.categories, exp_categories) - - cat = Categorical(["a", "c", "b", "d", np.nan], ordered=True) - res = cat.sort_values(ascending=False, na_position='last') - exp_val = np.array(["d", "c", "b", "a", np.nan], dtype=object) - exp_categories = Index(["a", "b", "c", "d"]) - self.assert_numpy_array_equal(res.__array__(), exp_val) - self.assert_index_equal(res.categories, exp_categories) - - cat = Categorical(["a", "c", "b", "d", np.nan], ordered=True) - res = cat.sort_values(ascending=False, na_position='first') - exp_val = np.array([np.nan, "d", "c", "b", "a"], dtype=object) - exp_categories = Index(["a", "b", "c", "d"]) - self.assert_numpy_array_equal(res.__array__(), exp_val) - self.assert_index_equal(res.categories, exp_categories) - - def test_slicing_directly(self): - cat = Categorical(["a", "b", "c", "d", "a", "b", "c"]) - sliced = cat[3] - self.assertEqual(sliced, "d") - sliced = cat[3:5] - expected = Categorical(["d", "a"], categories=['a', 'b', 'c', 'd']) - self.assert_numpy_array_equal(sliced._codes, expected._codes) - tm.assert_index_equal(sliced.categories, expected.categories) - - def test_set_item_nan(self): - cat = pd.Categorical([1, 2, 3]) - cat[1] = np.nan - - exp = pd.Categorical([1, np.nan, 3], categories=[1, 2, 3]) - tm.assert_categorical_equal(cat, exp) - - def test_shift(self): - # GH 9416 - cat = pd.Categorical(['a', 'b', 'c', 'd', 'a']) - - # shift forward - sp1 = cat.shift(1) - xp1 = pd.Categorical([np.nan, 'a', 'b', 'c', 'd']) - self.assert_categorical_equal(sp1, xp1) - self.assert_categorical_equal(cat[:-1], sp1[1:]) - - # shift back - sn2 = cat.shift(-2) - xp2 = pd.Categorical(['c', 'd', 'a', np.nan, np.nan], - categories=['a', 'b', 'c', 'd']) - self.assert_categorical_equal(sn2, xp2) - self.assert_categorical_equal(cat[2:], sn2[:-2]) - - # shift by zero - self.assert_categorical_equal(cat, cat.shift(0)) - - def test_nbytes(self): - cat = pd.Categorical([1, 2, 3]) - exp = cat._codes.nbytes + cat._categories.values.nbytes - self.assertEqual(cat.nbytes, exp) - - def test_memory_usage(self): - cat = pd.Categorical([1, 2, 3]) - - # .categories is an index, so we include the hashtable - self.assertTrue(cat.nbytes > 0 and cat.nbytes <= cat.memory_usage()) - self.assertTrue(cat.nbytes > 0 and - cat.nbytes <= cat.memory_usage(deep=True)) - - cat = pd.Categorical(['foo', 'foo', 'bar']) - self.assertTrue(cat.memory_usage(deep=True) > cat.nbytes) - - # sys.getsizeof will call the .memory_usage with - # deep=True, and add on some GC overhead - diff = cat.memory_usage(deep=True) - sys.getsizeof(cat) - self.assertTrue(abs(diff) < 100) - - def test_searchsorted(self): - # https://github.com/pandas-dev/pandas/issues/8420 - # https://github.com/pandas-dev/pandas/issues/14522 - - c1 = pd.Categorical(['cheese', 'milk', 'apple', 'bread', 'bread'], - categories=['cheese', 'milk', 'apple', 'bread'], - ordered=True) - s1 = pd.Series(c1) - c2 = pd.Categorical(['cheese', 'milk', 'apple', 'bread', 'bread'], - categories=['cheese', 'milk', 'apple', 'bread'], - ordered=False) - s2 = pd.Series(c2) - - # Searching for single item argument, side='left' (default) - res_cat = c1.searchsorted('apple') - res_ser = s1.searchsorted('apple') - exp = np.array([2], dtype=np.intp) - self.assert_numpy_array_equal(res_cat, exp) - self.assert_numpy_array_equal(res_ser, exp) - - # Searching for single item array, side='left' (default) - res_cat = c1.searchsorted(['bread']) - res_ser = s1.searchsorted(['bread']) - exp = np.array([3], dtype=np.intp) - self.assert_numpy_array_equal(res_cat, exp) - self.assert_numpy_array_equal(res_ser, exp) - - # Searching for several items array, side='right' - res_cat = c1.searchsorted(['apple', 'bread'], side='right') - res_ser = s1.searchsorted(['apple', 'bread'], side='right') - exp = np.array([3, 5], dtype=np.intp) - self.assert_numpy_array_equal(res_cat, exp) - self.assert_numpy_array_equal(res_ser, exp) - - # Searching for a single value that is not from the Categorical - self.assertRaises(ValueError, lambda: c1.searchsorted('cucumber')) - self.assertRaises(ValueError, lambda: s1.searchsorted('cucumber')) - - # Searching for multiple values one of each is not from the Categorical - self.assertRaises(ValueError, - lambda: c1.searchsorted(['bread', 'cucumber'])) - self.assertRaises(ValueError, - lambda: s1.searchsorted(['bread', 'cucumber'])) - - # searchsorted call for unordered Categorical - self.assertRaises(ValueError, lambda: c2.searchsorted('apple')) - self.assertRaises(ValueError, lambda: s2.searchsorted('apple')) - - with tm.assert_produces_warning(FutureWarning): - res = c1.searchsorted(v=['bread']) - exp = np.array([3], dtype=np.intp) - tm.assert_numpy_array_equal(res, exp) - - def test_deprecated_labels(self): - # TODO: labels is deprecated and should be removed in 0.18 or 2017, - # whatever is earlier - cat = pd.Categorical([1, 2, 3, np.nan], categories=[1, 2, 3]) - exp = cat.codes - with tm.assert_produces_warning(FutureWarning): - res = cat.labels - self.assert_numpy_array_equal(res, exp) - - def test_deprecated_from_array(self): - # GH13854, `.from_array` is deprecated - with tm.assert_produces_warning(FutureWarning): - Categorical.from_array([0, 1]) - - def test_datetime_categorical_comparison(self): - dt_cat = pd.Categorical( - pd.date_range('2014-01-01', periods=3), ordered=True) - self.assert_numpy_array_equal(dt_cat > dt_cat[0], - np.array([False, True, True])) - self.assert_numpy_array_equal(dt_cat[0] < dt_cat, - np.array([False, True, True])) - - def test_reflected_comparison_with_scalars(self): - # GH8658 - cat = pd.Categorical([1, 2, 3], ordered=True) - self.assert_numpy_array_equal(cat > cat[0], - np.array([False, True, True])) - self.assert_numpy_array_equal(cat[0] < cat, - np.array([False, True, True])) - - def test_comparison_with_unknown_scalars(self): - # https://github.com/pandas-dev/pandas/issues/9836#issuecomment-92123057 - # and following comparisons with scalars not in categories should raise - # for unequal comps, but not for equal/not equal - cat = pd.Categorical([1, 2, 3], ordered=True) - - self.assertRaises(TypeError, lambda: cat < 4) - self.assertRaises(TypeError, lambda: cat > 4) - self.assertRaises(TypeError, lambda: 4 < cat) - self.assertRaises(TypeError, lambda: 4 > cat) - - self.assert_numpy_array_equal(cat == 4, - np.array([False, False, False])) - self.assert_numpy_array_equal(cat != 4, - np.array([True, True, True])) - - def test_map(self): - c = pd.Categorical(list('ABABC'), categories=list('CBA'), - ordered=True) - result = c.map(lambda x: x.lower()) - exp = pd.Categorical(list('ababc'), categories=list('cba'), - ordered=True) - tm.assert_categorical_equal(result, exp) - - c = pd.Categorical(list('ABABC'), categories=list('ABC'), - ordered=False) - result = c.map(lambda x: x.lower()) - exp = pd.Categorical(list('ababc'), categories=list('abc'), - ordered=False) - tm.assert_categorical_equal(result, exp) - - result = c.map(lambda x: 1) - # GH 12766: Return an index not an array - tm.assert_index_equal(result, Index(np.array([1] * 5, dtype=np.int64))) - - def test_validate_inplace(self): - cat = Categorical(['A', 'B', 'B', 'C', 'A']) - invalid_values = [1, "True", [1, 2, 3], 5.0] - - for value in invalid_values: - with self.assertRaises(ValueError): - cat.set_ordered(value=True, inplace=value) - - with self.assertRaises(ValueError): - cat.as_ordered(inplace=value) - - with self.assertRaises(ValueError): - cat.as_unordered(inplace=value) - - with self.assertRaises(ValueError): - cat.set_categories(['X', 'Y', 'Z'], rename=True, inplace=value) - - with self.assertRaises(ValueError): - cat.rename_categories(['X', 'Y', 'Z'], inplace=value) - - with self.assertRaises(ValueError): - cat.reorder_categories( - ['X', 'Y', 'Z'], ordered=True, inplace=value) - - with self.assertRaises(ValueError): - cat.add_categories( - new_categories=['D', 'E', 'F'], inplace=value) - - with self.assertRaises(ValueError): - cat.remove_categories(removals=['D', 'E', 'F'], inplace=value) - - with self.assertRaises(ValueError): - cat.remove_unused_categories(inplace=value) - - with self.assertRaises(ValueError): - cat.sort_values(inplace=value) - - -class TestCategoricalAsBlock(tm.TestCase): - - def setUp(self): - self.factor = Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) - - df = DataFrame({'value': np.random.randint(0, 10000, 100)}) - labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] - - df = df.sort_values(by=['value'], ascending=True) - df['value_group'] = pd.cut(df.value, range(0, 10500, 500), right=False, - labels=labels) - self.cat = df - - def test_dtypes(self): - - # GH8143 - index = ['cat', 'obj', 'num'] - cat = pd.Categorical(['a', 'b', 'c']) - obj = pd.Series(['a', 'b', 'c']) - num = pd.Series([1, 2, 3]) - df = pd.concat([pd.Series(cat), obj, num], axis=1, keys=index) - - result = df.dtypes == 'object' - expected = Series([False, True, False], index=index) - tm.assert_series_equal(result, expected) - - result = df.dtypes == 'int64' - expected = Series([False, False, True], index=index) - tm.assert_series_equal(result, expected) - - result = df.dtypes == 'category' - expected = Series([True, False, False], index=index) - tm.assert_series_equal(result, expected) - - def test_codes_dtypes(self): - - # GH 8453 - result = Categorical(['foo', 'bar', 'baz']) - self.assertTrue(result.codes.dtype == 'int8') - - result = Categorical(['foo%05d' % i for i in range(400)]) - self.assertTrue(result.codes.dtype == 'int16') - - result = Categorical(['foo%05d' % i for i in range(40000)]) - self.assertTrue(result.codes.dtype == 'int32') - - # adding cats - result = Categorical(['foo', 'bar', 'baz']) - self.assertTrue(result.codes.dtype == 'int8') - result = result.add_categories(['foo%05d' % i for i in range(400)]) - self.assertTrue(result.codes.dtype == 'int16') - - # removing cats - result = result.remove_categories(['foo%05d' % i for i in range(300)]) - self.assertTrue(result.codes.dtype == 'int8') - - def test_basic(self): - - # test basic creation / coercion of categoricals - s = Series(self.factor, name='A') - self.assertEqual(s.dtype, 'category') - self.assertEqual(len(s), len(self.factor)) - str(s.values) - str(s) - - # in a frame - df = DataFrame({'A': self.factor}) - result = df['A'] - tm.assert_series_equal(result, s) - result = df.iloc[:, 0] - tm.assert_series_equal(result, s) - self.assertEqual(len(df), len(self.factor)) - str(df.values) - str(df) - - df = DataFrame({'A': s}) - result = df['A'] - tm.assert_series_equal(result, s) - self.assertEqual(len(df), len(self.factor)) - str(df.values) - str(df) - - # multiples - df = DataFrame({'A': s, 'B': s, 'C': 1}) - result1 = df['A'] - result2 = df['B'] - tm.assert_series_equal(result1, s) - tm.assert_series_equal(result2, s, check_names=False) - self.assertEqual(result2.name, 'B') - self.assertEqual(len(df), len(self.factor)) - str(df.values) - str(df) - - # GH8623 - x = pd.DataFrame([[1, 'John P. Doe'], [2, 'Jane Dove'], - [1, 'John P. Doe']], - columns=['person_id', 'person_name']) - x['person_name'] = pd.Categorical(x.person_name - ) # doing this breaks transform - - expected = x.iloc[0].person_name - result = x.person_name.iloc[0] - self.assertEqual(result, expected) - - result = x.person_name[0] - self.assertEqual(result, expected) - - result = x.person_name.loc[0] - self.assertEqual(result, expected) - - def test_creation_astype(self): - l = ["a", "b", "c", "a"] - s = pd.Series(l) - exp = pd.Series(Categorical(l)) - res = s.astype('category') - tm.assert_series_equal(res, exp) - - l = [1, 2, 3, 1] - s = pd.Series(l) - exp = pd.Series(Categorical(l)) - res = s.astype('category') - tm.assert_series_equal(res, exp) - - df = pd.DataFrame({"cats": [1, 2, 3, 4, 5, 6], - "vals": [1, 2, 3, 4, 5, 6]}) - cats = Categorical([1, 2, 3, 4, 5, 6]) - exp_df = pd.DataFrame({"cats": cats, "vals": [1, 2, 3, 4, 5, 6]}) - df["cats"] = df["cats"].astype("category") - tm.assert_frame_equal(exp_df, df) - - df = pd.DataFrame({"cats": ['a', 'b', 'b', 'a', 'a', 'd'], - "vals": [1, 2, 3, 4, 5, 6]}) - cats = Categorical(['a', 'b', 'b', 'a', 'a', 'd']) - exp_df = pd.DataFrame({"cats": cats, "vals": [1, 2, 3, 4, 5, 6]}) - df["cats"] = df["cats"].astype("category") - tm.assert_frame_equal(exp_df, df) - - # with keywords - l = ["a", "b", "c", "a"] - s = pd.Series(l) - exp = pd.Series(Categorical(l, ordered=True)) - res = s.astype('category', ordered=True) - tm.assert_series_equal(res, exp) - - exp = pd.Series(Categorical( - l, categories=list('abcdef'), ordered=True)) - res = s.astype('category', categories=list('abcdef'), ordered=True) - tm.assert_series_equal(res, exp) - - def test_construction_series(self): - - l = [1, 2, 3, 1] - exp = Series(l).astype('category') - res = Series(l, dtype='category') - tm.assert_series_equal(res, exp) - - l = ["a", "b", "c", "a"] - exp = Series(l).astype('category') - res = Series(l, dtype='category') - tm.assert_series_equal(res, exp) - - # insert into frame with different index - # GH 8076 - index = pd.date_range('20000101', periods=3) - expected = Series(Categorical(values=[np.nan, np.nan, np.nan], - categories=['a', 'b', 'c'])) - expected.index = index - - expected = DataFrame({'x': expected}) - df = DataFrame( - {'x': Series(['a', 'b', 'c'], dtype='category')}, index=index) - tm.assert_frame_equal(df, expected) - - def test_construction_frame(self): - - # GH8626 - - # dict creation - df = DataFrame({'A': list('abc')}, dtype='category') - expected = Series(list('abc'), dtype='category', name='A') - tm.assert_series_equal(df['A'], expected) - - # to_frame - s = Series(list('abc'), dtype='category') - result = s.to_frame() - expected = Series(list('abc'), dtype='category', name=0) - tm.assert_series_equal(result[0], expected) - result = s.to_frame(name='foo') - expected = Series(list('abc'), dtype='category', name='foo') - tm.assert_series_equal(result['foo'], expected) - - # list-like creation - df = DataFrame(list('abc'), dtype='category') - expected = Series(list('abc'), dtype='category', name=0) - tm.assert_series_equal(df[0], expected) - - # ndim != 1 - df = DataFrame([pd.Categorical(list('abc'))]) - expected = DataFrame({0: Series(list('abc'), dtype='category')}) - tm.assert_frame_equal(df, expected) - - df = DataFrame([pd.Categorical(list('abc')), pd.Categorical(list( - 'abd'))]) - expected = DataFrame({0: Series(list('abc'), dtype='category'), - 1: Series(list('abd'), dtype='category')}, - columns=[0, 1]) - tm.assert_frame_equal(df, expected) - - # mixed - df = DataFrame([pd.Categorical(list('abc')), list('def')]) - expected = DataFrame({0: Series(list('abc'), dtype='category'), - 1: list('def')}, columns=[0, 1]) - tm.assert_frame_equal(df, expected) - - # invalid (shape) - self.assertRaises( - ValueError, - lambda: DataFrame([pd.Categorical(list('abc')), - pd.Categorical(list('abdefg'))])) - - # ndim > 1 - self.assertRaises(NotImplementedError, - lambda: pd.Categorical(np.array([list('abcd')]))) - - def test_reshaping(self): - - p = tm.makePanel() - p['str'] = 'foo' - df = p.to_frame() - df['category'] = df['str'].astype('category') - result = df['category'].unstack() - - c = Categorical(['foo'] * len(p.major_axis)) - expected = DataFrame({'A': c.copy(), - 'B': c.copy(), - 'C': c.copy(), - 'D': c.copy()}, - columns=Index(list('ABCD'), name='minor'), - index=p.major_axis.set_names('major')) - tm.assert_frame_equal(result, expected) - - def test_reindex(self): - - index = pd.date_range('20000101', periods=3) - - # reindexing to an invalid Categorical - s = Series(['a', 'b', 'c'], dtype='category') - result = s.reindex(index) - expected = Series(Categorical(values=[np.nan, np.nan, np.nan], - categories=['a', 'b', 'c'])) - expected.index = index - tm.assert_series_equal(result, expected) - - # partial reindexing - expected = Series(Categorical(values=['b', 'c'], categories=['a', 'b', - 'c'])) - expected.index = [1, 2] - result = s.reindex([1, 2]) - tm.assert_series_equal(result, expected) - - expected = Series(Categorical( - values=['c', np.nan], categories=['a', 'b', 'c'])) - expected.index = [2, 3] - result = s.reindex([2, 3]) - tm.assert_series_equal(result, expected) - - def test_sideeffects_free(self): - # Passing a categorical to a Series and then changing values in either - # the series or the categorical should not change the values in the - # other one, IF you specify copy! - cat = Categorical(["a", "b", "c", "a"]) - s = pd.Series(cat, copy=True) - self.assertFalse(s.cat is cat) - s.cat.categories = [1, 2, 3] - exp_s = np.array([1, 2, 3, 1], dtype=np.int64) - exp_cat = np.array(["a", "b", "c", "a"], dtype=np.object_) - self.assert_numpy_array_equal(s.__array__(), exp_s) - self.assert_numpy_array_equal(cat.__array__(), exp_cat) - - # setting - s[0] = 2 - exp_s2 = np.array([2, 2, 3, 1], dtype=np.int64) - self.assert_numpy_array_equal(s.__array__(), exp_s2) - self.assert_numpy_array_equal(cat.__array__(), exp_cat) - - # however, copy is False by default - # so this WILL change values - cat = Categorical(["a", "b", "c", "a"]) - s = pd.Series(cat) - self.assertTrue(s.values is cat) - s.cat.categories = [1, 2, 3] - exp_s = np.array([1, 2, 3, 1], dtype=np.int64) - self.assert_numpy_array_equal(s.__array__(), exp_s) - self.assert_numpy_array_equal(cat.__array__(), exp_s) - - s[0] = 2 - exp_s2 = np.array([2, 2, 3, 1], dtype=np.int64) - self.assert_numpy_array_equal(s.__array__(), exp_s2) - self.assert_numpy_array_equal(cat.__array__(), exp_s2) - - def test_nan_handling(self): - - # NaNs are represented as -1 in labels - s = Series(Categorical(["a", "b", np.nan, "a"])) - self.assert_index_equal(s.cat.categories, Index(["a", "b"])) - self.assert_numpy_array_equal(s.values.codes, - np.array([0, 1, -1, 0], dtype=np.int8)) - - def test_cat_accessor(self): - s = Series(Categorical(["a", "b", np.nan, "a"])) - self.assert_index_equal(s.cat.categories, Index(["a", "b"])) - self.assertEqual(s.cat.ordered, False) - exp = Categorical(["a", "b", np.nan, "a"], categories=["b", "a"]) - s.cat.set_categories(["b", "a"], inplace=True) - tm.assert_categorical_equal(s.values, exp) - - res = s.cat.set_categories(["b", "a"]) - tm.assert_categorical_equal(res.values, exp) - - exp = Categorical(["a", "b", np.nan, "a"], categories=["b", "a"]) - s[:] = "a" - s = s.cat.remove_unused_categories() - self.assert_index_equal(s.cat.categories, Index(["a"])) - - def test_sequence_like(self): - - # GH 7839 - # make sure can iterate - df = DataFrame({"id": [1, 2, 3, 4, 5, 6], - "raw_grade": ['a', 'b', 'b', 'a', 'a', 'e']}) - df['grade'] = Categorical(df['raw_grade']) - - # basic sequencing testing - result = list(df.grade.values) - expected = np.array(df.grade.values).tolist() - tm.assert_almost_equal(result, expected) - - # iteration - for t in df.itertuples(index=False): - str(t) - - for row, s in df.iterrows(): - str(s) - - for c, col in df.iteritems(): - str(s) - - def test_series_delegations(self): - - # invalid accessor - self.assertRaises(AttributeError, lambda: Series([1, 2, 3]).cat) - tm.assertRaisesRegexp( - AttributeError, - r"Can only use .cat accessor with a 'category' dtype", - lambda: Series([1, 2, 3]).cat) - self.assertRaises(AttributeError, lambda: Series(['a', 'b', 'c']).cat) - self.assertRaises(AttributeError, lambda: Series(np.arange(5.)).cat) - self.assertRaises(AttributeError, - lambda: Series([Timestamp('20130101')]).cat) - - # Series should delegate calls to '.categories', '.codes', '.ordered' - # and the methods '.set_categories()' 'drop_unused_categories()' to the - # categorical - s = Series(Categorical(["a", "b", "c", "a"], ordered=True)) - exp_categories = Index(["a", "b", "c"]) - tm.assert_index_equal(s.cat.categories, exp_categories) - s.cat.categories = [1, 2, 3] - exp_categories = Index([1, 2, 3]) - self.assert_index_equal(s.cat.categories, exp_categories) - - exp_codes = Series([0, 1, 2, 0], dtype='int8') - tm.assert_series_equal(s.cat.codes, exp_codes) - - self.assertEqual(s.cat.ordered, True) - s = s.cat.as_unordered() - self.assertEqual(s.cat.ordered, False) - s.cat.as_ordered(inplace=True) - self.assertEqual(s.cat.ordered, True) - - # reorder - s = Series(Categorical(["a", "b", "c", "a"], ordered=True)) - exp_categories = Index(["c", "b", "a"]) - exp_values = np.array(["a", "b", "c", "a"], dtype=np.object_) - s = s.cat.set_categories(["c", "b", "a"]) - tm.assert_index_equal(s.cat.categories, exp_categories) - self.assert_numpy_array_equal(s.values.__array__(), exp_values) - self.assert_numpy_array_equal(s.__array__(), exp_values) - - # remove unused categories - s = Series(Categorical(["a", "b", "b", "a"], categories=["a", "b", "c" - ])) - exp_categories = Index(["a", "b"]) - exp_values = np.array(["a", "b", "b", "a"], dtype=np.object_) - s = s.cat.remove_unused_categories() - self.assert_index_equal(s.cat.categories, exp_categories) - self.assert_numpy_array_equal(s.values.__array__(), exp_values) - self.assert_numpy_array_equal(s.__array__(), exp_values) - - # This method is likely to be confused, so test that it raises an error - # on wrong inputs: - def f(): - s.set_categories([4, 3, 2, 1]) - - self.assertRaises(Exception, f) - # right: s.cat.set_categories([4,3,2,1]) - - def test_series_functions_no_warnings(self): - df = pd.DataFrame({'value': np.random.randint(0, 100, 20)}) - labels = ["{0} - {1}".format(i, i + 9) for i in range(0, 100, 10)] - with tm.assert_produces_warning(False): - df['group'] = pd.cut(df.value, range(0, 105, 10), right=False, - labels=labels) - - def test_assignment_to_dataframe(self): - # assignment - df = DataFrame({'value': np.array(np.random.randint(0, 10000, 100), - dtype='int32')}) - labels = ["{0} - {1}".format(i, i + 499) for i in range(0, 10000, 500)] - - df = df.sort_values(by=['value'], ascending=True) - s = pd.cut(df.value, range(0, 10500, 500), right=False, labels=labels) - d = s.values - df['D'] = d - str(df) - - result = df.dtypes - expected = Series( - [np.dtype('int32'), CategoricalDtype()], index=['value', 'D']) - tm.assert_series_equal(result, expected) - - df['E'] = s - str(df) - - result = df.dtypes - expected = Series([np.dtype('int32'), CategoricalDtype(), - CategoricalDtype()], - index=['value', 'D', 'E']) - tm.assert_series_equal(result, expected) - - result1 = df['D'] - result2 = df['E'] - self.assert_categorical_equal(result1._data._block.values, d) - - # sorting - s.name = 'E' - self.assert_series_equal(result2.sort_index(), s.sort_index()) - - cat = pd.Categorical([1, 2, 3, 10], categories=[1, 2, 3, 4, 10]) - df = pd.DataFrame(pd.Series(cat)) - - def test_describe(self): - - # Categoricals should not show up together with numerical columns - result = self.cat.describe() - self.assertEqual(len(result.columns), 1) - - # In a frame, describe() for the cat should be the same as for string - # arrays (count, unique, top, freq) - - cat = Categorical(["a", "b", "b", "b"], categories=['a', 'b', 'c'], - ordered=True) - s = Series(cat) - result = s.describe() - expected = Series([4, 2, "b", 3], - index=['count', 'unique', 'top', 'freq']) - tm.assert_series_equal(result, expected) - - cat = pd.Series(pd.Categorical(["a", "b", "c", "c"])) - df3 = pd.DataFrame({"cat": cat, "s": ["a", "b", "c", "c"]}) - res = df3.describe() - self.assert_numpy_array_equal(res["cat"].values, res["s"].values) - - def test_repr(self): - a = pd.Series(pd.Categorical([1, 2, 3, 4])) - exp = u("0 1\n1 2\n2 3\n3 4\n" + - "dtype: category\nCategories (4, int64): [1, 2, 3, 4]") - - self.assertEqual(exp, a.__unicode__()) - - a = pd.Series(pd.Categorical(["a", "b"] * 25)) - exp = u("0 a\n1 b\n" + " ..\n" + "48 a\n49 b\n" + - "dtype: category\nCategories (2, object): [a, b]") - with option_context("display.max_rows", 5): - self.assertEqual(exp, repr(a)) - - levs = list("abcdefghijklmnopqrstuvwxyz") - a = pd.Series(pd.Categorical( - ["a", "b"], categories=levs, ordered=True)) - exp = u("0 a\n1 b\n" + "dtype: category\n" - "Categories (26, object): [a < b < c < d ... w < x < y < z]") - self.assertEqual(exp, a.__unicode__()) - - def test_categorical_repr(self): - c = pd.Categorical([1, 2, 3]) - exp = """[1, 2, 3] -Categories (3, int64): [1, 2, 3]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical([1, 2, 3, 1, 2, 3], categories=[1, 2, 3]) - exp = """[1, 2, 3, 1, 2, 3] -Categories (3, int64): [1, 2, 3]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical([1, 2, 3, 4, 5] * 10) - exp = """[1, 2, 3, 4, 5, ..., 1, 2, 3, 4, 5] -Length: 50 -Categories (5, int64): [1, 2, 3, 4, 5]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(np.arange(20)) - exp = """[0, 1, 2, 3, 4, ..., 15, 16, 17, 18, 19] -Length: 20 -Categories (20, int64): [0, 1, 2, 3, ..., 16, 17, 18, 19]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_ordered(self): - c = pd.Categorical([1, 2, 3], ordered=True) - exp = """[1, 2, 3] -Categories (3, int64): [1 < 2 < 3]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical([1, 2, 3, 1, 2, 3], categories=[1, 2, 3], - ordered=True) - exp = """[1, 2, 3, 1, 2, 3] -Categories (3, int64): [1 < 2 < 3]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical([1, 2, 3, 4, 5] * 10, ordered=True) - exp = """[1, 2, 3, 4, 5, ..., 1, 2, 3, 4, 5] -Length: 50 -Categories (5, int64): [1 < 2 < 3 < 4 < 5]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(np.arange(20), ordered=True) - exp = """[0, 1, 2, 3, 4, ..., 15, 16, 17, 18, 19] -Length: 20 -Categories (20, int64): [0 < 1 < 2 < 3 ... 16 < 17 < 18 < 19]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_datetime(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - c = pd.Categorical(idx) - - # TODO(wesm): exceeding 80 characters in the console is not good - # behavior - exp = ( - "[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, " - "2011-01-01 12:00:00, 2011-01-01 13:00:00]\n" - "Categories (5, datetime64[ns]): [2011-01-01 09:00:00, " - "2011-01-01 10:00:00, 2011-01-01 11:00:00,\n" - " 2011-01-01 12:00:00, " - "2011-01-01 13:00:00]""") - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = ( - "[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, " - "2011-01-01 12:00:00, 2011-01-01 13:00:00, 2011-01-01 09:00:00, " - "2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, " - "2011-01-01 13:00:00]\n" - "Categories (5, datetime64[ns]): [2011-01-01 09:00:00, " - "2011-01-01 10:00:00, 2011-01-01 11:00:00,\n" - " 2011-01-01 12:00:00, " - "2011-01-01 13:00:00]") - - self.assertEqual(repr(c), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - c = pd.Categorical(idx) - exp = ( - "[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, " - "2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, " - "2011-01-01 13:00:00-05:00]\n" - "Categories (5, datetime64[ns, US/Eastern]): " - "[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00,\n" - " " - "2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00,\n" - " " - "2011-01-01 13:00:00-05:00]") - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = ( - "[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, " - "2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, " - "2011-01-01 13:00:00-05:00, 2011-01-01 09:00:00-05:00, " - "2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, " - "2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00]\n" - "Categories (5, datetime64[ns, US/Eastern]): " - "[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00,\n" - " " - "2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00,\n" - " " - "2011-01-01 13:00:00-05:00]") - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_datetime_ordered(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - c = pd.Categorical(idx, ordered=True) - exp = """[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, 2011-01-01 13:00:00] -Categories (5, datetime64[ns]): [2011-01-01 09:00:00 < 2011-01-01 10:00:00 < 2011-01-01 11:00:00 < - 2011-01-01 12:00:00 < 2011-01-01 13:00:00]""" # noqa - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, 2011-01-01 13:00:00, 2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, 2011-01-01 13:00:00] -Categories (5, datetime64[ns]): [2011-01-01 09:00:00 < 2011-01-01 10:00:00 < 2011-01-01 11:00:00 < - 2011-01-01 12:00:00 < 2011-01-01 13:00:00]""" # noqa - - self.assertEqual(repr(c), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - c = pd.Categorical(idx, ordered=True) - exp = """[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00] -Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00 < 2011-01-01 10:00:00-05:00 < - 2011-01-01 11:00:00-05:00 < 2011-01-01 12:00:00-05:00 < - 2011-01-01 13:00:00-05:00]""" # noqa - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00, 2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00] -Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00 < 2011-01-01 10:00:00-05:00 < - 2011-01-01 11:00:00-05:00 < 2011-01-01 12:00:00-05:00 < - 2011-01-01 13:00:00-05:00]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_period(self): - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - c = pd.Categorical(idx) - exp = """[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00] -Categories (5, period[H]): [2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, - 2011-01-01 13:00]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = """[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00, 2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00] -Categories (5, period[H]): [2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, - 2011-01-01 13:00]""" - - self.assertEqual(repr(c), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - c = pd.Categorical(idx) - exp = """[2011-01, 2011-02, 2011-03, 2011-04, 2011-05] -Categories (5, period[M]): [2011-01, 2011-02, 2011-03, 2011-04, 2011-05]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = """[2011-01, 2011-02, 2011-03, 2011-04, 2011-05, 2011-01, 2011-02, 2011-03, 2011-04, 2011-05] -Categories (5, period[M]): [2011-01, 2011-02, 2011-03, 2011-04, 2011-05]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_period_ordered(self): - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - c = pd.Categorical(idx, ordered=True) - exp = """[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00] -Categories (5, period[H]): [2011-01-01 09:00 < 2011-01-01 10:00 < 2011-01-01 11:00 < 2011-01-01 12:00 < - 2011-01-01 13:00]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00, 2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00] -Categories (5, period[H]): [2011-01-01 09:00 < 2011-01-01 10:00 < 2011-01-01 11:00 < 2011-01-01 12:00 < - 2011-01-01 13:00]""" - - self.assertEqual(repr(c), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - c = pd.Categorical(idx, ordered=True) - exp = """[2011-01, 2011-02, 2011-03, 2011-04, 2011-05] -Categories (5, period[M]): [2011-01 < 2011-02 < 2011-03 < 2011-04 < 2011-05]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[2011-01, 2011-02, 2011-03, 2011-04, 2011-05, 2011-01, 2011-02, 2011-03, 2011-04, 2011-05] -Categories (5, period[M]): [2011-01 < 2011-02 < 2011-03 < 2011-04 < 2011-05]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_timedelta(self): - idx = pd.timedelta_range('1 days', periods=5) - c = pd.Categorical(idx) - exp = """[1 days, 2 days, 3 days, 4 days, 5 days] -Categories (5, timedelta64[ns]): [1 days, 2 days, 3 days, 4 days, 5 days]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = """[1 days, 2 days, 3 days, 4 days, 5 days, 1 days, 2 days, 3 days, 4 days, 5 days] -Categories (5, timedelta64[ns]): [1 days, 2 days, 3 days, 4 days, 5 days]""" - - self.assertEqual(repr(c), exp) - - idx = pd.timedelta_range('1 hours', periods=20) - c = pd.Categorical(idx) - exp = """[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, ..., 15 days 01:00:00, 16 days 01:00:00, 17 days 01:00:00, 18 days 01:00:00, 19 days 01:00:00] -Length: 20 -Categories (20, timedelta64[ns]): [0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, - 3 days 01:00:00, ..., 16 days 01:00:00, 17 days 01:00:00, - 18 days 01:00:00, 19 days 01:00:00]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx) - exp = """[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, ..., 15 days 01:00:00, 16 days 01:00:00, 17 days 01:00:00, 18 days 01:00:00, 19 days 01:00:00] -Length: 40 -Categories (20, timedelta64[ns]): [0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, - 3 days 01:00:00, ..., 16 days 01:00:00, 17 days 01:00:00, - 18 days 01:00:00, 19 days 01:00:00]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_repr_timedelta_ordered(self): - idx = pd.timedelta_range('1 days', periods=5) - c = pd.Categorical(idx, ordered=True) - exp = """[1 days, 2 days, 3 days, 4 days, 5 days] -Categories (5, timedelta64[ns]): [1 days < 2 days < 3 days < 4 days < 5 days]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[1 days, 2 days, 3 days, 4 days, 5 days, 1 days, 2 days, 3 days, 4 days, 5 days] -Categories (5, timedelta64[ns]): [1 days < 2 days < 3 days < 4 days < 5 days]""" - - self.assertEqual(repr(c), exp) - - idx = pd.timedelta_range('1 hours', periods=20) - c = pd.Categorical(idx, ordered=True) - exp = """[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, ..., 15 days 01:00:00, 16 days 01:00:00, 17 days 01:00:00, 18 days 01:00:00, 19 days 01:00:00] -Length: 20 -Categories (20, timedelta64[ns]): [0 days 01:00:00 < 1 days 01:00:00 < 2 days 01:00:00 < - 3 days 01:00:00 ... 16 days 01:00:00 < 17 days 01:00:00 < - 18 days 01:00:00 < 19 days 01:00:00]""" - - self.assertEqual(repr(c), exp) - - c = pd.Categorical(idx.append(idx), categories=idx, ordered=True) - exp = """[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, ..., 15 days 01:00:00, 16 days 01:00:00, 17 days 01:00:00, 18 days 01:00:00, 19 days 01:00:00] -Length: 40 -Categories (20, timedelta64[ns]): [0 days 01:00:00 < 1 days 01:00:00 < 2 days 01:00:00 < - 3 days 01:00:00 ... 16 days 01:00:00 < 17 days 01:00:00 < - 18 days 01:00:00 < 19 days 01:00:00]""" - - self.assertEqual(repr(c), exp) - - def test_categorical_series_repr(self): - s = pd.Series(pd.Categorical([1, 2, 3])) - exp = """0 1 -1 2 -2 3 -dtype: category -Categories (3, int64): [1, 2, 3]""" - - self.assertEqual(repr(s), exp) - - s = pd.Series(pd.Categorical(np.arange(10))) - exp = """0 0 -1 1 -2 2 -3 3 -4 4 -5 5 -6 6 -7 7 -8 8 -9 9 -dtype: category -Categories (10, int64): [0, 1, 2, 3, ..., 6, 7, 8, 9]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_ordered(self): - s = pd.Series(pd.Categorical([1, 2, 3], ordered=True)) - exp = """0 1 -1 2 -2 3 -dtype: category -Categories (3, int64): [1 < 2 < 3]""" - - self.assertEqual(repr(s), exp) - - s = pd.Series(pd.Categorical(np.arange(10), ordered=True)) - exp = """0 0 -1 1 -2 2 -3 3 -4 4 -5 5 -6 6 -7 7 -8 8 -9 9 -dtype: category -Categories (10, int64): [0 < 1 < 2 < 3 ... 6 < 7 < 8 < 9]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_datetime(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - s = pd.Series(pd.Categorical(idx)) - exp = """0 2011-01-01 09:00:00 -1 2011-01-01 10:00:00 -2 2011-01-01 11:00:00 -3 2011-01-01 12:00:00 -4 2011-01-01 13:00:00 -dtype: category -Categories (5, datetime64[ns]): [2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, - 2011-01-01 12:00:00, 2011-01-01 13:00:00]""" - - self.assertEqual(repr(s), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - s = pd.Series(pd.Categorical(idx)) - exp = """0 2011-01-01 09:00:00-05:00 -1 2011-01-01 10:00:00-05:00 -2 2011-01-01 11:00:00-05:00 -3 2011-01-01 12:00:00-05:00 -4 2011-01-01 13:00:00-05:00 -dtype: category -Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, - 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, - 2011-01-01 13:00:00-05:00]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_datetime_ordered(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 2011-01-01 09:00:00 -1 2011-01-01 10:00:00 -2 2011-01-01 11:00:00 -3 2011-01-01 12:00:00 -4 2011-01-01 13:00:00 -dtype: category -Categories (5, datetime64[ns]): [2011-01-01 09:00:00 < 2011-01-01 10:00:00 < 2011-01-01 11:00:00 < - 2011-01-01 12:00:00 < 2011-01-01 13:00:00]""" - - self.assertEqual(repr(s), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 2011-01-01 09:00:00-05:00 -1 2011-01-01 10:00:00-05:00 -2 2011-01-01 11:00:00-05:00 -3 2011-01-01 12:00:00-05:00 -4 2011-01-01 13:00:00-05:00 -dtype: category -Categories (5, datetime64[ns, US/Eastern]): [2011-01-01 09:00:00-05:00 < 2011-01-01 10:00:00-05:00 < - 2011-01-01 11:00:00-05:00 < 2011-01-01 12:00:00-05:00 < - 2011-01-01 13:00:00-05:00]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_period(self): - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - s = pd.Series(pd.Categorical(idx)) - exp = """0 2011-01-01 09:00 -1 2011-01-01 10:00 -2 2011-01-01 11:00 -3 2011-01-01 12:00 -4 2011-01-01 13:00 -dtype: category -Categories (5, period[H]): [2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, - 2011-01-01 13:00]""" - - self.assertEqual(repr(s), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - s = pd.Series(pd.Categorical(idx)) - exp = """0 2011-01 -1 2011-02 -2 2011-03 -3 2011-04 -4 2011-05 -dtype: category -Categories (5, period[M]): [2011-01, 2011-02, 2011-03, 2011-04, 2011-05]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_period_ordered(self): - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 2011-01-01 09:00 -1 2011-01-01 10:00 -2 2011-01-01 11:00 -3 2011-01-01 12:00 -4 2011-01-01 13:00 -dtype: category -Categories (5, period[H]): [2011-01-01 09:00 < 2011-01-01 10:00 < 2011-01-01 11:00 < 2011-01-01 12:00 < - 2011-01-01 13:00]""" - - self.assertEqual(repr(s), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 2011-01 -1 2011-02 -2 2011-03 -3 2011-04 -4 2011-05 -dtype: category -Categories (5, period[M]): [2011-01 < 2011-02 < 2011-03 < 2011-04 < 2011-05]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_timedelta(self): - idx = pd.timedelta_range('1 days', periods=5) - s = pd.Series(pd.Categorical(idx)) - exp = """0 1 days -1 2 days -2 3 days -3 4 days -4 5 days -dtype: category -Categories (5, timedelta64[ns]): [1 days, 2 days, 3 days, 4 days, 5 days]""" - - self.assertEqual(repr(s), exp) - - idx = pd.timedelta_range('1 hours', periods=10) - s = pd.Series(pd.Categorical(idx)) - exp = """0 0 days 01:00:00 -1 1 days 01:00:00 -2 2 days 01:00:00 -3 3 days 01:00:00 -4 4 days 01:00:00 -5 5 days 01:00:00 -6 6 days 01:00:00 -7 7 days 01:00:00 -8 8 days 01:00:00 -9 9 days 01:00:00 -dtype: category -Categories (10, timedelta64[ns]): [0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, - 3 days 01:00:00, ..., 6 days 01:00:00, 7 days 01:00:00, - 8 days 01:00:00, 9 days 01:00:00]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_series_repr_timedelta_ordered(self): - idx = pd.timedelta_range('1 days', periods=5) - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 1 days -1 2 days -2 3 days -3 4 days -4 5 days -dtype: category -Categories (5, timedelta64[ns]): [1 days < 2 days < 3 days < 4 days < 5 days]""" - - self.assertEqual(repr(s), exp) - - idx = pd.timedelta_range('1 hours', periods=10) - s = pd.Series(pd.Categorical(idx, ordered=True)) - exp = """0 0 days 01:00:00 -1 1 days 01:00:00 -2 2 days 01:00:00 -3 3 days 01:00:00 -4 4 days 01:00:00 -5 5 days 01:00:00 -6 6 days 01:00:00 -7 7 days 01:00:00 -8 8 days 01:00:00 -9 9 days 01:00:00 -dtype: category -Categories (10, timedelta64[ns]): [0 days 01:00:00 < 1 days 01:00:00 < 2 days 01:00:00 < - 3 days 01:00:00 ... 6 days 01:00:00 < 7 days 01:00:00 < - 8 days 01:00:00 < 9 days 01:00:00]""" - - self.assertEqual(repr(s), exp) - - def test_categorical_index_repr(self): - idx = pd.CategoricalIndex(pd.Categorical([1, 2, 3])) - exp = """CategoricalIndex([1, 2, 3], categories=[1, 2, 3], ordered=False, dtype='category')""" - self.assertEqual(repr(idx), exp) - - i = pd.CategoricalIndex(pd.Categorical(np.arange(10))) - exp = """CategoricalIndex([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], categories=[0, 1, 2, 3, 4, 5, 6, 7, ...], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_ordered(self): - i = pd.CategoricalIndex(pd.Categorical([1, 2, 3], ordered=True)) - exp = """CategoricalIndex([1, 2, 3], categories=[1, 2, 3], ordered=True, dtype='category')""" - self.assertEqual(repr(i), exp) - - i = pd.CategoricalIndex(pd.Categorical(np.arange(10), ordered=True)) - exp = """CategoricalIndex([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], categories=[0, 1, 2, 3, 4, 5, 6, 7, ...], ordered=True, dtype='category')""" - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_datetime(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00:00', '2011-01-01 10:00:00', - '2011-01-01 11:00:00', '2011-01-01 12:00:00', - '2011-01-01 13:00:00'], - categories=[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, 2011-01-01 13:00:00], ordered=False, dtype='category')""" - - self.assertEqual(repr(i), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00:00-05:00', '2011-01-01 10:00:00-05:00', - '2011-01-01 11:00:00-05:00', '2011-01-01 12:00:00-05:00', - '2011-01-01 13:00:00-05:00'], - categories=[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00], ordered=False, dtype='category')""" - - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_datetime_ordered(self): - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['2011-01-01 09:00:00', '2011-01-01 10:00:00', - '2011-01-01 11:00:00', '2011-01-01 12:00:00', - '2011-01-01 13:00:00'], - categories=[2011-01-01 09:00:00, 2011-01-01 10:00:00, 2011-01-01 11:00:00, 2011-01-01 12:00:00, 2011-01-01 13:00:00], ordered=True, dtype='category')""" - - self.assertEqual(repr(i), exp) - - idx = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['2011-01-01 09:00:00-05:00', '2011-01-01 10:00:00-05:00', - '2011-01-01 11:00:00-05:00', '2011-01-01 12:00:00-05:00', - '2011-01-01 13:00:00-05:00'], - categories=[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00], ordered=True, dtype='category')""" - - self.assertEqual(repr(i), exp) - - i = pd.CategoricalIndex(pd.Categorical(idx.append(idx), ordered=True)) - exp = """CategoricalIndex(['2011-01-01 09:00:00-05:00', '2011-01-01 10:00:00-05:00', - '2011-01-01 11:00:00-05:00', '2011-01-01 12:00:00-05:00', - '2011-01-01 13:00:00-05:00', '2011-01-01 09:00:00-05:00', - '2011-01-01 10:00:00-05:00', '2011-01-01 11:00:00-05:00', - '2011-01-01 12:00:00-05:00', '2011-01-01 13:00:00-05:00'], - categories=[2011-01-01 09:00:00-05:00, 2011-01-01 10:00:00-05:00, 2011-01-01 11:00:00-05:00, 2011-01-01 12:00:00-05:00, 2011-01-01 13:00:00-05:00], ordered=True, dtype='category')""" - - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_period(self): - # test all length - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=1) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00'], categories=[2011-01-01 09:00], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=2) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00', '2011-01-01 10:00'], categories=[2011-01-01 09:00, 2011-01-01 10:00], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=3) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00', '2011-01-01 10:00', '2011-01-01 11:00'], categories=[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01-01 09:00', '2011-01-01 10:00', '2011-01-01 11:00', - '2011-01-01 12:00', '2011-01-01 13:00'], - categories=[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00], ordered=False, dtype='category')""" - - self.assertEqual(repr(i), exp) - - i = pd.CategoricalIndex(pd.Categorical(idx.append(idx))) - exp = """CategoricalIndex(['2011-01-01 09:00', '2011-01-01 10:00', '2011-01-01 11:00', - '2011-01-01 12:00', '2011-01-01 13:00', '2011-01-01 09:00', - '2011-01-01 10:00', '2011-01-01 11:00', '2011-01-01 12:00', - '2011-01-01 13:00'], - categories=[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00], ordered=False, dtype='category')""" - - self.assertEqual(repr(i), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['2011-01', '2011-02', '2011-03', '2011-04', '2011-05'], categories=[2011-01, 2011-02, 2011-03, 2011-04, 2011-05], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_period_ordered(self): - idx = pd.period_range('2011-01-01 09:00', freq='H', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['2011-01-01 09:00', '2011-01-01 10:00', '2011-01-01 11:00', - '2011-01-01 12:00', '2011-01-01 13:00'], - categories=[2011-01-01 09:00, 2011-01-01 10:00, 2011-01-01 11:00, 2011-01-01 12:00, 2011-01-01 13:00], ordered=True, dtype='category')""" - - self.assertEqual(repr(i), exp) - - idx = pd.period_range('2011-01', freq='M', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['2011-01', '2011-02', '2011-03', '2011-04', '2011-05'], categories=[2011-01, 2011-02, 2011-03, 2011-04, 2011-05], ordered=True, dtype='category')""" - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_timedelta(self): - idx = pd.timedelta_range('1 days', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['1 days', '2 days', '3 days', '4 days', '5 days'], categories=[1 days 00:00:00, 2 days 00:00:00, 3 days 00:00:00, 4 days 00:00:00, 5 days 00:00:00], ordered=False, dtype='category')""" - self.assertEqual(repr(i), exp) - - idx = pd.timedelta_range('1 hours', periods=10) - i = pd.CategoricalIndex(pd.Categorical(idx)) - exp = """CategoricalIndex(['0 days 01:00:00', '1 days 01:00:00', '2 days 01:00:00', - '3 days 01:00:00', '4 days 01:00:00', '5 days 01:00:00', - '6 days 01:00:00', '7 days 01:00:00', '8 days 01:00:00', - '9 days 01:00:00'], - categories=[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, 5 days 01:00:00, 6 days 01:00:00, 7 days 01:00:00, ...], ordered=False, dtype='category')""" - - self.assertEqual(repr(i), exp) - - def test_categorical_index_repr_timedelta_ordered(self): - idx = pd.timedelta_range('1 days', periods=5) - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['1 days', '2 days', '3 days', '4 days', '5 days'], categories=[1 days 00:00:00, 2 days 00:00:00, 3 days 00:00:00, 4 days 00:00:00, 5 days 00:00:00], ordered=True, dtype='category')""" - self.assertEqual(repr(i), exp) - - idx = pd.timedelta_range('1 hours', periods=10) - i = pd.CategoricalIndex(pd.Categorical(idx, ordered=True)) - exp = """CategoricalIndex(['0 days 01:00:00', '1 days 01:00:00', '2 days 01:00:00', - '3 days 01:00:00', '4 days 01:00:00', '5 days 01:00:00', - '6 days 01:00:00', '7 days 01:00:00', '8 days 01:00:00', - '9 days 01:00:00'], - categories=[0 days 01:00:00, 1 days 01:00:00, 2 days 01:00:00, 3 days 01:00:00, 4 days 01:00:00, 5 days 01:00:00, 6 days 01:00:00, 7 days 01:00:00, ...], ordered=True, dtype='category')""" - - self.assertEqual(repr(i), exp) - - def test_categorical_frame(self): - # normal DataFrame - dt = pd.date_range('2011-01-01 09:00', freq='H', periods=5, - tz='US/Eastern') - p = pd.period_range('2011-01', freq='M', periods=5) - df = pd.DataFrame({'dt': dt, 'p': p}) - exp = """ dt p -0 2011-01-01 09:00:00-05:00 2011-01 -1 2011-01-01 10:00:00-05:00 2011-02 -2 2011-01-01 11:00:00-05:00 2011-03 -3 2011-01-01 12:00:00-05:00 2011-04 -4 2011-01-01 13:00:00-05:00 2011-05""" - - df = pd.DataFrame({'dt': pd.Categorical(dt), 'p': pd.Categorical(p)}) - self.assertEqual(repr(df), exp) - - def test_info(self): - - # make sure it works - n = 2500 - df = DataFrame({'int64': np.random.randint(100, size=n)}) - df['category'] = Series(np.array(list('abcdefghij')).take( - np.random.randint(0, 10, size=n))).astype('category') - df.isnull() - buf = compat.StringIO() - df.info(buf=buf) - - df2 = df[df['category'] == 'd'] - buf = compat.StringIO() - df2.info(buf=buf) - - def test_groupby_sort(self): - - # http://stackoverflow.com/questions/23814368/sorting-pandas-categorical-labels-after-groupby - # This should result in a properly sorted Series so that the plot - # has a sorted x axis - # self.cat.groupby(['value_group'])['value_group'].count().plot(kind='bar') - - res = self.cat.groupby(['value_group'])['value_group'].count() - exp = res[sorted(res.index, key=lambda x: float(x.split()[0]))] - exp.index = pd.CategoricalIndex(exp.index, name=exp.index.name) - tm.assert_series_equal(res, exp) - - def test_min_max(self): - # unordered cats have no min/max - cat = Series(Categorical(["a", "b", "c", "d"], ordered=False)) - self.assertRaises(TypeError, lambda: cat.min()) - self.assertRaises(TypeError, lambda: cat.max()) - - cat = Series(Categorical(["a", "b", "c", "d"], ordered=True)) - _min = cat.min() - _max = cat.max() - self.assertEqual(_min, "a") - self.assertEqual(_max, "d") - - cat = Series(Categorical(["a", "b", "c", "d"], categories=[ - 'd', 'c', 'b', 'a'], ordered=True)) - _min = cat.min() - _max = cat.max() - self.assertEqual(_min, "d") - self.assertEqual(_max, "a") - - cat = Series(Categorical( - [np.nan, "b", "c", np.nan], categories=['d', 'c', 'b', 'a' - ], ordered=True)) - _min = cat.min() - _max = cat.max() - self.assertTrue(np.isnan(_min)) - self.assertEqual(_max, "b") - - cat = Series(Categorical( - [np.nan, 1, 2, np.nan], categories=[5, 4, 3, 2, 1], ordered=True)) - _min = cat.min() - _max = cat.max() - self.assertTrue(np.isnan(_min)) - self.assertEqual(_max, 1) - - def test_mode(self): - s = Series(Categorical([1, 1, 2, 4, 5, 5, 5], - categories=[5, 4, 3, 2, 1], ordered=True)) - res = s.mode() - exp = Series(Categorical([5], categories=[ - 5, 4, 3, 2, 1], ordered=True)) - tm.assert_series_equal(res, exp) - s = Series(Categorical([1, 1, 1, 4, 5, 5, 5], - categories=[5, 4, 3, 2, 1], ordered=True)) - res = s.mode() - exp = Series(Categorical([5, 1], categories=[ - 5, 4, 3, 2, 1], ordered=True)) - tm.assert_series_equal(res, exp) - s = Series(Categorical([1, 2, 3, 4, 5], categories=[5, 4, 3, 2, 1], - ordered=True)) - res = s.mode() - exp = Series(Categorical([5, 4, 3, 2, 1], categories=[5, 4, 3, 2, 1], ordered=True)) - tm.assert_series_equal(res, exp) - - def test_value_counts(self): - # GH 12835 - cats = pd.Categorical(["a", "b", "c", "c", "c", "b"], - categories=["c", "a", "b", "d"]) - s = pd.Series(cats, name='xxx') - res = s.value_counts(sort=False) - - exp_index = pd.CategoricalIndex(["c", "a", "b", "d"], - categories=cats.categories) - exp = Series([3, 1, 2, 0], name='xxx', index=exp_index) - tm.assert_series_equal(res, exp) - - res = s.value_counts(sort=True) - - exp_index = pd.CategoricalIndex(["c", "b", "a", "d"], - categories=cats.categories) - exp = Series([3, 2, 1, 0], name='xxx', index=exp_index) - tm.assert_series_equal(res, exp) - - # check object dtype handles the Series.name as the same - # (tested in test_base.py) - s = pd.Series(["a", "b", "c", "c", "c", "b"], name='xxx') - res = s.value_counts() - exp = Series([3, 2, 1], name='xxx', index=["c", "b", "a"]) - tm.assert_series_equal(res, exp) - - def test_value_counts_with_nan(self): - # see gh-9443 - - # sanity check - s = pd.Series(["a", "b", "a"], dtype="category") - exp = pd.Series([2, 1], index=pd.CategoricalIndex(["a", "b"])) - - res = s.value_counts(dropna=True) - tm.assert_series_equal(res, exp) - - res = s.value_counts(dropna=True) - tm.assert_series_equal(res, exp) - - # same Series via two different constructions --> same behaviour - series = [ - pd.Series(["a", "b", None, "a", None, None], dtype="category"), - pd.Series(pd.Categorical(["a", "b", None, "a", None, None], - categories=["a", "b"])) - ] - - for s in series: - # None is a NaN value, so we exclude its count here - exp = pd.Series([2, 1], index=pd.CategoricalIndex(["a", "b"])) - res = s.value_counts(dropna=True) - tm.assert_series_equal(res, exp) - - # we don't exclude the count of None and sort by counts - exp = pd.Series( - [3, 2, 1], index=pd.CategoricalIndex([np.nan, "a", "b"])) - res = s.value_counts(dropna=False) - tm.assert_series_equal(res, exp) - - # When we aren't sorting by counts, and np.nan isn't a - # category, it should be last. - exp = pd.Series( - [2, 1, 3], index=pd.CategoricalIndex(["a", "b", np.nan])) - res = s.value_counts(dropna=False, sort=False) - tm.assert_series_equal(res, exp) - - def test_groupby(self): - - cats = Categorical(["a", "a", "a", "b", "b", "b", "c", "c", "c"], - categories=["a", "b", "c", "d"], ordered=True) - data = DataFrame({"a": [1, 1, 1, 2, 2, 2, 3, 4, 5], "b": cats}) - - exp_index = pd.CategoricalIndex(['a', 'b', 'c', 'd'], name='b', - ordered=True) - expected = DataFrame({'a': [1, 2, 4, np.nan]}, index=exp_index) - result = data.groupby("b").mean() - tm.assert_frame_equal(result, expected) - - raw_cat1 = Categorical(["a", "a", "b", "b"], - categories=["a", "b", "z"], ordered=True) - raw_cat2 = Categorical(["c", "d", "c", "d"], - categories=["c", "d", "y"], ordered=True) - df = DataFrame({"A": raw_cat1, "B": raw_cat2, "values": [1, 2, 3, 4]}) - - # single grouper - gb = df.groupby("A") - exp_idx = pd.CategoricalIndex(['a', 'b', 'z'], name='A', ordered=True) - expected = DataFrame({'values': Series([3, 7, np.nan], index=exp_idx)}) - result = gb.sum() - tm.assert_frame_equal(result, expected) - - # multiple groupers - gb = df.groupby(['A', 'B']) - exp_index = pd.MultiIndex.from_product( - [Categorical(["a", "b", "z"], ordered=True), - Categorical(["c", "d", "y"], ordered=True)], - names=['A', 'B']) - expected = DataFrame({'values': [1, 2, np.nan, 3, 4, np.nan, - np.nan, np.nan, np.nan]}, - index=exp_index) - result = gb.sum() - tm.assert_frame_equal(result, expected) - - # multiple groupers with a non-cat - df = df.copy() - df['C'] = ['foo', 'bar'] * 2 - gb = df.groupby(['A', 'B', 'C']) - exp_index = pd.MultiIndex.from_product( - [Categorical(["a", "b", "z"], ordered=True), - Categorical(["c", "d", "y"], ordered=True), - ['foo', 'bar']], - names=['A', 'B', 'C']) - expected = DataFrame({'values': Series( - np.nan, index=exp_index)}).sort_index() - expected.iloc[[1, 2, 7, 8], 0] = [1, 2, 3, 4] - result = gb.sum() - tm.assert_frame_equal(result, expected) - - # GH 8623 - x = pd.DataFrame([[1, 'John P. Doe'], [2, 'Jane Dove'], - [1, 'John P. Doe']], - columns=['person_id', 'person_name']) - x['person_name'] = pd.Categorical(x.person_name) - - g = x.groupby(['person_id']) - result = g.transform(lambda x: x) - tm.assert_frame_equal(result, x[['person_name']]) - - result = x.drop_duplicates('person_name') - expected = x.iloc[[0, 1]] - tm.assert_frame_equal(result, expected) - - def f(x): - return x.drop_duplicates('person_name').iloc[0] - - result = g.apply(f) - expected = x.iloc[[0, 1]].copy() - expected.index = Index([1, 2], name='person_id') - expected['person_name'] = expected['person_name'].astype('object') - tm.assert_frame_equal(result, expected) - - # GH 9921 - # Monotonic - df = DataFrame({"a": [5, 15, 25]}) - c = pd.cut(df.a, bins=[0, 10, 20, 30, 40]) - - result = df.a.groupby(c).transform(sum) - tm.assert_series_equal(result, df['a']) - - tm.assert_series_equal( - df.a.groupby(c).transform(lambda xs: np.sum(xs)), df['a']) - tm.assert_frame_equal(df.groupby(c).transform(sum), df[['a']]) - tm.assert_frame_equal( - df.groupby(c).transform(lambda xs: np.max(xs)), df[['a']]) - - # Filter - tm.assert_series_equal(df.a.groupby(c).filter(np.all), df['a']) - tm.assert_frame_equal(df.groupby(c).filter(np.all), df) - - # Non-monotonic - df = DataFrame({"a": [5, 15, 25, -5]}) - c = pd.cut(df.a, bins=[-10, 0, 10, 20, 30, 40]) - - result = df.a.groupby(c).transform(sum) - tm.assert_series_equal(result, df['a']) - - tm.assert_series_equal( - df.a.groupby(c).transform(lambda xs: np.sum(xs)), df['a']) - tm.assert_frame_equal(df.groupby(c).transform(sum), df[['a']]) - tm.assert_frame_equal( - df.groupby(c).transform(lambda xs: np.sum(xs)), df[['a']]) - - # GH 9603 - df = pd.DataFrame({'a': [1, 0, 0, 0]}) - c = pd.cut(df.a, [0, 1, 2, 3, 4]) - result = df.groupby(c).apply(len) - - exp_index = pd.CategoricalIndex(c.values.categories, - ordered=c.values.ordered) - expected = pd.Series([1, 0, 0, 0], index=exp_index) - expected.index.name = 'a' - tm.assert_series_equal(result, expected) - - def test_pivot_table(self): - - raw_cat1 = Categorical(["a", "a", "b", "b"], - categories=["a", "b", "z"], ordered=True) - raw_cat2 = Categorical(["c", "d", "c", "d"], - categories=["c", "d", "y"], ordered=True) - df = DataFrame({"A": raw_cat1, "B": raw_cat2, "values": [1, 2, 3, 4]}) - result = pd.pivot_table(df, values='values', index=['A', 'B']) - - exp_index = pd.MultiIndex.from_product( - [Categorical(["a", "b", "z"], ordered=True), - Categorical(["c", "d", "y"], ordered=True)], - names=['A', 'B']) - expected = Series([1, 2, np.nan, 3, 4, np.nan, np.nan, np.nan, np.nan], - index=exp_index, name='values') - tm.assert_series_equal(result, expected) - - def test_count(self): - - s = Series(Categorical([np.nan, 1, 2, np.nan], - categories=[5, 4, 3, 2, 1], ordered=True)) - result = s.count() - self.assertEqual(result, 2) - - def test_sort_values(self): - - c = Categorical(["a", "b", "b", "a"], ordered=False) - cat = Series(c.copy()) - - # 'order' was deprecated in gh-10726 - # 'sort' was deprecated in gh-12882 - for func in ('order', 'sort'): - with tm.assert_produces_warning(FutureWarning): - getattr(c, func)() - - # sort in the categories order - expected = Series( - Categorical(["a", "a", "b", "b"], - ordered=False), index=[0, 3, 1, 2]) - result = cat.sort_values() - tm.assert_series_equal(result, expected) - - cat = Series(Categorical(["a", "c", "b", "d"], ordered=True)) - res = cat.sort_values() - exp = np.array(["a", "b", "c", "d"], dtype=np.object_) - self.assert_numpy_array_equal(res.__array__(), exp) - - cat = Series(Categorical(["a", "c", "b", "d"], categories=[ - "a", "b", "c", "d"], ordered=True)) - res = cat.sort_values() - exp = np.array(["a", "b", "c", "d"], dtype=np.object_) - self.assert_numpy_array_equal(res.__array__(), exp) - - res = cat.sort_values(ascending=False) - exp = np.array(["d", "c", "b", "a"], dtype=np.object_) - self.assert_numpy_array_equal(res.__array__(), exp) - - raw_cat1 = Categorical(["a", "b", "c", "d"], - categories=["a", "b", "c", "d"], ordered=False) - raw_cat2 = Categorical(["a", "b", "c", "d"], - categories=["d", "c", "b", "a"], ordered=True) - s = ["a", "b", "c", "d"] - df = DataFrame({"unsort": raw_cat1, - "sort": raw_cat2, - "string": s, - "values": [1, 2, 3, 4]}) - - # Cats must be sorted in a dataframe - res = df.sort_values(by=["string"], ascending=False) - exp = np.array(["d", "c", "b", "a"], dtype=np.object_) - self.assert_numpy_array_equal(res["sort"].values.__array__(), exp) - self.assertEqual(res["sort"].dtype, "category") - - res = df.sort_values(by=["sort"], ascending=False) - exp = df.sort_values(by=["string"], ascending=True) - self.assert_series_equal(res["values"], exp["values"]) - self.assertEqual(res["sort"].dtype, "category") - self.assertEqual(res["unsort"].dtype, "category") - - # unordered cat, but we allow this - df.sort_values(by=["unsort"], ascending=False) - - # multi-columns sort - # GH 7848 - df = DataFrame({"id": [6, 5, 4, 3, 2, 1], - "raw_grade": ['a', 'b', 'b', 'a', 'a', 'e']}) - df["grade"] = pd.Categorical(df["raw_grade"], ordered=True) - df['grade'] = df['grade'].cat.set_categories(['b', 'e', 'a']) - - # sorts 'grade' according to the order of the categories - result = df.sort_values(by=['grade']) - expected = df.iloc[[1, 2, 5, 0, 3, 4]] - tm.assert_frame_equal(result, expected) - - # multi - result = df.sort_values(by=['grade', 'id']) - expected = df.iloc[[2, 1, 5, 4, 3, 0]] - tm.assert_frame_equal(result, expected) - - def test_slicing(self): - cat = Series(Categorical([1, 2, 3, 4])) - reversed = cat[::-1] - exp = np.array([4, 3, 2, 1], dtype=np.int64) - self.assert_numpy_array_equal(reversed.__array__(), exp) - - df = DataFrame({'value': (np.arange(100) + 1).astype('int64')}) - df['D'] = pd.cut(df.value, bins=[0, 25, 50, 75, 100]) - - expected = Series([11, '(0, 25]'], index=['value', 'D'], name=10) - result = df.iloc[10] - tm.assert_series_equal(result, expected) - - expected = DataFrame({'value': np.arange(11, 21).astype('int64')}, - index=np.arange(10, 20).astype('int64')) - expected['D'] = pd.cut(expected.value, bins=[0, 25, 50, 75, 100]) - result = df.iloc[10:20] - tm.assert_frame_equal(result, expected) - - expected = Series([9, '(0, 25]'], index=['value', 'D'], name=8) - result = df.loc[8] - tm.assert_series_equal(result, expected) - - def test_slicing_and_getting_ops(self): - - # systematically test the slicing operations: - # for all slicing ops: - # - returning a dataframe - # - returning a column - # - returning a row - # - returning a single value - - cats = pd.Categorical( - ["a", "c", "b", "c", "c", "c", "c"], categories=["a", "b", "c"]) - idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values = [1, 2, 3, 4, 5, 6, 7] - df = pd.DataFrame({"cats": cats, "values": values}, index=idx) - - # the expected values - cats2 = pd.Categorical(["b", "c"], categories=["a", "b", "c"]) - idx2 = pd.Index(["j", "k"]) - values2 = [3, 4] - - # 2:4,: | "j":"k",: - exp_df = pd.DataFrame({"cats": cats2, "values": values2}, index=idx2) - - # :,"cats" | :,0 - exp_col = pd.Series(cats, index=idx, name='cats') - - # "j",: | 2,: - exp_row = pd.Series(["b", 3], index=["cats", "values"], dtype="object", - name="j") - - # "j","cats | 2,0 - exp_val = "b" - - # iloc - # frame - res_df = df.iloc[2:4, :] - tm.assert_frame_equal(res_df, exp_df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - # row - res_row = df.iloc[2, :] - tm.assert_series_equal(res_row, exp_row) - tm.assertIsInstance(res_row["cats"], compat.string_types) - - # col - res_col = df.iloc[:, 0] - tm.assert_series_equal(res_col, exp_col) - self.assertTrue(is_categorical_dtype(res_col)) - - # single value - res_val = df.iloc[2, 0] - self.assertEqual(res_val, exp_val) - - # loc - # frame - res_df = df.loc["j":"k", :] - tm.assert_frame_equal(res_df, exp_df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - # row - res_row = df.loc["j", :] - tm.assert_series_equal(res_row, exp_row) - tm.assertIsInstance(res_row["cats"], compat.string_types) - - # col - res_col = df.loc[:, "cats"] - tm.assert_series_equal(res_col, exp_col) - self.assertTrue(is_categorical_dtype(res_col)) - - # single value - res_val = df.loc["j", "cats"] - self.assertEqual(res_val, exp_val) - - # ix - # frame - # res_df = df.loc["j":"k",[0,1]] # doesn't work? - res_df = df.loc["j":"k", :] - tm.assert_frame_equal(res_df, exp_df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - # row - res_row = df.loc["j", :] - tm.assert_series_equal(res_row, exp_row) - tm.assertIsInstance(res_row["cats"], compat.string_types) - - # col - res_col = df.loc[:, "cats"] - tm.assert_series_equal(res_col, exp_col) - self.assertTrue(is_categorical_dtype(res_col)) - - # single value - res_val = df.loc["j", df.columns[0]] - self.assertEqual(res_val, exp_val) - - # iat - res_val = df.iat[2, 0] - self.assertEqual(res_val, exp_val) - - # at - res_val = df.at["j", "cats"] - self.assertEqual(res_val, exp_val) - - # fancy indexing - exp_fancy = df.iloc[[2]] - - res_fancy = df[df["cats"] == "b"] - tm.assert_frame_equal(res_fancy, exp_fancy) - res_fancy = df[df["values"] == 3] - tm.assert_frame_equal(res_fancy, exp_fancy) - - # get_value - res_val = df.get_value("j", "cats") - self.assertEqual(res_val, exp_val) - - # i : int, slice, or sequence of integers - res_row = df.iloc[2] - tm.assert_series_equal(res_row, exp_row) - tm.assertIsInstance(res_row["cats"], compat.string_types) - - res_df = df.iloc[slice(2, 4)] - tm.assert_frame_equal(res_df, exp_df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - res_df = df.iloc[[2, 3]] - tm.assert_frame_equal(res_df, exp_df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - res_col = df.iloc[:, 0] - tm.assert_series_equal(res_col, exp_col) - self.assertTrue(is_categorical_dtype(res_col)) - - res_df = df.iloc[:, slice(0, 2)] - tm.assert_frame_equal(res_df, df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - res_df = df.iloc[:, [0, 1]] - tm.assert_frame_equal(res_df, df) - self.assertTrue(is_categorical_dtype(res_df["cats"])) - - def test_slicing_doc_examples(self): - - # GH 7918 - cats = Categorical(["a", "b", "b", "b", "c", "c", "c"], - categories=["a", "b", "c"]) - idx = Index(["h", "i", "j", "k", "l", "m", "n", ]) - values = [1, 2, 2, 2, 3, 4, 5] - df = DataFrame({"cats": cats, "values": values}, index=idx) - - result = df.iloc[2:4, :] - expected = DataFrame( - {"cats": Categorical(['b', 'b'], categories=['a', 'b', 'c']), - "values": [2, 2]}, index=['j', 'k']) - tm.assert_frame_equal(result, expected) - - result = df.iloc[2:4, :].dtypes - expected = Series(['category', 'int64'], ['cats', 'values']) - tm.assert_series_equal(result, expected) - - result = df.loc["h":"j", "cats"] - expected = Series(Categorical(['a', 'b', 'b'], - categories=['a', 'b', 'c']), - index=['h', 'i', 'j'], name='cats') - tm.assert_series_equal(result, expected) - - result = df.loc["h":"j", df.columns[0:1]] - expected = DataFrame({'cats': Categorical(['a', 'b', 'b'], - categories=['a', 'b', 'c'])}, - index=['h', 'i', 'j']) - tm.assert_frame_equal(result, expected) - - def test_assigning_ops(self): - # systematically test the assigning operations: - # for all slicing ops: - # for value in categories and value not in categories: - - # - assign a single value -> exp_single_cats_value - - # - assign a complete row (mixed values) -> exp_single_row - - # assign multiple rows (mixed values) (-> array) -> exp_multi_row - - # assign a part of a column with dtype == categorical -> - # exp_parts_cats_col - - # assign a part of a column with dtype != categorical -> - # exp_parts_cats_col - - cats = pd.Categorical(["a", "a", "a", "a", "a", "a", "a"], - categories=["a", "b"]) - idx = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values = [1, 1, 1, 1, 1, 1, 1] - orig = pd.DataFrame({"cats": cats, "values": values}, index=idx) - - # the expected values - # changed single row - cats1 = pd.Categorical(["a", "a", "b", "a", "a", "a", "a"], - categories=["a", "b"]) - idx1 = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values1 = [1, 1, 2, 1, 1, 1, 1] - exp_single_row = pd.DataFrame({"cats": cats1, - "values": values1}, index=idx1) - - # changed multiple rows - cats2 = pd.Categorical(["a", "a", "b", "b", "a", "a", "a"], - categories=["a", "b"]) - idx2 = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values2 = [1, 1, 2, 2, 1, 1, 1] - exp_multi_row = pd.DataFrame({"cats": cats2, - "values": values2}, index=idx2) - - # changed part of the cats column - cats3 = pd.Categorical( - ["a", "a", "b", "b", "a", "a", "a"], categories=["a", "b"]) - idx3 = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values3 = [1, 1, 1, 1, 1, 1, 1] - exp_parts_cats_col = pd.DataFrame( - {"cats": cats3, - "values": values3}, index=idx3) - - # changed single value in cats col - cats4 = pd.Categorical( - ["a", "a", "b", "a", "a", "a", "a"], categories=["a", "b"]) - idx4 = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - values4 = [1, 1, 1, 1, 1, 1, 1] - exp_single_cats_value = pd.DataFrame( - {"cats": cats4, - "values": values4}, index=idx4) - - # iloc - # ############### - # - assign a single value -> exp_single_cats_value - df = orig.copy() - df.iloc[2, 0] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - df = orig.copy() - df.iloc[df.index == "j", 0] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - # - assign a single value not in the current categories set - def f(): - df = orig.copy() - df.iloc[2, 0] = "c" - - self.assertRaises(ValueError, f) - - # - assign a complete row (mixed values) -> exp_single_row - df = orig.copy() - df.iloc[2, :] = ["b", 2] - tm.assert_frame_equal(df, exp_single_row) - - # - assign a complete row (mixed values) not in categories set - def f(): - df = orig.copy() - df.iloc[2, :] = ["c", 2] - - self.assertRaises(ValueError, f) - - # - assign multiple rows (mixed values) -> exp_multi_row - df = orig.copy() - df.iloc[2:4, :] = [["b", 2], ["b", 2]] - tm.assert_frame_equal(df, exp_multi_row) - - def f(): - df = orig.copy() - df.iloc[2:4, :] = [["c", 2], ["c", 2]] - - self.assertRaises(ValueError, f) - - # assign a part of a column with dtype == categorical -> - # exp_parts_cats_col - df = orig.copy() - df.iloc[2:4, 0] = pd.Categorical(["b", "b"], categories=["a", "b"]) - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - # different categories -> not sure if this should fail or pass - df = orig.copy() - df.iloc[2:4, 0] = pd.Categorical( - ["b", "b"], categories=["a", "b", "c"]) - - with tm.assertRaises(ValueError): - # different values - df = orig.copy() - df.iloc[2:4, 0] = pd.Categorical( - ["c", "c"], categories=["a", "b", "c"]) - - # assign a part of a column with dtype != categorical -> - # exp_parts_cats_col - df = orig.copy() - df.iloc[2:4, 0] = ["b", "b"] - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - df.iloc[2:4, 0] = ["c", "c"] - - # loc - # ############## - # - assign a single value -> exp_single_cats_value - df = orig.copy() - df.loc["j", "cats"] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - df = orig.copy() - df.loc[df.index == "j", "cats"] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - # - assign a single value not in the current categories set - def f(): - df = orig.copy() - df.loc["j", "cats"] = "c" - - self.assertRaises(ValueError, f) - - # - assign a complete row (mixed values) -> exp_single_row - df = orig.copy() - df.loc["j", :] = ["b", 2] - tm.assert_frame_equal(df, exp_single_row) - - # - assign a complete row (mixed values) not in categories set - def f(): - df = orig.copy() - df.loc["j", :] = ["c", 2] - - self.assertRaises(ValueError, f) - - # - assign multiple rows (mixed values) -> exp_multi_row - df = orig.copy() - df.loc["j":"k", :] = [["b", 2], ["b", 2]] - tm.assert_frame_equal(df, exp_multi_row) - - def f(): - df = orig.copy() - df.loc["j":"k", :] = [["c", 2], ["c", 2]] - - self.assertRaises(ValueError, f) - - # assign a part of a column with dtype == categorical -> - # exp_parts_cats_col - df = orig.copy() - df.loc["j":"k", "cats"] = pd.Categorical( - ["b", "b"], categories=["a", "b"]) - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - # different categories -> not sure if this should fail or pass - df = orig.copy() - df.loc["j":"k", "cats"] = pd.Categorical( - ["b", "b"], categories=["a", "b", "c"]) - - with tm.assertRaises(ValueError): - # different values - df = orig.copy() - df.loc["j":"k", "cats"] = pd.Categorical( - ["c", "c"], categories=["a", "b", "c"]) - - # assign a part of a column with dtype != categorical -> - # exp_parts_cats_col - df = orig.copy() - df.loc["j":"k", "cats"] = ["b", "b"] - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - df.loc["j":"k", "cats"] = ["c", "c"] - - # loc - # ############## - # - assign a single value -> exp_single_cats_value - df = orig.copy() - df.loc["j", df.columns[0]] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - df = orig.copy() - df.loc[df.index == "j", df.columns[0]] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - # - assign a single value not in the current categories set - def f(): - df = orig.copy() - df.loc["j", df.columns[0]] = "c" - - self.assertRaises(ValueError, f) - - # - assign a complete row (mixed values) -> exp_single_row - df = orig.copy() - df.loc["j", :] = ["b", 2] - tm.assert_frame_equal(df, exp_single_row) - - # - assign a complete row (mixed values) not in categories set - def f(): - df = orig.copy() - df.loc["j", :] = ["c", 2] - - self.assertRaises(ValueError, f) - - # - assign multiple rows (mixed values) -> exp_multi_row - df = orig.copy() - df.loc["j":"k", :] = [["b", 2], ["b", 2]] - tm.assert_frame_equal(df, exp_multi_row) - - def f(): - df = orig.copy() - df.loc["j":"k", :] = [["c", 2], ["c", 2]] - - self.assertRaises(ValueError, f) - - # assign a part of a column with dtype == categorical -> - # exp_parts_cats_col - df = orig.copy() - df.loc["j":"k", df.columns[0]] = pd.Categorical( - ["b", "b"], categories=["a", "b"]) - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - # different categories -> not sure if this should fail or pass - df = orig.copy() - df.loc["j":"k", df.columns[0]] = pd.Categorical( - ["b", "b"], categories=["a", "b", "c"]) - - with tm.assertRaises(ValueError): - # different values - df = orig.copy() - df.loc["j":"k", df.columns[0]] = pd.Categorical( - ["c", "c"], categories=["a", "b", "c"]) - - # assign a part of a column with dtype != categorical -> - # exp_parts_cats_col - df = orig.copy() - df.loc["j":"k", df.columns[0]] = ["b", "b"] - tm.assert_frame_equal(df, exp_parts_cats_col) - - with tm.assertRaises(ValueError): - df.loc["j":"k", df.columns[0]] = ["c", "c"] - - # iat - df = orig.copy() - df.iat[2, 0] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - # - assign a single value not in the current categories set - def f(): - df = orig.copy() - df.iat[2, 0] = "c" - - self.assertRaises(ValueError, f) - - # at - # - assign a single value -> exp_single_cats_value - df = orig.copy() - df.at["j", "cats"] = "b" - tm.assert_frame_equal(df, exp_single_cats_value) - - # - assign a single value not in the current categories set - def f(): - df = orig.copy() - df.at["j", "cats"] = "c" - - self.assertRaises(ValueError, f) - - # fancy indexing - catsf = pd.Categorical(["a", "a", "c", "c", "a", "a", "a"], - categories=["a", "b", "c"]) - idxf = pd.Index(["h", "i", "j", "k", "l", "m", "n"]) - valuesf = [1, 1, 3, 3, 1, 1, 1] - df = pd.DataFrame({"cats": catsf, "values": valuesf}, index=idxf) - - exp_fancy = exp_multi_row.copy() - exp_fancy["cats"].cat.set_categories(["a", "b", "c"], inplace=True) - - df[df["cats"] == "c"] = ["b", 2] - # category c is kept in .categories - tm.assert_frame_equal(df, exp_fancy) - - # set_value - df = orig.copy() - df.set_value("j", "cats", "b") - tm.assert_frame_equal(df, exp_single_cats_value) - - def f(): - df = orig.copy() - df.set_value("j", "cats", "c") - - self.assertRaises(ValueError, f) - - # Assigning a Category to parts of a int/... column uses the values of - # the Catgorical - df = pd.DataFrame({"a": [1, 1, 1, 1, 1], - "b": ["a", "a", "a", "a", "a"]}) - exp = pd.DataFrame({"a": [1, "b", "b", 1, 1], - "b": ["a", "a", "b", "b", "a"]}) - df.loc[1:2, "a"] = pd.Categorical(["b", "b"], categories=["a", "b"]) - df.loc[2:3, "b"] = pd.Categorical(["b", "b"], categories=["a", "b"]) - tm.assert_frame_equal(df, exp) - - # Series - orig = Series(pd.Categorical(["b", "b"], categories=["a", "b"])) - s = orig.copy() - s[:] = "a" - exp = Series(pd.Categorical(["a", "a"], categories=["a", "b"])) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s[1] = "a" - exp = Series(pd.Categorical(["b", "a"], categories=["a", "b"])) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s[s.index > 0] = "a" - exp = Series(pd.Categorical(["b", "a"], categories=["a", "b"])) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s[[False, True]] = "a" - exp = Series(pd.Categorical(["b", "a"], categories=["a", "b"])) - tm.assert_series_equal(s, exp) - - s = orig.copy() - s.index = ["x", "y"] - s["y"] = "a" - exp = Series(pd.Categorical(["b", "a"], categories=["a", "b"]), - index=["x", "y"]) - tm.assert_series_equal(s, exp) - - # ensure that one can set something to np.nan - s = Series(Categorical([1, 2, 3])) - exp = Series(Categorical([1, np.nan, 3], categories=[1, 2, 3])) - s[1] = np.nan - tm.assert_series_equal(s, exp) - - def test_comparisons(self): - tests_data = [(list("abc"), list("cba"), list("bbb")), - ([1, 2, 3], [3, 2, 1], [2, 2, 2])] - for data, reverse, base in tests_data: - cat_rev = pd.Series(pd.Categorical(data, categories=reverse, - ordered=True)) - cat_rev_base = pd.Series(pd.Categorical(base, categories=reverse, - ordered=True)) - cat = pd.Series(pd.Categorical(data, ordered=True)) - cat_base = pd.Series(pd.Categorical( - base, categories=cat.cat.categories, ordered=True)) - s = Series(base) - a = np.array(base) - - # comparisons need to take categories ordering into account - res_rev = cat_rev > cat_rev_base - exp_rev = Series([True, False, False]) - tm.assert_series_equal(res_rev, exp_rev) - - res_rev = cat_rev < cat_rev_base - exp_rev = Series([False, False, True]) - tm.assert_series_equal(res_rev, exp_rev) - - res = cat > cat_base - exp = Series([False, False, True]) - tm.assert_series_equal(res, exp) - - scalar = base[1] - res = cat > scalar - exp = Series([False, False, True]) - exp2 = cat.values > scalar - tm.assert_series_equal(res, exp) - tm.assert_numpy_array_equal(res.values, exp2) - res_rev = cat_rev > scalar - exp_rev = Series([True, False, False]) - exp_rev2 = cat_rev.values > scalar - tm.assert_series_equal(res_rev, exp_rev) - tm.assert_numpy_array_equal(res_rev.values, exp_rev2) - - # Only categories with same categories can be compared - def f(): - cat > cat_rev - - self.assertRaises(TypeError, f) - - # categorical cannot be compared to Series or numpy array, and also - # not the other way around - self.assertRaises(TypeError, lambda: cat > s) - self.assertRaises(TypeError, lambda: cat_rev > s) - self.assertRaises(TypeError, lambda: cat > a) - self.assertRaises(TypeError, lambda: cat_rev > a) - - self.assertRaises(TypeError, lambda: s < cat) - self.assertRaises(TypeError, lambda: s < cat_rev) - - self.assertRaises(TypeError, lambda: a < cat) - self.assertRaises(TypeError, lambda: a < cat_rev) - - # unequal comparison should raise for unordered cats - cat = Series(Categorical(list("abc"))) - - def f(): - cat > "b" - - self.assertRaises(TypeError, f) - cat = Series(Categorical(list("abc"), ordered=False)) - - def f(): - cat > "b" - - self.assertRaises(TypeError, f) - - # https://github.com/pandas-dev/pandas/issues/9836#issuecomment-92123057 - # and following comparisons with scalars not in categories should raise - # for unequal comps, but not for equal/not equal - cat = Series(Categorical(list("abc"), ordered=True)) - - self.assertRaises(TypeError, lambda: cat < "d") - self.assertRaises(TypeError, lambda: cat > "d") - self.assertRaises(TypeError, lambda: "d" < cat) - self.assertRaises(TypeError, lambda: "d" > cat) - - self.assert_series_equal(cat == "d", Series([False, False, False])) - self.assert_series_equal(cat != "d", Series([True, True, True])) - - # And test NaN handling... - cat = Series(Categorical(["a", "b", "c", np.nan])) - exp = Series([True, True, True, False]) - res = (cat == cat) - tm.assert_series_equal(res, exp) - - def test_cat_equality(self): - - # GH 8938 - # allow equality comparisons - a = Series(list('abc'), dtype="category") - b = Series(list('abc'), dtype="object") - c = Series(['a', 'b', 'cc'], dtype="object") - d = Series(list('acb'), dtype="object") - e = Categorical(list('abc')) - f = Categorical(list('acb')) - - # vs scalar - self.assertFalse((a == 'a').all()) - self.assertTrue(((a != 'a') == ~(a == 'a')).all()) - - self.assertFalse(('a' == a).all()) - self.assertTrue((a == 'a')[0]) - self.assertTrue(('a' == a)[0]) - self.assertFalse(('a' != a)[0]) - - # vs list-like - self.assertTrue((a == a).all()) - self.assertFalse((a != a).all()) - - self.assertTrue((a == list(a)).all()) - self.assertTrue((a == b).all()) - self.assertTrue((b == a).all()) - self.assertTrue(((~(a == b)) == (a != b)).all()) - self.assertTrue(((~(b == a)) == (b != a)).all()) - - self.assertFalse((a == c).all()) - self.assertFalse((c == a).all()) - self.assertFalse((a == d).all()) - self.assertFalse((d == a).all()) - - # vs a cat-like - self.assertTrue((a == e).all()) - self.assertTrue((e == a).all()) - self.assertFalse((a == f).all()) - self.assertFalse((f == a).all()) - - self.assertTrue(((~(a == e) == (a != e)).all())) - self.assertTrue(((~(e == a) == (e != a)).all())) - self.assertTrue(((~(a == f) == (a != f)).all())) - self.assertTrue(((~(f == a) == (f != a)).all())) - - # non-equality is not comparable - self.assertRaises(TypeError, lambda: a < b) - self.assertRaises(TypeError, lambda: b < a) - self.assertRaises(TypeError, lambda: a > b) - self.assertRaises(TypeError, lambda: b > a) - - def test_concat_append(self): - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - vals = [1, 2] - df = pd.DataFrame({"cats": cat, "vals": vals}) - cat2 = pd.Categorical(["a", "b", "a", "b"], categories=["a", "b"]) - vals2 = [1, 2, 1, 2] - exp = pd.DataFrame({"cats": cat2, - "vals": vals2}, index=pd.Index([0, 1, 0, 1])) - - tm.assert_frame_equal(pd.concat([df, df]), exp) - tm.assert_frame_equal(df.append(df), exp) - - # GH 13524 can concat different categories - cat3 = pd.Categorical(["a", "b"], categories=["a", "b", "c"]) - vals3 = [1, 2] - df_different_categories = pd.DataFrame({"cats": cat3, "vals": vals3}) - - res = pd.concat([df, df_different_categories], ignore_index=True) - exp = pd.DataFrame({"cats": list('abab'), "vals": [1, 2, 1, 2]}) - tm.assert_frame_equal(res, exp) - - res = df.append(df_different_categories, ignore_index=True) - tm.assert_frame_equal(res, exp) - - def test_concat_append_gh7864(self): - # GH 7864 - # make sure ordering is preserverd - df = pd.DataFrame({"id": [1, 2, 3, 4, 5, 6], - "raw_grade": ['a', 'b', 'b', 'a', 'a', 'e']}) - df["grade"] = pd.Categorical(df["raw_grade"]) - df['grade'].cat.set_categories(['e', 'a', 'b']) - - df1 = df[0:3] - df2 = df[3:] - - self.assert_index_equal(df['grade'].cat.categories, - df1['grade'].cat.categories) - self.assert_index_equal(df['grade'].cat.categories, - df2['grade'].cat.categories) - - dfx = pd.concat([df1, df2]) - self.assert_index_equal(df['grade'].cat.categories, - dfx['grade'].cat.categories) - - dfa = df1.append(df2) - self.assert_index_equal(df['grade'].cat.categories, - dfa['grade'].cat.categories) - - def test_concat_preserve(self): - - # GH 8641 series concat not preserving category dtype - # GH 13524 can concat different categories - s = Series(list('abc'), dtype='category') - s2 = Series(list('abd'), dtype='category') - - exp = Series(list('abcabd')) - res = pd.concat([s, s2], ignore_index=True) - tm.assert_series_equal(res, exp) - - exp = Series(list('abcabc'), dtype='category') - res = pd.concat([s, s], ignore_index=True) - tm.assert_series_equal(res, exp) - - exp = Series(list('abcabc'), index=[0, 1, 2, 0, 1, 2], - dtype='category') - res = pd.concat([s, s]) - tm.assert_series_equal(res, exp) - - a = Series(np.arange(6, dtype='int64')) - b = Series(list('aabbca')) - - df2 = DataFrame({'A': a, - 'B': b.astype('category', categories=list('cab'))}) - res = pd.concat([df2, df2]) - exp = DataFrame({'A': pd.concat([a, a]), - 'B': pd.concat([b, b]).astype( - 'category', categories=list('cab'))}) - tm.assert_frame_equal(res, exp) - - def test_categorical_index_preserver(self): - - a = Series(np.arange(6, dtype='int64')) - b = Series(list('aabbca')) - - df2 = DataFrame({'A': a, - 'B': b.astype('category', categories=list('cab')) - }).set_index('B') - result = pd.concat([df2, df2]) - expected = DataFrame({'A': pd.concat([a, a]), - 'B': pd.concat([b, b]).astype( - 'category', categories=list('cab')) - }).set_index('B') - tm.assert_frame_equal(result, expected) - - # wrong catgories - df3 = DataFrame({'A': a, - 'B': pd.Categorical(b, categories=list('abc')) - }).set_index('B') - self.assertRaises(TypeError, lambda: pd.concat([df2, df3])) - - def test_merge(self): - # GH 9426 - - right = DataFrame({'c': {0: 'a', - 1: 'b', - 2: 'c', - 3: 'd', - 4: 'e'}, - 'd': {0: 'null', - 1: 'null', - 2: 'null', - 3: 'null', - 4: 'null'}}) - left = DataFrame({'a': {0: 'f', - 1: 'f', - 2: 'f', - 3: 'f', - 4: 'f'}, - 'b': {0: 'g', - 1: 'g', - 2: 'g', - 3: 'g', - 4: 'g'}}) - df = pd.merge(left, right, how='left', left_on='b', right_on='c') - - # object-object - expected = df.copy() - - # object-cat - # note that we propogate the category - # because we don't have any matching rows - cright = right.copy() - cright['d'] = cright['d'].astype('category') - result = pd.merge(left, cright, how='left', left_on='b', right_on='c') - expected['d'] = expected['d'].astype('category', categories=['null']) - tm.assert_frame_equal(result, expected) - - # cat-object - cleft = left.copy() - cleft['b'] = cleft['b'].astype('category') - result = pd.merge(cleft, cright, how='left', left_on='b', right_on='c') - tm.assert_frame_equal(result, expected) - - # cat-cat - cright = right.copy() - cright['d'] = cright['d'].astype('category') - cleft = left.copy() - cleft['b'] = cleft['b'].astype('category') - result = pd.merge(cleft, cright, how='left', left_on='b', right_on='c') - tm.assert_frame_equal(result, expected) - - def test_repeat(self): - # GH10183 - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - exp = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b"]) - res = cat.repeat(2) - self.assert_categorical_equal(res, exp) - - def test_numpy_repeat(self): - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - exp = pd.Categorical(["a", "a", "b", "b"], categories=["a", "b"]) - self.assert_categorical_equal(np.repeat(cat, 2), exp) - - msg = "the 'axis' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.repeat, cat, 2, axis=1) - - def test_reshape(self): - cat = pd.Categorical([], categories=["a", "b"]) - tm.assert_produces_warning(FutureWarning, cat.reshape, 0) - - with tm.assert_produces_warning(FutureWarning): - cat = pd.Categorical([], categories=["a", "b"]) - self.assert_categorical_equal(cat.reshape(0), cat) - - with tm.assert_produces_warning(FutureWarning): - cat = pd.Categorical([], categories=["a", "b"]) - self.assert_categorical_equal(cat.reshape((5, -1)), cat) - - with tm.assert_produces_warning(FutureWarning): - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - self.assert_categorical_equal(cat.reshape(cat.shape), cat) - - with tm.assert_produces_warning(FutureWarning): - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - self.assert_categorical_equal(cat.reshape(cat.size), cat) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - msg = "can only specify one unknown dimension" - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - tm.assertRaisesRegexp(ValueError, msg, cat.reshape, (-2, -1)) - - def test_numpy_reshape(self): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - cat = pd.Categorical(["a", "b"], categories=["a", "b"]) - self.assert_categorical_equal(np.reshape(cat, cat.shape), cat) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - msg = "the 'order' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.reshape, - cat, cat.shape, order='F') - - def test_na_actions(self): - - cat = pd.Categorical([1, 2, 3, np.nan], categories=[1, 2, 3]) - vals = ["a", "b", np.nan, "d"] - df = pd.DataFrame({"cats": cat, "vals": vals}) - cat2 = pd.Categorical([1, 2, 3, 3], categories=[1, 2, 3]) - vals2 = ["a", "b", "b", "d"] - df_exp_fill = pd.DataFrame({"cats": cat2, "vals": vals2}) - cat3 = pd.Categorical([1, 2, 3], categories=[1, 2, 3]) - vals3 = ["a", "b", np.nan] - df_exp_drop_cats = pd.DataFrame({"cats": cat3, "vals": vals3}) - cat4 = pd.Categorical([1, 2], categories=[1, 2, 3]) - vals4 = ["a", "b"] - df_exp_drop_all = pd.DataFrame({"cats": cat4, "vals": vals4}) - - # fillna - res = df.fillna(value={"cats": 3, "vals": "b"}) - tm.assert_frame_equal(res, df_exp_fill) - - def f(): - df.fillna(value={"cats": 4, "vals": "c"}) - - self.assertRaises(ValueError, f) - - res = df.fillna(method='pad') - tm.assert_frame_equal(res, df_exp_fill) - - res = df.dropna(subset=["cats"]) - tm.assert_frame_equal(res, df_exp_drop_cats) - - res = df.dropna() - tm.assert_frame_equal(res, df_exp_drop_all) - - # make sure that fillna takes missing values into account - c = Categorical([np.nan, "b", np.nan], categories=["a", "b"]) - df = pd.DataFrame({"cats": c, "vals": [1, 2, 3]}) - - cat_exp = Categorical(["a", "b", "a"], categories=["a", "b"]) - df_exp = pd.DataFrame({"cats": cat_exp, "vals": [1, 2, 3]}) - - res = df.fillna("a") - tm.assert_frame_equal(res, df_exp) - - # GH 14021 - # np.nan should always be a is a valid filler - cat = Categorical([np.nan, 2, np.nan]) - val = Categorical([np.nan, np.nan, np.nan]) - df = DataFrame({"cats": cat, "vals": val}) - res = df.fillna(df.median()) - v_exp = [np.nan, np.nan, np.nan] - df_exp = pd.DataFrame({"cats": [2, 2, 2], "vals": v_exp}, - dtype='category') - tm.assert_frame_equal(res, df_exp) - - result = df.cats.fillna(np.nan) - tm.assert_series_equal(result, df.cats) - result = df.vals.fillna(np.nan) - tm.assert_series_equal(result, df.vals) - - idx = pd.DatetimeIndex(['2011-01-01 09:00', '2016-01-01 23:45', - '2011-01-01 09:00', pd.NaT, pd.NaT]) - df = DataFrame({'a': pd.Categorical(idx)}) - tm.assert_frame_equal(df.fillna(value=pd.NaT), df) - - idx = pd.PeriodIndex(['2011-01', '2011-01', '2011-01', - pd.NaT, pd.NaT], freq='M') - df = DataFrame({'a': pd.Categorical(idx)}) - tm.assert_frame_equal(df.fillna(value=pd.NaT), df) - - idx = pd.TimedeltaIndex(['1 days', '2 days', - '1 days', pd.NaT, pd.NaT]) - df = pd.DataFrame({'a': pd.Categorical(idx)}) - tm.assert_frame_equal(df.fillna(value=pd.NaT), df) - - def test_astype_to_other(self): - - s = self.cat['value_group'] - expected = s - tm.assert_series_equal(s.astype('category'), expected) - tm.assert_series_equal(s.astype(CategoricalDtype()), expected) - self.assertRaises(ValueError, lambda: s.astype('float64')) - - cat = Series(Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'])) - exp = Series(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) - tm.assert_series_equal(cat.astype('str'), exp) - s2 = Series(Categorical(['1', '2', '3', '4'])) - exp2 = Series([1, 2, 3, 4]).astype(int) - tm.assert_series_equal(s2.astype('int'), exp2) - - # object don't sort correctly, so just compare that we have the same - # values - def cmp(a, b): - tm.assert_almost_equal( - np.sort(np.unique(a)), np.sort(np.unique(b))) - - expected = Series(np.array(s.values), name='value_group') - cmp(s.astype('object'), expected) - cmp(s.astype(np.object_), expected) - - # array conversion - tm.assert_almost_equal(np.array(s), np.array(s.values)) - - # valid conversion - for valid in [lambda x: x.astype('category'), - lambda x: x.astype(CategoricalDtype()), - lambda x: x.astype('object').astype('category'), - lambda x: x.astype('object').astype( - CategoricalDtype()) - ]: - - result = valid(s) - # compare series values - # internal .categories can't be compared because it is sorted - tm.assert_series_equal(result, s, check_categorical=False) - - # invalid conversion (these are NOT a dtype) - for invalid in [lambda x: x.astype(pd.Categorical), - lambda x: x.astype('object').astype(pd.Categorical)]: - self.assertRaises(TypeError, lambda: invalid(s)) - - def test_astype_categorical(self): - - cat = Categorical(['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c']) - tm.assert_categorical_equal(cat, cat.astype('category')) - tm.assert_almost_equal(np.array(cat), cat.astype('object')) - - self.assertRaises(ValueError, lambda: cat.astype(float)) - - def test_to_records(self): - - # GH8626 - - # dict creation - df = DataFrame({'A': list('abc')}, dtype='category') - expected = Series(list('abc'), dtype='category', name='A') - tm.assert_series_equal(df['A'], expected) - - # list-like creation - df = DataFrame(list('abc'), dtype='category') - expected = Series(list('abc'), dtype='category', name=0) - tm.assert_series_equal(df[0], expected) - - # to record array - # this coerces - result = df.to_records() - expected = np.rec.array([(0, 'a'), (1, 'b'), (2, 'c')], - dtype=[('index', '=i8'), ('0', 'O')]) - tm.assert_almost_equal(result, expected) - - def test_numeric_like_ops(self): - - # numeric ops should not succeed - for op in ['__add__', '__sub__', '__mul__', '__truediv__']: - self.assertRaises(TypeError, - lambda: getattr(self.cat, op)(self.cat)) - - # reduction ops should not succeed (unless specifically defined, e.g. - # min/max) - s = self.cat['value_group'] - for op in ['kurt', 'skew', 'var', 'std', 'mean', 'sum', 'median']: - self.assertRaises(TypeError, - lambda: getattr(s, op)(numeric_only=False)) - - # mad technically works because it takes always the numeric data - - # numpy ops - s = pd.Series(pd.Categorical([1, 2, 3, 4])) - self.assertRaises(TypeError, lambda: np.sum(s)) - - # numeric ops on a Series - for op in ['__add__', '__sub__', '__mul__', '__truediv__']: - self.assertRaises(TypeError, lambda: getattr(s, op)(2)) - - # invalid ufunc - self.assertRaises(TypeError, lambda: np.log(s)) - - def test_cat_tab_completition(self): - # test the tab completion display - ok_for_cat = ['categories', 'codes', 'ordered', 'set_categories', - 'add_categories', 'remove_categories', - 'rename_categories', 'reorder_categories', - 'remove_unused_categories', 'as_ordered', 'as_unordered'] - - def get_dir(s): - results = [r for r in s.cat.__dir__() if not r.startswith('_')] - return list(sorted(set(results))) - - s = Series(list('aabbcde')).astype('category') - results = get_dir(s) - tm.assert_almost_equal(results, list(sorted(set(ok_for_cat)))) - - def test_cat_accessor_api(self): - # GH 9322 - from pandas.core.categorical import CategoricalAccessor - self.assertIs(Series.cat, CategoricalAccessor) - s = Series(list('aabbcde')).astype('category') - self.assertIsInstance(s.cat, CategoricalAccessor) - - invalid = Series([1]) - with tm.assertRaisesRegexp(AttributeError, "only use .cat accessor"): - invalid.cat - self.assertFalse(hasattr(invalid, 'cat')) - - def test_cat_accessor_no_new_attributes(self): - # https://github.com/pandas-dev/pandas/issues/10673 - c = Series(list('aabbcde')).astype('category') - with tm.assertRaisesRegexp(AttributeError, - "You cannot add any new attribute"): - c.cat.xlabel = "a" - - def test_str_accessor_api_for_categorical(self): - # https://github.com/pandas-dev/pandas/issues/10661 - from pandas.core.strings import StringMethods - s = Series(list('aabb')) - s = s + " " + s - c = s.astype('category') - self.assertIsInstance(c.str, StringMethods) - - # str functions, which need special arguments - special_func_defs = [ - ('cat', (list("zyxw"),), {"sep": ","}), - ('center', (10,), {}), - ('contains', ("a",), {}), - ('count', ("a",), {}), - ('decode', ("UTF-8",), {}), - ('encode', ("UTF-8",), {}), - ('endswith', ("a",), {}), - ('extract', ("([a-z]*) ",), {"expand": False}), - ('extract', ("([a-z]*) ",), {"expand": True}), - ('extractall', ("([a-z]*) ",), {}), - ('find', ("a",), {}), - ('findall', ("a",), {}), - ('index', (" ",), {}), - ('ljust', (10,), {}), - ('match', ("a"), {}), # deprecated... - ('normalize', ("NFC",), {}), - ('pad', (10,), {}), - ('partition', (" ",), {"expand": False}), # not default - ('partition', (" ",), {"expand": True}), # default - ('repeat', (3,), {}), - ('replace', ("a", "z"), {}), - ('rfind', ("a",), {}), - ('rindex', (" ",), {}), - ('rjust', (10,), {}), - ('rpartition', (" ",), {"expand": False}), # not default - ('rpartition', (" ",), {"expand": True}), # default - ('slice', (0, 1), {}), - ('slice_replace', (0, 1, "z"), {}), - ('split', (" ",), {"expand": False}), # default - ('split', (" ",), {"expand": True}), # not default - ('startswith', ("a",), {}), - ('wrap', (2,), {}), - ('zfill', (10,), {}) - ] - _special_func_names = [f[0] for f in special_func_defs] - - # * get, join: they need a individual elements of type lists, but - # we can't make a categorical with lists as individual categories. - # -> `s.str.split(" ").astype("category")` will error! - # * `translate` has different interfaces for py2 vs. py3 - _ignore_names = ["get", "join", "translate"] - - str_func_names = [f - for f in dir(s.str) - if not (f.startswith("_") or f in _special_func_names - or f in _ignore_names)] - - func_defs = [(f, (), {}) for f in str_func_names] - func_defs.extend(special_func_defs) - - for func, args, kwargs in func_defs: - res = getattr(c.str, func)(*args, **kwargs) - exp = getattr(s.str, func)(*args, **kwargs) - - if isinstance(res, pd.DataFrame): - tm.assert_frame_equal(res, exp) - else: - tm.assert_series_equal(res, exp) - - invalid = Series([1, 2, 3]).astype('category') - with tm.assertRaisesRegexp(AttributeError, - "Can only use .str accessor with string"): - invalid.str - self.assertFalse(hasattr(invalid, 'str')) - - def test_dt_accessor_api_for_categorical(self): - # https://github.com/pandas-dev/pandas/issues/10661 - from pandas.tseries.common import Properties - - s_dr = Series(date_range('1/1/2015', periods=5, tz="MET")) - c_dr = s_dr.astype("category") - - s_pr = Series(period_range('1/1/2015', freq='D', periods=5)) - c_pr = s_pr.astype("category") - - s_tdr = Series(timedelta_range('1 days', '10 days')) - c_tdr = s_tdr.astype("category") - - # only testing field (like .day) - # and bool (is_month_start) - get_ops = lambda x: x._datetimelike_ops - - test_data = [ - ("Datetime", get_ops(DatetimeIndex), s_dr, c_dr), - ("Period", get_ops(PeriodIndex), s_pr, c_pr), - ("Timedelta", get_ops(TimedeltaIndex), s_tdr, c_tdr)] - - self.assertIsInstance(c_dr.dt, Properties) - - special_func_defs = [ - ('strftime', ("%Y-%m-%d",), {}), - ('tz_convert', ("EST",), {}), - ('round', ("D",), {}), - ('floor', ("D",), {}), - ('ceil', ("D",), {}), - ('asfreq', ("D",), {}), - # ('tz_localize', ("UTC",), {}), - ] - _special_func_names = [f[0] for f in special_func_defs] - - # the series is already localized - _ignore_names = ['tz_localize', 'components'] - - for name, attr_names, s, c in test_data: - func_names = [f - for f in dir(s.dt) - if not (f.startswith("_") or f in attr_names or f in - _special_func_names or f in _ignore_names)] - - func_defs = [(f, (), {}) for f in func_names] - for f_def in special_func_defs: - if f_def[0] in dir(s.dt): - func_defs.append(f_def) - - for func, args, kwargs in func_defs: - res = getattr(c.dt, func)(*args, **kwargs) - exp = getattr(s.dt, func)(*args, **kwargs) - - if isinstance(res, pd.DataFrame): - tm.assert_frame_equal(res, exp) - elif isinstance(res, pd.Series): - tm.assert_series_equal(res, exp) - else: - tm.assert_almost_equal(res, exp) - - for attr in attr_names: - try: - res = getattr(c.dt, attr) - exp = getattr(s.dt, attr) - except Exception as e: - print(name, attr) - raise e - - if isinstance(res, pd.DataFrame): - tm.assert_frame_equal(res, exp) - elif isinstance(res, pd.Series): - tm.assert_series_equal(res, exp) - else: - tm.assert_almost_equal(res, exp) - - invalid = Series([1, 2, 3]).astype('category') - with tm.assertRaisesRegexp( - AttributeError, "Can only use .dt accessor with datetimelike"): - invalid.dt - self.assertFalse(hasattr(invalid, 'str')) - - def test_concat_categorical(self): - # See GH 10177 - df1 = pd.DataFrame(np.arange(18, dtype='int64').reshape(6, 3), - columns=["a", "b", "c"]) - - df2 = pd.DataFrame(np.arange(14, dtype='int64').reshape(7, 2), - columns=["a", "c"]) - - cat_values = ["one", "one", "two", "one", "two", "two", "one"] - df2['h'] = pd.Series(pd.Categorical(cat_values)) - - res = pd.concat((df1, df2), axis=0, ignore_index=True) - exp = pd.DataFrame({'a': [0, 3, 6, 9, 12, 15, 0, 2, 4, 6, 8, 10, 12], - 'b': [1, 4, 7, 10, 13, 16, np.nan, np.nan, - np.nan, np.nan, np.nan, np.nan, np.nan], - 'c': [2, 5, 8, 11, 14, 17, 1, 3, 5, 7, 9, 11, 13], - 'h': [None] * 6 + cat_values}) - tm.assert_frame_equal(res, exp) - - -class TestCategoricalSubclassing(tm.TestCase): - - def test_constructor(self): - sc = tm.SubclassedCategorical(['a', 'b', 'c']) - self.assertIsInstance(sc, tm.SubclassedCategorical) - tm.assert_categorical_equal(sc, Categorical(['a', 'b', 'c'])) - - def test_from_array(self): - sc = tm.SubclassedCategorical.from_codes([1, 0, 2], ['a', 'b', 'c']) - self.assertIsInstance(sc, tm.SubclassedCategorical) - exp = Categorical.from_codes([1, 0, 2], ['a', 'b', 'c']) - tm.assert_categorical_equal(sc, exp) - - def test_map(self): - sc = tm.SubclassedCategorical(['a', 'b', 'c']) - res = sc.map(lambda x: x.upper()) - self.assertIsInstance(res, tm.SubclassedCategorical) - exp = Categorical(['A', 'B', 'C']) - tm.assert_categorical_equal(res, exp) - - def test_map(self): - sc = tm.SubclassedCategorical(['a', 'b', 'c']) - res = sc.map(lambda x: x.upper()) - self.assertIsInstance(res, tm.SubclassedCategorical) - exp = Categorical(['A', 'B', 'C']) - tm.assert_categorical_equal(res, exp) diff --git a/pandas/tests/test_common.py b/pandas/tests/test_common.py index 90b1157572be1..18eb760e31db8 100644 --- a/pandas/tests/test_common.py +++ b/pandas/tests/test_common.py @@ -1,29 +1,24 @@ # -*- coding: utf-8 -*- +import collections +from functools import partial +import string + import numpy as np +import pytest +import pandas as pd from pandas import Series, Timestamp -from pandas.compat import range, lmap -import pandas.core.common as com -import pandas.util.testing as tm - - -def test_mut_exclusive(): - msg = "mutually exclusive arguments: '[ab]' and '[ab]'" - with tm.assertRaisesRegexp(TypeError, msg): - com._mut_exclusive(a=1, b=2) - assert com._mut_exclusive(a=1, b=None) == 1 - assert com._mut_exclusive(major=None, major_axis=None) is None +from pandas.core import common as com, ops def test_get_callable_name(): - from functools import partial - getname = com._get_callable_name + getname = com.get_callable_name def fn(x): return x - lambda_ = lambda x: x + lambda_ = lambda x: x # noqa: E731 part1 = partial(fn) part2 = partial(part1) @@ -51,145 +46,73 @@ def test_all_not_none(): assert (not com._all_not_none(None, None, None, None)) -def test_iterpairs(): - data = [1, 2, 3, 4] - expected = [(1, 2), (2, 3), (3, 4)] - - result = list(com.iterpairs(data)) - - assert (result == expected) - - -def test_split_ranges(): - def _bin(x, width): - "return int(x) as a base2 string of given width" - return ''.join(str((x >> i) & 1) for i in range(width - 1, -1, -1)) - - def test_locs(mask): - nfalse = sum(np.array(mask) == 0) - - remaining = 0 - for s, e in com.split_ranges(mask): - remaining += e - s - - assert 0 not in mask[s:e] - - # make sure the total items covered by the ranges are a complete cover - assert remaining + nfalse == len(mask) - - # exhaustively test all possible mask sequences of length 8 - ncols = 8 - for i in range(2 ** ncols): - cols = lmap(int, list(_bin(i, ncols))) # count up in base2 - mask = [cols[i] == 1 for i in range(len(cols))] - test_locs(mask) - - # base cases - test_locs([]) - test_locs([0]) - test_locs([1]) - - -def test_map_indices_py(): - data = [4, 3, 2, 1] - expected = {4: 0, 3: 1, 2: 2, 1: 3} - - result = com.map_indices_py(data) - - assert (result == expected) - - -def test_union(): - a = [1, 2, 3] - b = [4, 5, 6] - - union = sorted(com.union(a, b)) - - assert ((a + b) == union) - - -def test_difference(): - a = [1, 2, 3] - b = [1, 2, 3, 4, 5, 6] - - inter = sorted(com.difference(b, a)) - - assert ([4, 5, 6] == inter) - - -def test_intersection(): - a = [1, 2, 3] - b = [1, 2, 3, 4, 5, 6] - - inter = sorted(com.intersection(a, b)) - - assert (a == inter) - - -def test_groupby(): - values = ['foo', 'bar', 'baz', 'baz2', 'qux', 'foo3'] - expected = {'f': ['foo', 'foo3'], - 'b': ['bar', 'baz', 'baz2'], - 'q': ['qux']} - - grouped = com.groupby(values, lambda x: x[0]) - - for k, v in grouped: - assert v == expected[k] - - def test_random_state(): import numpy.random as npr # Check with seed - state = com._random_state(5) - tm.assert_equal(state.uniform(), npr.RandomState(5).uniform()) + state = com.random_state(5) + assert state.uniform() == npr.RandomState(5).uniform() # Check with random state object state2 = npr.RandomState(10) - tm.assert_equal( - com._random_state(state2).uniform(), npr.RandomState(10).uniform()) + assert com.random_state(state2).uniform() == npr.RandomState(10).uniform() # check with no arg random state - assert com._random_state() is np.random + assert com.random_state() is np.random # Error for floats or strings - with tm.assertRaises(ValueError): - com._random_state('test') + with pytest.raises(ValueError): + com.random_state('test') + + with pytest.raises(ValueError): + com.random_state(5.5) - with tm.assertRaises(ValueError): - com._random_state(5.5) +@pytest.mark.parametrize('left, right, expected', [ + (Series([1], name='x'), Series([2], name='x'), 'x'), + (Series([1], name='x'), Series([2], name='y'), None), + (Series([1]), Series([2], name='x'), None), + (Series([1], name='x'), Series([2]), None), + (Series([1], name='x'), [2], 'x'), + ([1], Series([2], name='y'), 'y')]) +def test_maybe_match_name(left, right, expected): + assert ops._maybe_match_name(left, right) == expected -def test_maybe_match_name(): - matched = com._maybe_match_name( - Series([1], name='x'), Series( - [2], name='x')) - assert (matched == 'x') +def test_dict_compat(): + data_datetime64 = {np.datetime64('1990-03-15'): 1, + np.datetime64('2015-03-15'): 2} + data_unchanged = {1: 2, 3: 4, 5: 6} + expected = {Timestamp('1990-3-15'): 1, Timestamp('2015-03-15'): 2} + assert (com.dict_compat(data_datetime64) == expected) + assert (com.dict_compat(expected) == expected) + assert (com.dict_compat(data_unchanged) == data_unchanged) - matched = com._maybe_match_name( - Series([1], name='x'), Series( - [2], name='y')) - assert (matched is None) - matched = com._maybe_match_name(Series([1]), Series([2], name='x')) - assert (matched is None) +def test_standardize_mapping(): + # No uninitialized defaultdicts + with pytest.raises(TypeError): + com.standardize_mapping(collections.defaultdict) - matched = com._maybe_match_name(Series([1], name='x'), Series([2])) - assert (matched is None) + # No non-mapping subtypes, instance + with pytest.raises(TypeError): + com.standardize_mapping([]) - matched = com._maybe_match_name(Series([1], name='x'), [2]) - assert (matched == 'x') + # No non-mapping subtypes, class + with pytest.raises(TypeError): + com.standardize_mapping(list) - matched = com._maybe_match_name([1], Series([2], name='y')) - assert (matched == 'y') + fill = {'bad': 'data'} + assert (com.standardize_mapping(fill) == dict) + # Convert instance to type + assert (com.standardize_mapping({}) == dict) -def test_dict_compat(): - data_datetime64 = {np.datetime64('1990-03-15'): 1, - np.datetime64('2015-03-15'): 2} - data_unchanged = {1: 2, 3: 4, 5: 6} - expected = {Timestamp('1990-3-15'): 1, Timestamp('2015-03-15'): 2} - assert (com._dict_compat(data_datetime64) == expected) - assert (com._dict_compat(expected) == expected) - assert (com._dict_compat(data_unchanged) == data_unchanged) + dd = collections.defaultdict(list) + assert isinstance(com.standardize_mapping(dd), partial) + + +def test_git_version(): + # GH 21295 + git_version = pd.__git_version__ + assert len(git_version) == 40 + assert all(c in string.hexdigits for c in git_version) diff --git a/pandas/tests/test_compat.py b/pandas/tests/test_compat.py index 68c0b81eb18ce..d1a3ee43a4623 100644 --- a/pandas/tests/test_compat.py +++ b/pandas/tests/test_compat.py @@ -3,24 +3,30 @@ Testing that functions from compat work as expected """ -from pandas.compat import (range, zip, map, filter, lrange, lzip, lmap, - lfilter, builtins, iterkeys, itervalues, iteritems, - next) -import pandas.util.testing as tm +import re +import pytest -class TestBuiltinIterators(tm.TestCase): +from pandas.compat import ( + PY2, builtins, filter, get_range_parameters, iteritems, iterkeys, + itervalues, lfilter, lmap, lrange, lzip, map, next, range, re_type, zip) - def check_result(self, actual, expected, lengths): + +class TestBuiltinIterators(object): + + @classmethod + def check_result(cls, actual, expected, lengths): for (iter_res, list_res), exp, length in zip(actual, expected, lengths): - self.assertNotIsInstance(iter_res, list) - tm.assertIsInstance(list_res, list) + assert not isinstance(iter_res, list) + assert isinstance(list_res, list) + iter_res = list(iter_res) - self.assertEqual(len(list_res), length) - self.assertEqual(len(iter_res), length) - self.assertEqual(iter_res, exp) - self.assertEqual(list_res, exp) + + assert len(list_res) == length + assert len(iter_res) == length + assert iter_res == exp + assert list_res == exp def test_range(self): actual1 = range(10) @@ -64,6 +70,29 @@ def test_zip(self): self.check_result(actual, expected, lengths) def test_dict_iterators(self): - self.assertEqual(next(itervalues({1: 2})), 2) - self.assertEqual(next(iterkeys({1: 2})), 1) - self.assertEqual(next(iteritems({1: 2})), (1, 2)) + assert next(itervalues({1: 2})) == 2 + assert next(iterkeys({1: 2})) == 1 + assert next(iteritems({1: 2})) == (1, 2) + + +class TestCompatFunctions(object): + + @pytest.mark.parametrize( + 'start,stop,step', [(0, 10, 2), (11, -2, -1), (0, -5, 1), (2, 4, 8)]) + def test_get_range_parameters(self, start, stop, step): + rng = range(start, stop, step) + if PY2 and len(rng) == 0: + start_expected, stop_expected, step_expected = 0, 0, 1 + elif PY2 and len(rng) == 1: + start_expected, stop_expected, step_expected = start, start + 1, 1 + else: + start_expected, stop_expected, step_expected = start, stop, step + + start_result, stop_result, step_result = get_range_parameters(rng) + assert start_result == start_expected + assert stop_result == stop_expected + assert step_result == step_expected + + +def test_re_type(): + assert isinstance(re.compile(''), re_type) diff --git a/pandas/tests/test_config.py b/pandas/tests/test_config.py index c58aada193b15..baca66e0361ad 100644 --- a/pandas/tests/test_config.py +++ b/pandas/tests/test_config.py @@ -1,28 +1,39 @@ # -*- coding: utf-8 -*- -import pandas as pd -import unittest import warnings +import pytest + +from pandas.compat import PY2 + +import pandas as pd +from pandas.core.config import OptionError -class TestConfig(unittest.TestCase): - def __init__(self, *args): - super(TestConfig, self).__init__(*args) +class TestConfig(object): + @classmethod + def setup_class(cls): from copy import deepcopy - self.cf = pd.core.config - self.gc = deepcopy(getattr(self.cf, '_global_config')) - self.do = deepcopy(getattr(self.cf, '_deprecated_options')) - self.ro = deepcopy(getattr(self.cf, '_registered_options')) - def setUp(self): + cls.cf = pd.core.config + cls.gc = deepcopy(getattr(cls.cf, '_global_config')) + cls.do = deepcopy(getattr(cls.cf, '_deprecated_options')) + cls.ro = deepcopy(getattr(cls.cf, '_registered_options')) + + def setup_method(self, method): setattr(self.cf, '_global_config', {}) - setattr( - self.cf, 'options', self.cf.DictWrapper(self.cf._global_config)) + setattr(self.cf, 'options', self.cf.DictWrapper( + self.cf._global_config)) setattr(self.cf, '_deprecated_options', {}) setattr(self.cf, '_registered_options', {}) - def tearDown(self): + # Our test fixture in conftest.py sets "chained_assignment" + # to "raise" only after all test methods have been setup. + # However, after this setup, there is no longer any + # "chained_assignment" option, so re-register it. + self.cf.register_option('chained_assignment', 'raise') + + def teardown_method(self, method): setattr(self.cf, '_global_config', self.gc) setattr(self.cf, '_deprecated_options', self.do) setattr(self.cf, '_registered_options', self.ro) @@ -30,36 +41,45 @@ def tearDown(self): def test_api(self): # the pandas object exposes the user API - self.assertTrue(hasattr(pd, 'get_option')) - self.assertTrue(hasattr(pd, 'set_option')) - self.assertTrue(hasattr(pd, 'reset_option')) - self.assertTrue(hasattr(pd, 'describe_option')) + assert hasattr(pd, 'get_option') + assert hasattr(pd, 'set_option') + assert hasattr(pd, 'reset_option') + assert hasattr(pd, 'describe_option') def test_is_one_of_factory(self): v = self.cf.is_one_of_factory([None, 12]) v(12) v(None) - self.assertRaises(ValueError, v, 1.1) + msg = r"Value must be one of None\|12" + with pytest.raises(ValueError, match=msg): + v(1.1) def test_register_option(self): self.cf.register_option('a', 1, 'doc') # can't register an already registered option - self.assertRaises(KeyError, self.cf.register_option, 'a', 1, 'doc') + msg = "Option 'a' has already been registered" + with pytest.raises(OptionError, match=msg): + self.cf.register_option('a', 1, 'doc') # can't register an already registered option - self.assertRaises(KeyError, self.cf.register_option, 'a.b.c.d1', 1, - 'doc') - self.assertRaises(KeyError, self.cf.register_option, 'a.b.c.d2', 1, - 'doc') + msg = "Path prefix to option 'a' is already an option" + with pytest.raises(OptionError, match=msg): + self.cf.register_option('a.b.c.d1', 1, 'doc') + with pytest.raises(OptionError, match=msg): + self.cf.register_option('a.b.c.d2', 1, 'doc') # no python keywords - self.assertRaises(ValueError, self.cf.register_option, 'for', 0) - self.assertRaises(ValueError, self.cf.register_option, 'a.for.b', 0) + msg = "for is a python keyword" + with pytest.raises(ValueError, match=msg): + self.cf.register_option('for', 0) + with pytest.raises(ValueError, match=msg): + self.cf.register_option('a.for.b', 0) # must be valid identifier (ensure attribute access works) - self.assertRaises(ValueError, self.cf.register_option, - 'Oh my Goddess!', 0) + msg = "oh my goddess! is not a valid identifier" + with pytest.raises(ValueError, match=msg): + self.cf.register_option('Oh my Goddess!', 0) # we can register options several levels deep # without predefining the intermediate steps @@ -82,56 +102,46 @@ def test_describe_option(self): self.cf.register_option('l', "foo") # non-existent keys raise KeyError - self.assertRaises(KeyError, self.cf.describe_option, 'no.such.key') + msg = r"No such keys\(s\)" + with pytest.raises(OptionError, match=msg): + self.cf.describe_option('no.such.key') # we can get the description for any key we registered - self.assertTrue( - 'doc' in self.cf.describe_option('a', _print_desc=False)) - self.assertTrue( - 'doc2' in self.cf.describe_option('b', _print_desc=False)) - self.assertTrue( - 'precated' in self.cf.describe_option('b', _print_desc=False)) - - self.assertTrue( - 'doc3' in self.cf.describe_option('c.d.e1', _print_desc=False)) - self.assertTrue( - 'doc4' in self.cf.describe_option('c.d.e2', _print_desc=False)) + assert 'doc' in self.cf.describe_option('a', _print_desc=False) + assert 'doc2' in self.cf.describe_option('b', _print_desc=False) + assert 'precated' in self.cf.describe_option('b', _print_desc=False) + assert 'doc3' in self.cf.describe_option('c.d.e1', _print_desc=False) + assert 'doc4' in self.cf.describe_option('c.d.e2', _print_desc=False) # if no doc is specified we get a default message # saying "description not available" - self.assertTrue( - 'vailable' in self.cf.describe_option('f', _print_desc=False)) - self.assertTrue( - 'vailable' in self.cf.describe_option('g.h', _print_desc=False)) - self.assertTrue( - 'precated' in self.cf.describe_option('g.h', _print_desc=False)) - self.assertTrue( - 'k' in self.cf.describe_option('g.h', _print_desc=False)) + assert 'vailable' in self.cf.describe_option('f', _print_desc=False) + assert 'vailable' in self.cf.describe_option('g.h', _print_desc=False) + assert 'precated' in self.cf.describe_option('g.h', _print_desc=False) + assert 'k' in self.cf.describe_option('g.h', _print_desc=False) # default is reported - self.assertTrue( - 'foo' in self.cf.describe_option('l', _print_desc=False)) + assert 'foo' in self.cf.describe_option('l', _print_desc=False) # current value is reported - self.assertFalse( - 'bar' in self.cf.describe_option('l', _print_desc=False)) + assert 'bar' not in self.cf.describe_option('l', _print_desc=False) self.cf.set_option("l", "bar") - self.assertTrue( - 'bar' in self.cf.describe_option('l', _print_desc=False)) + assert 'bar' in self.cf.describe_option('l', _print_desc=False) def test_case_insensitive(self): self.cf.register_option('KanBAN', 1, 'doc') - self.assertTrue( - 'doc' in self.cf.describe_option('kanbaN', _print_desc=False)) - self.assertEqual(self.cf.get_option('kanBaN'), 1) + assert 'doc' in self.cf.describe_option('kanbaN', _print_desc=False) + assert self.cf.get_option('kanBaN') == 1 self.cf.set_option('KanBan', 2) - self.assertEqual(self.cf.get_option('kAnBaN'), 2) + assert self.cf.get_option('kAnBaN') == 2 # gets of non-existent keys fail - self.assertRaises(KeyError, self.cf.get_option, 'no_such_option') + msg = r"No such keys\(s\): 'no_such_option'" + with pytest.raises(OptionError, match=msg): + self.cf.get_option('no_such_option') self.cf.deprecate_option('KanBan') - self.assertTrue(self.cf._is_deprecated('kAnBaN')) + assert self.cf._is_deprecated('kAnBaN') def test_get_option(self): self.cf.register_option('a', 1, 'doc') @@ -139,130 +149,145 @@ def test_get_option(self): self.cf.register_option('b.b', None, 'doc2') # gets of existing keys succeed - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') - self.assertTrue(self.cf.get_option('b.b') is None) + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' + assert self.cf.get_option('b.b') is None # gets of non-existent keys fail - self.assertRaises(KeyError, self.cf.get_option, 'no_such_option') + msg = r"No such keys\(s\): 'no_such_option'" + with pytest.raises(OptionError, match=msg): + self.cf.get_option('no_such_option') def test_set_option(self): self.cf.register_option('a', 1, 'doc') self.cf.register_option('b.c', 'hullo', 'doc2') self.cf.register_option('b.b', None, 'doc2') - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') - self.assertTrue(self.cf.get_option('b.b') is None) + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' + assert self.cf.get_option('b.b') is None self.cf.set_option('a', 2) self.cf.set_option('b.c', 'wurld') self.cf.set_option('b.b', 1.1) - self.assertEqual(self.cf.get_option('a'), 2) - self.assertEqual(self.cf.get_option('b.c'), 'wurld') - self.assertEqual(self.cf.get_option('b.b'), 1.1) + assert self.cf.get_option('a') == 2 + assert self.cf.get_option('b.c') == 'wurld' + assert self.cf.get_option('b.b') == 1.1 - self.assertRaises(KeyError, self.cf.set_option, 'no.such.key', None) + msg = r"No such keys\(s\): 'no.such.key'" + with pytest.raises(OptionError, match=msg): + self.cf.set_option('no.such.key', None) def test_set_option_empty_args(self): - self.assertRaises(ValueError, self.cf.set_option) + msg = "Must provide an even number of non-keyword arguments" + with pytest.raises(ValueError, match=msg): + self.cf.set_option() def test_set_option_uneven_args(self): - self.assertRaises(ValueError, self.cf.set_option, 'a.b', 2, 'b.c') + msg = "Must provide an even number of non-keyword arguments" + with pytest.raises(ValueError, match=msg): + self.cf.set_option('a.b', 2, 'b.c') def test_set_option_invalid_single_argument_type(self): - self.assertRaises(ValueError, self.cf.set_option, 2) + msg = "Must provide an even number of non-keyword arguments" + with pytest.raises(ValueError, match=msg): + self.cf.set_option(2) def test_set_option_multiple(self): self.cf.register_option('a', 1, 'doc') self.cf.register_option('b.c', 'hullo', 'doc2') self.cf.register_option('b.b', None, 'doc2') - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') - self.assertTrue(self.cf.get_option('b.b') is None) + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' + assert self.cf.get_option('b.b') is None self.cf.set_option('a', '2', 'b.c', None, 'b.b', 10.0) - self.assertEqual(self.cf.get_option('a'), '2') - self.assertTrue(self.cf.get_option('b.c') is None) - self.assertEqual(self.cf.get_option('b.b'), 10.0) + assert self.cf.get_option('a') == '2' + assert self.cf.get_option('b.c') is None + assert self.cf.get_option('b.b') == 10.0 + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_validation(self): self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) self.cf.register_option('b.c', 'hullo', 'doc2', validator=self.cf.is_text) - self.assertRaises(ValueError, self.cf.register_option, 'a.b.c.d2', - 'NO', 'doc', validator=self.cf.is_int) + msg = "Value must have type ''" + with pytest.raises(ValueError, match=msg): + self.cf.register_option( + 'a.b.c.d2', 'NO', 'doc', validator=self.cf.is_int) self.cf.set_option('a', 2) # int is_int self.cf.set_option('b.c', 'wurld') # str is_str - self.assertRaises( - ValueError, self.cf.set_option, 'a', None) # None not is_int - self.assertRaises(ValueError, self.cf.set_option, 'a', 'ab') - self.assertRaises(ValueError, self.cf.set_option, 'b.c', 1) + # None not is_int + with pytest.raises(ValueError, match=msg): + self.cf.set_option('a', None) + with pytest.raises(ValueError, match=msg): + self.cf.set_option('a', 'ab') + + msg = r"Value must be an instance of \|" + with pytest.raises(ValueError, match=msg): + self.cf.set_option('b.c', 1) validator = self.cf.is_one_of_factory([None, self.cf.is_callable]) self.cf.register_option('b', lambda: None, 'doc', validator=validator) self.cf.set_option('b', '%.1f'.format) # Formatter is callable self.cf.set_option('b', None) # Formatter is none (default) - self.assertRaises(ValueError, self.cf.set_option, 'b', '%.1f') + with pytest.raises(ValueError, match="Value must be a callable"): + self.cf.set_option('b', '%.1f') def test_reset_option(self): self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) self.cf.register_option('b.c', 'hullo', 'doc2', validator=self.cf.is_str) - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' self.cf.set_option('a', 2) self.cf.set_option('b.c', 'wurld') - self.assertEqual(self.cf.get_option('a'), 2) - self.assertEqual(self.cf.get_option('b.c'), 'wurld') + assert self.cf.get_option('a') == 2 + assert self.cf.get_option('b.c') == 'wurld' self.cf.reset_option('a') - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'wurld') + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'wurld' self.cf.reset_option('b.c') - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' def test_reset_option_all(self): self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) self.cf.register_option('b.c', 'hullo', 'doc2', validator=self.cf.is_str) - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' self.cf.set_option('a', 2) self.cf.set_option('b.c', 'wurld') - self.assertEqual(self.cf.get_option('a'), 2) - self.assertEqual(self.cf.get_option('b.c'), 'wurld') + assert self.cf.get_option('a') == 2 + assert self.cf.get_option('b.c') == 'wurld' self.cf.reset_option("all") - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b.c'), 'hullo') + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b.c') == 'hullo' def test_deprecate_option(self): # we can deprecate non-existent options self.cf.deprecate_option('foo') - self.assertTrue(self.cf._is_deprecated('foo')) + assert self.cf._is_deprecated('foo') with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - try: + with pytest.raises( + KeyError, + match="No such keys.s.: 'foo'"): self.cf.get_option('foo') - except KeyError: - pass - else: - self.fail("Nonexistent option didn't raise KeyError") - - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'deprecated' in str(w[-1])) # we get the default message + assert len(w) == 1 # should have raised one warning + assert 'deprecated' in str(w[-1]) # we get the default message self.cf.register_option('a', 1, 'doc', validator=self.cf.is_int) self.cf.register_option('b.c', 'hullo', 'doc2') @@ -273,80 +298,73 @@ def test_deprecate_option(self): warnings.simplefilter('always') self.cf.get_option('a') - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'eprecated' in str(w[-1])) # we get the default message - self.assertTrue( - 'nifty_ver' in str(w[-1])) # with the removal_ver quoted + assert len(w) == 1 # should have raised one warning + assert 'eprecated' in str(w[-1]) # we get the default message + assert 'nifty_ver' in str(w[-1]) # with the removal_ver quoted - self.assertRaises( - KeyError, self.cf.deprecate_option, 'a') # can't depr. twice + msg = "Option 'a' has already been defined as deprecated" + with pytest.raises(OptionError, match=msg): + self.cf.deprecate_option('a') self.cf.deprecate_option('b.c', 'zounds!') with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') self.cf.get_option('b.c') - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'zounds!' in str(w[-1])) # we get the custom message + assert len(w) == 1 # should have raised one warning + assert 'zounds!' in str(w[-1]) # we get the custom message # test rerouting keys self.cf.register_option('d.a', 'foo', 'doc2') self.cf.register_option('d.dep', 'bar', 'doc2') - self.assertEqual(self.cf.get_option('d.a'), 'foo') - self.assertEqual(self.cf.get_option('d.dep'), 'bar') + assert self.cf.get_option('d.a') == 'foo' + assert self.cf.get_option('d.dep') == 'bar' self.cf.deprecate_option('d.dep', rkey='d.a') # reroute d.dep to d.a with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - self.assertEqual(self.cf.get_option('d.dep'), 'foo') + assert self.cf.get_option('d.dep') == 'foo' - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'eprecated' in str(w[-1])) # we get the custom message + assert len(w) == 1 # should have raised one warning + assert 'eprecated' in str(w[-1]) # we get the custom message with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') self.cf.set_option('d.dep', 'baz') # should overwrite "d.a" - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'eprecated' in str(w[-1])) # we get the custom message + assert len(w) == 1 # should have raised one warning + assert 'eprecated' in str(w[-1]) # we get the custom message with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - self.assertEqual(self.cf.get_option('d.dep'), 'baz') + assert self.cf.get_option('d.dep') == 'baz' - self.assertEqual(len(w), 1) # should have raised one warning - self.assertTrue( - 'eprecated' in str(w[-1])) # we get the custom message + assert len(w) == 1 # should have raised one warning + assert 'eprecated' in str(w[-1]) # we get the custom message def test_config_prefix(self): with self.cf.config_prefix("base"): self.cf.register_option('a', 1, "doc1") self.cf.register_option('b', 2, "doc2") - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b'), 2) + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b') == 2 self.cf.set_option('a', 3) self.cf.set_option('b', 4) - self.assertEqual(self.cf.get_option('a'), 3) - self.assertEqual(self.cf.get_option('b'), 4) + assert self.cf.get_option('a') == 3 + assert self.cf.get_option('b') == 4 - self.assertEqual(self.cf.get_option('base.a'), 3) - self.assertEqual(self.cf.get_option('base.b'), 4) - self.assertTrue( - 'doc1' in self.cf.describe_option('base.a', _print_desc=False)) - self.assertTrue( - 'doc2' in self.cf.describe_option('base.b', _print_desc=False)) + assert self.cf.get_option('base.a') == 3 + assert self.cf.get_option('base.b') == 4 + assert 'doc1' in self.cf.describe_option('base.a', _print_desc=False) + assert 'doc2' in self.cf.describe_option('base.b', _print_desc=False) self.cf.reset_option('base.a') self.cf.reset_option('base.b') with self.cf.config_prefix("base"): - self.assertEqual(self.cf.get_option('a'), 1) - self.assertEqual(self.cf.get_option('b'), 2) + assert self.cf.get_option('a') == 1 + assert self.cf.get_option('b') == 2 def test_callback(self): k = [None] @@ -361,21 +379,21 @@ def callback(key): del k[-1], v[-1] self.cf.set_option("d.a", "fooz") - self.assertEqual(k[-1], "d.a") - self.assertEqual(v[-1], "fooz") + assert k[-1] == "d.a" + assert v[-1] == "fooz" del k[-1], v[-1] self.cf.set_option("d.b", "boo") - self.assertEqual(k[-1], "d.b") - self.assertEqual(v[-1], "boo") + assert k[-1] == "d.b" + assert v[-1] == "boo" del k[-1], v[-1] self.cf.reset_option("d.b") - self.assertEqual(k[-1], "d.b") + assert k[-1] == "d.b" def test_set_ContextManager(self): def eq(val): - self.assertEqual(self.cf.get_option("a"), val) + assert self.cf.get_option("a") == val self.cf.register_option('a', 0) eq(0) @@ -392,12 +410,6 @@ def eq(val): def test_attribute_access(self): holder = [] - def f(): - options.b = 1 - - def f2(): - options.display = 1 - def f3(key): holder.append(True) @@ -405,22 +417,25 @@ def f3(key): self.cf.register_option('c', 0, cb=f3) options = self.cf.options - self.assertEqual(options.a, 0) + assert options.a == 0 with self.cf.option_context("a", 15): - self.assertEqual(options.a, 15) + assert options.a == 15 options.a = 500 - self.assertEqual(self.cf.get_option("a"), 500) + assert self.cf.get_option("a") == 500 self.cf.reset_option("a") - self.assertEqual(options.a, self.cf.get_option("a", 0)) + assert options.a == self.cf.get_option("a", 0) - self.assertRaises(KeyError, f) - self.assertRaises(KeyError, f2) + msg = "You can only set the value of existing options" + with pytest.raises(OptionError, match=msg): + options.b = 1 + with pytest.raises(OptionError, match=msg): + options.display = 1 # make sure callback kicks when using this form of setting options.c = 1 - self.assertEqual(len(holder), 1) + assert len(holder) == 1 def test_option_context_scope(self): # Ensure that creating a context does not affect the existing @@ -435,11 +450,18 @@ def test_option_context_scope(self): # Ensure creating contexts didn't affect the current context. ctx = self.cf.option_context(option_name, context_value) - self.assertEqual(self.cf.get_option(option_name), original_value) + assert self.cf.get_option(option_name) == original_value # Ensure the correct value is available inside the context. with ctx: - self.assertEqual(self.cf.get_option(option_name), context_value) + assert self.cf.get_option(option_name) == context_value # Ensure the current context is reset - self.assertEqual(self.cf.get_option(option_name), original_value) + assert self.cf.get_option(option_name) == original_value + + def test_dictwrapper_getattr(self): + options = self.cf.options + # GH 19789 + with pytest.raises(OptionError, match="No such option"): + options.bananas + assert not hasattr(options, 'bananas') diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py new file mode 100644 index 0000000000000..92b4e5a99041a --- /dev/null +++ b/pandas/tests/test_downstream.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +""" +Testing that we work in the downstream packages +""" +import importlib +import subprocess +import sys + +import numpy as np # noqa +import pytest + +from pandas.compat import PY2, PY36, is_platform_windows + +from pandas import DataFrame +from pandas.util import testing as tm + + +def import_module(name): + # we *only* want to skip if the module is truly not available + # and NOT just an actual import error because of pandas changes + + if PY36: + try: + return importlib.import_module(name) + except ModuleNotFoundError: # noqa + pytest.skip("skipping as {} not available".format(name)) + + else: + try: + return importlib.import_module(name) + except ImportError as e: + if "No module named" in str(e) and name in str(e): + pytest.skip("skipping as {} not available".format(name)) + raise + + +@pytest.fixture +def df(): + return DataFrame({'A': [1, 2, 3]}) + + +def test_dask(df): + + toolz = import_module('toolz') # noqa + dask = import_module('dask') # noqa + + import dask.dataframe as dd + + ddf = dd.from_pandas(df, npartitions=3) + assert ddf.A is not None + assert ddf.compute() is not None + + +def test_xarray(df): + + xarray = import_module('xarray') # noqa + + assert df.to_xarray() is not None + + +@pytest.mark.skipif(is_platform_windows() and PY2, + reason="Broken on Windows / Py2") +def test_oo_optimizable(): + # GH 21071 + subprocess.check_call([sys.executable, "-OO", "-c", "import pandas"]) + + +@tm.network +# Cython import warning +@pytest.mark.filterwarnings("ignore:can't:ImportWarning") +def test_statsmodels(): + + statsmodels = import_module('statsmodels') # noqa + import statsmodels.api as sm + import statsmodels.formula.api as smf + df = sm.datasets.get_rdataset("Guerry", "HistData").data + smf.ols('Lottery ~ Literacy + np.log(Pop1831)', data=df).fit() + + +# Cython import warning +@pytest.mark.filterwarnings("ignore:can't:ImportWarning") +def test_scikit_learn(df): + + sklearn = import_module('sklearn') # noqa + from sklearn import svm, datasets + + digits = datasets.load_digits() + clf = svm.SVC(gamma=0.001, C=100.) + clf.fit(digits.data[:-1], digits.target[:-1]) + clf.predict(digits.data[-1:]) + + +# Cython import warning and traitlets +@tm.network +@pytest.mark.filterwarnings("ignore") +def test_seaborn(): + + seaborn = import_module('seaborn') + tips = seaborn.load_dataset("tips") + seaborn.stripplot(x="day", y="total_bill", data=tips) + + +def test_pandas_gbq(df): + + pandas_gbq = import_module('pandas_gbq') # noqa + + +@pytest.mark.xfail(reason="0.7.0 pending") +@tm.network +def test_pandas_datareader(): + + pandas_datareader = import_module('pandas_datareader') # noqa + pandas_datareader.DataReader( + 'F', 'quandl', '2017-01-01', '2017-02-01') + + +# importing from pandas, Cython import warning +@pytest.mark.filterwarnings("ignore:The 'warn':DeprecationWarning") +@pytest.mark.filterwarnings("ignore:pandas.util:DeprecationWarning") +@pytest.mark.filterwarnings("ignore:can't resolve:ImportWarning") +def test_geopandas(): + + geopandas = import_module('geopandas') # noqa + fp = geopandas.datasets.get_path('naturalearth_lowres') + assert geopandas.read_file(fp) is not None + + +# Cython import warning +@pytest.mark.filterwarnings("ignore:can't resolve:ImportWarning") +def test_pyarrow(df): + + pyarrow = import_module('pyarrow') # noqa + table = pyarrow.Table.from_pandas(df) + result = table.to_pandas() + tm.assert_frame_equal(result, df) diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py new file mode 100644 index 0000000000000..d3b6a237a97a1 --- /dev/null +++ b/pandas/tests/test_errors.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas.errors import AbstractMethodError + +import pandas as pd # noqa + + +@pytest.mark.parametrize( + "exc", ['UnsupportedFunctionCall', 'UnsortedIndexError', + 'OutOfBoundsDatetime', + 'ParserError', 'PerformanceWarning', 'DtypeWarning', + 'EmptyDataError', 'ParserWarning', 'MergeError']) +def test_exception_importable(exc): + from pandas import errors + e = getattr(errors, exc) + assert e is not None + + # check that we can raise on them + with pytest.raises(e): + raise e() + + +def test_catch_oob(): + from pandas import errors + + try: + pd.Timestamp('15000101') + except errors.OutOfBoundsDatetime: + pass + + +def test_error_rename(): + # see gh-12665 + from pandas.errors import ParserError + from pandas.io.common import CParserError + + try: + raise CParserError() + except ParserError: + pass + + try: + raise ParserError() + except CParserError: + pass + + +class Foo(object): + @classmethod + def classmethod(cls): + raise AbstractMethodError(cls, methodtype='classmethod') + + @property + def property(self): + raise AbstractMethodError(self, methodtype='property') + + def method(self): + raise AbstractMethodError(self) + + +def test_AbstractMethodError_classmethod(): + xpr = "This classmethod must be defined in the concrete class Foo" + with pytest.raises(AbstractMethodError, match=xpr): + Foo.classmethod() + + xpr = "This property must be defined in the concrete class Foo" + with pytest.raises(AbstractMethodError, match=xpr): + Foo().property + + xpr = "This method must be defined in the concrete class Foo" + with pytest.raises(AbstractMethodError, match=xpr): + Foo().method() diff --git a/pandas/tests/test_expressions.py b/pandas/tests/test_expressions.py index f669ebe371f9d..7a2680135ea80 100644 --- a/pandas/tests/test_expressions.py +++ b/pandas/tests/test_expressions.py @@ -1,23 +1,23 @@ # -*- coding: utf-8 -*- from __future__ import print_function -# pylint: disable-msg=W0612,E1101 -import re import operator -import pytest - -from numpy.random import randn +import re import numpy as np +from numpy.random import randn +import pytest -from pandas.core.api import DataFrame, Panel -from pandas.computation import expressions as expr -from pandas import compat, _np_version_under1p11 -from pandas.util.testing import (assert_almost_equal, assert_series_equal, - assert_frame_equal, assert_panel_equal, - assert_panel4d_equal, slow) -from pandas.formats.printing import pprint_thing +from pandas import _np_version_under1p13, compat +from pandas.core.api import DataFrame +from pandas.core.computation import expressions as expr import pandas.util.testing as tm +from pandas.util.testing import ( + assert_almost_equal, assert_frame_equal, assert_series_equal) + +from pandas.io.formats.printing import pprint_thing + +# pylint: disable-msg=W0612,E1101 _frame = DataFrame(randn(10000, 4), columns=list('ABCD'), dtype='float64') @@ -32,25 +32,16 @@ 'D': _frame2['D'].astype('int32')}) _integer = DataFrame( np.random.randint(1, 100, - size=(10001, 4)), columns=list('ABCD'), dtype='int64') + size=(10001, 4)), + columns=list('ABCD'), dtype='int64') _integer2 = DataFrame(np.random.randint(1, 100, size=(101, 4)), columns=list('ABCD'), dtype='int64') -_frame_panel = Panel(dict(ItemA=_frame.copy(), ItemB=( - _frame.copy() + 3), ItemC=_frame.copy(), ItemD=_frame.copy())) -_frame2_panel = Panel(dict(ItemA=_frame2.copy(), ItemB=(_frame2.copy() + 3), - ItemC=_frame2.copy(), ItemD=_frame2.copy())) -_integer_panel = Panel(dict(ItemA=_integer, ItemB=(_integer + 34).astype( - 'int64'))) -_integer2_panel = Panel(dict(ItemA=_integer2, ItemB=(_integer2 + 34).astype( - 'int64'))) -_mixed_panel = Panel(dict(ItemA=_mixed, ItemB=(_mixed + 3))) -_mixed2_panel = Panel(dict(ItemA=_mixed2, ItemB=(_mixed2 + 3))) @pytest.mark.skipif(not expr._USE_NUMEXPR, reason='not using numexpr') -class TestExpressions(tm.TestCase): +class TestExpressions(object): - def setUp(self): + def setup_method(self, method): self.frame = _frame.copy() self.frame2 = _frame2.copy() @@ -59,23 +50,17 @@ def setUp(self): self.integer = _integer.copy() self._MIN_ELEMENTS = expr._MIN_ELEMENTS - def tearDown(self): + def teardown_method(self, method): expr._MIN_ELEMENTS = self._MIN_ELEMENTS def run_arithmetic(self, df, other, assert_func, check_dtype=False, test_flex=True): expr._MIN_ELEMENTS = 0 - operations = ['add', 'sub', 'mul', 'mod', 'truediv', 'floordiv', 'pow'] + operations = ['add', 'sub', 'mul', 'mod', 'truediv', 'floordiv'] if not compat.PY3: operations.append('div') for arith in operations: - # numpy >= 1.11 doesn't handle integers - # raised to integer powers - # https://github.com/pandas-dev/pandas/issues/15363 - if arith == 'pow' and not _np_version_under1p11: - continue - operator_name = arith if arith == 'div': operator_name = 'truediv' @@ -107,7 +92,7 @@ def test_integer_arithmetic(self): check_dtype=True) def run_binary(self, df, other, assert_func, test_flex=False, - numexpr_ops=set(['gt', 'lt', 'ge', 'le', 'eq', 'ne'])): + numexpr_ops={'gt', 'lt', 'ge', 'le', 'eq', 'ne'}): """ tests solely that the result is the same whether or not numexpr is enabled. Need to test whether the function does the correct thing @@ -116,6 +101,7 @@ def run_binary(self, df, other, assert_func, test_flex=False, expr._MIN_ELEMENTS = 0 expr.set_test_mode(True) operations = ['gt', 'lt', 'ge', 'le', 'eq', 'ne'] + for arith in operations: if test_flex: op = lambda x, y: getattr(df, arith)(y) @@ -168,46 +154,18 @@ def run_series(self, ser, other, binary_comp=None, **kwargs): # self.run_binary(ser, binary_comp, assert_frame_equal, # test_flex=True, **kwargs) - def run_panel(self, panel, other, binary_comp=None, run_binary=True, - assert_func=assert_panel_equal, **kwargs): - self.run_arithmetic(panel, other, assert_func, test_flex=False, - **kwargs) - self.run_arithmetic(panel, other, assert_func, test_flex=True, - **kwargs) - if run_binary: - if binary_comp is None: - binary_comp = other + 1 - self.run_binary(panel, binary_comp, assert_func, - test_flex=False, **kwargs) - self.run_binary(panel, binary_comp, assert_func, - test_flex=True, **kwargs) - def test_integer_arithmetic_frame(self): self.run_frame(self.integer, self.integer) def test_integer_arithmetic_series(self): self.run_series(self.integer.iloc[:, 0], self.integer.iloc[:, 0]) - @slow - def test_integer_panel(self): - self.run_panel(_integer2_panel, np.random.randint(1, 100)) - def test_float_arithemtic_frame(self): self.run_frame(self.frame2, self.frame2) def test_float_arithmetic_series(self): self.run_series(self.frame2.iloc[:, 0], self.frame2.iloc[:, 0]) - @slow - def test_float_panel(self): - self.run_panel(_frame2_panel, np.random.randn() + 0.1, binary_comp=0.8) - - @slow - def test_panel4d(self): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.run_panel(tm.makePanel4D(), np.random.randn() + 0.5, - assert_func=assert_panel4d_equal, binary_comp=3) - def test_mixed_arithmetic_frame(self): # TODO: FIGURE OUT HOW TO GET IT TO WORK... # can't do arithmetic because comparison methods try to do *entire* @@ -218,11 +176,6 @@ def test_mixed_arithmetic_series(self): for col in self.mixed2.columns: self.run_series(self.mixed2[col], self.mixed2[col], binary_comp=4) - @slow - def test_mixed_panel(self): - self.run_panel(_mixed2_panel, np.random.randint(1, 100), - binary_comp=-2) - def test_float_arithemtic(self): self.run_arithmetic(self.frame, self.frame, assert_frame_equal) self.run_arithmetic(self.frame.iloc[:, 0], self.frame.iloc[:, 0], @@ -246,22 +199,22 @@ def test_invalid(self): # no op result = expr._can_use_numexpr(operator.add, None, self.frame, self.frame, 'evaluate') - self.assertFalse(result) + assert not result # mixed result = expr._can_use_numexpr(operator.add, '+', self.mixed, self.frame, 'evaluate') - self.assertFalse(result) + assert not result # min elements result = expr._can_use_numexpr(operator.add, '+', self.frame2, self.frame2, 'evaluate') - self.assertFalse(result) + assert not result # ok, we only check on first part of expression result = expr._can_use_numexpr(operator.add, '+', self.frame, self.frame2, 'evaluate') - self.assertTrue(result) + assert result def test_binary_ops(self): def testit(): @@ -272,10 +225,7 @@ def testit(): for op, op_str in [('add', '+'), ('sub', '-'), ('mul', '*'), ('div', '/'), ('pow', '**')]: - # numpy >= 1.11 doesn't handle integers - # raised to integer powers - # https://github.com/pandas-dev/pandas/issues/15363 - if op == 'pow' and not _np_version_under1p11: + if op == 'pow': continue if op == 'div': @@ -285,7 +235,7 @@ def testit(): if op is not None: result = expr._can_use_numexpr(op, op_str, f, f, 'evaluate') - self.assertNotEqual(result, f._is_mixed_type) + assert result != f._is_mixed_type result = expr.evaluate(op, op_str, f, f, use_numexpr=True) @@ -300,7 +250,7 @@ def testit(): result = expr._can_use_numexpr(op, op_str, f2, f2, 'evaluate') - self.assertFalse(result) + assert not result expr.set_use_numexpr(False) testit() @@ -328,7 +278,7 @@ def testit(): result = expr._can_use_numexpr(op, op_str, f11, f12, 'evaluate') - self.assertNotEqual(result, f11._is_mixed_type) + assert result != f11._is_mixed_type result = expr.evaluate(op, op_str, f11, f12, use_numexpr=True) @@ -341,7 +291,7 @@ def testit(): result = expr._can_use_numexpr(op, op_str, f21, f22, 'evaluate') - self.assertFalse(result) + assert not result expr.set_use_numexpr(False) testit() @@ -382,22 +332,22 @@ def test_bool_ops_raise_on_arithmetic(self): f = getattr(operator, name) err_msg = re.escape(msg % op) - with tm.assertRaisesRegexp(NotImplementedError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(df, df) - with tm.assertRaisesRegexp(NotImplementedError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(df.a, df.b) - with tm.assertRaisesRegexp(NotImplementedError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(df.a, True) - with tm.assertRaisesRegexp(NotImplementedError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(False, df.a) - with tm.assertRaisesRegexp(TypeError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(False, df) - with tm.assertRaisesRegexp(TypeError, err_msg): + with pytest.raises(NotImplementedError, match=err_msg): f(df, True) def test_bool_ops_warn_on_arithmetic(self): @@ -412,6 +362,10 @@ def test_bool_ops_warn_on_arithmetic(self): f = getattr(operator, name) fe = getattr(operator, sub_funcs[subs[op]]) + # >= 1.13.0 these are now TypeErrors + if op == '-' and not _np_version_under1p13: + continue + with tm.use_numexpr(True, min_elements=5): with tm.assert_produces_warning(check_stacklevel=False): r = f(df, df) @@ -442,3 +396,19 @@ def test_bool_ops_warn_on_arithmetic(self): r = f(df, True) e = fe(df, True) tm.assert_frame_equal(r, e) + + @pytest.mark.parametrize("test_input,expected", [ + (DataFrame([[0, 1, 2, 'aa'], [0, 1, 2, 'aa']], + columns=['a', 'b', 'c', 'dtype']), + DataFrame([[False, False], [False, False]], + columns=['a', 'dtype'])), + (DataFrame([[0, 3, 2, 'aa'], [0, 4, 2, 'aa'], [0, 1, 1, 'bb']], + columns=['a', 'b', 'c', 'dtype']), + DataFrame([[False, False], [False, False], + [False, False]], columns=['a', 'dtype'])), + ]) + def test_bool_ops_column_name_dtype(self, test_input, expected): + # GH 22383 - .ne fails if columns containing column name 'dtype' + result = test_input.loc[:, ['a', 'dtype']].ne( + test_input.loc[:, ['a', 'dtype']]) + assert_frame_equal(result, expected) diff --git a/pandas/tests/test_generic.py b/pandas/tests/test_generic.py deleted file mode 100644 index a2329e2d1768e..0000000000000 --- a/pandas/tests/test_generic.py +++ /dev/null @@ -1,2057 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable-msg=E1101,W0612 - -from operator import methodcaller -from copy import copy, deepcopy -import pytest -import numpy as np -from numpy import nan -import pandas as pd - -from distutils.version import LooseVersion -from pandas.types.common import is_scalar -from pandas import (Index, Series, DataFrame, Panel, isnull, - date_range, period_range, Panel4D) -from pandas.core.index import MultiIndex - -import pandas.formats.printing as printing - -from pandas.compat import range, zip, PY3 -from pandas import compat -from pandas.util.testing import (assertRaisesRegexp, - assert_series_equal, - assert_frame_equal, - assert_panel_equal, - assert_panel4d_equal, - assert_almost_equal) - -import pandas.util.testing as tm - - -# ---------------------------------------------------------------------- -# Generic types test cases - - -class Generic(object): - - def setUp(self): - pass - - @property - def _ndim(self): - return self._typ._AXIS_LEN - - def _axes(self): - """ return the axes for my object typ """ - return self._typ._AXIS_ORDERS - - def _construct(self, shape, value=None, dtype=None, **kwargs): - """ construct an object for the given shape - if value is specified use that if its a scalar - if value is an array, repeat it as needed """ - - if isinstance(shape, int): - shape = tuple([shape] * self._ndim) - if value is not None: - if is_scalar(value): - if value == 'empty': - arr = None - - # remove the info axis - kwargs.pop(self._typ._info_axis_name, None) - else: - arr = np.empty(shape, dtype=dtype) - arr.fill(value) - else: - fshape = np.prod(shape) - arr = value.ravel() - new_shape = fshape / arr.shape[0] - if fshape % arr.shape[0] != 0: - raise Exception("invalid value passed in _construct") - - arr = np.repeat(arr, new_shape).reshape(shape) - else: - arr = np.random.randn(*shape) - return self._typ(arr, dtype=dtype, **kwargs) - - def _compare(self, result, expected): - self._comparator(result, expected) - - def test_rename(self): - - # single axis - idx = list('ABCD') - # relabeling values passed into self.rename - args = [ - str.lower, - {x: x.lower() for x in idx}, - Series({x: x.lower() for x in idx}), - ] - - for axis in self._axes(): - kwargs = {axis: idx} - obj = self._construct(4, **kwargs) - - for arg in args: - # rename a single axis - result = obj.rename(**{axis: arg}) - expected = obj.copy() - setattr(expected, axis, list('abcd')) - self._compare(result, expected) - - # multiple axes at once - - def test_rename_axis(self): - idx = list('ABCD') - # relabeling values passed into self.rename - args = [ - str.lower, - {x: x.lower() for x in idx}, - Series({x: x.lower() for x in idx}), - ] - - for axis in self._axes(): - kwargs = {axis: idx} - obj = self._construct(4, **kwargs) - - for arg in args: - # rename a single axis - result = obj.rename_axis(arg, axis=axis) - expected = obj.copy() - setattr(expected, axis, list('abcd')) - self._compare(result, expected) - # scalar values - for arg in ['foo', None]: - result = obj.rename_axis(arg, axis=axis) - expected = obj.copy() - getattr(expected, axis).name = arg - self._compare(result, expected) - - def test_get_numeric_data(self): - - n = 4 - kwargs = {} - for i in range(self._ndim): - kwargs[self._typ._AXIS_NAMES[i]] = list(range(n)) - - # get the numeric data - o = self._construct(n, **kwargs) - result = o._get_numeric_data() - self._compare(result, o) - - # non-inclusion - result = o._get_bool_data() - expected = self._construct(n, value='empty', **kwargs) - self._compare(result, expected) - - # get the bool data - arr = np.array([True, True, False, True]) - o = self._construct(n, value=arr, **kwargs) - result = o._get_numeric_data() - self._compare(result, o) - - # _get_numeric_data is includes _get_bool_data, so can't test for - # non-inclusion - - def test_get_default(self): - - # GH 7725 - d0 = "a", "b", "c", "d" - d1 = np.arange(4, dtype='int64') - others = "e", 10 - - for data, index in ((d0, d1), (d1, d0)): - s = Series(data, index=index) - for i, d in zip(index, data): - self.assertEqual(s.get(i), d) - self.assertEqual(s.get(i, d), d) - self.assertEqual(s.get(i, "z"), d) - for other in others: - self.assertEqual(s.get(other, "z"), "z") - self.assertEqual(s.get(other, other), other) - - def test_nonzero(self): - - # GH 4633 - # look at the boolean/nonzero behavior for objects - obj = self._construct(shape=4) - self.assertRaises(ValueError, lambda: bool(obj == 0)) - self.assertRaises(ValueError, lambda: bool(obj == 1)) - self.assertRaises(ValueError, lambda: bool(obj)) - - obj = self._construct(shape=4, value=1) - self.assertRaises(ValueError, lambda: bool(obj == 0)) - self.assertRaises(ValueError, lambda: bool(obj == 1)) - self.assertRaises(ValueError, lambda: bool(obj)) - - obj = self._construct(shape=4, value=np.nan) - self.assertRaises(ValueError, lambda: bool(obj == 0)) - self.assertRaises(ValueError, lambda: bool(obj == 1)) - self.assertRaises(ValueError, lambda: bool(obj)) - - # empty - obj = self._construct(shape=0) - self.assertRaises(ValueError, lambda: bool(obj)) - - # invalid behaviors - - obj1 = self._construct(shape=4, value=1) - obj2 = self._construct(shape=4, value=1) - - def f(): - if obj1: - printing.pprint_thing("this works and shouldn't") - - self.assertRaises(ValueError, f) - self.assertRaises(ValueError, lambda: obj1 and obj2) - self.assertRaises(ValueError, lambda: obj1 or obj2) - self.assertRaises(ValueError, lambda: not obj1) - - def test_numpy_1_7_compat_numeric_methods(self): - # GH 4435 - # numpy in 1.7 tries to pass addtional arguments to pandas functions - - o = self._construct(shape=4) - for op in ['min', 'max', 'max', 'var', 'std', 'prod', 'sum', 'cumsum', - 'cumprod', 'median', 'skew', 'kurt', 'compound', 'cummax', - 'cummin', 'all', 'any']: - f = getattr(np, op, None) - if f is not None: - f(o) - - def test_downcast(self): - # test close downcasting - - o = self._construct(shape=4, value=9, dtype=np.int64) - result = o.copy() - result._data = o._data.downcast(dtypes='infer') - self._compare(result, o) - - o = self._construct(shape=4, value=9.) - expected = o.astype(np.int64) - result = o.copy() - result._data = o._data.downcast(dtypes='infer') - self._compare(result, expected) - - o = self._construct(shape=4, value=9.5) - result = o.copy() - result._data = o._data.downcast(dtypes='infer') - self._compare(result, o) - - # are close - o = self._construct(shape=4, value=9.000000000005) - result = o.copy() - result._data = o._data.downcast(dtypes='infer') - expected = o.astype(np.int64) - self._compare(result, expected) - - def test_constructor_compound_dtypes(self): - # GH 5191 - # compound dtypes should raise not-implementederror - - def f(dtype): - return self._construct(shape=3, dtype=dtype) - - self.assertRaises(NotImplementedError, f, [("A", "datetime64[h]"), - ("B", "str"), - ("C", "int32")]) - - # these work (though results may be unexpected) - f('int64') - f('float64') - f('M8[ns]') - - def check_metadata(self, x, y=None): - for m in x._metadata: - v = getattr(x, m, None) - if y is None: - self.assertIsNone(v) - else: - self.assertEqual(v, getattr(y, m, None)) - - def test_metadata_propagation(self): - # check that the metadata matches up on the resulting ops - - o = self._construct(shape=3) - o.name = 'foo' - o2 = self._construct(shape=3) - o2.name = 'bar' - - # TODO - # Once panel can do non-trivial combine operations - # (currently there is an a raise in the Panel arith_ops to prevent - # this, though it actually does work) - # can remove all of these try: except: blocks on the actual operations - - # ---------- - # preserving - # ---------- - - # simple ops with scalars - for op in ['__add__', '__sub__', '__truediv__', '__mul__']: - result = getattr(o, op)(1) - self.check_metadata(o, result) - - # ops with like - for op in ['__add__', '__sub__', '__truediv__', '__mul__']: - try: - result = getattr(o, op)(o) - self.check_metadata(o, result) - except (ValueError, AttributeError): - pass - - # simple boolean - for op in ['__eq__', '__le__', '__ge__']: - v1 = getattr(o, op)(o) - self.check_metadata(o, v1) - - try: - self.check_metadata(o, v1 & v1) - except (ValueError): - pass - - try: - self.check_metadata(o, v1 | v1) - except (ValueError): - pass - - # combine_first - try: - result = o.combine_first(o2) - self.check_metadata(o, result) - except (AttributeError): - pass - - # --------------------------- - # non-preserving (by default) - # --------------------------- - - # add non-like - try: - result = o + o2 - self.check_metadata(result) - except (ValueError, AttributeError): - pass - - # simple boolean - for op in ['__eq__', '__le__', '__ge__']: - - # this is a name matching op - v1 = getattr(o, op)(o) - - v2 = getattr(o, op)(o2) - self.check_metadata(v2) - - try: - self.check_metadata(v1 & v2) - except (ValueError): - pass - - try: - self.check_metadata(v1 | v2) - except (ValueError): - pass - - def test_head_tail(self): - # GH5370 - - o = self._construct(shape=10) - - # check all index types - for index in [tm.makeFloatIndex, tm.makeIntIndex, tm.makeStringIndex, - tm.makeUnicodeIndex, tm.makeDateIndex, - tm.makePeriodIndex]: - axis = o._get_axis_name(0) - setattr(o, axis, index(len(getattr(o, axis)))) - - # Panel + dims - try: - o.head() - except (NotImplementedError): - pytest.skip('not implemented on {0}'.format( - o.__class__.__name__)) - - self._compare(o.head(), o.iloc[:5]) - self._compare(o.tail(), o.iloc[-5:]) - - # 0-len - self._compare(o.head(0), o.iloc[0:0]) - self._compare(o.tail(0), o.iloc[0:0]) - - # bounded - self._compare(o.head(len(o) + 1), o) - self._compare(o.tail(len(o) + 1), o) - - # neg index - self._compare(o.head(-3), o.head(7)) - self._compare(o.tail(-3), o.tail(7)) - - def test_sample(self): - # Fixes issue: 2419 - - o = self._construct(shape=10) - - ### - # Check behavior of random_state argument - ### - - # Check for stability when receives seed or random state -- run 10 - # times. - for test in range(10): - seed = np.random.randint(0, 100) - self._compare( - o.sample(n=4, random_state=seed), o.sample(n=4, - random_state=seed)) - self._compare( - o.sample(frac=0.7, random_state=seed), o.sample( - frac=0.7, random_state=seed)) - - self._compare( - o.sample(n=4, random_state=np.random.RandomState(test)), - o.sample(n=4, random_state=np.random.RandomState(test))) - - self._compare( - o.sample(frac=0.7, random_state=np.random.RandomState(test)), - o.sample(frac=0.7, random_state=np.random.RandomState(test))) - - os1, os2 = [], [] - for _ in range(2): - np.random.seed(test) - os1.append(o.sample(n=4)) - os2.append(o.sample(frac=0.7)) - self._compare(*os1) - self._compare(*os2) - - # Check for error when random_state argument invalid. - with tm.assertRaises(ValueError): - o.sample(random_state='astring!') - - ### - # Check behavior of `frac` and `N` - ### - - # Giving both frac and N throws error - with tm.assertRaises(ValueError): - o.sample(n=3, frac=0.3) - - # Check that raises right error for negative lengths - with tm.assertRaises(ValueError): - o.sample(n=-3) - with tm.assertRaises(ValueError): - o.sample(frac=-0.3) - - # Make sure float values of `n` give error - with tm.assertRaises(ValueError): - o.sample(n=3.2) - - # Check lengths are right - self.assertTrue(len(o.sample(n=4) == 4)) - self.assertTrue(len(o.sample(frac=0.34) == 3)) - self.assertTrue(len(o.sample(frac=0.36) == 4)) - - ### - # Check weights - ### - - # Weight length must be right - with tm.assertRaises(ValueError): - o.sample(n=3, weights=[0, 1]) - - with tm.assertRaises(ValueError): - bad_weights = [0.5] * 11 - o.sample(n=3, weights=bad_weights) - - with tm.assertRaises(ValueError): - bad_weight_series = Series([0, 0, 0.2]) - o.sample(n=4, weights=bad_weight_series) - - # Check won't accept negative weights - with tm.assertRaises(ValueError): - bad_weights = [-0.1] * 10 - o.sample(n=3, weights=bad_weights) - - # Check inf and -inf throw errors: - with tm.assertRaises(ValueError): - weights_with_inf = [0.1] * 10 - weights_with_inf[0] = np.inf - o.sample(n=3, weights=weights_with_inf) - - with tm.assertRaises(ValueError): - weights_with_ninf = [0.1] * 10 - weights_with_ninf[0] = -np.inf - o.sample(n=3, weights=weights_with_ninf) - - # All zeros raises errors - zero_weights = [0] * 10 - with tm.assertRaises(ValueError): - o.sample(n=3, weights=zero_weights) - - # All missing weights - nan_weights = [np.nan] * 10 - with tm.assertRaises(ValueError): - o.sample(n=3, weights=nan_weights) - - # Check np.nan are replaced by zeros. - weights_with_nan = [np.nan] * 10 - weights_with_nan[5] = 0.5 - self._compare( - o.sample(n=1, axis=0, weights=weights_with_nan), o.iloc[5:6]) - - # Check None are also replaced by zeros. - weights_with_None = [None] * 10 - weights_with_None[5] = 0.5 - self._compare( - o.sample(n=1, axis=0, weights=weights_with_None), o.iloc[5:6]) - - def test_size_compat(self): - # GH8846 - # size property should be defined - - o = self._construct(shape=10) - self.assertTrue(o.size == np.prod(o.shape)) - self.assertTrue(o.size == 10 ** len(o.axes)) - - def test_split_compat(self): - # xref GH8846 - o = self._construct(shape=10) - self.assertTrue(len(np.array_split(o, 5)) == 5) - self.assertTrue(len(np.array_split(o, 2)) == 2) - - def test_unexpected_keyword(self): # GH8597 - df = DataFrame(np.random.randn(5, 2), columns=['jim', 'joe']) - ca = pd.Categorical([0, 0, 2, 2, 3, np.nan]) - ts = df['joe'].copy() - ts[2] = np.nan - - with assertRaisesRegexp(TypeError, 'unexpected keyword'): - df.drop('joe', axis=1, in_place=True) - - with assertRaisesRegexp(TypeError, 'unexpected keyword'): - df.reindex([1, 0], inplace=True) - - with assertRaisesRegexp(TypeError, 'unexpected keyword'): - ca.fillna(0, inplace=True) - - with assertRaisesRegexp(TypeError, 'unexpected keyword'): - ts.fillna(0, in_place=True) - - # See gh-12301 - def test_stat_unexpected_keyword(self): - obj = self._construct(5) - starwars = 'Star Wars' - errmsg = 'unexpected keyword' - - with assertRaisesRegexp(TypeError, errmsg): - obj.max(epic=starwars) # stat_function - with assertRaisesRegexp(TypeError, errmsg): - obj.var(epic=starwars) # stat_function_ddof - with assertRaisesRegexp(TypeError, errmsg): - obj.sum(epic=starwars) # cum_function - with assertRaisesRegexp(TypeError, errmsg): - obj.any(epic=starwars) # logical_function - - def test_api_compat(self): - - # GH 12021 - # compat for __name__, __qualname__ - - obj = self._construct(5) - for func in ['sum', 'cumsum', 'any', 'var']: - f = getattr(obj, func) - self.assertEqual(f.__name__, func) - if PY3: - self.assertTrue(f.__qualname__.endswith(func)) - - def test_stat_non_defaults_args(self): - obj = self._construct(5) - out = np.array([0]) - errmsg = "the 'out' parameter is not supported" - - with assertRaisesRegexp(ValueError, errmsg): - obj.max(out=out) # stat_function - with assertRaisesRegexp(ValueError, errmsg): - obj.var(out=out) # stat_function_ddof - with assertRaisesRegexp(ValueError, errmsg): - obj.sum(out=out) # cum_function - with assertRaisesRegexp(ValueError, errmsg): - obj.any(out=out) # logical_function - - def test_clip(self): - lower = 1 - upper = 3 - col = np.arange(5) - - obj = self._construct(len(col), value=col) - - if isinstance(obj, Panel): - msg = "clip is not supported yet for panels" - tm.assertRaisesRegexp(NotImplementedError, msg, - obj.clip, lower=lower, - upper=upper) - - else: - out = obj.clip(lower=lower, upper=upper) - expected = self._construct(len(col), value=col - .clip(lower, upper)) - self._compare(out, expected) - - bad_axis = 'foo' - msg = ('No axis named {axis} ' - 'for object').format(axis=bad_axis) - assertRaisesRegexp(ValueError, msg, obj.clip, - lower=lower, upper=upper, - axis=bad_axis) - - def test_truncate_out_of_bounds(self): - # GH11382 - - # small - shape = [int(2e3)] + ([1] * (self._ndim - 1)) - small = self._construct(shape, dtype='int8') - self._compare(small.truncate(), small) - self._compare(small.truncate(before=0, after=3e3), small) - self._compare(small.truncate(before=-1, after=2e3), small) - - # big - shape = [int(2e6)] + ([1] * (self._ndim - 1)) - big = self._construct(shape, dtype='int8') - self._compare(big.truncate(), big) - self._compare(big.truncate(before=0, after=3e6), big) - self._compare(big.truncate(before=-1, after=2e6), big) - - def test_numpy_clip(self): - lower = 1 - upper = 3 - col = np.arange(5) - - obj = self._construct(len(col), value=col) - - if isinstance(obj, Panel): - msg = "clip is not supported yet for panels" - tm.assertRaisesRegexp(NotImplementedError, msg, - np.clip, obj, - lower, upper) - else: - out = np.clip(obj, lower, upper) - expected = self._construct(len(col), value=col - .clip(lower, upper)) - self._compare(out, expected) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, - np.clip, obj, - lower, upper, out=col) - - def test_validate_bool_args(self): - df = DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]}) - invalid_values = [1, "True", [1, 2, 3], 5.0] - - for value in invalid_values: - with self.assertRaises(ValueError): - super(DataFrame, df).rename_axis(mapper={'a': 'x', 'b': 'y'}, - axis=1, inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).drop('a', axis=1, inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).sort_index(inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df)._consolidate(inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).fillna(value=0, inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).replace(to_replace=1, value=7, - inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).interpolate(inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df)._where(cond=df.a > 2, inplace=value) - - with self.assertRaises(ValueError): - super(DataFrame, df).mask(cond=df.a > 2, inplace=value) - - def test_copy_and_deepcopy(self): - # GH 15444 - for shape in [0, 1, 2]: - obj = self._construct(shape) - for func in [copy, - deepcopy, - lambda x: x.copy(deep=False), - lambda x: x.copy(deep=True)]: - obj_copy = func(obj) - self.assertIsNot(obj_copy, obj) - self._compare(obj_copy, obj) - - -class TestSeries(tm.TestCase, Generic): - _typ = Series - _comparator = lambda self, x, y: assert_series_equal(x, y) - - def setUp(self): - self.ts = tm.makeTimeSeries() # Was at top level in test_series - self.ts.name = 'ts' - - self.series = tm.makeStringSeries() - self.series.name = 'series' - - def test_rename_mi(self): - s = Series([11, 21, 31], - index=MultiIndex.from_tuples( - [("A", x) for x in ["a", "B", "c"]])) - s.rename(str.lower) - - def test_set_axis_name(self): - s = Series([1, 2, 3], index=['a', 'b', 'c']) - funcs = ['rename_axis', '_set_axis_name'] - name = 'foo' - for func in funcs: - result = methodcaller(func, name)(s) - self.assertTrue(s.index.name is None) - self.assertEqual(result.index.name, name) - - def test_set_axis_name_mi(self): - s = Series([11, 21, 31], index=MultiIndex.from_tuples( - [("A", x) for x in ["a", "B", "c"]], - names=['l1', 'l2']) - ) - funcs = ['rename_axis', '_set_axis_name'] - for func in funcs: - result = methodcaller(func, ['L1', 'L2'])(s) - self.assertTrue(s.index.name is None) - self.assertEqual(s.index.names, ['l1', 'l2']) - self.assertTrue(result.index.name is None) - self.assertTrue(result.index.names, ['L1', 'L2']) - - def test_set_axis_name_raises(self): - s = pd.Series([1]) - with tm.assertRaises(ValueError): - s._set_axis_name(name='a', axis=1) - - def test_get_numeric_data_preserve_dtype(self): - - # get the numeric data - o = Series([1, 2, 3]) - result = o._get_numeric_data() - self._compare(result, o) - - o = Series([1, '2', 3.]) - result = o._get_numeric_data() - expected = Series([], dtype=object, index=pd.Index([], dtype=object)) - self._compare(result, expected) - - o = Series([True, False, True]) - result = o._get_numeric_data() - self._compare(result, o) - - o = Series([True, False, True]) - result = o._get_bool_data() - self._compare(result, o) - - o = Series(date_range('20130101', periods=3)) - result = o._get_numeric_data() - expected = Series([], dtype='M8[ns]', index=pd.Index([], dtype=object)) - self._compare(result, expected) - - def test_nonzero_single_element(self): - - # allow single item via bool method - s = Series([True]) - self.assertTrue(s.bool()) - - s = Series([False]) - self.assertFalse(s.bool()) - - # single item nan to raise - for s in [Series([np.nan]), Series([pd.NaT]), Series([True]), - Series([False])]: - self.assertRaises(ValueError, lambda: bool(s)) - - for s in [Series([np.nan]), Series([pd.NaT])]: - self.assertRaises(ValueError, lambda: s.bool()) - - # multiple bool are still an error - for s in [Series([True, True]), Series([False, False])]: - self.assertRaises(ValueError, lambda: bool(s)) - self.assertRaises(ValueError, lambda: s.bool()) - - # single non-bool are an error - for s in [Series([1]), Series([0]), Series(['a']), Series([0.0])]: - self.assertRaises(ValueError, lambda: bool(s)) - self.assertRaises(ValueError, lambda: s.bool()) - - def test_metadata_propagation_indiv(self): - # check that the metadata matches up on the resulting ops - - o = Series(range(3), range(3)) - o.name = 'foo' - o2 = Series(range(3), range(3)) - o2.name = 'bar' - - result = o.T - self.check_metadata(o, result) - - # resample - ts = Series(np.random.rand(1000), - index=date_range('20130101', periods=1000, freq='s'), - name='foo') - result = ts.resample('1T').mean() - self.check_metadata(ts, result) - - result = ts.resample('1T').min() - self.check_metadata(ts, result) - - result = ts.resample('1T').apply(lambda x: x.sum()) - self.check_metadata(ts, result) - - _metadata = Series._metadata - _finalize = Series.__finalize__ - Series._metadata = ['name', 'filename'] - o.filename = 'foo' - o2.filename = 'bar' - - def finalize(self, other, method=None, **kwargs): - for name in self._metadata: - if method == 'concat' and name == 'filename': - value = '+'.join([getattr( - o, name) for o in other.objs if getattr(o, name, None) - ]) - object.__setattr__(self, name, value) - else: - object.__setattr__(self, name, getattr(other, name, None)) - - return self - - Series.__finalize__ = finalize - - result = pd.concat([o, o2]) - self.assertEqual(result.filename, 'foo+bar') - self.assertIsNone(result.name) - - # reset - Series._metadata = _metadata - Series.__finalize__ = _finalize - - def test_describe(self): - self.series.describe() - self.ts.describe() - - def test_describe_objects(self): - s = Series(['a', 'b', 'b', np.nan, np.nan, np.nan, 'c', 'd', 'a', 'a']) - result = s.describe() - expected = Series({'count': 7, 'unique': 4, - 'top': 'a', 'freq': 3, 'second': 'b', - 'second_freq': 2}, index=result.index) - assert_series_equal(result, expected) - - dt = list(self.ts.index) - dt.append(dt[0]) - ser = Series(dt) - rs = ser.describe() - min_date = min(dt) - max_date = max(dt) - xp = Series({'count': len(dt), - 'unique': len(self.ts.index), - 'first': min_date, 'last': max_date, 'freq': 2, - 'top': min_date}, index=rs.index) - assert_series_equal(rs, xp) - - def test_describe_empty(self): - result = pd.Series().describe() - - self.assertEqual(result['count'], 0) - self.assertTrue(result.drop('count').isnull().all()) - - nanSeries = Series([np.nan]) - nanSeries.name = 'NaN' - result = nanSeries.describe() - self.assertEqual(result['count'], 0) - self.assertTrue(result.drop('count').isnull().all()) - - def test_describe_none(self): - noneSeries = Series([None]) - noneSeries.name = 'None' - expected = Series([0, 0], index=['count', 'unique'], name='None') - assert_series_equal(noneSeries.describe(), expected) - - def test_to_xarray(self): - - tm._skip_if_no_xarray() - import xarray - from xarray import DataArray - - s = Series([]) - s.index.name = 'foo' - result = s.to_xarray() - self.assertEqual(len(result), 0) - self.assertEqual(len(result.coords), 1) - assert_almost_equal(list(result.coords.keys()), ['foo']) - self.assertIsInstance(result, DataArray) - - def testit(index, check_index_type=True, check_categorical=True): - s = Series(range(6), index=index(6)) - s.index.name = 'foo' - result = s.to_xarray() - repr(result) - self.assertEqual(len(result), 6) - self.assertEqual(len(result.coords), 1) - assert_almost_equal(list(result.coords.keys()), ['foo']) - self.assertIsInstance(result, DataArray) - - # idempotency - assert_series_equal(result.to_series(), s, - check_index_type=check_index_type, - check_categorical=check_categorical) - - l = [tm.makeFloatIndex, tm.makeIntIndex, - tm.makeStringIndex, tm.makeUnicodeIndex, - tm.makeDateIndex, tm.makePeriodIndex, - tm.makeTimedeltaIndex] - - if LooseVersion(xarray.__version__) >= '0.8.0': - l.append(tm.makeCategoricalIndex) - - for index in l: - testit(index) - - s = Series(range(6)) - s.index.name = 'foo' - s.index = pd.MultiIndex.from_product([['a', 'b'], range(3)], - names=['one', 'two']) - result = s.to_xarray() - self.assertEqual(len(result), 2) - assert_almost_equal(list(result.coords.keys()), ['one', 'two']) - self.assertIsInstance(result, DataArray) - assert_series_equal(result.to_series(), s) - - -class TestDataFrame(tm.TestCase, Generic): - _typ = DataFrame - _comparator = lambda self, x, y: assert_frame_equal(x, y) - - def test_rename_mi(self): - df = DataFrame([ - 11, 21, 31 - ], index=MultiIndex.from_tuples([("A", x) for x in ["a", "B", "c"]])) - df.rename(str.lower) - - def test_set_axis_name(self): - df = pd.DataFrame([[1, 2], [3, 4]]) - funcs = ['_set_axis_name', 'rename_axis'] - for func in funcs: - result = methodcaller(func, 'foo')(df) - self.assertTrue(df.index.name is None) - self.assertEqual(result.index.name, 'foo') - - result = methodcaller(func, 'cols', axis=1)(df) - self.assertTrue(df.columns.name is None) - self.assertEqual(result.columns.name, 'cols') - - def test_set_axis_name_mi(self): - df = DataFrame( - np.empty((3, 3)), - index=MultiIndex.from_tuples([("A", x) for x in list('aBc')]), - columns=MultiIndex.from_tuples([('C', x) for x in list('xyz')]) - ) - - level_names = ['L1', 'L2'] - funcs = ['_set_axis_name', 'rename_axis'] - for func in funcs: - result = methodcaller(func, level_names)(df) - self.assertEqual(result.index.names, level_names) - self.assertEqual(result.columns.names, [None, None]) - - result = methodcaller(func, level_names, axis=1)(df) - self.assertEqual(result.columns.names, ["L1", "L2"]) - self.assertEqual(result.index.names, [None, None]) - - def test_nonzero_single_element(self): - - # allow single item via bool method - df = DataFrame([[True]]) - self.assertTrue(df.bool()) - - df = DataFrame([[False]]) - self.assertFalse(df.bool()) - - df = DataFrame([[False, False]]) - self.assertRaises(ValueError, lambda: df.bool()) - self.assertRaises(ValueError, lambda: bool(df)) - - def test_get_numeric_data_preserve_dtype(self): - - # get the numeric data - o = DataFrame({'A': [1, '2', 3.]}) - result = o._get_numeric_data() - expected = DataFrame(index=[0, 1, 2], dtype=object) - self._compare(result, expected) - - def test_describe(self): - tm.makeDataFrame().describe() - tm.makeMixedDataFrame().describe() - tm.makeTimeDataFrame().describe() - - def test_describe_percentiles_percent_or_raw(self): - msg = 'percentiles should all be in the interval \\[0, 1\\]' - - df = tm.makeDataFrame() - with tm.assertRaisesRegexp(ValueError, msg): - df.describe(percentiles=[10, 50, 100]) - - with tm.assertRaisesRegexp(ValueError, msg): - df.describe(percentiles=[2]) - - with tm.assertRaisesRegexp(ValueError, msg): - df.describe(percentiles=[-2]) - - def test_describe_percentiles_equivalence(self): - df = tm.makeDataFrame() - d1 = df.describe() - d2 = df.describe(percentiles=[.25, .75]) - assert_frame_equal(d1, d2) - - def test_describe_percentiles_insert_median(self): - df = tm.makeDataFrame() - d1 = df.describe(percentiles=[.25, .75]) - d2 = df.describe(percentiles=[.25, .5, .75]) - assert_frame_equal(d1, d2) - self.assertTrue('25%' in d1.index) - self.assertTrue('75%' in d2.index) - - # none above - d1 = df.describe(percentiles=[.25, .45]) - d2 = df.describe(percentiles=[.25, .45, .5]) - assert_frame_equal(d1, d2) - self.assertTrue('25%' in d1.index) - self.assertTrue('45%' in d2.index) - - # none below - d1 = df.describe(percentiles=[.75, 1]) - d2 = df.describe(percentiles=[.5, .75, 1]) - assert_frame_equal(d1, d2) - self.assertTrue('75%' in d1.index) - self.assertTrue('100%' in d2.index) - - # edge - d1 = df.describe(percentiles=[0, 1]) - d2 = df.describe(percentiles=[0, .5, 1]) - assert_frame_equal(d1, d2) - self.assertTrue('0%' in d1.index) - self.assertTrue('100%' in d2.index) - - def test_describe_percentiles_insert_median_ndarray(self): - # GH14908 - df = tm.makeDataFrame() - result = df.describe(percentiles=np.array([.25, .75])) - expected = df.describe(percentiles=[.25, .75]) - assert_frame_equal(result, expected) - - def test_describe_percentiles_unique(self): - # GH13104 - df = tm.makeDataFrame() - with self.assertRaises(ValueError): - df.describe(percentiles=[0.1, 0.2, 0.4, 0.5, 0.2, 0.6]) - with self.assertRaises(ValueError): - df.describe(percentiles=[0.1, 0.2, 0.4, 0.2, 0.6]) - - def test_describe_percentiles_formatting(self): - # GH13104 - df = tm.makeDataFrame() - - # default - result = df.describe().index - expected = Index(['count', 'mean', 'std', 'min', '25%', '50%', '75%', - 'max'], - dtype='object') - tm.assert_index_equal(result, expected) - - result = df.describe(percentiles=[0.0001, 0.0005, 0.001, 0.999, - 0.9995, 0.9999]).index - expected = Index(['count', 'mean', 'std', 'min', '0.01%', '0.05%', - '0.1%', '50%', '99.9%', '99.95%', '99.99%', 'max'], - dtype='object') - tm.assert_index_equal(result, expected) - - result = df.describe(percentiles=[0.00499, 0.005, 0.25, 0.50, - 0.75]).index - expected = Index(['count', 'mean', 'std', 'min', '0.499%', '0.5%', - '25%', '50%', '75%', 'max'], - dtype='object') - tm.assert_index_equal(result, expected) - - result = df.describe(percentiles=[0.00499, 0.01001, 0.25, 0.50, - 0.75]).index - expected = Index(['count', 'mean', 'std', 'min', '0.5%', '1.0%', - '25%', '50%', '75%', 'max'], - dtype='object') - tm.assert_index_equal(result, expected) - - def test_describe_column_index_type(self): - # GH13288 - df = pd.DataFrame([1, 2, 3, 4]) - df.columns = pd.Index([0], dtype=object) - result = df.describe().columns - expected = Index([0], dtype=object) - tm.assert_index_equal(result, expected) - - df = pd.DataFrame({'A': list("BCDE"), 0: [1, 2, 3, 4]}) - result = df.describe().columns - expected = Index([0], dtype=object) - tm.assert_index_equal(result, expected) - - def test_describe_no_numeric(self): - df = DataFrame({'A': ['foo', 'foo', 'bar'] * 8, - 'B': ['a', 'b', 'c', 'd'] * 6}) - desc = df.describe() - expected = DataFrame(dict((k, v.describe()) - for k, v in compat.iteritems(df)), - columns=df.columns) - assert_frame_equal(desc, expected) - - ts = tm.makeTimeSeries() - df = DataFrame({'time': ts.index}) - desc = df.describe() - self.assertEqual(desc.time['first'], min(ts.index)) - - def test_describe_empty(self): - df = DataFrame() - tm.assertRaisesRegexp(ValueError, 'DataFrame without columns', - df.describe) - - df = DataFrame(columns=['A', 'B']) - result = df.describe() - expected = DataFrame(0, columns=['A', 'B'], index=['count', 'unique']) - tm.assert_frame_equal(result, expected) - - def test_describe_empty_int_columns(self): - df = DataFrame([[0, 1], [1, 2]]) - desc = df[df[0] < 0].describe() # works - assert_series_equal(desc.xs('count'), - Series([0, 0], dtype=float, name='count')) - self.assertTrue(isnull(desc.iloc[1:]).all().all()) - - def test_describe_objects(self): - df = DataFrame({"C1": ['a', 'a', 'c'], "C2": ['d', 'd', 'f']}) - result = df.describe() - expected = DataFrame({"C1": [3, 2, 'a', 2], "C2": [3, 2, 'd', 2]}, - index=['count', 'unique', 'top', 'freq']) - assert_frame_equal(result, expected) - - df = DataFrame({"C1": pd.date_range('2010-01-01', periods=4, freq='D') - }) - df.loc[4] = pd.Timestamp('2010-01-04') - result = df.describe() - expected = DataFrame({"C1": [5, 4, pd.Timestamp('2010-01-04'), 2, - pd.Timestamp('2010-01-01'), - pd.Timestamp('2010-01-04')]}, - index=['count', 'unique', 'top', 'freq', - 'first', 'last']) - assert_frame_equal(result, expected) - - # mix time and str - df['C2'] = ['a', 'a', 'b', 'c', 'a'] - result = df.describe() - expected['C2'] = [5, 3, 'a', 3, np.nan, np.nan] - assert_frame_equal(result, expected) - - # just str - expected = DataFrame({'C2': [5, 3, 'a', 4]}, - index=['count', 'unique', 'top', 'freq']) - result = df[['C2']].describe() - - # mix of time, str, numeric - df['C3'] = [2, 4, 6, 8, 2] - result = df.describe() - expected = DataFrame({"C3": [5., 4.4, 2.607681, 2., 2., 4., 6., 8.]}, - index=['count', 'mean', 'std', 'min', '25%', - '50%', '75%', 'max']) - assert_frame_equal(result, expected) - assert_frame_equal(df.describe(), df[['C3']].describe()) - - assert_frame_equal(df[['C1', 'C3']].describe(), df[['C3']].describe()) - assert_frame_equal(df[['C2', 'C3']].describe(), df[['C3']].describe()) - - def test_describe_typefiltering(self): - df = DataFrame({'catA': ['foo', 'foo', 'bar'] * 8, - 'catB': ['a', 'b', 'c', 'd'] * 6, - 'numC': np.arange(24, dtype='int64'), - 'numD': np.arange(24.) + .5, - 'ts': tm.makeTimeSeries()[:24].index}) - - descN = df.describe() - expected_cols = ['numC', 'numD', ] - expected = DataFrame(dict((k, df[k].describe()) - for k in expected_cols), - columns=expected_cols) - assert_frame_equal(descN, expected) - - desc = df.describe(include=['number']) - assert_frame_equal(desc, descN) - desc = df.describe(exclude=['object', 'datetime']) - assert_frame_equal(desc, descN) - desc = df.describe(include=['float']) - assert_frame_equal(desc, descN.drop('numC', 1)) - - descC = df.describe(include=['O']) - expected_cols = ['catA', 'catB'] - expected = DataFrame(dict((k, df[k].describe()) - for k in expected_cols), - columns=expected_cols) - assert_frame_equal(descC, expected) - - descD = df.describe(include=['datetime']) - assert_series_equal(descD.ts, df.ts.describe()) - - desc = df.describe(include=['object', 'number', 'datetime']) - assert_frame_equal(desc.loc[:, ["numC", "numD"]].dropna(), descN) - assert_frame_equal(desc.loc[:, ["catA", "catB"]].dropna(), descC) - descDs = descD.sort_index() # the index order change for mixed-types - assert_frame_equal(desc.loc[:, "ts":].dropna().sort_index(), descDs) - - desc = df.loc[:, 'catA':'catB'].describe(include='all') - assert_frame_equal(desc, descC) - desc = df.loc[:, 'numC':'numD'].describe(include='all') - assert_frame_equal(desc, descN) - - desc = df.describe(percentiles=[], include='all') - cnt = Series(data=[4, 4, 6, 6, 6], - index=['catA', 'catB', 'numC', 'numD', 'ts']) - assert_series_equal(desc.count(), cnt) - self.assertTrue('count' in desc.index) - self.assertTrue('unique' in desc.index) - self.assertTrue('50%' in desc.index) - self.assertTrue('first' in desc.index) - - desc = df.drop("ts", 1).describe(percentiles=[], include='all') - assert_series_equal(desc.count(), cnt.drop("ts")) - self.assertTrue('first' not in desc.index) - desc = df.drop(["numC", "numD"], 1).describe(percentiles=[], - include='all') - assert_series_equal(desc.count(), cnt.drop(["numC", "numD"])) - self.assertTrue('50%' not in desc.index) - - def test_describe_typefiltering_category_bool(self): - df = DataFrame({'A_cat': pd.Categorical(['foo', 'foo', 'bar'] * 8), - 'B_str': ['a', 'b', 'c', 'd'] * 6, - 'C_bool': [True] * 12 + [False] * 12, - 'D_num': np.arange(24.) + .5, - 'E_ts': tm.makeTimeSeries()[:24].index}) - - desc = df.describe() - expected_cols = ['D_num'] - expected = DataFrame(dict((k, df[k].describe()) - for k in expected_cols), - columns=expected_cols) - assert_frame_equal(desc, expected) - - desc = df.describe(include=["category"]) - self.assertTrue(desc.columns.tolist() == ["A_cat"]) - - # 'all' includes numpy-dtypes + category - desc1 = df.describe(include="all") - desc2 = df.describe(include=[np.generic, "category"]) - assert_frame_equal(desc1, desc2) - - def test_describe_timedelta(self): - df = DataFrame({"td": pd.to_timedelta(np.arange(24) % 20, "D")}) - self.assertTrue(df.describe().loc["mean"][0] == pd.to_timedelta( - "8d4h")) - - def test_describe_typefiltering_dupcol(self): - df = DataFrame({'catA': ['foo', 'foo', 'bar'] * 8, - 'catB': ['a', 'b', 'c', 'd'] * 6, - 'numC': np.arange(24), - 'numD': np.arange(24.) + .5, - 'ts': tm.makeTimeSeries()[:24].index}) - s = df.describe(include='all').shape[1] - df = pd.concat([df, df], axis=1) - s2 = df.describe(include='all').shape[1] - self.assertTrue(s2 == 2 * s) - - def test_describe_typefiltering_groupby(self): - df = DataFrame({'catA': ['foo', 'foo', 'bar'] * 8, - 'catB': ['a', 'b', 'c', 'd'] * 6, - 'numC': np.arange(24), - 'numD': np.arange(24.) + .5, - 'ts': tm.makeTimeSeries()[:24].index}) - G = df.groupby('catA') - self.assertTrue(G.describe(include=['number']).shape == (2, 16)) - self.assertTrue(G.describe(include=['number', 'object']).shape == (2, - 33)) - self.assertTrue(G.describe(include='all').shape == (2, 52)) - - def test_describe_multi_index_df_column_names(self): - """ Test that column names persist after the describe operation.""" - - df = pd.DataFrame( - {'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'], - 'B': ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'], - 'C': np.random.randn(8), - 'D': np.random.randn(8)}) - - # GH 11517 - # test for hierarchical index - hierarchical_index_df = df.groupby(['A', 'B']).mean().T - self.assertTrue(hierarchical_index_df.columns.names == ['A', 'B']) - self.assertTrue(hierarchical_index_df.describe().columns.names == - ['A', 'B']) - - # test for non-hierarchical index - non_hierarchical_index_df = df.groupby(['A']).mean().T - self.assertTrue(non_hierarchical_index_df.columns.names == ['A']) - self.assertTrue(non_hierarchical_index_df.describe().columns.names == - ['A']) - - def test_metadata_propagation_indiv(self): - - # groupby - df = DataFrame( - {'A': ['foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'foo'], - 'B': ['one', 'one', 'two', 'three', 'two', 'two', 'one', 'three'], - 'C': np.random.randn(8), - 'D': np.random.randn(8)}) - result = df.groupby('A').sum() - self.check_metadata(df, result) - - # resample - df = DataFrame(np.random.randn(1000, 2), - index=date_range('20130101', periods=1000, freq='s')) - result = df.resample('1T') - self.check_metadata(df, result) - - # merging with override - # GH 6923 - _metadata = DataFrame._metadata - _finalize = DataFrame.__finalize__ - - np.random.seed(10) - df1 = DataFrame(np.random.randint(0, 4, (3, 2)), columns=['a', 'b']) - df2 = DataFrame(np.random.randint(0, 4, (3, 2)), columns=['c', 'd']) - DataFrame._metadata = ['filename'] - df1.filename = 'fname1.csv' - df2.filename = 'fname2.csv' - - def finalize(self, other, method=None, **kwargs): - - for name in self._metadata: - if method == 'merge': - left, right = other.left, other.right - value = getattr(left, name, '') + '|' + getattr(right, - name, '') - object.__setattr__(self, name, value) - else: - object.__setattr__(self, name, getattr(other, name, '')) - - return self - - DataFrame.__finalize__ = finalize - result = df1.merge(df2, left_on=['a'], right_on=['c'], how='inner') - self.assertEqual(result.filename, 'fname1.csv|fname2.csv') - - # concat - # GH 6927 - DataFrame._metadata = ['filename'] - df1 = DataFrame(np.random.randint(0, 4, (3, 2)), columns=list('ab')) - df1.filename = 'foo' - - def finalize(self, other, method=None, **kwargs): - for name in self._metadata: - if method == 'concat': - value = '+'.join([getattr( - o, name) for o in other.objs if getattr(o, name, None) - ]) - object.__setattr__(self, name, value) - else: - object.__setattr__(self, name, getattr(other, name, None)) - - return self - - DataFrame.__finalize__ = finalize - - result = pd.concat([df1, df1]) - self.assertEqual(result.filename, 'foo+foo') - - # reset - DataFrame._metadata = _metadata - DataFrame.__finalize__ = _finalize - - def test_tz_convert_and_localize(self): - l0 = date_range('20140701', periods=5, freq='D') - - # TODO: l1 should be a PeriodIndex for testing - # after GH2106 is addressed - with tm.assertRaises(NotImplementedError): - period_range('20140701', periods=1).tz_convert('UTC') - with tm.assertRaises(NotImplementedError): - period_range('20140701', periods=1).tz_localize('UTC') - # l1 = period_range('20140701', periods=5, freq='D') - l1 = date_range('20140701', periods=5, freq='D') - - int_idx = Index(range(5)) - - for fn in ['tz_localize', 'tz_convert']: - - if fn == 'tz_convert': - l0 = l0.tz_localize('UTC') - l1 = l1.tz_localize('UTC') - - for idx in [l0, l1]: - - l0_expected = getattr(idx, fn)('US/Pacific') - l1_expected = getattr(idx, fn)('US/Pacific') - - df1 = DataFrame(np.ones(5), index=l0) - df1 = getattr(df1, fn)('US/Pacific') - self.assert_index_equal(df1.index, l0_expected) - - # MultiIndex - # GH7846 - df2 = DataFrame(np.ones(5), MultiIndex.from_arrays([l0, l1])) - - df3 = getattr(df2, fn)('US/Pacific', level=0) - self.assertFalse(df3.index.levels[0].equals(l0)) - self.assert_index_equal(df3.index.levels[0], l0_expected) - self.assert_index_equal(df3.index.levels[1], l1) - self.assertFalse(df3.index.levels[1].equals(l1_expected)) - - df3 = getattr(df2, fn)('US/Pacific', level=1) - self.assert_index_equal(df3.index.levels[0], l0) - self.assertFalse(df3.index.levels[0].equals(l0_expected)) - self.assert_index_equal(df3.index.levels[1], l1_expected) - self.assertFalse(df3.index.levels[1].equals(l1)) - - df4 = DataFrame(np.ones(5), - MultiIndex.from_arrays([int_idx, l0])) - - # TODO: untested - df5 = getattr(df4, fn)('US/Pacific', level=1) # noqa - - self.assert_index_equal(df3.index.levels[0], l0) - self.assertFalse(df3.index.levels[0].equals(l0_expected)) - self.assert_index_equal(df3.index.levels[1], l1_expected) - self.assertFalse(df3.index.levels[1].equals(l1)) - - # Bad Inputs - for fn in ['tz_localize', 'tz_convert']: - # Not DatetimeIndex / PeriodIndex - with tm.assertRaisesRegexp(TypeError, 'DatetimeIndex'): - df = DataFrame(index=int_idx) - df = getattr(df, fn)('US/Pacific') - - # Not DatetimeIndex / PeriodIndex - with tm.assertRaisesRegexp(TypeError, 'DatetimeIndex'): - df = DataFrame(np.ones(5), - MultiIndex.from_arrays([int_idx, l0])) - df = getattr(df, fn)('US/Pacific', level=0) - - # Invalid level - with tm.assertRaisesRegexp(ValueError, 'not valid'): - df = DataFrame(index=l0) - df = getattr(df, fn)('US/Pacific', level=1) - - def test_set_attribute(self): - # Test for consistent setattr behavior when an attribute and a column - # have the same name (Issue #8994) - df = DataFrame({'x': [1, 2, 3]}) - - df.y = 2 - df['y'] = [2, 4, 6] - df.y = 5 - - self.assertEqual(df.y, 5) - assert_series_equal(df['y'], Series([2, 4, 6], name='y')) - - def test_pct_change(self): - # GH 11150 - pnl = DataFrame([np.arange(0, 40, 10), np.arange(0, 40, 10), np.arange( - 0, 40, 10)]).astype(np.float64) - pnl.iat[1, 0] = np.nan - pnl.iat[1, 1] = np.nan - pnl.iat[2, 3] = 60 - - mask = pnl.isnull() - - for axis in range(2): - expected = pnl.ffill(axis=axis) / pnl.ffill(axis=axis).shift( - axis=axis) - 1 - expected[mask] = np.nan - result = pnl.pct_change(axis=axis, fill_method='pad') - - self.assert_frame_equal(result, expected) - - def test_to_xarray(self): - - tm._skip_if_no_xarray() - from xarray import Dataset - - df = DataFrame({'a': list('abc'), - 'b': list(range(1, 4)), - 'c': np.arange(3, 6).astype('u1'), - 'd': np.arange(4.0, 7.0, dtype='float64'), - 'e': [True, False, True], - 'f': pd.Categorical(list('abc')), - 'g': pd.date_range('20130101', periods=3), - 'h': pd.date_range('20130101', - periods=3, - tz='US/Eastern')} - ) - - df.index.name = 'foo' - result = df[0:0].to_xarray() - self.assertEqual(result.dims['foo'], 0) - self.assertIsInstance(result, Dataset) - - for index in [tm.makeFloatIndex, tm.makeIntIndex, - tm.makeStringIndex, tm.makeUnicodeIndex, - tm.makeDateIndex, tm.makePeriodIndex, - tm.makeCategoricalIndex, tm.makeTimedeltaIndex]: - df.index = index(3) - df.index.name = 'foo' - df.columns.name = 'bar' - result = df.to_xarray() - self.assertEqual(result.dims['foo'], 3) - self.assertEqual(len(result.coords), 1) - self.assertEqual(len(result.data_vars), 8) - assert_almost_equal(list(result.coords.keys()), ['foo']) - self.assertIsInstance(result, Dataset) - - # idempotency - # categoricals are not preserved - # datetimes w/tz are not preserved - # column names are lost - expected = df.copy() - expected['f'] = expected['f'].astype(object) - expected['h'] = expected['h'].astype('datetime64[ns]') - expected.columns.name = None - assert_frame_equal(result.to_dataframe(), expected, - check_index_type=False, check_categorical=False) - - # available in 0.7.1 - # MultiIndex - df.index = pd.MultiIndex.from_product([['a'], range(3)], - names=['one', 'two']) - result = df.to_xarray() - self.assertEqual(result.dims['one'], 1) - self.assertEqual(result.dims['two'], 3) - self.assertEqual(len(result.coords), 2) - self.assertEqual(len(result.data_vars), 8) - assert_almost_equal(list(result.coords.keys()), ['one', 'two']) - self.assertIsInstance(result, Dataset) - - result = result.to_dataframe() - expected = df.copy() - expected['f'] = expected['f'].astype(object) - expected['h'] = expected['h'].astype('datetime64[ns]') - expected.columns.name = None - assert_frame_equal(result, - expected, - check_index_type=False) - - def test_deepcopy_empty(self): - # This test covers empty frame copying with non-empty column sets - # as reported in issue GH15370 - empty_frame = DataFrame(data=[], index=[], columns=['A']) - empty_frame_copy = deepcopy(empty_frame) - - self._compare(empty_frame_copy, empty_frame) - - -class TestPanel(tm.TestCase, Generic): - _typ = Panel - _comparator = lambda self, x, y: assert_panel_equal(x, y, by_blocks=True) - - def test_to_xarray(self): - - tm._skip_if_no_xarray() - from xarray import DataArray - - p = tm.makePanel() - - result = p.to_xarray() - self.assertIsInstance(result, DataArray) - self.assertEqual(len(result.coords), 3) - assert_almost_equal(list(result.coords.keys()), - ['items', 'major_axis', 'minor_axis']) - self.assertEqual(len(result.dims), 3) - - # idempotency - assert_panel_equal(result.to_pandas(), p) - - -class TestPanel4D(tm.TestCase, Generic): - _typ = Panel4D - _comparator = lambda self, x, y: assert_panel4d_equal(x, y, by_blocks=True) - - def test_sample(self): - pytest.skip("sample on Panel4D") - - def test_copy_and_deepcopy(self): - pytest.skip("copy_and_deepcopy on Panel4D") - - def test_to_xarray(self): - - tm._skip_if_no_xarray() - from xarray import DataArray - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - p = tm.makePanel4D() - - result = p.to_xarray() - self.assertIsInstance(result, DataArray) - self.assertEqual(len(result.coords), 4) - assert_almost_equal(list(result.coords.keys()), - ['labels', 'items', 'major_axis', - 'minor_axis']) - self.assertEqual(len(result.dims), 4) - - # non-convertible - self.assertRaises(ValueError, lambda: result.to_pandas()) - - -# run all the tests, but wrap each in a warning catcher -for t in ['test_rename', 'test_rename_axis', 'test_get_numeric_data', - 'test_get_default', 'test_nonzero', - 'test_numpy_1_7_compat_numeric_methods', - 'test_downcast', 'test_constructor_compound_dtypes', - 'test_head_tail', - 'test_size_compat', 'test_split_compat', - 'test_unexpected_keyword', - 'test_stat_unexpected_keyword', 'test_api_compat', - 'test_stat_non_defaults_args', - 'test_clip', 'test_truncate_out_of_bounds', 'test_numpy_clip', - 'test_metadata_propagation']: - - def f(): - def tester(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - return getattr(super(TestPanel4D, self), t)() - return tester - - setattr(TestPanel4D, t, f()) - - -class TestNDFrame(tm.TestCase): - # tests that don't fit elsewhere - - def test_sample(sel): - # Fixes issue: 2419 - # additional specific object based tests - - # A few dataframe test with degenerate weights. - easy_weight_list = [0] * 10 - easy_weight_list[5] = 1 - - df = pd.DataFrame({'col1': range(10, 20), - 'col2': range(20, 30), - 'colString': ['a'] * 10, - 'easyweights': easy_weight_list}) - sample1 = df.sample(n=1, weights='easyweights') - assert_frame_equal(sample1, df.iloc[5:6]) - - # Ensure proper error if string given as weight for Series, panel, or - # DataFrame with axis = 1. - s = Series(range(10)) - with tm.assertRaises(ValueError): - s.sample(n=3, weights='weight_column') - - panel = pd.Panel(items=[0, 1, 2], major_axis=[2, 3, 4], - minor_axis=[3, 4, 5]) - with tm.assertRaises(ValueError): - panel.sample(n=1, weights='weight_column') - - with tm.assertRaises(ValueError): - df.sample(n=1, weights='weight_column', axis=1) - - # Check weighting key error - with tm.assertRaises(KeyError): - df.sample(n=3, weights='not_a_real_column_name') - - # Check that re-normalizes weights that don't sum to one. - weights_less_than_1 = [0] * 10 - weights_less_than_1[0] = 0.5 - tm.assert_frame_equal( - df.sample(n=1, weights=weights_less_than_1), df.iloc[:1]) - - ### - # Test axis argument - ### - - # Test axis argument - df = pd.DataFrame({'col1': range(10), 'col2': ['a'] * 10}) - second_column_weight = [0, 1] - assert_frame_equal( - df.sample(n=1, axis=1, weights=second_column_weight), df[['col2']]) - - # Different axis arg types - assert_frame_equal(df.sample(n=1, axis='columns', - weights=second_column_weight), - df[['col2']]) - - weight = [0] * 10 - weight[5] = 0.5 - assert_frame_equal(df.sample(n=1, axis='rows', weights=weight), - df.iloc[5:6]) - assert_frame_equal(df.sample(n=1, axis='index', weights=weight), - df.iloc[5:6]) - - # Check out of range axis values - with tm.assertRaises(ValueError): - df.sample(n=1, axis=2) - - with tm.assertRaises(ValueError): - df.sample(n=1, axis='not_a_name') - - with tm.assertRaises(ValueError): - s = pd.Series(range(10)) - s.sample(n=1, axis=1) - - # Test weight length compared to correct axis - with tm.assertRaises(ValueError): - df.sample(n=1, axis=1, weights=[0.5] * 10) - - # Check weights with axis = 1 - easy_weight_list = [0] * 3 - easy_weight_list[2] = 1 - - df = pd.DataFrame({'col1': range(10, 20), - 'col2': range(20, 30), - 'colString': ['a'] * 10}) - sample1 = df.sample(n=1, axis=1, weights=easy_weight_list) - assert_frame_equal(sample1, df[['colString']]) - - # Test default axes - p = pd.Panel(items=['a', 'b', 'c'], major_axis=[2, 4, 6], - minor_axis=[1, 3, 5]) - assert_panel_equal( - p.sample(n=3, random_state=42), p.sample(n=3, axis=1, - random_state=42)) - assert_frame_equal( - df.sample(n=3, random_state=42), df.sample(n=3, axis=0, - random_state=42)) - - # Test that function aligns weights with frame - df = DataFrame( - {'col1': [5, 6, 7], - 'col2': ['a', 'b', 'c'], }, index=[9, 5, 3]) - s = Series([1, 0, 0], index=[3, 5, 9]) - assert_frame_equal(df.loc[[3]], df.sample(1, weights=s)) - - # Weights have index values to be dropped because not in - # sampled DataFrame - s2 = Series([0.001, 0, 10000], index=[3, 5, 10]) - assert_frame_equal(df.loc[[3]], df.sample(1, weights=s2)) - - # Weights have empty values to be filed with zeros - s3 = Series([0.01, 0], index=[3, 5]) - assert_frame_equal(df.loc[[3]], df.sample(1, weights=s3)) - - # No overlap in weight and sampled DataFrame indices - s4 = Series([1, 0], index=[1, 2]) - with tm.assertRaises(ValueError): - df.sample(1, weights=s4) - - def test_squeeze(self): - # noop - for s in [tm.makeFloatSeries(), tm.makeStringSeries(), - tm.makeObjectSeries()]: - tm.assert_series_equal(s.squeeze(), s) - for df in [tm.makeTimeDataFrame()]: - tm.assert_frame_equal(df.squeeze(), df) - for p in [tm.makePanel()]: - tm.assert_panel_equal(p.squeeze(), p) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - for p4d in [tm.makePanel4D()]: - tm.assert_panel4d_equal(p4d.squeeze(), p4d) - - # squeezing - df = tm.makeTimeDataFrame().reindex(columns=['A']) - tm.assert_series_equal(df.squeeze(), df['A']) - - p = tm.makePanel().reindex(items=['ItemA']) - tm.assert_frame_equal(p.squeeze(), p['ItemA']) - - p = tm.makePanel().reindex(items=['ItemA'], minor_axis=['A']) - tm.assert_series_equal(p.squeeze(), p.loc['ItemA', :, 'A']) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - p4d = tm.makePanel4D().reindex(labels=['label1']) - tm.assert_panel_equal(p4d.squeeze(), p4d['label1']) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - p4d = tm.makePanel4D().reindex(labels=['label1'], items=['ItemA']) - tm.assert_frame_equal(p4d.squeeze(), p4d.loc['label1', 'ItemA']) - - # don't fail with 0 length dimensions GH11229 & GH8999 - empty_series = pd.Series([], name='five') - empty_frame = pd.DataFrame([empty_series]) - empty_panel = pd.Panel({'six': empty_frame}) - - [tm.assert_series_equal(empty_series, higher_dim.squeeze()) - for higher_dim in [empty_series, empty_frame, empty_panel]] - - # axis argument - df = tm.makeTimeDataFrame(nper=1).iloc[:, :1] - tm.assert_equal(df.shape, (1, 1)) - tm.assert_series_equal(df.squeeze(axis=0), df.iloc[0]) - tm.assert_series_equal(df.squeeze(axis='index'), df.iloc[0]) - tm.assert_series_equal(df.squeeze(axis=1), df.iloc[:, 0]) - tm.assert_series_equal(df.squeeze(axis='columns'), df.iloc[:, 0]) - tm.assert_equal(df.squeeze(), df.iloc[0, 0]) - tm.assertRaises(ValueError, df.squeeze, axis=2) - tm.assertRaises(ValueError, df.squeeze, axis='x') - - df = tm.makeTimeDataFrame(3) - tm.assert_frame_equal(df.squeeze(axis=0), df) - - def test_numpy_squeeze(self): - s = tm.makeFloatSeries() - tm.assert_series_equal(np.squeeze(s), s) - - df = tm.makeTimeDataFrame().reindex(columns=['A']) - tm.assert_series_equal(np.squeeze(df), df['A']) - - def test_transpose(self): - msg = (r"transpose\(\) got multiple values for " - r"keyword argument 'axes'") - for s in [tm.makeFloatSeries(), tm.makeStringSeries(), - tm.makeObjectSeries()]: - # calls implementation in pandas/core/base.py - tm.assert_series_equal(s.transpose(), s) - for df in [tm.makeTimeDataFrame()]: - tm.assert_frame_equal(df.transpose().transpose(), df) - for p in [tm.makePanel()]: - tm.assert_panel_equal(p.transpose(2, 0, 1) - .transpose(1, 2, 0), p) - tm.assertRaisesRegexp(TypeError, msg, p.transpose, - 2, 0, 1, axes=(2, 0, 1)) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - for p4d in [tm.makePanel4D()]: - tm.assert_panel4d_equal(p4d.transpose(2, 0, 3, 1) - .transpose(1, 3, 0, 2), p4d) - tm.assertRaisesRegexp(TypeError, msg, p4d.transpose, - 2, 0, 3, 1, axes=(2, 0, 3, 1)) - - def test_numpy_transpose(self): - msg = "the 'axes' parameter is not supported" - - s = tm.makeFloatSeries() - tm.assert_series_equal( - np.transpose(s), s) - tm.assertRaisesRegexp(ValueError, msg, - np.transpose, s, axes=1) - - df = tm.makeTimeDataFrame() - tm.assert_frame_equal(np.transpose( - np.transpose(df)), df) - tm.assertRaisesRegexp(ValueError, msg, - np.transpose, df, axes=1) - - p = tm.makePanel() - tm.assert_panel_equal(np.transpose( - np.transpose(p, axes=(2, 0, 1)), - axes=(1, 2, 0)), p) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - p4d = tm.makePanel4D() - tm.assert_panel4d_equal(np.transpose( - np.transpose(p4d, axes=(2, 0, 3, 1)), - axes=(1, 3, 0, 2)), p4d) - - def test_take(self): - indices = [1, 5, -2, 6, 3, -1] - for s in [tm.makeFloatSeries(), tm.makeStringSeries(), - tm.makeObjectSeries()]: - out = s.take(indices) - expected = Series(data=s.values.take(indices), - index=s.index.take(indices)) - tm.assert_series_equal(out, expected) - for df in [tm.makeTimeDataFrame()]: - out = df.take(indices) - expected = DataFrame(data=df.values.take(indices, axis=0), - index=df.index.take(indices), - columns=df.columns) - tm.assert_frame_equal(out, expected) - - indices = [-3, 2, 0, 1] - for p in [tm.makePanel()]: - out = p.take(indices) - expected = Panel(data=p.values.take(indices, axis=0), - items=p.items.take(indices), - major_axis=p.major_axis, - minor_axis=p.minor_axis) - tm.assert_panel_equal(out, expected) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - for p4d in [tm.makePanel4D()]: - out = p4d.take(indices) - expected = Panel4D(data=p4d.values.take(indices, axis=0), - labels=p4d.labels.take(indices), - major_axis=p4d.major_axis, - minor_axis=p4d.minor_axis, - items=p4d.items) - tm.assert_panel4d_equal(out, expected) - - def test_take_invalid_kwargs(self): - indices = [-3, 2, 0, 1] - s = tm.makeFloatSeries() - df = tm.makeTimeDataFrame() - p = tm.makePanel() - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - p4d = tm.makePanel4D() - - for obj in (s, df, p, p4d): - msg = r"take\(\) got an unexpected keyword argument 'foo'" - tm.assertRaisesRegexp(TypeError, msg, obj.take, - indices, foo=2) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, obj.take, - indices, out=indices) - - msg = "the 'mode' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, obj.take, - indices, mode='clip') - - def test_equals(self): - s1 = pd.Series([1, 2, 3], index=[0, 2, 1]) - s2 = s1.copy() - self.assertTrue(s1.equals(s2)) - - s1[1] = 99 - self.assertFalse(s1.equals(s2)) - - # NaNs compare as equal - s1 = pd.Series([1, np.nan, 3, np.nan], index=[0, 2, 1, 3]) - s2 = s1.copy() - self.assertTrue(s1.equals(s2)) - - s2[0] = 9.9 - self.assertFalse(s1.equals(s2)) - - idx = MultiIndex.from_tuples([(0, 'a'), (1, 'b'), (2, 'c')]) - s1 = Series([1, 2, np.nan], index=idx) - s2 = s1.copy() - self.assertTrue(s1.equals(s2)) - - # Add object dtype column with nans - index = np.random.random(10) - df1 = DataFrame( - np.random.random(10, ), index=index, columns=['floats']) - df1['text'] = 'the sky is so blue. we could use more chocolate.'.split( - ) - df1['start'] = date_range('2000-1-1', periods=10, freq='T') - df1['end'] = date_range('2000-1-1', periods=10, freq='D') - df1['diff'] = df1['end'] - df1['start'] - df1['bool'] = (np.arange(10) % 3 == 0) - df1.loc[::2] = nan - df2 = df1.copy() - self.assertTrue(df1['text'].equals(df2['text'])) - self.assertTrue(df1['start'].equals(df2['start'])) - self.assertTrue(df1['end'].equals(df2['end'])) - self.assertTrue(df1['diff'].equals(df2['diff'])) - self.assertTrue(df1['bool'].equals(df2['bool'])) - self.assertTrue(df1.equals(df2)) - self.assertFalse(df1.equals(object)) - - # different dtype - different = df1.copy() - different['floats'] = different['floats'].astype('float32') - self.assertFalse(df1.equals(different)) - - # different index - different_index = -index - different = df2.set_index(different_index) - self.assertFalse(df1.equals(different)) - - # different columns - different = df2.copy() - different.columns = df2.columns[::-1] - self.assertFalse(df1.equals(different)) - - # DatetimeIndex - index = pd.date_range('2000-1-1', periods=10, freq='T') - df1 = df1.set_index(index) - df2 = df1.copy() - self.assertTrue(df1.equals(df2)) - - # MultiIndex - df3 = df1.set_index(['text'], append=True) - df2 = df1.set_index(['text'], append=True) - self.assertTrue(df3.equals(df2)) - - df2 = df1.set_index(['floats'], append=True) - self.assertFalse(df3.equals(df2)) - - # NaN in index - df3 = df1.set_index(['floats'], append=True) - df2 = df1.set_index(['floats'], append=True) - self.assertTrue(df3.equals(df2)) - - # GH 8437 - a = pd.Series([False, np.nan]) - b = pd.Series([False, np.nan]) - c = pd.Series(index=range(2)) - d = pd.Series(index=range(2)) - e = pd.Series(index=range(2)) - f = pd.Series(index=range(2)) - c[:-1] = d[:-1] = e[0] = f[0] = False - self.assertTrue(a.equals(a)) - self.assertTrue(a.equals(b)) - self.assertTrue(a.equals(c)) - self.assertTrue(a.equals(d)) - self.assertFalse(a.equals(e)) - self.assertTrue(e.equals(f)) - - def test_describe_raises(self): - with tm.assertRaises(NotImplementedError): - tm.makePanel().describe() - - def test_pipe(self): - df = DataFrame({'A': [1, 2, 3]}) - f = lambda x, y: x ** y - result = df.pipe(f, 2) - expected = DataFrame({'A': [1, 4, 9]}) - self.assert_frame_equal(result, expected) - - result = df.A.pipe(f, 2) - self.assert_series_equal(result, expected.A) - - def test_pipe_tuple(self): - df = DataFrame({'A': [1, 2, 3]}) - f = lambda x, y: y - result = df.pipe((f, 'y'), 0) - self.assert_frame_equal(result, df) - - result = df.A.pipe((f, 'y'), 0) - self.assert_series_equal(result, df.A) - - def test_pipe_tuple_error(self): - df = DataFrame({"A": [1, 2, 3]}) - f = lambda x, y: y - with tm.assertRaises(ValueError): - df.pipe((f, 'y'), x=1, y=0) - - with tm.assertRaises(ValueError): - df.A.pipe((f, 'y'), x=1, y=0) - - def test_pipe_panel(self): - wp = Panel({'r1': DataFrame({"A": [1, 2, 3]})}) - f = lambda x, y: x + y - result = wp.pipe(f, 2) - expected = wp + 2 - assert_panel_equal(result, expected) - - result = wp.pipe((f, 'y'), x=1) - expected = wp + 1 - assert_panel_equal(result, expected) - - with tm.assertRaises(ValueError): - result = wp.pipe((f, 'y'), x=1, y=1) diff --git a/pandas/tests/test_join.py b/pandas/tests/test_join.py index 6723494d1529b..5b6656de15731 100644 --- a/pandas/tests/test_join.py +++ b/pandas/tests/test_join.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- import numpy as np -from pandas import Index from pandas._libs import join as _join + +from pandas import Categorical, DataFrame, Index, merge import pandas.util.testing as tm -from pandas.util.testing import assert_almost_equal +from pandas.util.testing import assert_almost_equal, assert_frame_equal -class TestIndexer(tm.TestCase): +class TestIndexer(object): def test_outer_join_indexer(self): typemap = [('int32', _join.outer_join_indexer_int32), @@ -23,9 +24,9 @@ def test_outer_join_indexer(self): empty = np.array([], dtype=dtype) result, lindexer, rindexer = indexer(left, right) - tm.assertIsInstance(result, np.ndarray) - tm.assertIsInstance(lindexer, np.ndarray) - tm.assertIsInstance(rindexer, np.ndarray) + assert isinstance(result, np.ndarray) + assert isinstance(lindexer, np.ndarray) + assert isinstance(rindexer, np.ndarray) tm.assert_numpy_array_equal(result, np.arange(5, dtype=dtype)) exp = np.array([0, 1, 2, -1, -1], dtype=np.int64) tm.assert_numpy_array_equal(lindexer, exp) @@ -53,7 +54,7 @@ def test_left_join_indexer_unique(): result = _join.left_join_indexer_unique_int64(b, a) expected = np.array([1, 1, 2, 3, 3], dtype=np.int64) - assert (np.array_equal(result, expected)) + tm.assert_numpy_array_equal(result, expected) def test_left_outer_join_bug(): @@ -69,13 +70,14 @@ def test_left_outer_join_bug(): lidx, ridx = _join.left_outer_join(left, right, max_groups, sort=False) - exp_lidx = np.arange(len(left)) - exp_ridx = -np.ones(len(left)) + exp_lidx = np.arange(len(left), dtype=np.int64) + exp_ridx = -np.ones(len(left), dtype=np.int64) + exp_ridx[left == 1] = 1 exp_ridx[left == 3] = 0 - assert (np.array_equal(lidx, exp_lidx)) - assert (np.array_equal(ridx, exp_ridx)) + tm.assert_numpy_array_equal(lidx, exp_lidx) + tm.assert_numpy_array_equal(ridx, exp_ridx) def test_inner_join_indexer(): @@ -192,3 +194,43 @@ def test_inner_join_indexer2(): exp_ridx = np.array([0, 1, 2, 3], dtype=np.int64) assert_almost_equal(ridx, exp_ridx) + + +def test_merge_join_categorical_multiindex(): + # From issue 16627 + a = {'Cat1': Categorical(['a', 'b', 'a', 'c', 'a', 'b'], + ['a', 'b', 'c']), + 'Int1': [0, 1, 0, 1, 0, 0]} + a = DataFrame(a) + + b = {'Cat': Categorical(['a', 'b', 'c', 'a', 'b', 'c'], + ['a', 'b', 'c']), + 'Int': [0, 0, 0, 1, 1, 1], + 'Factor': [1.1, 1.2, 1.3, 1.4, 1.5, 1.6]} + b = DataFrame(b).set_index(['Cat', 'Int'])['Factor'] + + expected = merge(a, b.reset_index(), left_on=['Cat1', 'Int1'], + right_on=['Cat', 'Int'], how='left') + result = a.join(b, on=['Cat1', 'Int1']) + expected = expected.drop(['Cat', 'Int'], axis=1) + assert_frame_equal(expected, result) + + # Same test, but with ordered categorical + a = {'Cat1': Categorical(['a', 'b', 'a', 'c', 'a', 'b'], + ['b', 'a', 'c'], + ordered=True), + 'Int1': [0, 1, 0, 1, 0, 0]} + a = DataFrame(a) + + b = {'Cat': Categorical(['a', 'b', 'c', 'a', 'b', 'c'], + ['b', 'a', 'c'], + ordered=True), + 'Int': [0, 0, 0, 1, 1, 1], + 'Factor': [1.1, 1.2, 1.3, 1.4, 1.5, 1.6]} + b = DataFrame(b).set_index(['Cat', 'Int'])['Factor'] + + expected = merge(a, b.reset_index(), left_on=['Cat1', 'Int1'], + right_on=['Cat', 'Int'], how='left') + result = a.join(b, on=['Cat1', 'Int1']) + expected = expected.drop(['Cat', 'Int'], axis=1) + assert_frame_equal(expected, result) diff --git a/pandas/tests/test_lib.py b/pandas/tests/test_lib.py index a925cf13900e9..c5dcfc89faa67 100644 --- a/pandas/tests/test_lib.py +++ b/pandas/tests/test_lib.py @@ -1,29 +1,32 @@ # -*- coding: utf-8 -*- import numpy as np -import pandas as pd -import pandas._libs.lib as lib +import pytest + +from pandas._libs import lib, writers as libwriters + +from pandas import Index import pandas.util.testing as tm -class TestMisc(tm.TestCase): +class TestMisc(object): def test_max_len_string_array(self): arr = a = np.array(['foo', 'b', np.nan], dtype='object') - self.assertTrue(lib.max_len_string_array(arr), 3) + assert libwriters.max_len_string_array(arr) == 3 # unicode arr = a.astype('U').astype(object) - self.assertTrue(lib.max_len_string_array(arr), 3) + assert libwriters.max_len_string_array(arr) == 3 # bytes for python3 arr = a.astype('S').astype(object) - self.assertTrue(lib.max_len_string_array(arr), 3) + assert libwriters.max_len_string_array(arr) == 3 # raises - tm.assertRaises(TypeError, - lambda: lib.max_len_string_array(arr.astype('U'))) + with pytest.raises(TypeError): + libwriters.max_len_string_array(arr.astype('U')) def test_fast_unique_multiple_list_gen_sort(self): keys = [['p', 'a'], ['n', 'd'], ['a', 's']] @@ -39,7 +42,7 @@ def test_fast_unique_multiple_list_gen_sort(self): tm.assert_numpy_array_equal(np.array(out), expected) -class TestIndexing(tm.TestCase): +class TestIndexing(object): def test_maybe_indices_to_slice_left_edge(self): target = np.arange(100) @@ -47,32 +50,36 @@ def test_maybe_indices_to_slice_left_edge(self): # slice indices = np.array([], dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) for end in [1, 2, 5, 20, 99]: for step in [1, 2, 4]: indices = np.arange(0, end, step, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # reverse indices = indices[::-1] maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # not slice for case in [[2, 1, 2, 0], [2, 2, 1, 0], [0, 1, 2, 1], [-2, 0, 2], [2, 0, -2]]: indices = np.array(case, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) def test_maybe_indices_to_slice_right_edge(self): target = np.arange(100) @@ -82,42 +89,49 @@ def test_maybe_indices_to_slice_right_edge(self): for step in [1, 2, 4]: indices = np.arange(start, 99, step, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # reverse indices = indices[::-1] maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # not slice indices = np.array([97, 98, 99, 100], dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - with self.assertRaises(IndexError): + + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + + with pytest.raises(IndexError): target[indices] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): target[maybe_slice] indices = np.array([100, 99, 98, 97], dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - with self.assertRaises(IndexError): + + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + + with pytest.raises(IndexError): target[indices] - with self.assertRaises(IndexError): + with pytest.raises(IndexError): target[maybe_slice] for case in [[99, 97, 99, 96], [99, 99, 98, 97], [98, 98, 97, 96]]: indices = np.array(case, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) def test_maybe_indices_to_slice_both_edges(self): target = np.arange(10) @@ -126,22 +140,22 @@ def test_maybe_indices_to_slice_both_edges(self): for step in [1, 2, 4, 5, 8, 9]: indices = np.arange(0, 9, step, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) # reverse indices = indices[::-1] maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) # not slice for case in [[4, 2, 0, -2], [2, 2, 1, 0], [0, 1, 2, 1]]: indices = np.array(case, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) def test_maybe_indices_to_slice_middle(self): target = np.arange(100) @@ -151,84 +165,43 @@ def test_maybe_indices_to_slice_middle(self): for step in [1, 2, 4, 20]: indices = np.arange(start, end, step, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # reverse indices = indices[::-1] maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertTrue(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(target[indices], - target[maybe_slice]) + + assert isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(target[indices], + target[maybe_slice]) # not slice for case in [[14, 12, 10, 12], [12, 12, 11, 10], [10, 11, 12, 11]]: indices = np.array(case, dtype=np.int64) maybe_slice = lib.maybe_indices_to_slice(indices, len(target)) - self.assertFalse(isinstance(maybe_slice, slice)) - self.assert_numpy_array_equal(maybe_slice, indices) - self.assert_numpy_array_equal(target[indices], target[maybe_slice]) + + assert not isinstance(maybe_slice, slice) + tm.assert_numpy_array_equal(maybe_slice, indices) + tm.assert_numpy_array_equal(target[indices], target[maybe_slice]) def test_maybe_booleans_to_slice(self): arr = np.array([0, 0, 1, 1, 1, 0, 1], dtype=np.uint8) result = lib.maybe_booleans_to_slice(arr) - self.assertTrue(result.dtype == np.bool_) + assert result.dtype == np.bool_ result = lib.maybe_booleans_to_slice(arr[:0]) - self.assertTrue(result == slice(0, 0)) + assert result == slice(0, 0) def test_get_reverse_indexer(self): indexer = np.array([-1, -1, 1, 2, 0, -1, 3, 4], dtype=np.int64) result = lib.get_reverse_indexer(indexer, 5) expected = np.array([4, 2, 3, 6, 7], dtype=np.int64) - self.assertTrue(np.array_equal(result, expected)) - - -class TestNullObj(tm.TestCase): - - _1d_methods = ['isnullobj', 'isnullobj_old'] - _2d_methods = ['isnullobj2d', 'isnullobj2d_old'] - - def _check_behavior(self, arr, expected): - for method in TestNullObj._1d_methods: - result = getattr(lib, method)(arr) - tm.assert_numpy_array_equal(result, expected) - - arr = np.atleast_2d(arr) - expected = np.atleast_2d(expected) - - for method in TestNullObj._2d_methods: - result = getattr(lib, method)(arr) - tm.assert_numpy_array_equal(result, expected) - - def test_basic(self): - arr = np.array([1, None, 'foo', -5.1, pd.NaT, np.nan]) - expected = np.array([False, True, False, False, True, True]) - - self._check_behavior(arr, expected) - - def test_non_obj_dtype(self): - arr = np.array([1, 3, np.nan, 5], dtype=float) - expected = np.array([False, False, True, False]) - - self._check_behavior(arr, expected) - - def test_empty_arr(self): - arr = np.array([]) - expected = np.array([], dtype=bool) - - self._check_behavior(arr, expected) - - def test_empty_str_inp(self): - arr = np.array([""]) # empty but not null - expected = np.array([False]) - - self._check_behavior(arr, expected) + tm.assert_numpy_array_equal(result, expected) - def test_empty_like(self): - # see gh-13717: no segfaults! - arr = np.empty_like([None]) - expected = np.array([True]) - self._check_behavior(arr, expected) +def test_cache_readonly_preserve_docstrings(): + # GH18197 + assert Index.hasnans.__doc__ is not None diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py old mode 100755 new mode 100644 index 5584c1ac6a239..a9a59c6d95373 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -1,39 +1,42 @@ # -*- coding: utf-8 -*- # pylint: disable-msg=W0612,E1101,W0141 -from warnings import catch_warnings import datetime import itertools -import pytest +from warnings import catch_warnings, simplefilter -from numpy.random import randn import numpy as np +from numpy.random import randn +import pytest +import pytz -from pandas.core.index import Index, MultiIndex -from pandas import Panel, DataFrame, Series, notnull, isnull, Timestamp +from pandas.compat import ( + StringIO, lrange, lzip, product as cart_product, range, u, zip) + +from pandas.core.dtypes.common import is_float_dtype, is_integer_dtype -from pandas.types.common import is_float_dtype, is_integer_dtype -import pandas.core.common as com -import pandas.util.testing as tm -from pandas.compat import (range, lrange, StringIO, lzip, u, product as - cart_product, zip) import pandas as pd -import pandas._libs.index as _index +from pandas import DataFrame, Series, Timestamp, isna +from pandas.core.index import Index, MultiIndex +import pandas.util.testing as tm + +AGG_FUNCTIONS = ['sum', 'prod', 'min', 'max', 'median', 'mean', 'skew', 'mad', + 'std', 'var', 'sem'] class Base(object): - def setUp(self): + def setup_method(self, method): index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], + codes=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], + [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], names=['first', 'second']) self.frame = DataFrame(np.random.randn(10, 3), index=index, columns=Index(['A', 'B', 'C'], name='exp')) self.single_level = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux']], - labels=[[0, 1, 2, 3]], names=['first']) + codes=[[0, 1, 2, 3]], names=['first']) # create test series object arrays = [['bar', 'bar', 'baz', 'baz', 'qux', 'qux', 'foo', 'foo'], @@ -44,8 +47,7 @@ def setUp(self): s[3] = np.NaN self.series = s - tm.N = 100 - self.tdf = tm.makeTimeDataFrame() + self.tdf = tm.makeTimeDataFrame(100) self.ymd = self.tdf.groupby([lambda x: x.year, lambda x: x.month, lambda x: x.day]).sum() @@ -56,7 +58,7 @@ def setUp(self): self.ymd.index.set_names(['year', 'month', 'day'], inplace=True) -class TestMultiLevel(Base, tm.TestCase): +class TestMultiLevel(Base): def test_append(self): a, b = self.frame[:5], self.frame[5:] @@ -68,8 +70,6 @@ def test_append(self): tm.assert_series_equal(result, self.frame['A']) def test_append_index(self): - tm._skip_if_no_pytz() - idx1 = Index([1.1, 1.2, 1.3]) idx2 = pd.date_range('2011-01-01', freq='D', periods=3, tz='Asia/Tokyo') @@ -80,8 +80,7 @@ def test_append_index(self): result = idx1.append(midx_lv2) - # GH 7112 - import pytz + # see gh-7112 tz = pytz.timezone('Asia/Tokyo') expected_tuples = [(1.1, tz.localize(datetime.datetime(2011, 1, 1))), (1.2, tz.localize(datetime.datetime(2011, 1, 2))), @@ -113,25 +112,25 @@ def test_dataframe_constructor(self): multi = DataFrame(np.random.randn(4, 4), index=[np.array(['a', 'a', 'b', 'b']), np.array(['x', 'y', 'x', 'y'])]) - tm.assertIsInstance(multi.index, MultiIndex) - self.assertNotIsInstance(multi.columns, MultiIndex) + assert isinstance(multi.index, MultiIndex) + assert not isinstance(multi.columns, MultiIndex) multi = DataFrame(np.random.randn(4, 4), columns=[['a', 'a', 'b', 'b'], ['x', 'y', 'x', 'y']]) - tm.assertIsInstance(multi.columns, MultiIndex) + assert isinstance(multi.columns, MultiIndex) def test_series_constructor(self): multi = Series(1., index=[np.array(['a', 'a', 'b', 'b']), np.array( ['x', 'y', 'x', 'y'])]) - tm.assertIsInstance(multi.index, MultiIndex) + assert isinstance(multi.index, MultiIndex) multi = Series(1., index=[['a', 'a', 'b', 'b'], ['x', 'y', 'x', 'y']]) - tm.assertIsInstance(multi.index, MultiIndex) + assert isinstance(multi.index, MultiIndex) multi = Series(lrange(4), index=[['a', 'a', 'b', 'b'], ['x', 'y', 'x', 'y']]) - tm.assertIsInstance(multi.index, MultiIndex) + assert isinstance(multi.index, MultiIndex) def test_reindex_level(self): # axis=0 @@ -178,7 +177,7 @@ def _check_op(opname): def test_pickle(self): def _test_roundtrip(frame): - unpickled = self.round_trip_pickle(frame) + unpickled = tm.round_trip_pickle(frame) tm.assert_frame_equal(frame, unpickled) _test_roundtrip(self.frame) @@ -192,27 +191,29 @@ def test_reindex(self): tm.assert_frame_equal(reindexed, expected) with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) reindexed = self.frame.ix[[('foo', 'one'), ('bar', 'one')]] tm.assert_frame_equal(reindexed, expected) def test_reindex_preserve_levels(self): new_index = self.ymd.index[::10] chunk = self.ymd.reindex(new_index) - self.assertIs(chunk.index, new_index) + assert chunk.index is new_index chunk = self.ymd.loc[new_index] - self.assertIs(chunk.index, new_index) + assert chunk.index is new_index with catch_warnings(record=True): + simplefilter("ignore", DeprecationWarning) chunk = self.ymd.ix[new_index] - self.assertIs(chunk.index, new_index) + assert chunk.index is new_index ymdT = self.ymd.T chunk = ymdT.reindex(columns=new_index) - self.assertIs(chunk.columns, new_index) + assert chunk.columns is new_index chunk = ymdT.loc[:, new_index] - self.assertIs(chunk.columns, new_index) + assert chunk.columns is new_index def test_repr_to_string(self): repr(self.frame) @@ -233,483 +234,7 @@ def test_repr_name_coincide(self): df = DataFrame({'value': [0, 1]}, index=index) lines = repr(df).split('\n') - self.assertTrue(lines[2].startswith('a 0 foo')) - - def test_getitem_simple(self): - df = self.frame.T - - col = df['foo', 'one'] - tm.assert_almost_equal(col.values, df.values[:, 0]) - with pytest.raises(KeyError): - df[('foo', 'four')] - with pytest.raises(KeyError): - df['foobar'] - - def test_series_getitem(self): - s = self.ymd['A'] - - result = s[2000, 3] - - # TODO(wesm): unused? - # result2 = s.loc[2000, 3] - - expected = s.reindex(s.index[42:65]) - expected.index = expected.index.droplevel(0).droplevel(0) - tm.assert_series_equal(result, expected) - - result = s[2000, 3, 10] - expected = s[49] - self.assertEqual(result, expected) - - # fancy - expected = s.reindex(s.index[49:51]) - result = s.loc[[(2000, 3, 10), (2000, 3, 13)]] - tm.assert_series_equal(result, expected) - - with catch_warnings(record=True): - result = s.ix[[(2000, 3, 10), (2000, 3, 13)]] - tm.assert_series_equal(result, expected) - - # key error - self.assertRaises(KeyError, s.__getitem__, (2000, 3, 4)) - - def test_series_getitem_corner(self): - s = self.ymd['A'] - - # don't segfault, GH #495 - # out of bounds access - self.assertRaises(IndexError, s.__getitem__, len(self.ymd)) - - # generator - result = s[(x > 0 for x in s)] - expected = s[s > 0] - tm.assert_series_equal(result, expected) - - def test_series_setitem(self): - s = self.ymd['A'] - - s[2000, 3] = np.nan - self.assertTrue(isnull(s.values[42:65]).all()) - self.assertTrue(notnull(s.values[:42]).all()) - self.assertTrue(notnull(s.values[65:]).all()) - - s[2000, 3, 10] = np.nan - self.assertTrue(isnull(s[49])) - - def test_series_slice_partial(self): - pass - - def test_frame_getitem_setitem_boolean(self): - df = self.frame.T.copy() - values = df.values - - result = df[df > 0] - expected = df.where(df > 0) - tm.assert_frame_equal(result, expected) - - df[df > 0] = 5 - values[values > 0] = 5 - tm.assert_almost_equal(df.values, values) - - df[df == 5] = 0 - values[values == 5] = 0 - tm.assert_almost_equal(df.values, values) - - # a df that needs alignment first - df[df[:-1] < 0] = 2 - np.putmask(values[:-1], values[:-1] < 0, 2) - tm.assert_almost_equal(df.values, values) - - with tm.assertRaisesRegexp(TypeError, 'boolean values only'): - df[df * 0] = 2 - - def test_frame_getitem_setitem_slice(self): - # getitem - result = self.frame.iloc[:4] - expected = self.frame[:4] - tm.assert_frame_equal(result, expected) - - # setitem - cp = self.frame.copy() - cp.iloc[:4] = 0 - - self.assertTrue((cp.values[:4] == 0).all()) - self.assertTrue((cp.values[4:] != 0).all()) - - def test_frame_getitem_setitem_multislice(self): - levels = [['t1', 't2'], ['a', 'b', 'c']] - labels = [[0, 0, 0, 1, 1], [0, 1, 2, 0, 1]] - midx = MultiIndex(labels=labels, levels=levels, names=[None, 'id']) - df = DataFrame({'value': [1, 2, 3, 7, 8]}, index=midx) - - result = df.loc[:, 'value'] - tm.assert_series_equal(df['value'], result) - - with catch_warnings(record=True): - result = df.ix[:, 'value'] - tm.assert_series_equal(df['value'], result) - - result = df.loc[df.index[1:3], 'value'] - tm.assert_series_equal(df['value'][1:3], result) - - result = df.loc[:, :] - tm.assert_frame_equal(df, result) - - result = df - df.loc[:, 'value'] = 10 - result['value'] = 10 - tm.assert_frame_equal(df, result) - - df.loc[:, :] = 10 - tm.assert_frame_equal(df, result) - - def test_frame_getitem_multicolumn_empty_level(self): - f = DataFrame({'a': ['1', '2', '3'], 'b': ['2', '3', '4']}) - f.columns = [['level1 item1', 'level1 item2'], ['', 'level2 item2'], - ['level3 item1', 'level3 item2']] - - result = f['level1 item1'] - expected = DataFrame([['1'], ['2'], ['3']], index=f.index, - columns=['level3 item1']) - tm.assert_frame_equal(result, expected) - - def test_frame_setitem_multi_column(self): - df = DataFrame(randn(10, 4), columns=[['a', 'a', 'b', 'b'], - [0, 1, 0, 1]]) - - cp = df.copy() - cp['a'] = cp['b'] - tm.assert_frame_equal(cp['a'], cp['b']) - - # set with ndarray - cp = df.copy() - cp['a'] = cp['b'].values - tm.assert_frame_equal(cp['a'], cp['b']) - - # --------------------------------------- - # #1803 - columns = MultiIndex.from_tuples([('A', '1'), ('A', '2'), ('B', '1')]) - df = DataFrame(index=[1, 3, 5], columns=columns) - - # Works, but adds a column instead of updating the two existing ones - df['A'] = 0.0 # Doesn't work - self.assertTrue((df['A'].values == 0).all()) - - # it broadcasts - df['B', '1'] = [1, 2, 3] - df['A'] = df['B', '1'] - - sliced_a1 = df['A', '1'] - sliced_a2 = df['A', '2'] - sliced_b1 = df['B', '1'] - tm.assert_series_equal(sliced_a1, sliced_b1, check_names=False) - tm.assert_series_equal(sliced_a2, sliced_b1, check_names=False) - self.assertEqual(sliced_a1.name, ('A', '1')) - self.assertEqual(sliced_a2.name, ('A', '2')) - self.assertEqual(sliced_b1.name, ('B', '1')) - - def test_getitem_tuple_plus_slice(self): - # GH #671 - df = DataFrame({'a': lrange(10), - 'b': lrange(10), - 'c': np.random.randn(10), - 'd': np.random.randn(10)}) - - idf = df.set_index(['a', 'b']) - - result = idf.loc[(0, 0), :] - expected = idf.loc[0, 0] - expected2 = idf.xs((0, 0)) - with catch_warnings(record=True): - expected3 = idf.ix[0, 0] - - tm.assert_series_equal(result, expected) - tm.assert_series_equal(result, expected2) - tm.assert_series_equal(result, expected3) - - def test_getitem_setitem_tuple_plus_columns(self): - # GH #1013 - - df = self.ymd[:5] - - result = df.loc[(2000, 1, 6), ['A', 'B', 'C']] - expected = df.loc[2000, 1, 6][['A', 'B', 'C']] - tm.assert_series_equal(result, expected) - - def test_xs(self): - xs = self.frame.xs(('bar', 'two')) - xs2 = self.frame.loc[('bar', 'two')] - - tm.assert_series_equal(xs, xs2) - tm.assert_almost_equal(xs.values, self.frame.values[4]) - - # GH 6574 - # missing values in returned index should be preserrved - acc = [ - ('a', 'abcde', 1), - ('b', 'bbcde', 2), - ('y', 'yzcde', 25), - ('z', 'xbcde', 24), - ('z', None, 26), - ('z', 'zbcde', 25), - ('z', 'ybcde', 26), - ] - df = DataFrame(acc, - columns=['a1', 'a2', 'cnt']).set_index(['a1', 'a2']) - expected = DataFrame({'cnt': [24, 26, 25, 26]}, index=Index( - ['xbcde', np.nan, 'zbcde', 'ybcde'], name='a2')) - - result = df.xs('z', level='a1') - tm.assert_frame_equal(result, expected) - - def test_xs_partial(self): - result = self.frame.xs('foo') - result2 = self.frame.loc['foo'] - expected = self.frame.T['foo'].T - tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result, result2) - - result = self.ymd.xs((2000, 4)) - expected = self.ymd.loc[2000, 4] - tm.assert_frame_equal(result, expected) - - # ex from #1796 - index = MultiIndex(levels=[['foo', 'bar'], ['one', 'two'], [-1, 1]], - labels=[[0, 0, 0, 0, 1, 1, 1, 1], - [0, 0, 1, 1, 0, 0, 1, 1], [0, 1, 0, 1, 0, 1, - 0, 1]]) - df = DataFrame(np.random.randn(8, 4), index=index, - columns=list('abcd')) - - result = df.xs(['foo', 'one']) - expected = df.loc['foo', 'one'] - tm.assert_frame_equal(result, expected) - - def test_xs_level(self): - result = self.frame.xs('two', level='second') - expected = self.frame[self.frame.index.get_level_values(1) == 'two'] - expected.index = expected.index.droplevel(1) - - tm.assert_frame_equal(result, expected) - - index = MultiIndex.from_tuples([('x', 'y', 'z'), ('a', 'b', 'c'), ( - 'p', 'q', 'r')]) - df = DataFrame(np.random.randn(3, 5), index=index) - result = df.xs('c', level=2) - expected = df[1:2] - expected.index = expected.index.droplevel(2) - tm.assert_frame_equal(result, expected) - - # this is a copy in 0.14 - result = self.frame.xs('two', level='second') - - # setting this will give a SettingWithCopyError - # as we are trying to write a view - def f(x): - x[:] = 10 - - self.assertRaises(com.SettingWithCopyError, f, result) - - def test_xs_level_multiple(self): - from pandas import read_table - text = """ A B C D E -one two three four -a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 -a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 -x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" - - df = read_table(StringIO(text), sep=r'\s+', engine='python') - - result = df.xs(('a', 4), level=['one', 'four']) - expected = df.xs('a').xs(4, level='four') - tm.assert_frame_equal(result, expected) - - # this is a copy in 0.14 - result = df.xs(('a', 4), level=['one', 'four']) - - # setting this will give a SettingWithCopyError - # as we are trying to write a view - def f(x): - x[:] = 10 - - self.assertRaises(com.SettingWithCopyError, f, result) - - # GH2107 - dates = lrange(20111201, 20111205) - ids = 'abcde' - idx = MultiIndex.from_tuples([x for x in cart_product(dates, ids)]) - idx.names = ['date', 'secid'] - df = DataFrame(np.random.randn(len(idx), 3), idx, ['X', 'Y', 'Z']) - - rs = df.xs(20111201, level='date') - xp = df.loc[20111201, :] - tm.assert_frame_equal(rs, xp) - - def test_xs_level0(self): - from pandas import read_table - text = """ A B C D E -one two three four -a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 -a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 -x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" - - df = read_table(StringIO(text), sep=r'\s+', engine='python') - - result = df.xs('a', level=0) - expected = df.xs('a') - self.assertEqual(len(result), 2) - tm.assert_frame_equal(result, expected) - - def test_xs_level_series(self): - s = self.frame['A'] - result = s[:, 'two'] - expected = self.frame.xs('two', level=1)['A'] - tm.assert_series_equal(result, expected) - - s = self.ymd['A'] - result = s[2000, 5] - expected = self.ymd.loc[2000, 5]['A'] - tm.assert_series_equal(result, expected) - - # not implementing this for now - - self.assertRaises(TypeError, s.__getitem__, (2000, slice(3, 4))) - - # result = s[2000, 3:4] - # lv =s.index.get_level_values(1) - # expected = s[(lv == 3) | (lv == 4)] - # expected.index = expected.index.droplevel(0) - # tm.assert_series_equal(result, expected) - - # can do this though - - def test_get_loc_single_level(self): - s = Series(np.random.randn(len(self.single_level)), - index=self.single_level) - for k in self.single_level.values: - s[k] - - def test_getitem_toplevel(self): - df = self.frame.T - - result = df['foo'] - expected = df.reindex(columns=df.columns[:3]) - expected.columns = expected.columns.droplevel(0) - tm.assert_frame_equal(result, expected) - - result = df['bar'] - result2 = df.loc[:, 'bar'] - - expected = df.reindex(columns=df.columns[3:5]) - expected.columns = expected.columns.droplevel(0) - tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result, result2) - - def test_getitem_setitem_slice_integers(self): - index = MultiIndex(levels=[[0, 1, 2], [0, 2]], - labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]) - - frame = DataFrame(np.random.randn(len(index), 4), index=index, - columns=['a', 'b', 'c', 'd']) - res = frame.loc[1:2] - exp = frame.reindex(frame.index[2:]) - tm.assert_frame_equal(res, exp) - - frame.loc[1:2] = 7 - self.assertTrue((frame.loc[1:2] == 7).values.all()) - - series = Series(np.random.randn(len(index)), index=index) - - res = series.loc[1:2] - exp = series.reindex(series.index[2:]) - tm.assert_series_equal(res, exp) - - series.loc[1:2] = 7 - self.assertTrue((series.loc[1:2] == 7).values.all()) - - def test_getitem_int(self): - levels = [[0, 1], [0, 1, 2]] - labels = [[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]] - index = MultiIndex(levels=levels, labels=labels) - - frame = DataFrame(np.random.randn(6, 2), index=index) - - result = frame.loc[1] - expected = frame[-3:] - expected.index = expected.index.droplevel(0) - tm.assert_frame_equal(result, expected) - - # raises exception - self.assertRaises(KeyError, frame.loc.__getitem__, 3) - - # however this will work - result = self.frame.iloc[2] - expected = self.frame.xs(self.frame.index[2]) - tm.assert_series_equal(result, expected) - - def test_getitem_partial(self): - ymd = self.ymd.T - result = ymd[2000, 2] - - expected = ymd.reindex(columns=ymd.columns[ymd.columns.labels[1] == 1]) - expected.columns = expected.columns.droplevel(0).droplevel(0) - tm.assert_frame_equal(result, expected) - - def test_setitem_change_dtype(self): - dft = self.frame.T - s = dft['foo', 'two'] - dft['foo', 'two'] = s > s.median() - tm.assert_series_equal(dft['foo', 'two'], s > s.median()) - # tm.assertIsInstance(dft._data.blocks[1].items, MultiIndex) - - reindexed = dft.reindex(columns=[('foo', 'two')]) - tm.assert_series_equal(reindexed['foo', 'two'], s > s.median()) - - def test_frame_setitem_ix(self): - self.frame.loc[('bar', 'two'), 'B'] = 5 - self.assertEqual(self.frame.loc[('bar', 'two'), 'B'], 5) - - # with integer labels - df = self.frame.copy() - df.columns = lrange(3) - df.loc[('bar', 'two'), 1] = 7 - self.assertEqual(df.loc[('bar', 'two'), 1], 7) - - with catch_warnings(record=True): - df = self.frame.copy() - df.columns = lrange(3) - df.ix[('bar', 'two'), 1] = 7 - self.assertEqual(df.loc[('bar', 'two'), 1], 7) - - def test_fancy_slice_partial(self): - result = self.frame.loc['bar':'baz'] - expected = self.frame[3:7] - tm.assert_frame_equal(result, expected) - - result = self.ymd.loc[(2000, 2):(2000, 4)] - lev = self.ymd.index.labels[1] - expected = self.ymd[(lev >= 1) & (lev <= 3)] - tm.assert_frame_equal(result, expected) - - def test_getitem_partial_column_select(self): - idx = MultiIndex(labels=[[0, 0, 0], [0, 1, 1], [1, 0, 1]], - levels=[['a', 'b'], ['x', 'y'], ['p', 'q']]) - df = DataFrame(np.random.rand(3, 2), index=idx) - - result = df.loc[('a', 'y'), :] - expected = df.loc[('a', 'y')] - tm.assert_frame_equal(result, expected) - - result = df.loc[('a', 'y'), [1, 0]] - expected = df.loc[('a', 'y')][[1, 0]] - tm.assert_frame_equal(result, expected) - - with catch_warnings(record=True): - result = df.ix[('a', 'y'), [1, 0]] - tm.assert_frame_equal(result, expected) - - self.assertRaises(KeyError, df.loc.__getitem__, - (('a', 'foo'), slice(None, None))) + assert lines[2].startswith('a 0 foo') def test_delevel_infer_dtype(self): tuples = [tuple @@ -719,20 +244,22 @@ def test_delevel_infer_dtype(self): df = DataFrame(np.random.randn(8, 3), columns=['A', 'B', 'C'], index=index) deleveled = df.reset_index() - self.assertTrue(is_integer_dtype(deleveled['prm1'])) - self.assertTrue(is_float_dtype(deleveled['prm2'])) + assert is_integer_dtype(deleveled['prm1']) + assert is_float_dtype(deleveled['prm2']) def test_reset_index_with_drop(self): deleveled = self.ymd.reset_index(drop=True) - self.assertEqual(len(deleveled.columns), len(self.ymd.columns)) + assert len(deleveled.columns) == len(self.ymd.columns) + assert deleveled.index.name == self.ymd.index.name deleveled = self.series.reset_index() - tm.assertIsInstance(deleveled, DataFrame) - self.assertEqual(len(deleveled.columns), - len(self.series.index.levels) + 1) + assert isinstance(deleveled, DataFrame) + assert len(deleveled.columns) == len(self.series.index.levels) + 1 + assert deleveled.index.name == self.series.index.name deleveled = self.series.reset_index(drop=True) - tm.assertIsInstance(deleveled, Series) + assert isinstance(deleveled, Series) + assert deleveled.index.name == self.series.index.name def test_count_level(self): def _check_counts(frame, axis=0): @@ -755,17 +282,17 @@ def _check_counts(frame, axis=0): # can't call with level on regular DataFrame df = tm.makeTimeDataFrame() - tm.assertRaisesRegexp(TypeError, 'hierarchical', df.count, level=0) + with pytest.raises(TypeError, match='hierarchical'): + df.count(level=0) self.frame['D'] = 'foo' result = self.frame.count(level=0, numeric_only=True) - tm.assert_index_equal(result.columns, - pd.Index(['A', 'B', 'C'], name='exp')) + tm.assert_index_equal(result.columns, Index(list('ABC'), name='exp')) def test_count_level_series(self): index = MultiIndex(levels=[['foo', 'bar', 'baz'], ['one', 'two', 'three', 'four']], - labels=[[0, 0, 0, 2, 2], [2, 0, 1, 1, 2]]) + codes=[[0, 0, 0, 2, 2], [2, 0, 1, 1, 2]]) s = Series(np.random.randn(len(index)), index=index) @@ -792,9 +319,9 @@ def test_count_level_corner(self): tm.assert_frame_equal(result, expected) def test_get_level_number_out_of_bounds(self): - with tm.assertRaisesRegexp(IndexError, "Too many levels"): + with pytest.raises(IndexError, match="Too many levels"): self.frame.index._get_level_number(2) - with tm.assertRaisesRegexp(IndexError, "not a valid level number"): + with pytest.raises(IndexError, match="not a valid level number"): self.frame.index._get_level_number(-3) def test_unstack(self): @@ -874,7 +401,7 @@ def test_stack(self): # GH10417 def check(left, right): tm.assert_series_equal(left, right) - self.assertFalse(left.index.is_unique) + assert left.index.is_unique is False li, ri = left.index, right.index tm.assert_index_equal(li, ri) @@ -883,7 +410,7 @@ def check(left, right): columns=['1st', '2nd', '3rd']) mi = MultiIndex(levels=[['a', 'b'], ['1st', '2nd', '3rd']], - labels=[np.tile( + codes=[np.tile( np.arange(2).repeat(3), 2), np.tile( np.arange(3), 4)]) @@ -891,7 +418,7 @@ def check(left, right): check(left, right) df.columns = ['1st', '2nd', '1st'] - mi = MultiIndex(levels=[['a', 'b'], ['1st', '2nd']], labels=[np.tile( + mi = MultiIndex(levels=[['a', 'b'], ['1st', '2nd']], codes=[np.tile( np.arange(2).repeat(3), 2), np.tile( [0, 1, 0], 4)]) @@ -901,7 +428,7 @@ def check(left, right): tpls = ('a', 2), ('b', 1), ('a', 1), ('b', 2) df.index = MultiIndex.from_tuples(tpls) mi = MultiIndex(levels=[['a', 'b'], [1, 2], ['1st', '2nd']], - labels=[np.tile( + codes=[np.tile( np.arange(2).repeat(3), 2), np.repeat( [1, 0, 1], [3, 6, 3]), np.tile( [0, 1, 0], 4)]) @@ -937,10 +464,10 @@ def test_stack_mixed_dtype(self): df = df.sort_index(level=1, axis=1) stacked = df.stack() - result = df['foo'].stack() + result = df['foo'].stack().sort_index() tm.assert_series_equal(stacked['foo'], result, check_names=False) - self.assertIs(result.name, None) - self.assertEqual(stacked['bar'].dtype, np.float_) + assert result.name is None + assert stacked['bar'].dtype == np.float_ def test_unstack_bug(self): df = DataFrame({'state': ['naive', 'naive', 'naive', 'activ', 'activ', @@ -959,11 +486,11 @@ def test_unstack_bug(self): def test_stack_unstack_preserve_names(self): unstacked = self.frame.unstack() - self.assertEqual(unstacked.index.name, 'first') - self.assertEqual(unstacked.columns.names, ['exp', 'second']) + assert unstacked.index.name == 'first' + assert unstacked.columns.names == ['exp', 'second'] restacked = unstacked.stack() - self.assertEqual(restacked.index.names, self.frame.index.names) + assert restacked.index.names == self.frame.index.names def test_unstack_level_name(self): result = self.frame.unstack('second') @@ -984,7 +511,7 @@ def test_stack_unstack_multiple(self): unstacked = self.ymd.unstack(['year', 'month']) expected = self.ymd.unstack('year').unstack('month') tm.assert_frame_equal(unstacked, expected) - self.assertEqual(unstacked.columns.names, expected.columns.names) + assert unstacked.columns.names == expected.columns.names # series s = self.ymd['A'] @@ -996,7 +523,7 @@ def test_stack_unstack_multiple(self): restacked = restacked.sort_index(level=0) tm.assert_frame_equal(restacked, self.ymd) - self.assertEqual(restacked.index.names, self.ymd.index.names) + assert restacked.index.names == self.ymd.index.names # GH #451 unstacked = self.ymd.unstack([1, 2]) @@ -1011,16 +538,16 @@ def test_stack_names_and_numbers(self): unstacked = self.ymd.unstack(['year', 'month']) # Can't use mixture of names and numbers to stack - with tm.assertRaisesRegexp(ValueError, "level should contain"): + with pytest.raises(ValueError, match="level should contain"): unstacked.stack([0, 'month']) def test_stack_multiple_out_of_bounds(self): # nlevels == 3 unstacked = self.ymd.unstack(['year', 'month']) - with tm.assertRaisesRegexp(IndexError, "Too many levels"): + with pytest.raises(IndexError, match="Too many levels"): unstacked.stack([2, 3]) - with tm.assertRaisesRegexp(IndexError, "not a valid level number"): + with pytest.raises(IndexError, match="not a valid level number"): unstacked.stack([-4, -3]) def test_unstack_period_series(self): @@ -1052,7 +579,7 @@ def test_unstack_period_series(self): idx2 = pd.PeriodIndex(['2013-12', '2013-11', '2013-10', '2013-09', '2013-08', '2013-07'], freq='M', name='period2') - idx = pd.MultiIndex.from_arrays([idx1, idx2]) + idx = MultiIndex.from_arrays([idx1, idx2]) s = Series(value, index=idx) result1 = s.unstack() @@ -1082,8 +609,8 @@ def test_unstack_period_frame(self): '2013-10', '2014-02'], freq='M', name='period2') value = {'A': [1, 2, 3, 4, 5, 6], 'B': [6, 5, 4, 3, 2, 1]} - idx = pd.MultiIndex.from_arrays([idx1, idx2]) - df = pd.DataFrame(value, index=idx) + idx = MultiIndex.from_arrays([idx1, idx2]) + df = DataFrame(value, index=idx) result1 = df.unstack() result2 = df.unstack(level=1) @@ -1092,7 +619,7 @@ def test_unstack_period_frame(self): e_1 = pd.PeriodIndex(['2014-01', '2014-02'], freq='M', name='period1') e_2 = pd.PeriodIndex(['2013-10', '2013-12', '2014-02', '2013-10', '2013-12', '2014-02'], freq='M', name='period2') - e_cols = pd.MultiIndex.from_arrays(['A A A B B B'.split(), e_2]) + e_cols = MultiIndex.from_arrays(['A A A B B B'.split(), e_2]) expected = DataFrame([[5, 1, 6, 2, 6, 1], [4, 2, 3, 3, 5, 4]], index=e_1, columns=e_cols) @@ -1103,7 +630,7 @@ def test_unstack_period_frame(self): '2014-02'], freq='M', name='period1') e_2 = pd.PeriodIndex( ['2013-10', '2013-12', '2014-02'], freq='M', name='period2') - e_cols = pd.MultiIndex.from_arrays(['A A B B'.split(), e_1]) + e_cols = MultiIndex.from_arrays(['A A B B'.split(), e_1]) expected = DataFrame([[5, 4, 2, 3], [1, 2, 6, 5], [6, 3, 1, 4]], index=e_2, columns=e_cols) @@ -1129,11 +656,11 @@ def test_stack_multiple_bug(self): def test_stack_dropna(self): # GH #3997 - df = pd.DataFrame({'A': ['a1', 'a2'], 'B': ['b1', 'b2'], 'C': [1, 1]}) + df = DataFrame({'A': ['a1', 'a2'], 'B': ['b1', 'b2'], 'C': [1, 1]}) df = df.set_index(['A', 'B']) stacked = df.unstack().stack(dropna=False) - self.assertTrue(len(stacked) > len(stacked.dropna())) + assert len(stacked) > len(stacked.dropna()) stacked = df.unstack().stack(dropna=True) tm.assert_frame_equal(stacked, stacked.dropna()) @@ -1181,21 +708,60 @@ def test_unstack_sparse_keyspace(self): def test_unstack_unobserved_keys(self): # related to #2278 refactoring levels = [[0, 1], [0, 1, 2, 3]] - labels = [[0, 0, 1, 1], [0, 2, 0, 2]] + codes = [[0, 0, 1, 1], [0, 2, 0, 2]] - index = MultiIndex(levels, labels) + index = MultiIndex(levels, codes) df = DataFrame(np.random.randn(4, 2), index=index) result = df.unstack() - self.assertEqual(len(result.columns), 4) + assert len(result.columns) == 4 recons = result.stack() tm.assert_frame_equal(recons, df) + @pytest.mark.slow + def test_unstack_number_of_levels_larger_than_int32(self): + # GH 20601 + df = DataFrame(np.random.randn(2 ** 16, 2), + index=[np.arange(2 ** 16), np.arange(2 ** 16)]) + with pytest.raises(ValueError, match='int32 overflow'): + df.unstack() + + def test_stack_order_with_unsorted_levels(self): + # GH 16323 + + def manual_compare_stacked(df, df_stacked, lev0, lev1): + assert all(df.loc[row, col] == + df_stacked.loc[(row, col[lev0]), col[lev1]] + for row in df.index for col in df.columns) + + # deep check for 1-row case + for width in [2, 3]: + levels_poss = itertools.product( + itertools.permutations([0, 1, 2], width), + repeat=2) + + for levels in levels_poss: + columns = MultiIndex(levels=levels, + codes=[[0, 0, 1, 1], + [0, 1, 0, 1]]) + df = DataFrame(columns=columns, data=[range(4)]) + for stack_lev in range(2): + df_stacked = df.stack(stack_lev) + manual_compare_stacked(df, df_stacked, + stack_lev, 1 - stack_lev) + + # check multi-row case + mi = MultiIndex(levels=[["A", "C", "B"], ["B", "A", "C"]], + codes=[np.repeat(range(3), 3), np.tile(range(3), 3)]) + df = DataFrame(columns=mi, index=range(5), + data=np.arange(5 * len(mi)).reshape(5, -1)) + manual_compare_stacked(df, df.stack(0), 0, 1) + def test_groupby_corner(self): midx = MultiIndex(levels=[['foo'], ['bar'], ['baz']], - labels=[[0], [0], [0]], + codes=[[0], [0], [0]], names=['one', 'two', 'three']) df = DataFrame([np.random.rand(4)], columns=['a', 'b', 'c', 'd'], index=midx) @@ -1208,11 +774,12 @@ def test_groupby_level_no_obs(self): 'f2', 's1'), ('f2', 's2'), ('f3', 's1'), ('f3', 's2')]) df = DataFrame( [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]], columns=midx) - df1 = df.select(lambda u: u[0] in ['f2', 'f3'], axis=1) + df1 = df.loc(axis=1)[df.columns.map( + lambda u: u[0] in ['f2', 'f3'])] grouped = df1.groupby(axis=1, level=0) result = grouped.sum() - self.assertTrue((result.columns == ['f2', 'f3']).all()) + assert (result.columns == ['f2', 'f3']).all() def test_join(self): a = self.frame.loc[self.frame.index[:5], ['A']] @@ -1222,7 +789,7 @@ def test_join(self): expected = self.frame.copy() expected.values[np.isnan(joined.values)] = np.nan - self.assertFalse(np.isnan(joined.values).all()) + assert not np.isnan(joined.values).all() # TODO what should join do with names ? tm.assert_frame_equal(joined, expected, check_names=False) @@ -1232,7 +799,7 @@ def test_swaplevel(self): swapped2 = self.frame['A'].swaplevel(0) swapped3 = self.frame['A'].swaplevel(0, 1) swapped4 = self.frame['A'].swaplevel('first', 'second') - self.assertFalse(swapped.index.equals(self.frame.index)) + assert not swapped.index.equals(self.frame.index) tm.assert_series_equal(swapped, swapped2) tm.assert_series_equal(swapped, swapped3) tm.assert_series_equal(swapped, swapped4) @@ -1241,7 +808,7 @@ def test_swaplevel(self): back2 = swapped.swaplevel(0) back3 = swapped.swaplevel(0, 1) back4 = swapped.swaplevel('second', 'first') - self.assertTrue(back.index.equals(self.frame.index)) + assert back.index.equals(self.frame.index) tm.assert_series_equal(back, back2) tm.assert_series_equal(back, back3) tm.assert_series_equal(back, back4) @@ -1251,16 +818,6 @@ def test_swaplevel(self): exp = self.frame.swaplevel('first', 'second').T tm.assert_frame_equal(swapped, exp) - def test_swaplevel_panel(self): - panel = Panel({'ItemA': self.frame, 'ItemB': self.frame * 2}) - expected = panel.copy() - expected.major_axis = expected.major_axis.swaplevel(0, 1) - - for result in (panel.swaplevel(axis='major'), - panel.swaplevel(0, axis='major'), - panel.swaplevel(0, 1, axis='major')): - tm.assert_panel_equal(result, expected) - def test_reorder_levels(self): result = self.ymd.reorder_levels(['month', 'day', 'year']) expected = self.ymd.swaplevel(0, 1).swaplevel(1, 2) @@ -1274,17 +831,17 @@ def test_reorder_levels(self): expected = self.ymd.T.swaplevel(0, 1, axis=1).swaplevel(1, 2, axis=1) tm.assert_frame_equal(result, expected) - with tm.assertRaisesRegexp(TypeError, 'hierarchical axis'): + with pytest.raises(TypeError, match='hierarchical axis'): self.ymd.reorder_levels([1, 2], axis=1) - with tm.assertRaisesRegexp(IndexError, 'Too many levels'): + with pytest.raises(IndexError, match='Too many levels'): self.ymd.index.reorder_levels([1, 2, 3]) def test_insert_index(self): df = self.ymd[:5].T df[2000, 1, 10] = df[2000, 1, 7] - tm.assertIsInstance(df.columns, MultiIndex) - self.assertTrue((df[2000, 1, 10] == df[2000, 1, 7]).all()) + assert isinstance(df.columns, MultiIndex) + assert (df[2000, 1, 10] == df[2000, 1, 7]).all() def test_alignment(self): x = Series(data=[1, 2, 3], index=MultiIndex.from_tuples([("A", 1), ( @@ -1304,31 +861,6 @@ def test_alignment(self): exp = x.reindex(exp_index) - y.reindex(exp_index) tm.assert_series_equal(res, exp) - def test_frame_getitem_view(self): - df = self.frame.T.copy() - - # this works because we are modifying the underlying array - # really a no-no - df['foo'].values[:] = 0 - self.assertTrue((df['foo'].values == 0).all()) - - # but not if it's mixed-type - df['foo', 'four'] = 'foo' - df = df.sort_index(level=0, axis=1) - - # this will work, but will raise/warn as its chained assignment - def f(): - df['foo']['one'] = 2 - return df - - self.assertRaises(com.SettingWithCopyError, f) - - try: - df = f() - except: - pass - self.assertTrue((df['foo', 'one'] == 0).all()) - def test_count(self): frame = self.frame.copy() frame.index.names = ['a', 'b'] @@ -1347,61 +879,70 @@ def test_count(self): result = series.count(level='b') expect = self.series.count(level=1) tm.assert_series_equal(result, expect, check_names=False) - self.assertEqual(result.index.name, 'b') + assert result.index.name == 'b' result = series.count(level='a') expect = self.series.count(level=0) tm.assert_series_equal(result, expect, check_names=False) - self.assertEqual(result.index.name, 'a') - - self.assertRaises(KeyError, series.count, 'x') - self.assertRaises(KeyError, frame.count, level='x') - - AGG_FUNCTIONS = ['sum', 'prod', 'min', 'max', 'median', 'mean', 'skew', - 'mad', 'std', 'var', 'sem'] - - def test_series_group_min_max(self): - for op, level, skipna in cart_product(self.AGG_FUNCTIONS, lrange(2), - [False, True]): - grouped = self.series.groupby(level=level) - aggf = lambda x: getattr(x, op)(skipna=skipna) - # skipna=True - leftside = grouped.agg(aggf) - rightside = getattr(self.series, op)(level=level, skipna=skipna) - tm.assert_series_equal(leftside, rightside) - - def test_frame_group_ops(self): + assert result.index.name == 'a' + + msg = "Level x not found" + with pytest.raises(KeyError, match=msg): + series.count('x') + with pytest.raises(KeyError, match=msg): + frame.count(level='x') + + @pytest.mark.parametrize('op', AGG_FUNCTIONS) + @pytest.mark.parametrize('level', [0, 1]) + @pytest.mark.parametrize('skipna', [True, False]) + @pytest.mark.parametrize('sort', [True, False]) + def test_series_group_min_max(self, op, level, skipna, sort): + # GH 17537 + grouped = self.series.groupby(level=level, sort=sort) + # skipna=True + leftside = grouped.agg(lambda x: getattr(x, op)(skipna=skipna)) + rightside = getattr(self.series, op)(level=level, skipna=skipna) + if sort: + rightside = rightside.sort_index(level=level) + tm.assert_series_equal(leftside, rightside) + + @pytest.mark.parametrize('op', AGG_FUNCTIONS) + @pytest.mark.parametrize('level', [0, 1]) + @pytest.mark.parametrize('axis', [0, 1]) + @pytest.mark.parametrize('skipna', [True, False]) + @pytest.mark.parametrize('sort', [True, False]) + def test_frame_group_ops(self, op, level, axis, skipna, sort): + # GH 17537 self.frame.iloc[1, [1, 2]] = np.nan self.frame.iloc[7, [0, 1]] = np.nan - for op, level, axis, skipna in cart_product(self.AGG_FUNCTIONS, - lrange(2), lrange(2), - [False, True]): + if axis == 0: + frame = self.frame + else: + frame = self.frame.T - if axis == 0: - frame = self.frame - else: - frame = self.frame.T + grouped = frame.groupby(level=level, axis=axis, sort=sort) - grouped = frame.groupby(level=level, axis=axis) + pieces = [] - pieces = [] + def aggf(x): + pieces.append(x) + return getattr(x, op)(skipna=skipna, axis=axis) - def aggf(x): - pieces.append(x) - return getattr(x, op)(skipna=skipna, axis=axis) + leftside = grouped.agg(aggf) + rightside = getattr(frame, op)(level=level, axis=axis, + skipna=skipna) + if sort: + rightside = rightside.sort_index(level=level, axis=axis) + frame = frame.sort_index(level=level, axis=axis) - leftside = grouped.agg(aggf) - rightside = getattr(frame, op)(level=level, axis=axis, - skipna=skipna) + # for good measure, groupby detail + level_index = frame._get_axis(axis).levels[level] - # for good measure, groupby detail - level_index = frame._get_axis(axis).levels[level] + tm.assert_index_equal(leftside._get_axis(axis), level_index) + tm.assert_index_equal(rightside._get_axis(axis), level_index) - tm.assert_index_equal(leftside._get_axis(axis), level_index) - tm.assert_index_equal(rightside._get_axis(axis), level_index) - - tm.assert_frame_equal(leftside, rightside) + tm.assert_frame_equal(leftside, rightside) def test_stat_op_corner(self): obj = Series([10.0], index=MultiIndex.from_tuples([(2, 3)])) @@ -1461,7 +1002,7 @@ def test_groupby_multilevel(self): # TODO groupby with level_values drops names tm.assert_frame_equal(result, expected, check_names=False) - self.assertEqual(result.index.names, self.ymd.index.names[:2]) + assert result.index.names == self.ymd.index.names[:2] result2 = self.ymd.groupby(level=self.ymd.index.names[:2]).mean() tm.assert_frame_equal(result, result2) @@ -1479,33 +1020,13 @@ def test_multilevel_consolidate(self): def test_ix_preserve_names(self): result = self.ymd.loc[2000] result2 = self.ymd['A'].loc[2000] - self.assertEqual(result.index.names, self.ymd.index.names[1:]) - self.assertEqual(result2.index.names, self.ymd.index.names[1:]) + assert result.index.names == self.ymd.index.names[1:] + assert result2.index.names == self.ymd.index.names[1:] result = self.ymd.loc[2000, 2] result2 = self.ymd['A'].loc[2000, 2] - self.assertEqual(result.index.name, self.ymd.index.names[2]) - self.assertEqual(result2.index.name, self.ymd.index.names[2]) - - def test_partial_set(self): - # GH #397 - df = self.ymd.copy() - exp = self.ymd.copy() - df.loc[2000, 4] = 0 - exp.loc[2000, 4].values[:] = 0 - tm.assert_frame_equal(df, exp) - - df['A'].loc[2000, 4] = 1 - exp['A'].loc[2000, 4].values[:] = 1 - tm.assert_frame_equal(df, exp) - - df.loc[2000] = 5 - exp.loc[2000].values[:] = 5 - tm.assert_frame_equal(df, exp) - - # this works...for now - df['A'].iloc[14] = 5 - self.assertEqual(df['A'][14], 5) + assert result.index.name == self.ymd.index.names[2] + assert result2.index.name == self.ymd.index.names[2] def test_unstack_preserve_types(self): # GH #403 @@ -1513,20 +1034,20 @@ def test_unstack_preserve_types(self): self.ymd['F'] = 2 unstacked = self.ymd.unstack('month') - self.assertEqual(unstacked['A', 1].dtype, np.float64) - self.assertEqual(unstacked['E', 1].dtype, np.object_) - self.assertEqual(unstacked['F', 1].dtype, np.float64) + assert unstacked['A', 1].dtype == np.float64 + assert unstacked['E', 1].dtype == np.object_ + assert unstacked['F', 1].dtype == np.float64 def test_unstack_group_index_overflow(self): - labels = np.tile(np.arange(500), 2) + codes = np.tile(np.arange(500), 2) level = np.arange(500) index = MultiIndex(levels=[level] * 8 + [[0, 1]], - labels=[labels] * 8 + [np.arange(2).repeat(500)]) + codes=[codes] * 8 + [np.arange(2).repeat(500)]) s = Series(np.arange(1000), index=index) result = s.unstack() - self.assertEqual(result.shape, (500, 2)) + assert result.shape == (500, 2) # test roundtrip stacked = result.stack() @@ -1534,49 +1055,52 @@ def test_unstack_group_index_overflow(self): # put it at beginning index = MultiIndex(levels=[[0, 1]] + [level] * 8, - labels=[np.arange(2).repeat(500)] + [labels] * 8) + codes=[np.arange(2).repeat(500)] + [codes] * 8) s = Series(np.arange(1000), index=index) result = s.unstack(0) - self.assertEqual(result.shape, (500, 2)) + assert result.shape == (500, 2) # put it in middle index = MultiIndex(levels=[level] * 4 + [[0, 1]] + [level] * 4, - labels=([labels] * 4 + [np.arange(2).repeat(500)] + - [labels] * 4)) + codes=([codes] * 4 + [np.arange(2).repeat(500)] + + [codes] * 4)) s = Series(np.arange(1000), index=index) result = s.unstack(4) - self.assertEqual(result.shape, (500, 2)) - - def test_getitem_lowerdim_corner(self): - self.assertRaises(KeyError, self.frame.loc.__getitem__, - (('bar', 'three'), 'B')) - - # in theory should be inserting in a sorted space???? - self.frame.loc[('bar', 'three'), 'B'] = 0 - self.assertEqual(self.frame.sort_index().loc[('bar', 'three'), 'B'], 0) - - # --------------------------------------------------------------------- - # AMBIGUOUS CASES! - - def test_partial_ix_missing(self): - pytest.skip("skipping for now") - - result = self.ymd.loc[2000, 0] - expected = self.ymd.loc[2000]['A'] - tm.assert_series_equal(result, expected) - - # need to put in some work here - - # self.ymd.loc[2000, 0] = 0 - # self.assertTrue((self.ymd.loc[2000]['A'] == 0).all()) - - # Pretty sure the second (and maybe even the first) is already wrong. - self.assertRaises(Exception, self.ymd.loc.__getitem__, (2000, 6)) - self.assertRaises(Exception, self.ymd.loc.__getitem__, (2000, 6), 0) - - # --------------------------------------------------------------------- + assert result.shape == (500, 2) + + def test_pyint_engine(self): + # GH 18519 : when combinations of codes cannot be represented in 64 + # bits, the index underlying the MultiIndex engine works with Python + # integers, rather than uint64. + N = 5 + keys = [tuple(l) for l in [[0] * 10 * N, + [1] * 10 * N, + [2] * 10 * N, + [np.nan] * N + [2] * 9 * N, + [0] * N + [2] * 9 * N, + [np.nan] * N + [2] * 8 * N + [0] * N]] + # Each level contains 4 elements (including NaN), so it is represented + # in 2 bits, for a total of 2*N*10 = 100 > 64 bits. If we were using a + # 64 bit engine and truncating the first levels, the fourth and fifth + # keys would collide; if truncating the last levels, the fifth and + # sixth; if rotating bits rather than shifting, the third and fifth. + + for idx in range(len(keys)): + index = MultiIndex.from_tuples(keys) + assert index.get_loc(keys[idx]) == idx + + expected = np.arange(idx + 1, dtype=np.intp) + result = index.get_indexer([keys[i] for i in expected]) + tm.assert_numpy_array_equal(result, expected) + + # With missing key: + idces = range(len(keys)) + expected = np.array([-1] + list(idces), dtype=np.intp) + missing = tuple([0, 1] * 5 * N) + result = index.get_indexer([missing] + [keys[i] for i in idces]) + tm.assert_numpy_array_equal(result, expected) def test_to_html(self): self.ymd.columns.name = 'foo' @@ -1586,7 +1110,7 @@ def test_to_html(self): def test_level_with_tuples(self): index = MultiIndex(levels=[[('foo', 'bar', 0), ('foo', 'baz', 0), ( 'foo', 'qux', 0)], [0, 1]], - labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]) + codes=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]) series = Series(np.random.randn(6), index=index) frame = DataFrame(np.random.randn(6, 4), index=index) @@ -1598,7 +1122,8 @@ def test_level_with_tuples(self): tm.assert_series_equal(result, expected) tm.assert_series_equal(result2, expected) - self.assertRaises(KeyError, series.__getitem__, (('foo', 'bar', 0), 2)) + with pytest.raises(KeyError, match=r"^\(\('foo', 'bar', 0\), 2\)$"): + series[('foo', 'bar', 0), 2] result = frame.loc[('foo', 'bar', 0)] result2 = frame.xs(('foo', 'bar', 0)) @@ -1609,7 +1134,7 @@ def test_level_with_tuples(self): index = MultiIndex(levels=[[('foo', 'bar'), ('foo', 'baz'), ( 'foo', 'qux')], [0, 1]], - labels=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]) + codes=[[0, 0, 1, 1, 2, 2], [0, 1, 0, 1, 0, 1]]) series = Series(np.random.randn(6), index=index) frame = DataFrame(np.random.randn(6, 4), index=index) @@ -1628,55 +1153,6 @@ def test_level_with_tuples(self): tm.assert_frame_equal(result, expected) tm.assert_frame_equal(result2, expected) - def test_int_series_slicing(self): - s = self.ymd['A'] - result = s[5:] - expected = s.reindex(s.index[5:]) - tm.assert_series_equal(result, expected) - - exp = self.ymd['A'].copy() - s[5:] = 0 - exp.values[5:] = 0 - tm.assert_numpy_array_equal(s.values, exp.values) - - result = self.ymd[5:] - expected = self.ymd.reindex(s.index[5:]) - tm.assert_frame_equal(result, expected) - - def test_mixed_depth_get(self): - arrays = [['a', 'top', 'top', 'routine1', 'routine1', 'routine2'], - ['', 'OD', 'OD', 'result1', 'result2', 'result1'], - ['', 'wx', 'wy', '', '', '']] - - tuples = sorted(zip(*arrays)) - index = MultiIndex.from_tuples(tuples) - df = DataFrame(randn(4, 6), columns=index) - - result = df['a'] - expected = df['a', '', ''] - tm.assert_series_equal(result, expected, check_names=False) - self.assertEqual(result.name, 'a') - - result = df['routine1', 'result1'] - expected = df['routine1', 'result1', ''] - tm.assert_series_equal(result, expected, check_names=False) - self.assertEqual(result.name, ('routine1', 'result1')) - - def test_mixed_depth_insert(self): - arrays = [['a', 'top', 'top', 'routine1', 'routine1', 'routine2'], - ['', 'OD', 'OD', 'result1', 'result2', 'result1'], - ['', 'wx', 'wy', '', '', '']] - - tuples = sorted(zip(*arrays)) - index = MultiIndex.from_tuples(tuples) - df = DataFrame(randn(4, 6), columns=index) - - result = df.copy() - expected = df.copy() - result['b'] = [1, 2, 3, 4] - expected['b', '', ''] = [1, 2, 3, 4] - tm.assert_frame_equal(result, expected) - def test_mixed_depth_drop(self): arrays = [['a', 'top', 'top', 'routine1', 'routine1', 'routine2'], ['', 'OD', 'OD', 'result1', 'result2', 'result1'], @@ -1743,7 +1219,7 @@ def test_mixed_depth_pop(self): expected = df2.pop(('a', '', '')) tm.assert_series_equal(expected, result, check_names=False) tm.assert_frame_equal(df1, df2) - self.assertEqual(result.name, 'a') + assert result.name == 'a' expected = df1['top'] df1 = df1.drop(['top'], axis=1) @@ -1756,7 +1232,7 @@ def test_reindex_level_partial_selection(self): expected = self.frame.iloc[[0, 1, 2, 7, 8, 9]] tm.assert_frame_equal(result, expected) - result = self.frame.T.reindex_axis(['foo', 'qux'], axis=1, level=0) + result = self.frame.T.reindex(['foo', 'qux'], axis=1, level=0) tm.assert_frame_equal(result, expected.T) result = self.frame.loc[['foo', 'qux']] @@ -1768,35 +1244,6 @@ def test_reindex_level_partial_selection(self): result = self.frame.T.loc[:, ['foo', 'qux']] tm.assert_frame_equal(result, expected.T) - def test_setitem_multiple_partial(self): - expected = self.frame.copy() - result = self.frame.copy() - result.loc[['foo', 'bar']] = 0 - expected.loc['foo'] = 0 - expected.loc['bar'] = 0 - tm.assert_frame_equal(result, expected) - - expected = self.frame.copy() - result = self.frame.copy() - result.loc['foo':'bar'] = 0 - expected.loc['foo'] = 0 - expected.loc['bar'] = 0 - tm.assert_frame_equal(result, expected) - - expected = self.frame['A'].copy() - result = self.frame['A'].copy() - result.loc[['foo', 'bar']] = 0 - expected.loc['foo'] = 0 - expected.loc['bar'] = 0 - tm.assert_series_equal(result, expected) - - expected = self.frame['A'].copy() - result = self.frame['A'].copy() - result.loc['foo':'bar'] = 0 - expected.loc['foo'] = 0 - expected.loc['bar'] = 0 - tm.assert_series_equal(result, expected) - def test_drop_level(self): result = self.frame.drop(['bar', 'qux'], level='first') expected = self.frame.iloc[[0, 1, 2, 5, 6]] @@ -1816,7 +1263,7 @@ def test_drop_level(self): def test_drop_level_nonunique_datetime(self): # GH 12701 - idx = pd.Index([2, 3, 4, 4, 5], name='id') + idx = Index([2, 3, 4, 4, 5], name='id') idxdt = pd.to_datetime(['201603231400', '201603231500', '201603231600', @@ -1826,13 +1273,26 @@ def test_drop_level_nonunique_datetime(self): columns=list('ab'), index=idx) df['tstamp'] = idxdt df = df.set_index('tstamp', append=True) - ts = pd.Timestamp('201603231600') - self.assertFalse(df.index.is_unique) + ts = Timestamp('201603231600') + assert df.index.is_unique is False result = df.drop(ts, level='tstamp') expected = df.loc[idx != 4] tm.assert_frame_equal(result, expected) + @pytest.mark.parametrize('box', [Series, DataFrame]) + def test_drop_tz_aware_timestamp_across_dst(self, box): + # GH 21761 + start = Timestamp('2017-10-29', tz='Europe/Berlin') + end = Timestamp('2017-10-29 04:00:00', tz='Europe/Berlin') + index = pd.date_range(start, end, freq='15min') + data = box(data=[1] * len(index), index=index) + result = data.drop(start) + expected_start = Timestamp('2017-10-29 00:15:00', tz='Europe/Berlin') + expected_idx = pd.date_range(expected_start, end, freq='15min') + expected = box(data=[1] * len(expected_idx), index=expected_idx) + tm.assert_equal(result, expected) + def test_drop_preserve_names(self): index = MultiIndex.from_arrays([[0, 0, 0, 1, 1, 1], [1, 2, 3, 1, 2, 3]], @@ -1841,13 +1301,13 @@ def test_drop_preserve_names(self): df = DataFrame(np.random.randn(6, 3), index=index) result = df.drop([(0, 2)]) - self.assertEqual(result.index.names, ('one', 'two')) + assert result.index.names == ('one', 'two') def test_unicode_repr_issues(self): levels = [Index([u('a/\u03c3'), u('b/\u03c3'), u('c/\u03c3')]), Index([0, 1])] - labels = [np.arange(3).repeat(2), np.tile(np.arange(2), 3)] - index = MultiIndex(levels=levels, labels=labels) + codes = [np.arange(3).repeat(2), np.tile(np.arange(2), 3)] + index = MultiIndex(levels=levels, codes=codes) repr(index.levels) @@ -1863,15 +1323,6 @@ def test_unicode_repr_level_names(self): repr(s) repr(df) - def test_dataframe_insert_column_all_na(self): - # GH #1534 - mix = MultiIndex.from_tuples([('1a', '2a'), ('1a', '2b'), ('1a', '2c') - ]) - df = DataFrame([[1, 2], [3, 4], [5, 6]], index=mix) - s = Series({(1, 1): 1, (1, 2): 2}) - df['new'] = s - self.assertTrue(df['new'].isnull().all()) - def test_join_segfault(self): # 1532 df1 = DataFrame({'a': [1, 1], 'b': [1, 2], 'x': [1, 2]}) @@ -1882,16 +1333,6 @@ def test_join_segfault(self): for how in ['left', 'right', 'outer']: df1.join(df2, how=how) - def test_set_column_scalar_with_ix(self): - subset = self.frame.index[[1, 4, 5]] - - self.frame.loc[subset] = 99 - self.assertTrue((self.frame.loc[subset].values == 99).all()) - - col = self.frame['B'] - col[subset] = 97 - self.assertTrue((self.frame.loc[subset, 'B'] == 97).all()) - def test_frame_dict_constructor_empty_series(self): s1 = Series([ 1, 2, 3, 4 @@ -1905,47 +1346,6 @@ def test_frame_dict_constructor_empty_series(self): DataFrame({'foo': s1, 'bar': s2, 'baz': s3}) DataFrame.from_dict({'foo': s1, 'baz': s3, 'bar': s2}) - def test_indexing_ambiguity_bug_1678(self): - columns = MultiIndex.from_tuples([('Ohio', 'Green'), ('Ohio', 'Red'), ( - 'Colorado', 'Green')]) - index = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2) - ]) - - frame = DataFrame(np.arange(12).reshape((4, 3)), index=index, - columns=columns) - - result = frame.iloc[:, 1] - exp = frame.loc[:, ('Ohio', 'Red')] - tm.assertIsInstance(result, Series) - tm.assert_series_equal(result, exp) - - def test_nonunique_assignment_1750(self): - df = DataFrame([[1, 1, "x", "X"], [1, 1, "y", "Y"], [1, 2, "z", "Z"]], - columns=list("ABCD")) - - df = df.set_index(['A', 'B']) - ix = MultiIndex.from_tuples([(1, 1)]) - - df.loc[ix, "C"] = '_' - - self.assertTrue((df.xs((1, 1))['C'] == '_').all()) - - def test_indexing_over_hashtable_size_cutoff(self): - n = 10000 - - old_cutoff = _index._SIZE_CUTOFF - _index._SIZE_CUTOFF = 20000 - - s = Series(np.arange(n), - MultiIndex.from_arrays((["a"] * n, np.arange(n)))) - - # hai it works! - self.assertEqual(s[("a", 5)], 5) - self.assertEqual(s[("a", 6)], 6) - self.assertEqual(s[("a", 7)], 7) - - _index._SIZE_CUTOFF = old_cutoff - def test_multiindex_na_repr(self): # only an issue with long columns @@ -1967,23 +1367,23 @@ def test_assign_index_sequences(self): df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}).set_index(["a", "b"]) - l = list(df.index) - l[0] = ("faz", "boo") - df.index = l + index = list(df.index) + index[0] = ("faz", "boo") + df.index = index repr(df) # this travels an improper code path - l[0] = ["faz", "boo"] - df.index = l + index[0] = ["faz", "boo"] + df.index = index repr(df) def test_tuples_have_na(self): index = MultiIndex(levels=[[1, 0], [0, 1, 2, 3]], - labels=[[1, 1, 1, 1, -1, 0, 0, 0], [0, 1, 2, 3, 0, - 1, 2, 3]]) + codes=[[1, 1, 1, 1, -1, 0, 0, 0], + [0, 1, 2, 3, 0, 1, 2, 3]]) - self.assertTrue(isnull(index[4][0])) - self.assertTrue(isnull(index.values[4][0])) + assert isna(index[4][0]) + assert isna(index.values[4][0]) def test_duplicate_groupby_issues(self): idx_tp = [('600809', '20061231'), ('600809', '20070331'), @@ -1994,7 +1394,7 @@ def test_duplicate_groupby_issues(self): s = Series(dt, index=idx) result = s.groupby(s.index).first() - self.assertEqual(len(result), 3) + assert len(result) == 3 def test_duplicate_mi(self): # GH 4516 @@ -2019,21 +1419,21 @@ def test_duplicated_drop_duplicates(self): [False, False, False, True, False, False], dtype=bool) duplicated = idx.duplicated() tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool expected = MultiIndex.from_arrays(([1, 2, 3, 2, 3], [1, 1, 1, 2, 2])) tm.assert_index_equal(idx.drop_duplicates(), expected) expected = np.array([True, False, False, False, False, False]) duplicated = idx.duplicated(keep='last') tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool expected = MultiIndex.from_arrays(([2, 3, 1, 2, 3], [1, 1, 1, 2, 2])) tm.assert_index_equal(idx.drop_duplicates(keep='last'), expected) expected = np.array([True, False, False, True, False, False]) duplicated = idx.duplicated(keep=False) tm.assert_numpy_array_equal(duplicated, expected) - self.assertTrue(duplicated.dtype == bool) + assert duplicated.dtype == bool expected = MultiIndex.from_arrays(([2, 3, 2, 3], [1, 1, 2, 2])) tm.assert_index_equal(idx.drop_duplicates(keep=False), expected) @@ -2070,9 +1470,9 @@ def test_datetimeindex(self): for d1, d2 in itertools.product( [date1, date2, date3], [date1, date2, date3]): - index = pd.MultiIndex.from_product([[d1], [d2]]) - self.assertIsInstance(index.levels[0], pd.DatetimeIndex) - self.assertIsInstance(index.levels[1], pd.DatetimeIndex) + index = MultiIndex.from_product([[d1], [d2]]) + assert isinstance(index.levels[0], pd.DatetimeIndex) + assert isinstance(index.levels[1], pd.DatetimeIndex) def test_constructor_with_tz(self): @@ -2091,14 +1491,14 @@ def test_constructor_with_tz(self): def test_set_index_datetime(self): # GH 3950 - df = pd.DataFrame( + df = DataFrame( {'label': ['a', 'a', 'a', 'b', 'b', 'b'], 'datetime': ['2011-07-19 07:00:00', '2011-07-19 08:00:00', '2011-07-19 09:00:00', '2011-07-19 07:00:00', '2011-07-19 08:00:00', '2011-07-19 09:00:00'], 'value': range(6)}) df.index = pd.to_datetime(df.pop('datetime'), utc=True) - df.index = df.index.tz_localize('UTC').tz_convert('US/Pacific') + df.index = df.index.tz_convert('US/Pacific') expected = pd.DatetimeIndex(['2011-07-19 07:00:00', '2011-07-19 08:00:00', @@ -2108,11 +1508,11 @@ def test_set_index_datetime(self): df = df.set_index('label', append=True) tm.assert_index_equal(df.index.levels[0], expected) tm.assert_index_equal(df.index.levels[1], - pd.Index(['a', 'b'], name='label')) + Index(['a', 'b'], name='label')) df = df.swaplevel(0, 1) tm.assert_index_equal(df.index.levels[0], - pd.Index(['a', 'b'], name='label')) + Index(['a', 'b'], name='label')) tm.assert_index_equal(df.index.levels[1], expected) df = DataFrame(np.random.random(6)) @@ -2150,82 +1550,80 @@ def test_reset_index_datetime(self): for tz in ['UTC', 'Asia/Tokyo', 'US/Eastern']: idx1 = pd.date_range('1/1/2011', periods=5, freq='D', tz=tz, name='idx1') - idx2 = pd.Index(range(5), name='idx2', dtype='int64') - idx = pd.MultiIndex.from_arrays([idx1, idx2]) - df = pd.DataFrame( + idx2 = Index(range(5), name='idx2', dtype='int64') + idx = MultiIndex.from_arrays([idx1, idx2]) + df = DataFrame( {'a': np.arange(5, dtype='int64'), 'b': ['A', 'B', 'C', 'D', 'E']}, index=idx) - expected = pd.DataFrame({'idx1': [datetime.datetime(2011, 1, 1), - datetime.datetime(2011, 1, 2), - datetime.datetime(2011, 1, 3), - datetime.datetime(2011, 1, 4), - datetime.datetime(2011, 1, 5)], - 'idx2': np.arange(5, dtype='int64'), - 'a': np.arange(5, dtype='int64'), - 'b': ['A', 'B', 'C', 'D', 'E']}, - columns=['idx1', 'idx2', 'a', 'b']) + expected = DataFrame({'idx1': [datetime.datetime(2011, 1, 1), + datetime.datetime(2011, 1, 2), + datetime.datetime(2011, 1, 3), + datetime.datetime(2011, 1, 4), + datetime.datetime(2011, 1, 5)], + 'idx2': np.arange(5, dtype='int64'), + 'a': np.arange(5, dtype='int64'), + 'b': ['A', 'B', 'C', 'D', 'E']}, + columns=['idx1', 'idx2', 'a', 'b']) expected['idx1'] = expected['idx1'].apply( - lambda d: pd.Timestamp(d, tz=tz)) + lambda d: Timestamp(d, tz=tz)) tm.assert_frame_equal(df.reset_index(), expected) idx3 = pd.date_range('1/1/2012', periods=5, freq='MS', tz='Europe/Paris', name='idx3') - idx = pd.MultiIndex.from_arrays([idx1, idx2, idx3]) - df = pd.DataFrame( + idx = MultiIndex.from_arrays([idx1, idx2, idx3]) + df = DataFrame( {'a': np.arange(5, dtype='int64'), 'b': ['A', 'B', 'C', 'D', 'E']}, index=idx) - expected = pd.DataFrame({'idx1': [datetime.datetime(2011, 1, 1), - datetime.datetime(2011, 1, 2), - datetime.datetime(2011, 1, 3), - datetime.datetime(2011, 1, 4), - datetime.datetime(2011, 1, 5)], - 'idx2': np.arange(5, dtype='int64'), - 'idx3': [datetime.datetime(2012, 1, 1), - datetime.datetime(2012, 2, 1), - datetime.datetime(2012, 3, 1), - datetime.datetime(2012, 4, 1), - datetime.datetime(2012, 5, 1)], - 'a': np.arange(5, dtype='int64'), - 'b': ['A', 'B', 'C', 'D', 'E']}, - columns=['idx1', 'idx2', 'idx3', 'a', 'b']) + expected = DataFrame({'idx1': [datetime.datetime(2011, 1, 1), + datetime.datetime(2011, 1, 2), + datetime.datetime(2011, 1, 3), + datetime.datetime(2011, 1, 4), + datetime.datetime(2011, 1, 5)], + 'idx2': np.arange(5, dtype='int64'), + 'idx3': [datetime.datetime(2012, 1, 1), + datetime.datetime(2012, 2, 1), + datetime.datetime(2012, 3, 1), + datetime.datetime(2012, 4, 1), + datetime.datetime(2012, 5, 1)], + 'a': np.arange(5, dtype='int64'), + 'b': ['A', 'B', 'C', 'D', 'E']}, + columns=['idx1', 'idx2', 'idx3', 'a', 'b']) expected['idx1'] = expected['idx1'].apply( - lambda d: pd.Timestamp(d, tz=tz)) + lambda d: Timestamp(d, tz=tz)) expected['idx3'] = expected['idx3'].apply( - lambda d: pd.Timestamp(d, tz='Europe/Paris')) + lambda d: Timestamp(d, tz='Europe/Paris')) tm.assert_frame_equal(df.reset_index(), expected) # GH 7793 - idx = pd.MultiIndex.from_product([['a', 'b'], pd.date_range( + idx = MultiIndex.from_product([['a', 'b'], pd.date_range( '20130101', periods=3, tz=tz)]) - df = pd.DataFrame( + df = DataFrame( np.arange(6, dtype='int64').reshape( 6, 1), columns=['a'], index=idx) - expected = pd.DataFrame({'level_0': 'a a a b b b'.split(), - 'level_1': [ - datetime.datetime(2013, 1, 1), - datetime.datetime(2013, 1, 2), - datetime.datetime(2013, 1, 3)] * 2, - 'a': np.arange(6, dtype='int64')}, - columns=['level_0', 'level_1', 'a']) + expected = DataFrame({'level_0': 'a a a b b b'.split(), + 'level_1': [ + datetime.datetime(2013, 1, 1), + datetime.datetime(2013, 1, 2), + datetime.datetime(2013, 1, 3)] * 2, + 'a': np.arange(6, dtype='int64')}, + columns=['level_0', 'level_1', 'a']) expected['level_1'] = expected['level_1'].apply( - lambda d: pd.Timestamp(d, freq='D', tz=tz)) + lambda d: Timestamp(d, freq='D', tz=tz)) tm.assert_frame_equal(df.reset_index(), expected) def test_reset_index_period(self): # GH 7746 - idx = pd.MultiIndex.from_product([pd.period_range('20130101', - periods=3, freq='M'), - ['a', 'b', 'c']], - names=['month', 'feature']) - - df = pd.DataFrame(np.arange(9, dtype='int64') - .reshape(-1, 1), - index=idx, columns=['a']) - expected = pd.DataFrame({ + idx = MultiIndex.from_product( + [pd.period_range('20130101', periods=3, freq='M'), list('abc')], + names=['month', 'feature']) + + df = DataFrame(np.arange(9, dtype='int64').reshape(-1, 1), + index=idx, columns=['a']) + expected = DataFrame({ 'month': ([pd.Period('2013-01', freq='M')] * 3 + [pd.Period('2013-02', freq='M')] * 3 + [pd.Period('2013-03', freq='M')] * 3), @@ -2234,6 +1632,57 @@ def test_reset_index_period(self): }, columns=['month', 'feature', 'a']) tm.assert_frame_equal(df.reset_index(), expected) + def test_reset_index_multiindex_columns(self): + levels = [['A', ''], ['B', 'b']] + df = DataFrame([[0, 2], [1, 3]], + columns=MultiIndex.from_tuples(levels)) + result = df[['B']].rename_axis('A').reset_index() + tm.assert_frame_equal(result, df) + + # gh-16120: already existing column + with pytest.raises(ValueError, + match=(r"cannot insert \('A', ''\), " + "already exists")): + df.rename_axis('A').reset_index() + + # gh-16164: multiindex (tuple) full key + result = df.set_index([('A', '')]).reset_index() + tm.assert_frame_equal(result, df) + + # with additional (unnamed) index level + idx_col = DataFrame([[0], [1]], + columns=MultiIndex.from_tuples([('level_0', '')])) + expected = pd.concat([idx_col, df[[('B', 'b'), ('A', '')]]], axis=1) + result = df.set_index([('B', 'b')], append=True).reset_index() + tm.assert_frame_equal(result, expected) + + # with index name which is a too long tuple... + with pytest.raises(ValueError, + match=("Item must have length equal " + "to number of levels.")): + df.rename_axis([('C', 'c', 'i')]).reset_index() + + # or too short... + levels = [['A', 'a', ''], ['B', 'b', 'i']] + df2 = DataFrame([[0, 2], [1, 3]], + columns=MultiIndex.from_tuples(levels)) + idx_col = DataFrame([[0], [1]], + columns=MultiIndex.from_tuples([('C', 'c', 'ii')])) + expected = pd.concat([idx_col, df2], axis=1) + result = df2.rename_axis([('C', 'c')]).reset_index(col_fill='ii') + tm.assert_frame_equal(result, expected) + + # ... which is incompatible with col_fill=None + with pytest.raises(ValueError, + match=("col_fill=None is incompatible with " + r"incomplete column name \('C', 'c'\)")): + df2.rename_axis([('C', 'c')]).reset_index(col_fill=None) + + # with col_level != 0 + result = df2.rename_axis([('c', 'ii')]).reset_index(col_level=1, + col_fill='C') + tm.assert_frame_equal(result, expected) + def test_set_index_period(self): # GH 6631 df = DataFrame(np.random.random(6)) @@ -2261,46 +1710,18 @@ def test_set_index_period(self): def test_repeat(self): # GH 9361 # fixed by # GH 7891 - m_idx = pd.MultiIndex.from_tuples([(1, 2), (3, 4), (5, 6), (7, 8)]) + m_idx = MultiIndex.from_tuples([(1, 2), (3, 4), (5, 6), (7, 8)]) data = ['a', 'b', 'c', 'd'] - m_df = pd.Series(data, index=m_idx) + m_df = Series(data, index=m_idx) assert m_df.repeat(3).shape == (3 * len(data), ) - def test_iloc_mi(self): - # GH 13797 - # Test if iloc can handle integer locations in MultiIndexed DataFrame - - data = [ - ['str00', 'str01'], - ['str10', 'str11'], - ['str20', 'srt21'], - ['str30', 'str31'], - ['str40', 'str41'] - ] - - mi = pd.MultiIndex.from_tuples( - [('CC', 'A'), - ('CC', 'B'), - ('CC', 'B'), - ('BB', 'a'), - ('BB', 'b') - ]) - expected = pd.DataFrame(data) - df_mi = pd.DataFrame(data, index=mi) - - result = pd.DataFrame([[df_mi.iloc[r, c] for c in range(2)] - for r in range(5)]) - - tm.assert_frame_equal(result, expected) - - -class TestSorted(Base, tm.TestCase): - """ everthing you wanted to test about sorting """ +class TestSorted(Base): + """ everything you wanted to test about sorting """ def test_sort_index_preserve_levels(self): result = self.frame.sort_index() - self.assertEqual(result.index.names, self.frame.index.names) + assert result.index.names == self.frame.index.names def test_sorting_repr_8017(self): @@ -2322,7 +1743,7 @@ def test_sorting_repr_8017(self): # check that the repr is good # make sure that we have a correct sparsified repr # e.g. only 1 header of read - self.assertEqual(str(df2).splitlines()[0].split(), ['red']) + assert str(df2).splitlines()[0].split() == ['red'] # GH 8017 # sorting fails after columns added @@ -2353,7 +1774,7 @@ def test_sort_index_level(self): a_sorted = self.frame['A'].sort_index(level=0) # preserve names - self.assertEqual(a_sorted.index.names, self.frame.index.names) + assert a_sorted.index.names == self.frame.index.names # inplace rs = self.frame.copy() @@ -2368,7 +1789,7 @@ def test_sort_index_level_large_cardinality(self): # it works! result = df.sort_index(level=0) - self.assertTrue(result.index.lexsort_depth == 3) + assert result.index.lexsort_depth == 3 # #2684 (int32) index = MultiIndex.from_arrays([np.arange(4000)] * 3) @@ -2376,8 +1797,8 @@ def test_sort_index_level_large_cardinality(self): # it works! result = df.sort_index(level=0) - self.assertTrue((result.dtypes.values == df.dtypes.values).all()) - self.assertTrue(result.index.lexsort_depth == 3) + assert (result.dtypes.values == df.dtypes.values).all() + assert result.index.lexsort_depth == 3 def test_sort_index_level_by_name(self): self.frame.index.names = ['first', 'second'] @@ -2406,71 +1827,229 @@ def test_is_lexsorted(self): levels = [[0, 1], [0, 1, 2]] index = MultiIndex(levels=levels, - labels=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]]) - self.assertTrue(index.is_lexsorted()) + codes=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 1, 2]]) + assert index.is_lexsorted() index = MultiIndex(levels=levels, - labels=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 2, 1]]) - self.assertFalse(index.is_lexsorted()) + codes=[[0, 0, 0, 1, 1, 1], [0, 1, 2, 0, 2, 1]]) + assert not index.is_lexsorted() index = MultiIndex(levels=levels, - labels=[[0, 0, 1, 0, 1, 1], [0, 1, 0, 2, 2, 1]]) - self.assertFalse(index.is_lexsorted()) - self.assertEqual(index.lexsort_depth, 0) - - def test_getitem_multilevel_index_tuple_not_sorted(self): - index_columns = list("abc") - df = DataFrame([[0, 1, 0, "x"], [0, 0, 1, "y"]], - columns=index_columns + ["data"]) - df = df.set_index(index_columns) - query_index = df.index[:1] - rs = df.loc[query_index, "data"] - - xp_idx = MultiIndex.from_tuples([(0, 1, 0)], names=['a', 'b', 'c']) - xp = Series(['x'], index=xp_idx, name='data') - tm.assert_series_equal(rs, xp) - - def test_getitem_slice_not_sorted(self): - df = self.frame.sort_index(level=1).T - - # buglet with int typechecking - result = df.iloc[:, :np.int32(3)] - expected = df.reindex(columns=df.columns[:3]) + codes=[[0, 0, 1, 0, 1, 1], [0, 1, 0, 2, 2, 1]]) + assert not index.is_lexsorted() + assert index.lexsort_depth == 0 + + def test_sort_index_and_reconstruction(self): + + # 15622 + # lexsortedness should be identical + # across MultiIndex consruction methods + + df = DataFrame([[1, 1], [2, 2]], index=list('ab')) + expected = DataFrame([[1, 1], [2, 2], [1, 1], [2, 2]], + index=MultiIndex.from_tuples([(0.5, 'a'), + (0.5, 'b'), + (0.8, 'a'), + (0.8, 'b')])) + assert expected.index.is_lexsorted() + + result = DataFrame( + [[1, 1], [2, 2], [1, 1], [2, 2]], + index=MultiIndex.from_product([[0.5, 0.8], list('ab')])) + result = result.sort_index() + assert result.index.is_lexsorted() + assert result.index.is_monotonic + tm.assert_frame_equal(result, expected) - def test_frame_getitem_not_sorted(self): - df = self.frame.T - df['foo', 'four'] = 'foo' + result = DataFrame( + [[1, 1], [2, 2], [1, 1], [2, 2]], + index=MultiIndex(levels=[[0.5, 0.8], ['a', 'b']], + codes=[[0, 0, 1, 1], [0, 1, 0, 1]])) + result = result.sort_index() + assert result.index.is_lexsorted() + + tm.assert_frame_equal(result, expected) + + concatted = pd.concat([df, df], keys=[0.8, 0.5]) + result = concatted.sort_index() - arrays = [np.array(x) for x in zip(*df.columns.values)] + assert result.index.is_lexsorted() + assert result.index.is_monotonic - result = df['foo'] - result2 = df.loc[:, 'foo'] - expected = df.reindex(columns=df.columns[arrays[0] == 'foo']) - expected.columns = expected.columns.droplevel(0) tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result2, expected) - df = df.T - result = df.xs('foo') - result2 = df.loc['foo'] - expected = df.reindex(df.index[arrays[0] == 'foo']) - expected.index = expected.index.droplevel(0) + # 14015 + df = DataFrame([[1, 2], [6, 7]], + columns=MultiIndex.from_tuples( + [(0, '20160811 12:00:00'), + (0, '20160809 12:00:00')], + names=['l1', 'Date'])) + + df.columns.set_levels(pd.to_datetime(df.columns.levels[1]), + level=1, + inplace=True) + assert not df.columns.is_lexsorted() + assert not df.columns.is_monotonic + result = df.sort_index(axis=1) + assert result.columns.is_lexsorted() + assert result.columns.is_monotonic + result = df.sort_index(axis=1, level=1) + assert result.columns.is_lexsorted() + assert result.columns.is_monotonic + + def test_sort_index_and_reconstruction_doc_example(self): + # doc example + df = DataFrame({'value': [1, 2, 3, 4]}, + index=MultiIndex( + levels=[['a', 'b'], ['bb', 'aa']], + codes=[[0, 0, 1, 1], [0, 1, 0, 1]])) + assert df.index.is_lexsorted() + assert not df.index.is_monotonic + + # sort it + expected = DataFrame({'value': [2, 1, 4, 3]}, + index=MultiIndex( + levels=[['a', 'b'], ['aa', 'bb']], + codes=[[0, 0, 1, 1], [0, 1, 0, 1]])) + result = df.sort_index() + assert result.index.is_lexsorted() + assert result.index.is_monotonic + tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(result2, expected) - def test_series_getitem_not_sorted(self): - arrays = [['bar', 'bar', 'baz', 'baz', 'qux', 'qux', 'foo', 'foo'], - ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']] + # reconstruct + result = df.sort_index().copy() + result.index = result.index._sort_levels_monotonic() + assert result.index.is_lexsorted() + assert result.index.is_monotonic + + tm.assert_frame_equal(result, expected) + + def test_sort_index_reorder_on_ops(self): + # 15687 + df = DataFrame( + np.random.randn(8, 2), + index=MultiIndex.from_product( + [['a', 'b'], ['big', 'small'], ['red', 'blu']], + names=['letter', 'size', 'color']), + columns=['near', 'far']) + df = df.sort_index() + + def my_func(group): + group.index = ['newz', 'newa'] + return group + + result = df.groupby(level=['letter', 'size']).apply( + my_func).sort_index() + expected = MultiIndex.from_product( + [['a', 'b'], ['big', 'small'], ['newa', 'newz']], + names=['letter', 'size', None]) + + tm.assert_index_equal(result.index, expected) + + def test_sort_non_lexsorted(self): + # degenerate case where we sort but don't + # have a satisfying result :< + # GH 15797 + idx = MultiIndex([['A', 'B', 'C'], + ['c', 'b', 'a']], + [[0, 1, 2, 0, 1, 2], + [0, 2, 1, 1, 0, 2]]) + + df = DataFrame({'col': range(len(idx))}, + index=idx, + dtype='int64') + assert df.index.is_lexsorted() is False + assert df.index.is_monotonic is False + + sorted = df.sort_index() + assert sorted.index.is_lexsorted() is True + assert sorted.index.is_monotonic is True + + expected = DataFrame( + {'col': [1, 4, 5, 2]}, + index=MultiIndex.from_tuples([('B', 'a'), ('B', 'c'), + ('C', 'a'), ('C', 'b')]), + dtype='int64') + result = sorted.loc[pd.IndexSlice['B':'C', 'a':'c'], :] + tm.assert_frame_equal(result, expected) + + def test_sort_index_nan(self): + # GH 14784 + # incorrect sorting w.r.t. nans + tuples = [[12, 13], [np.nan, np.nan], [np.nan, 3], [1, 2]] + mi = MultiIndex.from_tuples(tuples) + + df = DataFrame(np.arange(16).reshape(4, 4), + index=mi, columns=list('ABCD')) + s = Series(np.arange(4), index=mi) + + df2 = DataFrame({ + 'date': pd.to_datetime([ + '20121002', '20121007', '20130130', '20130202', '20130305', + '20121002', '20121207', '20130130', '20130202', '20130305', + '20130202', '20130305' + ]), + 'user_id': [1, 1, 1, 1, 1, 3, 3, 3, 5, 5, 5, 5], + 'whole_cost': [1790, np.nan, 280, 259, np.nan, 623, 90, 312, + np.nan, 301, 359, 801], + 'cost': [12, 15, 10, 24, 39, 1, 0, np.nan, 45, 34, 1, 12] + }).set_index(['date', 'user_id']) + + # sorting frame, default nan position is last + result = df.sort_index() + expected = df.iloc[[3, 0, 2, 1], :] + tm.assert_frame_equal(result, expected) + + # sorting frame, nan position last + result = df.sort_index(na_position='last') + expected = df.iloc[[3, 0, 2, 1], :] + tm.assert_frame_equal(result, expected) + + # sorting frame, nan position first + result = df.sort_index(na_position='first') + expected = df.iloc[[1, 2, 3, 0], :] + tm.assert_frame_equal(result, expected) + + # sorting frame with removed rows + result = df2.dropna().sort_index() + expected = df2.sort_index().dropna() + tm.assert_frame_equal(result, expected) + + # sorting series, default nan position is last + result = s.sort_index() + expected = s.iloc[[3, 0, 2, 1]] + tm.assert_series_equal(result, expected) + + # sorting series, nan position last + result = s.sort_index(na_position='last') + expected = s.iloc[[3, 0, 2, 1]] + tm.assert_series_equal(result, expected) + + # sorting series, nan position first + result = s.sort_index(na_position='first') + expected = s.iloc[[1, 2, 3, 0]] + tm.assert_series_equal(result, expected) + + def test_sort_ascending_list(self): + # GH: 16934 + + # Set up a Series with a three level MultiIndex + arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'], + ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two'], + [4, 3, 2, 1, 4, 3, 2, 1]] tuples = lzip(*arrays) - index = MultiIndex.from_tuples(tuples) - s = Series(randn(8), index=index) + mi = MultiIndex.from_tuples(tuples, names=['first', 'second', 'third']) + s = Series(range(8), index=mi) - arrays = [np.array(x) for x in zip(*index.values)] + # Sort with boolean ascending + result = s.sort_index(level=['third', 'first'], ascending=False) + expected = s.iloc[[4, 0, 5, 1, 6, 2, 7, 3]] + tm.assert_series_equal(result, expected) - result = s['qux'] - result2 = s.loc['qux'] - expected = s[arrays[0] == 'qux'] - expected.index = expected.index.droplevel(0) + # Sort with list of boolean ascending + result = s.sort_index(level=['third', 'first'], + ascending=[False, True]) + expected = s.iloc[[0, 4, 1, 5, 2, 6, 3, 7]] tm.assert_series_equal(result, expected) - tm.assert_series_equal(result2, expected) diff --git a/pandas/tests/test_nanops.py b/pandas/tests/test_nanops.py index 54de8c1e34031..d1893b7efbc41 100644 --- a/pandas/tests/test_nanops.py +++ b/pandas/tests/test_nanops.py @@ -2,38 +2,47 @@ from __future__ import division, print_function from functools import partial - import warnings + import numpy as np -from pandas import Series, isnull, _np_version_under1p9 -from pandas.types.common import is_integer_dtype +import pytest + +from pandas.compat import PY2 +from pandas.compat.numpy import _np_version_under1p13 +import pandas.util._test_decorators as td + +from pandas.core.dtypes.common import is_integer_dtype + +import pandas as pd +from pandas import Series, isna +from pandas.core.arrays import DatetimeArray import pandas.core.nanops as nanops import pandas.util.testing as tm use_bn = nanops._USE_BOTTLENECK -class TestnanopsDataFrame(tm.TestCase): +class TestnanopsDataFrame(object): - def setUp(self): + def setup_method(self, method): np.random.seed(11235) nanops._USE_BOTTLENECK = False - self.arr_shape = (11, 7, 5) + arr_shape = (11, 7, 5) - self.arr_float = np.random.randn(*self.arr_shape) - self.arr_float1 = np.random.randn(*self.arr_shape) + self.arr_float = np.random.randn(*arr_shape) + self.arr_float1 = np.random.randn(*arr_shape) self.arr_complex = self.arr_float + self.arr_float1 * 1j - self.arr_int = np.random.randint(-10, 10, self.arr_shape) - self.arr_bool = np.random.randint(0, 2, self.arr_shape) == 0 + self.arr_int = np.random.randint(-10, 10, arr_shape) + self.arr_bool = np.random.randint(0, 2, arr_shape) == 0 self.arr_str = np.abs(self.arr_float).astype('S') self.arr_utf = np.abs(self.arr_float).astype('U') self.arr_date = np.random.randint(0, 20000, - self.arr_shape).astype('M8[ns]') + arr_shape).astype('M8[ns]') self.arr_tdelta = np.random.randint(0, 20000, - self.arr_shape).astype('m8[ns]') + arr_shape).astype('m8[ns]') - self.arr_nan = np.tile(np.nan, self.arr_shape) + self.arr_nan = np.tile(np.nan, arr_shape) self.arr_float_nan = np.vstack([self.arr_float, self.arr_nan]) self.arr_float1_nan = np.vstack([self.arr_float1, self.arr_nan]) self.arr_nan_float1 = np.vstack([self.arr_nan, self.arr_float1]) @@ -41,22 +50,22 @@ def setUp(self): self.arr_inf = self.arr_float * np.inf self.arr_float_inf = np.vstack([self.arr_float, self.arr_inf]) - self.arr_float1_inf = np.vstack([self.arr_float1, self.arr_inf]) - self.arr_inf_float1 = np.vstack([self.arr_inf, self.arr_float1]) - self.arr_inf_inf = np.vstack([self.arr_inf, self.arr_inf]) self.arr_nan_inf = np.vstack([self.arr_nan, self.arr_inf]) self.arr_float_nan_inf = np.vstack([self.arr_float, self.arr_nan, self.arr_inf]) - self.arr_nan_float1_inf = np.vstack([self.arr_float, self.arr_inf, - self.arr_nan]) self.arr_nan_nan_inf = np.vstack([self.arr_nan, self.arr_nan, self.arr_inf]) - self.arr_obj = np.vstack([self.arr_float.astype( - 'O'), self.arr_int.astype('O'), self.arr_bool.astype( - 'O'), self.arr_complex.astype('O'), self.arr_str.astype( - 'O'), self.arr_utf.astype('O'), self.arr_date.astype('O'), - self.arr_tdelta.astype('O')]) + self.arr_obj = np.vstack([ + self.arr_float.astype('O'), + self.arr_int.astype('O'), + self.arr_bool.astype('O'), + self.arr_complex.astype('O'), + self.arr_str.astype('O'), + self.arr_utf.astype('O'), + self.arr_date.astype('O'), + self.arr_tdelta.astype('O') + ]) with np.errstate(invalid='ignore'): self.arr_nan_nanj = self.arr_nan + self.arr_nan * 1j @@ -69,53 +78,21 @@ def setUp(self): self.arr_float_2d = self.arr_float[:, :, 0] self.arr_float1_2d = self.arr_float1[:, :, 0] - self.arr_complex_2d = self.arr_complex[:, :, 0] - self.arr_int_2d = self.arr_int[:, :, 0] - self.arr_bool_2d = self.arr_bool[:, :, 0] - self.arr_str_2d = self.arr_str[:, :, 0] - self.arr_utf_2d = self.arr_utf[:, :, 0] - self.arr_date_2d = self.arr_date[:, :, 0] - self.arr_tdelta_2d = self.arr_tdelta[:, :, 0] self.arr_nan_2d = self.arr_nan[:, :, 0] self.arr_float_nan_2d = self.arr_float_nan[:, :, 0] self.arr_float1_nan_2d = self.arr_float1_nan[:, :, 0] self.arr_nan_float1_2d = self.arr_nan_float1[:, :, 0] - self.arr_nan_nan_2d = self.arr_nan_nan[:, :, 0] - self.arr_nan_nanj_2d = self.arr_nan_nanj[:, :, 0] - self.arr_complex_nan_2d = self.arr_complex_nan[:, :, 0] - - self.arr_inf_2d = self.arr_inf[:, :, 0] - self.arr_float_inf_2d = self.arr_float_inf[:, :, 0] - self.arr_nan_inf_2d = self.arr_nan_inf[:, :, 0] - self.arr_float_nan_inf_2d = self.arr_float_nan_inf[:, :, 0] - self.arr_nan_nan_inf_2d = self.arr_nan_nan_inf[:, :, 0] self.arr_float_1d = self.arr_float[:, 0, 0] self.arr_float1_1d = self.arr_float1[:, 0, 0] - self.arr_complex_1d = self.arr_complex[:, 0, 0] - self.arr_int_1d = self.arr_int[:, 0, 0] - self.arr_bool_1d = self.arr_bool[:, 0, 0] - self.arr_str_1d = self.arr_str[:, 0, 0] - self.arr_utf_1d = self.arr_utf[:, 0, 0] - self.arr_date_1d = self.arr_date[:, 0, 0] - self.arr_tdelta_1d = self.arr_tdelta[:, 0, 0] self.arr_nan_1d = self.arr_nan[:, 0, 0] self.arr_float_nan_1d = self.arr_float_nan[:, 0, 0] self.arr_float1_nan_1d = self.arr_float1_nan[:, 0, 0] self.arr_nan_float1_1d = self.arr_nan_float1[:, 0, 0] - self.arr_nan_nan_1d = self.arr_nan_nan[:, 0, 0] - self.arr_nan_nanj_1d = self.arr_nan_nanj[:, 0, 0] - self.arr_complex_nan_1d = self.arr_complex_nan[:, 0, 0] - self.arr_inf_1d = self.arr_inf.ravel() - self.arr_float_inf_1d = self.arr_float_inf[:, 0, 0] - self.arr_nan_inf_1d = self.arr_nan_inf[:, 0, 0] - self.arr_float_nan_inf_1d = self.arr_float_nan_inf[:, 0, 0] - self.arr_nan_nan_inf_1d = self.arr_nan_nan_inf[:, 0, 0] - - def tearDown(self): + def teardown_method(self, method): nanops._USE_BOTTLENECK = use_bn def check_results(self, targ, res, axis, check_dtype=True): @@ -136,12 +113,12 @@ def _coerce_tds(targ, res): if axis != 0 and hasattr( targ, 'shape') and targ.ndim and targ.shape != res.shape: res = np.split(res, [targ.shape[0]], axis=0)[0] - except: + except (ValueError, IndexError): targ, res = _coerce_tds(targ, res) try: tm.assert_almost_equal(targ, res, check_dtype=check_dtype) - except: + except AssertionError: # handle timedelta dtypes if hasattr(targ, 'dtype') and targ.dtype == 'm8[ns]': @@ -162,11 +139,11 @@ def _coerce_tds(targ, res): else: try: res = res.astype('c16') - except: + except RuntimeError: res = res.astype('f8') try: targ = targ.astype('c16') - except: + except RuntimeError: targ = targ.astype('f8') # there should never be a case where numpy returns an object # but nanops doesn't, so make that an exception @@ -178,12 +155,17 @@ def _coerce_tds(targ, res): check_dtype=check_dtype) def check_fun_data(self, testfunc, targfunc, testarval, targarval, - targarnanval, check_dtype=True, **kwargs): + targarnanval, check_dtype=True, empty_targfunc=None, + **kwargs): for axis in list(range(targarval.ndim)) + [None]: for skipna in [False, True]: targartempval = targarval if skipna else targarnanval - try: + if skipna and empty_targfunc and isna(targartempval).all(): + targ = empty_targfunc(targartempval, axis=axis, **kwargs) + else: targ = targfunc(targartempval, axis=axis, **kwargs) + + try: res = testfunc(testarval, axis=axis, skipna=skipna, **kwargs) self.check_results(targ, res, axis, @@ -215,10 +197,11 @@ def check_fun_data(self, testfunc, targfunc, testarval, targarval, except ValueError: return self.check_fun_data(testfunc, targfunc, testarval2, targarval2, - targarnanval2, check_dtype=check_dtype, **kwargs) + targarnanval2, check_dtype=check_dtype, + empty_targfunc=empty_targfunc, **kwargs) def check_fun(self, testfunc, targfunc, testar, targar=None, - targarnan=None, **kwargs): + targarnan=None, empty_targfunc=None, **kwargs): if targar is None: targar = testar if targarnan is None: @@ -228,7 +211,8 @@ def check_fun(self, testfunc, targfunc, testar, targar=None, targarnanval = getattr(self, targarnan) try: self.check_fun_data(testfunc, targfunc, testarval, targarval, - targarnanval, **kwargs) + targarnanval, empty_targfunc=empty_targfunc, + **kwargs) except BaseException as exc: exc.args += ('testar: %s' % testar, 'targar: %s' % targar, 'targarnan: %s' % targarnan) @@ -289,24 +273,6 @@ def check_funs(self, testfunc, targfunc, allow_complex=True, allow_complex=allow_complex) self.check_fun(testfunc, targfunc, 'arr_obj', **kwargs) - def check_funs_ddof(self, - testfunc, - targfunc, - allow_complex=True, - allow_all_nan=True, - allow_str=True, - allow_date=False, - allow_tdelta=False, - allow_obj=True, ): - for ddof in range(3): - try: - self.check_funs(testfunc, targfunc, allow_complex, - allow_all_nan, allow_str, allow_date, - allow_tdelta, allow_obj, ddof=ddof) - except BaseException as exc: - exc.args += ('ddof %s' % ddof, ) - raise - def _badobj_wrap(self, value, func, allow_complex=True, **kwargs): if value.dtype.kind == 'O': if allow_complex: @@ -325,7 +291,8 @@ def test_nanall(self): def test_nansum(self): self.check_funs(nanops.nansum, np.sum, allow_str=False, - allow_date=False, allow_tdelta=True, check_dtype=False) + allow_date=False, allow_tdelta=True, check_dtype=False, + empty_targfunc=np.nansum) def test_nanmean(self): self.check_funs(nanops.nanmean, np.mean, allow_complex=False, @@ -337,15 +304,13 @@ def test_nanmean_overflow(self): # In the previous implementation mean can overflow for int dtypes, it # is now consistent with numpy - # numpy < 1.9.0 is not computing this correctly - if not _np_version_under1p9: - for a in [2 ** 55, -2 ** 55, 20150515061816532]: - s = Series(a, index=range(500), dtype=np.int64) - result = s.mean() - np_result = s.values.mean() - self.assertEqual(result, a) - self.assertEqual(result, np_result) - self.assertTrue(result.dtype == np.float64) + for a in [2 ** 55, -2 ** 55, 20150515061816532]: + s = Series(a, index=range(500), dtype=np.int64) + result = s.mean() + np_result = s.values.mean() + assert result == a + assert result == np_result + assert result.dtype == np.float64 def test_returned_dtype(self): @@ -360,58 +325,64 @@ def test_returned_dtype(self): for method in group_a + group_b: result = getattr(s, method)() if is_integer_dtype(dtype) and method in group_a: - self.assertTrue( - result.dtype == np.float64, - "return dtype expected from %s is np.float64, " - "got %s instead" % (method, result.dtype)) + assert result.dtype == np.float64 else: - self.assertTrue( - result.dtype == dtype, - "return dtype expected from %s is %s, " - "got %s instead" % (method, dtype, result.dtype)) + assert result.dtype == dtype def test_nanmedian(self): with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) self.check_funs(nanops.nanmedian, np.median, allow_complex=False, allow_str=False, allow_date=False, allow_tdelta=True, allow_obj='convert') - def test_nanvar(self): - self.check_funs_ddof(nanops.nanvar, np.var, allow_complex=False, - allow_str=False, allow_date=False, - allow_tdelta=True, allow_obj='convert') - - def test_nanstd(self): - self.check_funs_ddof(nanops.nanstd, np.std, allow_complex=False, - allow_str=False, allow_date=False, - allow_tdelta=True, allow_obj='convert') - - def test_nansem(self): - tm.skip_if_no_package('scipy', min_version='0.17.0') + @pytest.mark.parametrize('ddof', range(3)) + def test_nanvar(self, ddof): + self.check_funs(nanops.nanvar, np.var, allow_complex=False, + allow_str=False, allow_date=False, + allow_tdelta=True, allow_obj='convert', ddof=ddof) + + @pytest.mark.parametrize('ddof', range(3)) + def test_nanstd(self, ddof): + self.check_funs(nanops.nanstd, np.std, allow_complex=False, + allow_str=False, allow_date=False, + allow_tdelta=True, allow_obj='convert', ddof=ddof) + + @td.skip_if_no('scipy', min_version='0.17.0') + @pytest.mark.parametrize('ddof', range(3)) + def test_nansem(self, ddof): from scipy.stats import sem with np.errstate(invalid='ignore'): - self.check_funs_ddof(nanops.nansem, sem, allow_complex=False, - allow_str=False, allow_date=False, - allow_tdelta=False, allow_obj='convert') + self.check_funs(nanops.nansem, sem, allow_complex=False, + allow_str=False, allow_date=False, + allow_tdelta=False, allow_obj='convert', ddof=ddof) def _minmax_wrap(self, value, axis=None, func=None): + + # numpy warns if all nan res = func(value, axis) if res.dtype.kind == 'm': res = np.atleast_1d(res) return res def test_nanmin(self): - func = partial(self._minmax_wrap, func=np.min) - self.check_funs(nanops.nanmin, func, allow_str=False, allow_obj=False) + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) + func = partial(self._minmax_wrap, func=np.min) + self.check_funs(nanops.nanmin, func, + allow_str=False, allow_obj=False) def test_nanmax(self): - func = partial(self._minmax_wrap, func=np.max) - self.check_funs(nanops.nanmax, func, allow_str=False, allow_obj=False) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + func = partial(self._minmax_wrap, func=np.max) + self.check_funs(nanops.nanmax, func, + allow_str=False, allow_obj=False) def _argminmax_wrap(self, value, axis=None, func=None): res = func(value, axis) nans = np.min(value, axis) - nullnan = isnull(nans) + nullnan = isna(nans) if res.ndim: res[nullnan] = -1 elif (hasattr(nullnan, 'all') and nullnan.all() or @@ -420,17 +391,17 @@ def _argminmax_wrap(self, value, axis=None, func=None): return res def test_nanargmax(self): - func = partial(self._argminmax_wrap, func=np.argmax) - self.check_funs(nanops.nanargmax, func, allow_str=False, - allow_obj=False, allow_date=True, allow_tdelta=True) + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) + func = partial(self._argminmax_wrap, func=np.argmax) + self.check_funs(nanops.nanargmax, func, + allow_str=False, allow_obj=False, + allow_date=True, allow_tdelta=True) def test_nanargmin(self): - func = partial(self._argminmax_wrap, func=np.argmin) - if tm.sys.version_info[0:2] == (2, 6): - self.check_funs(nanops.nanargmin, func, allow_date=True, - allow_tdelta=True, allow_str=False, - allow_obj=False) - else: + with warnings.catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) + func = partial(self._argminmax_wrap, func=np.argmin) self.check_funs(nanops.nanargmin, func, allow_str=False, allow_obj=False) @@ -446,8 +417,8 @@ def _skew_kurt_wrap(self, values, axis=None, func=None): return 0. return result + @td.skip_if_no('scipy', min_version='0.17.0') def test_nanskew(self): - tm.skip_if_no_package('scipy', min_version='0.17.0') from scipy.stats import skew func = partial(self._skew_kurt_wrap, func=skew) with np.errstate(invalid='ignore'): @@ -455,8 +426,8 @@ def test_nanskew(self): allow_str=False, allow_date=False, allow_tdelta=False) + @td.skip_if_no('scipy', min_version='0.17.0') def test_nankurt(self): - tm.skip_if_no_package('scipy', min_version='0.17.0') from scipy.stats import kurtosis func1 = partial(kurtosis, fisher=True) func = partial(self._skew_kurt_wrap, func=func1) @@ -467,7 +438,8 @@ def test_nankurt(self): def test_nanprod(self): self.check_funs(nanops.nanprod, np.prod, allow_str=False, - allow_date=False, allow_tdelta=False) + allow_date=False, allow_tdelta=False, + empty_targfunc=np.nanprod) def check_nancorr_nancov_2d(self, checkfun, targ0, targ1, **kwargs): res00 = checkfun(self.arr_float_2d, self.arr_float1_2d, **kwargs) @@ -554,8 +526,8 @@ def test_nancorr_pearson(self): self.check_nancorr_nancov_1d(nanops.nancorr, targ0, targ1, method='pearson') + @td.skip_if_no_scipy def test_nancorr_kendall(self): - tm.skip_if_no_package('scipy.stats') from scipy.stats import kendalltau targ0 = kendalltau(self.arr_float_2d, self.arr_float1_2d)[0] targ1 = kendalltau(self.arr_float_2d.flat, self.arr_float1_2d.flat)[0] @@ -566,8 +538,8 @@ def test_nancorr_kendall(self): self.check_nancorr_nancov_1d(nanops.nancorr, targ0, targ1, method='kendall') + @td.skip_if_no_scipy def test_nancorr_spearman(self): - tm.skip_if_no_package('scipy.stats') from scipy.stats import spearmanr targ0 = spearmanr(self.arr_float_2d, self.arr_float1_2d)[0] targ1 = spearmanr(self.arr_float_2d.flat, self.arr_float1_2d.flat)[0] @@ -655,9 +627,9 @@ def check_bool(self, func, value, correct, *args, **kwargs): try: res0 = func(value, *args, **kwargs) if correct: - self.assertTrue(res0) + assert res0 else: - self.assertFalse(res0) + assert not res0 except BaseException as exc: exc.args += ('dim: %s' % getattr(value, 'ndim', value), ) raise @@ -734,67 +706,71 @@ def test__isfinite(self): raise def test__bn_ok_dtype(self): - self.assertTrue(nanops._bn_ok_dtype(self.arr_float.dtype, 'test')) - self.assertTrue(nanops._bn_ok_dtype(self.arr_complex.dtype, 'test')) - self.assertTrue(nanops._bn_ok_dtype(self.arr_int.dtype, 'test')) - self.assertTrue(nanops._bn_ok_dtype(self.arr_bool.dtype, 'test')) - self.assertTrue(nanops._bn_ok_dtype(self.arr_str.dtype, 'test')) - self.assertTrue(nanops._bn_ok_dtype(self.arr_utf.dtype, 'test')) - self.assertFalse(nanops._bn_ok_dtype(self.arr_date.dtype, 'test')) - self.assertFalse(nanops._bn_ok_dtype(self.arr_tdelta.dtype, 'test')) - self.assertFalse(nanops._bn_ok_dtype(self.arr_obj.dtype, 'test')) + assert nanops._bn_ok_dtype(self.arr_float.dtype, 'test') + assert nanops._bn_ok_dtype(self.arr_complex.dtype, 'test') + assert nanops._bn_ok_dtype(self.arr_int.dtype, 'test') + assert nanops._bn_ok_dtype(self.arr_bool.dtype, 'test') + assert nanops._bn_ok_dtype(self.arr_str.dtype, 'test') + assert nanops._bn_ok_dtype(self.arr_utf.dtype, 'test') + assert not nanops._bn_ok_dtype(self.arr_date.dtype, 'test') + assert not nanops._bn_ok_dtype(self.arr_tdelta.dtype, 'test') + assert not nanops._bn_ok_dtype(self.arr_obj.dtype, 'test') -class TestEnsureNumeric(tm.TestCase): +class TestEnsureNumeric(object): def test_numeric_values(self): # Test integer - self.assertEqual(nanops._ensure_numeric(1), 1, 'Failed for int') + assert nanops._ensure_numeric(1) == 1 + # Test float - self.assertEqual(nanops._ensure_numeric(1.1), 1.1, 'Failed for float') + assert nanops._ensure_numeric(1.1) == 1.1 + # Test complex - self.assertEqual(nanops._ensure_numeric(1 + 2j), 1 + 2j, - 'Failed for complex') + assert nanops._ensure_numeric(1 + 2j) == 1 + 2j + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_ndarray(self): # Test numeric ndarray values = np.array([1, 2, 3]) - self.assertTrue(np.allclose(nanops._ensure_numeric(values), values), - 'Failed for numeric ndarray') + assert np.allclose(nanops._ensure_numeric(values), values) # Test object ndarray o_values = values.astype(object) - self.assertTrue(np.allclose(nanops._ensure_numeric(o_values), values), - 'Failed for object ndarray') + assert np.allclose(nanops._ensure_numeric(o_values), values) # Test convertible string ndarray s_values = np.array(['1', '2', '3'], dtype=object) - self.assertTrue(np.allclose(nanops._ensure_numeric(s_values), values), - 'Failed for convertible string ndarray') + assert np.allclose(nanops._ensure_numeric(s_values), values) # Test non-convertible string ndarray s_values = np.array(['foo', 'bar', 'baz'], dtype=object) - self.assertRaises(ValueError, lambda: nanops._ensure_numeric(s_values)) + msg = r"could not convert string to float: '(foo|baz)'" + with pytest.raises(ValueError, match=msg): + nanops._ensure_numeric(s_values) def test_convertable_values(self): - self.assertTrue(np.allclose(nanops._ensure_numeric('1'), 1.0), - 'Failed for convertible integer string') - self.assertTrue(np.allclose(nanops._ensure_numeric('1.1'), 1.1), - 'Failed for convertible float string') - self.assertTrue(np.allclose(nanops._ensure_numeric('1+1j'), 1 + 1j), - 'Failed for convertible complex string') + assert np.allclose(nanops._ensure_numeric('1'), 1.0) + assert np.allclose(nanops._ensure_numeric('1.1'), 1.1) + assert np.allclose(nanops._ensure_numeric('1+1j'), 1 + 1j) def test_non_convertable_values(self): - self.assertRaises(TypeError, lambda: nanops._ensure_numeric('foo')) - self.assertRaises(TypeError, lambda: nanops._ensure_numeric({})) - self.assertRaises(TypeError, lambda: nanops._ensure_numeric([])) + msg = "Could not convert foo to numeric" + with pytest.raises(TypeError, match=msg): + nanops._ensure_numeric('foo') + msg = "Could not convert {} to numeric" + with pytest.raises(TypeError, match=msg): + nanops._ensure_numeric({}) + msg = r"Could not convert \[\] to numeric" + with pytest.raises(TypeError, match=msg): + nanops._ensure_numeric([]) -class TestNanvarFixedValues(tm.TestCase): +class TestNanvarFixedValues(object): # xref GH10242 - def setUp(self): + def setup_method(self, method): # Samples from a normal distribution. self.variance = variance = 3.0 self.samples = self.prng.normal(scale=variance ** 0.5, size=100000) @@ -881,14 +857,14 @@ def test_ground_truth(self): for ddof in range(3): var = nanops.nanvar(samples, skipna=True, axis=axis, ddof=ddof) tm.assert_almost_equal(var[:3], variance[axis, ddof]) - self.assertTrue(np.isnan(var[3])) + assert np.isnan(var[3]) # Test nanstd. for axis in range(2): for ddof in range(3): std = nanops.nanstd(samples, skipna=True, axis=axis, ddof=ddof) tm.assert_almost_equal(std[:3], variance[axis, ddof] ** 0.5) - self.assertTrue(np.isnan(std[3])) + assert np.isnan(std[3]) def test_nanstd_roundoff(self): # Regression test for GH 10242 (test data taken from GH 10489). Ensure @@ -896,18 +872,18 @@ def test_nanstd_roundoff(self): data = Series(766897346 * np.ones(10)) for ddof in range(3): result = data.std(ddof=ddof) - self.assertEqual(result, 0.0) + assert result == 0.0 @property def prng(self): return np.random.RandomState(1234) -class TestNanskewFixedValues(tm.TestCase): +class TestNanskewFixedValues(object): # xref GH 11974 - def setUp(self): + def setup_method(self, method): # Test data + skewness value (computed with scipy.stats.skew) self.samples = np.sin(np.linspace(0, 1, 200)) self.actual_skew = -0.1875895205961754 @@ -917,20 +893,20 @@ def test_constant_series(self): for val in [3075.2, 3075.3, 3075.5]: data = val * np.ones(300) skew = nanops.nanskew(data) - self.assertEqual(skew, 0.0) + assert skew == 0.0 def test_all_finite(self): alpha, beta = 0.3, 0.1 left_tailed = self.prng.beta(alpha, beta, size=100) - self.assertLess(nanops.nanskew(left_tailed), 0) + assert nanops.nanskew(left_tailed) < 0 alpha, beta = 0.1, 0.3 right_tailed = self.prng.beta(alpha, beta, size=100) - self.assertGreater(nanops.nanskew(right_tailed), 0) + assert nanops.nanskew(right_tailed) > 0 def test_ground_truth(self): skew = nanops.nanskew(self.samples) - self.assertAlmostEqual(skew, self.actual_skew) + tm.assert_almost_equal(skew, self.actual_skew) def test_axis(self): samples = np.vstack([self.samples, @@ -941,7 +917,7 @@ def test_axis(self): def test_nans(self): samples = np.hstack([self.samples, np.nan]) skew = nanops.nanskew(samples, skipna=False) - self.assertTrue(np.isnan(skew)) + assert np.isnan(skew) def test_nans_skipna(self): samples = np.hstack([self.samples, np.nan]) @@ -953,11 +929,11 @@ def prng(self): return np.random.RandomState(1234) -class TestNankurtFixedValues(tm.TestCase): +class TestNankurtFixedValues(object): # xref GH 11974 - def setUp(self): + def setup_method(self, method): # Test data + kurtosis value (computed with scipy.stats.kurtosis) self.samples = np.sin(np.linspace(0, 1, 200)) self.actual_kurt = -1.2058303433799713 @@ -967,20 +943,20 @@ def test_constant_series(self): for val in [3075.2, 3075.3, 3075.5]: data = val * np.ones(300) kurt = nanops.nankurt(data) - self.assertEqual(kurt, 0.0) + assert kurt == 0.0 def test_all_finite(self): alpha, beta = 0.3, 0.1 left_tailed = self.prng.beta(alpha, beta, size=100) - self.assertLess(nanops.nankurt(left_tailed), 0) + assert nanops.nankurt(left_tailed) < 0 alpha, beta = 0.1, 0.3 right_tailed = self.prng.beta(alpha, beta, size=100) - self.assertGreater(nanops.nankurt(right_tailed), 0) + assert nanops.nankurt(right_tailed) > 0 def test_ground_truth(self): kurt = nanops.nankurt(self.samples) - self.assertAlmostEqual(kurt, self.actual_kurt) + tm.assert_almost_equal(kurt, self.actual_kurt) def test_axis(self): samples = np.vstack([self.samples, @@ -991,7 +967,7 @@ def test_axis(self): def test_nans(self): samples = np.hstack([self.samples, np.nan]) kurt = nanops.nankurt(samples, skipna=False) - self.assertTrue(np.isnan(kurt)) + assert np.isnan(kurt) def test_nans_skipna(self): samples = np.hstack([self.samples, np.nan]) @@ -1001,3 +977,93 @@ def test_nans_skipna(self): @property def prng(self): return np.random.RandomState(1234) + + +class TestDatetime64NaNOps(object): + @pytest.mark.parametrize('tz', [None, 'UTC']) + @pytest.mark.xfail(reason="disabled") + # Enabling mean changes the behavior of DataFrame.mean + # See https://github.com/pandas-dev/pandas/issues/24752 + def test_nanmean(self, tz): + dti = pd.date_range('2016-01-01', periods=3, tz=tz) + expected = dti[1] + + for obj in [dti, DatetimeArray(dti), Series(dti)]: + result = nanops.nanmean(obj) + assert result == expected + + dti2 = dti.insert(1, pd.NaT) + + for obj in [dti2, DatetimeArray(dti2), Series(dti2)]: + result = nanops.nanmean(obj) + assert result == expected + + +def test_use_bottleneck(): + + if nanops._BOTTLENECK_INSTALLED: + + pd.set_option('use_bottleneck', True) + assert pd.get_option('use_bottleneck') + + pd.set_option('use_bottleneck', False) + assert not pd.get_option('use_bottleneck') + + pd.set_option('use_bottleneck', use_bn) + + +@pytest.mark.parametrize("numpy_op, expected", [ + (np.sum, 10), + (np.nansum, 10), + (np.mean, 2.5), + (np.nanmean, 2.5), + (np.median, 2.5), + (np.nanmedian, 2.5), + (np.min, 1), + (np.max, 4), +]) +def test_numpy_ops(numpy_op, expected): + # GH8383 + result = numpy_op(pd.Series([1, 2, 3, 4])) + assert result == expected + + +@pytest.mark.parametrize("numpy_op, expected", [ + (np.nanmin, 1), + (np.nanmax, 4), +]) +def test_numpy_ops_np_version_under1p13(numpy_op, expected): + # GH8383 + result = numpy_op(pd.Series([1, 2, 3, 4])) + if _np_version_under1p13: + # bug for numpy < 1.13, where result is a series, should be a scalar + with pytest.raises(ValueError): + assert result == expected + else: + assert result == expected + + +@pytest.mark.parametrize("operation", [ + nanops.nanany, + nanops.nanall, + nanops.nansum, + nanops.nanmean, + nanops.nanmedian, + nanops.nanstd, + nanops.nanvar, + nanops.nansem, + nanops.nanargmax, + nanops.nanargmin, + nanops.nanmax, + nanops.nanmin, + nanops.nanskew, + nanops.nankurt, + nanops.nanprod, +]) +def test_nanops_independent_of_mask_param(operation): + # GH22764 + s = pd.Series([1, 2, np.nan, 3, np.nan, 4]) + mask = s.isna() + median_expected = operation(s) + median_result = operation(s, mask=mask) + assert median_expected == median_result diff --git a/pandas/tests/test_panel.py b/pandas/tests/test_panel.py deleted file mode 100644 index ab0322abbcf06..0000000000000 --- a/pandas/tests/test_panel.py +++ /dev/null @@ -1,2535 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=W0612,E1101 - -from warnings import catch_warnings -from datetime import datetime - -import operator -import pytest - -import numpy as np -import pandas as pd - -from pandas.types.common import is_float_dtype -from pandas import (Series, DataFrame, Index, date_range, isnull, notnull, - pivot, MultiIndex) -from pandas.core.nanops import nanall, nanany -from pandas.core.panel import Panel -from pandas.core.series import remove_na - -from pandas.formats.printing import pprint_thing -from pandas import compat -from pandas.compat import range, lrange, StringIO, OrderedDict, signature - -from pandas.tseries.offsets import BDay, MonthEnd -from pandas.util.testing import (assert_panel_equal, assert_frame_equal, - assert_series_equal, assert_almost_equal, - ensure_clean, assertRaisesRegexp, - makeCustomDataframe as mkdf, - makeMixedDataFrame) -import pandas.core.panel as panelm -import pandas.util.testing as tm - - -class PanelTests(object): - panel = None - - def test_pickle(self): - unpickled = self.round_trip_pickle(self.panel) - assert_frame_equal(unpickled['ItemA'], self.panel['ItemA']) - - def test_rank(self): - self.assertRaises(NotImplementedError, lambda: self.panel.rank()) - - def test_cumsum(self): - cumsum = self.panel.cumsum() - assert_frame_equal(cumsum['ItemA'], self.panel['ItemA'].cumsum()) - - def not_hashable(self): - c_empty = Panel() - c = Panel(Panel([[[1]]])) - self.assertRaises(TypeError, hash, c_empty) - self.assertRaises(TypeError, hash, c) - - -class SafeForLongAndSparse(object): - - def test_repr(self): - repr(self.panel) - - def test_copy_names(self): - for attr in ('major_axis', 'minor_axis'): - getattr(self.panel, attr).name = None - cp = self.panel.copy() - getattr(cp, attr).name = 'foo' - self.assertIsNone(getattr(self.panel, attr).name) - - def test_iter(self): - tm.equalContents(list(self.panel), self.panel.items) - - def test_count(self): - f = lambda s: notnull(s).sum() - self._check_stat_op('count', f, obj=self.panel, has_skipna=False) - - def test_sum(self): - self._check_stat_op('sum', np.sum) - - def test_mean(self): - self._check_stat_op('mean', np.mean) - - def test_prod(self): - self._check_stat_op('prod', np.prod) - - def test_median(self): - def wrapper(x): - if isnull(x).any(): - return np.nan - return np.median(x) - - self._check_stat_op('median', wrapper) - - def test_min(self): - self._check_stat_op('min', np.min) - - def test_max(self): - self._check_stat_op('max', np.max) - - def test_skew(self): - try: - from scipy.stats import skew - except ImportError: - pytest.skip("no scipy.stats.skew") - - def this_skew(x): - if len(x) < 3: - return np.nan - return skew(x, bias=False) - - self._check_stat_op('skew', this_skew) - - # def test_mad(self): - # f = lambda x: np.abs(x - x.mean()).mean() - # self._check_stat_op('mad', f) - - def test_var(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.var(x, ddof=1) - - self._check_stat_op('var', alt) - - def test_std(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.std(x, ddof=1) - - self._check_stat_op('std', alt) - - def test_sem(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.std(x, ddof=1) / np.sqrt(len(x)) - - self._check_stat_op('sem', alt) - - def _check_stat_op(self, name, alternative, obj=None, has_skipna=True): - if obj is None: - obj = self.panel - - # # set some NAs - # obj.loc[5:10] = np.nan - # obj.loc[15:20, -2:] = np.nan - - f = getattr(obj, name) - - if has_skipna: - - def skipna_wrapper(x): - nona = remove_na(x) - if len(nona) == 0: - return np.nan - return alternative(nona) - - def wrapper(x): - return alternative(np.asarray(x)) - - for i in range(obj.ndim): - result = f(axis=i, skipna=False) - assert_frame_equal(result, obj.apply(wrapper, axis=i)) - else: - skipna_wrapper = alternative - wrapper = alternative - - for i in range(obj.ndim): - result = f(axis=i) - if not tm._incompat_bottleneck_version(name): - assert_frame_equal(result, obj.apply(skipna_wrapper, axis=i)) - - self.assertRaises(Exception, f, axis=obj.ndim) - - # Unimplemented numeric_only parameter. - if 'numeric_only' in signature(f).args: - self.assertRaisesRegexp(NotImplementedError, name, f, - numeric_only=True) - - -class SafeForSparse(object): - - @classmethod - def assert_panel_equal(cls, x, y): - assert_panel_equal(x, y) - - def test_get_axis(self): - assert (self.panel._get_axis(0) is self.panel.items) - assert (self.panel._get_axis(1) is self.panel.major_axis) - assert (self.panel._get_axis(2) is self.panel.minor_axis) - - def test_set_axis(self): - new_items = Index(np.arange(len(self.panel.items))) - new_major = Index(np.arange(len(self.panel.major_axis))) - new_minor = Index(np.arange(len(self.panel.minor_axis))) - - # ensure propagate to potentially prior-cached items too - item = self.panel['ItemA'] - self.panel.items = new_items - - if hasattr(self.panel, '_item_cache'): - self.assertNotIn('ItemA', self.panel._item_cache) - self.assertIs(self.panel.items, new_items) - - # TODO: unused? - item = self.panel[0] # noqa - - self.panel.major_axis = new_major - self.assertIs(self.panel[0].index, new_major) - self.assertIs(self.panel.major_axis, new_major) - - # TODO: unused? - item = self.panel[0] # noqa - - self.panel.minor_axis = new_minor - self.assertIs(self.panel[0].columns, new_minor) - self.assertIs(self.panel.minor_axis, new_minor) - - def test_get_axis_number(self): - self.assertEqual(self.panel._get_axis_number('items'), 0) - self.assertEqual(self.panel._get_axis_number('major'), 1) - self.assertEqual(self.panel._get_axis_number('minor'), 2) - - with tm.assertRaisesRegexp(ValueError, "No axis named foo"): - self.panel._get_axis_number('foo') - - with tm.assertRaisesRegexp(ValueError, "No axis named foo"): - self.panel.__ge__(self.panel, axis='foo') - - def test_get_axis_name(self): - self.assertEqual(self.panel._get_axis_name(0), 'items') - self.assertEqual(self.panel._get_axis_name(1), 'major_axis') - self.assertEqual(self.panel._get_axis_name(2), 'minor_axis') - - def test_get_plane_axes(self): - # what to do here? - - index, columns = self.panel._get_plane_axes('items') - index, columns = self.panel._get_plane_axes('major_axis') - index, columns = self.panel._get_plane_axes('minor_axis') - index, columns = self.panel._get_plane_axes(0) - - def test_truncate(self): - dates = self.panel.major_axis - start, end = dates[1], dates[5] - - trunced = self.panel.truncate(start, end, axis='major') - expected = self.panel['ItemA'].truncate(start, end) - - assert_frame_equal(trunced['ItemA'], expected) - - trunced = self.panel.truncate(before=start, axis='major') - expected = self.panel['ItemA'].truncate(before=start) - - assert_frame_equal(trunced['ItemA'], expected) - - trunced = self.panel.truncate(after=end, axis='major') - expected = self.panel['ItemA'].truncate(after=end) - - assert_frame_equal(trunced['ItemA'], expected) - - # XXX test other axes - - def test_arith(self): - self._test_op(self.panel, operator.add) - self._test_op(self.panel, operator.sub) - self._test_op(self.panel, operator.mul) - self._test_op(self.panel, operator.truediv) - self._test_op(self.panel, operator.floordiv) - self._test_op(self.panel, operator.pow) - - self._test_op(self.panel, lambda x, y: y + x) - self._test_op(self.panel, lambda x, y: y - x) - self._test_op(self.panel, lambda x, y: y * x) - self._test_op(self.panel, lambda x, y: y / x) - self._test_op(self.panel, lambda x, y: y ** x) - - self._test_op(self.panel, lambda x, y: x + y) # panel + 1 - self._test_op(self.panel, lambda x, y: x - y) # panel - 1 - self._test_op(self.panel, lambda x, y: x * y) # panel * 1 - self._test_op(self.panel, lambda x, y: x / y) # panel / 1 - self._test_op(self.panel, lambda x, y: x ** y) # panel ** 1 - - self.assertRaises(Exception, self.panel.__add__, self.panel['ItemA']) - - @staticmethod - def _test_op(panel, op): - result = op(panel, 1) - assert_frame_equal(result['ItemA'], op(panel['ItemA'], 1)) - - def test_keys(self): - tm.equalContents(list(self.panel.keys()), self.panel.items) - - def test_iteritems(self): - # Test panel.iteritems(), aka panel.iteritems() - # just test that it works - for k, v in self.panel.iteritems(): - pass - - self.assertEqual(len(list(self.panel.iteritems())), - len(self.panel.items)) - - def test_combineFrame(self): - def check_op(op, name): - # items - df = self.panel['ItemA'] - - func = getattr(self.panel, name) - - result = func(df, axis='items') - - assert_frame_equal(result['ItemB'], op(self.panel['ItemB'], df)) - - # major - xs = self.panel.major_xs(self.panel.major_axis[0]) - result = func(xs, axis='major') - - idx = self.panel.major_axis[1] - - assert_frame_equal(result.major_xs(idx), - op(self.panel.major_xs(idx), xs)) - - # minor - xs = self.panel.minor_xs(self.panel.minor_axis[0]) - result = func(xs, axis='minor') - - idx = self.panel.minor_axis[1] - - assert_frame_equal(result.minor_xs(idx), - op(self.panel.minor_xs(idx), xs)) - - ops = ['add', 'sub', 'mul', 'truediv', 'floordiv', 'pow', 'mod'] - if not compat.PY3: - ops.append('div') - - for op in ops: - try: - check_op(getattr(operator, op), op) - except: - pprint_thing("Failing operation: %r" % op) - raise - if compat.PY3: - try: - check_op(operator.truediv, 'div') - except: - pprint_thing("Failing operation: %r" % 'div') - raise - - def test_combinePanel(self): - result = self.panel.add(self.panel) - self.assert_panel_equal(result, self.panel * 2) - - def test_neg(self): - self.assert_panel_equal(-self.panel, self.panel * -1) - - # issue 7692 - def test_raise_when_not_implemented(self): - p = Panel(np.arange(3 * 4 * 5).reshape(3, 4, 5), - items=['ItemA', 'ItemB', 'ItemC'], - major_axis=pd.date_range('20130101', periods=4), - minor_axis=list('ABCDE')) - d = p.sum(axis=1).iloc[0] - ops = ['add', 'sub', 'mul', 'truediv', 'floordiv', 'div', 'mod', 'pow'] - for op in ops: - with self.assertRaises(NotImplementedError): - getattr(p, op)(d, axis=0) - - def test_select(self): - p = self.panel - - # select items - result = p.select(lambda x: x in ('ItemA', 'ItemC'), axis='items') - expected = p.reindex(items=['ItemA', 'ItemC']) - self.assert_panel_equal(result, expected) - - # select major_axis - result = p.select(lambda x: x >= datetime(2000, 1, 15), axis='major') - new_major = p.major_axis[p.major_axis >= datetime(2000, 1, 15)] - expected = p.reindex(major=new_major) - self.assert_panel_equal(result, expected) - - # select minor_axis - result = p.select(lambda x: x in ('D', 'A'), axis=2) - expected = p.reindex(minor=['A', 'D']) - self.assert_panel_equal(result, expected) - - # corner case, empty thing - result = p.select(lambda x: x in ('foo', ), axis='items') - self.assert_panel_equal(result, p.reindex(items=[])) - - def test_get_value(self): - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: - result = self.panel.get_value(item, mjr, mnr) - expected = self.panel[item][mnr][mjr] - assert_almost_equal(result, expected) - - def test_abs(self): - - result = self.panel.abs() - result2 = abs(self.panel) - expected = np.abs(self.panel) - self.assert_panel_equal(result, expected) - self.assert_panel_equal(result2, expected) - - df = self.panel['ItemA'] - result = df.abs() - result2 = abs(df) - expected = np.abs(df) - assert_frame_equal(result, expected) - assert_frame_equal(result2, expected) - - s = df['A'] - result = s.abs() - result2 = abs(s) - expected = np.abs(s) - assert_series_equal(result, expected) - assert_series_equal(result2, expected) - self.assertEqual(result.name, 'A') - self.assertEqual(result2.name, 'A') - - -class CheckIndexing(object): - - def test_getitem(self): - self.assertRaises(Exception, self.panel.__getitem__, 'ItemQ') - - def test_delitem_and_pop(self): - expected = self.panel['ItemA'] - result = self.panel.pop('ItemA') - assert_frame_equal(expected, result) - self.assertNotIn('ItemA', self.panel.items) - - del self.panel['ItemB'] - self.assertNotIn('ItemB', self.panel.items) - self.assertRaises(Exception, self.panel.__delitem__, 'ItemB') - - values = np.empty((3, 3, 3)) - values[0] = 0 - values[1] = 1 - values[2] = 2 - - panel = Panel(values, lrange(3), lrange(3), lrange(3)) - - # did we delete the right row? - - panelc = panel.copy() - del panelc[0] - assert_frame_equal(panelc[1], panel[1]) - assert_frame_equal(panelc[2], panel[2]) - - panelc = panel.copy() - del panelc[1] - assert_frame_equal(panelc[0], panel[0]) - assert_frame_equal(panelc[2], panel[2]) - - panelc = panel.copy() - del panelc[2] - assert_frame_equal(panelc[1], panel[1]) - assert_frame_equal(panelc[0], panel[0]) - - def test_setitem(self): - # LongPanel with one item - lp = self.panel.filter(['ItemA', 'ItemB']).to_frame() - with tm.assertRaises(ValueError): - self.panel['ItemE'] = lp - - # DataFrame - df = self.panel['ItemA'][2:].filter(items=['A', 'B']) - self.panel['ItemF'] = df - self.panel['ItemE'] = df - - df2 = self.panel['ItemF'] - - assert_frame_equal(df, df2.reindex(index=df.index, columns=df.columns)) - - # scalar - self.panel['ItemG'] = 1 - self.panel['ItemE'] = True - self.assertEqual(self.panel['ItemG'].values.dtype, np.int64) - self.assertEqual(self.panel['ItemE'].values.dtype, np.bool_) - - # object dtype - self.panel['ItemQ'] = 'foo' - self.assertEqual(self.panel['ItemQ'].values.dtype, np.object_) - - # boolean dtype - self.panel['ItemP'] = self.panel['ItemA'] > 0 - self.assertEqual(self.panel['ItemP'].values.dtype, np.bool_) - - self.assertRaises(TypeError, self.panel.__setitem__, 'foo', - self.panel.loc[['ItemP']]) - - # bad shape - p = Panel(np.random.randn(4, 3, 2)) - with tm.assertRaisesRegexp(ValueError, - r"shape of value must be \(3, 2\), " - r"shape of given object was \(4, 2\)"): - p[0] = np.random.randn(4, 2) - - def test_setitem_ndarray(self): - timeidx = date_range(start=datetime(2009, 1, 1), - end=datetime(2009, 12, 31), - freq=MonthEnd()) - lons_coarse = np.linspace(-177.5, 177.5, 72) - lats_coarse = np.linspace(-87.5, 87.5, 36) - P = Panel(items=timeidx, major_axis=lons_coarse, - minor_axis=lats_coarse) - data = np.random.randn(72 * 36).reshape((72, 36)) - key = datetime(2009, 2, 28) - P[key] = data - - assert_almost_equal(P[key].values, data) - - def test_set_minor_major(self): - # GH 11014 - df1 = DataFrame(['a', 'a', 'a', np.nan, 'a', np.nan]) - df2 = DataFrame([1.0, np.nan, 1.0, np.nan, 1.0, 1.0]) - panel = Panel({'Item1': df1, 'Item2': df2}) - - newminor = notnull(panel.iloc[:, :, 0]) - panel.loc[:, :, 'NewMinor'] = newminor - assert_frame_equal(panel.loc[:, :, 'NewMinor'], - newminor.astype(object)) - - newmajor = notnull(panel.iloc[:, 0, :]) - panel.loc[:, 'NewMajor', :] = newmajor - assert_frame_equal(panel.loc[:, 'NewMajor', :], - newmajor.astype(object)) - - def test_major_xs(self): - ref = self.panel['ItemA'] - - idx = self.panel.major_axis[5] - xs = self.panel.major_xs(idx) - - result = xs['ItemA'] - assert_series_equal(result, ref.xs(idx), check_names=False) - self.assertEqual(result.name, 'ItemA') - - # not contained - idx = self.panel.major_axis[0] - BDay() - self.assertRaises(Exception, self.panel.major_xs, idx) - - def test_major_xs_mixed(self): - self.panel['ItemD'] = 'foo' - xs = self.panel.major_xs(self.panel.major_axis[0]) - self.assertEqual(xs['ItemA'].dtype, np.float64) - self.assertEqual(xs['ItemD'].dtype, np.object_) - - def test_minor_xs(self): - ref = self.panel['ItemA'] - - idx = self.panel.minor_axis[1] - xs = self.panel.minor_xs(idx) - - assert_series_equal(xs['ItemA'], ref[idx], check_names=False) - - # not contained - self.assertRaises(Exception, self.panel.minor_xs, 'E') - - def test_minor_xs_mixed(self): - self.panel['ItemD'] = 'foo' - - xs = self.panel.minor_xs('D') - self.assertEqual(xs['ItemA'].dtype, np.float64) - self.assertEqual(xs['ItemD'].dtype, np.object_) - - def test_xs(self): - itemA = self.panel.xs('ItemA', axis=0) - expected = self.panel['ItemA'] - assert_frame_equal(itemA, expected) - - # get a view by default - itemA_view = self.panel.xs('ItemA', axis=0) - itemA_view.values[:] = np.nan - self.assertTrue(np.isnan(self.panel['ItemA'].values).all()) - - # mixed-type yields a copy - self.panel['strings'] = 'foo' - result = self.panel.xs('D', axis=2) - self.assertIsNotNone(result.is_copy) - - def test_getitem_fancy_labels(self): - p = self.panel - - items = p.items[[1, 0]] - dates = p.major_axis[::2] - cols = ['D', 'C', 'F'] - - # all 3 specified - assert_panel_equal(p.loc[items, dates, cols], - p.reindex(items=items, major=dates, minor=cols)) - - # 2 specified - assert_panel_equal(p.loc[:, dates, cols], - p.reindex(major=dates, minor=cols)) - - assert_panel_equal(p.loc[items, :, cols], - p.reindex(items=items, minor=cols)) - - assert_panel_equal(p.loc[items, dates, :], - p.reindex(items=items, major=dates)) - - # only 1 - assert_panel_equal(p.loc[items, :, :], p.reindex(items=items)) - - assert_panel_equal(p.loc[:, dates, :], p.reindex(major=dates)) - - assert_panel_equal(p.loc[:, :, cols], p.reindex(minor=cols)) - - def test_getitem_fancy_slice(self): - pass - - def test_getitem_fancy_ints(self): - p = self.panel - - # #1603 - result = p.iloc[:, -1, :] - expected = p.loc[:, p.major_axis[-1], :] - assert_frame_equal(result, expected) - - def test_getitem_fancy_xs(self): - p = self.panel - item = 'ItemB' - - date = p.major_axis[5] - col = 'C' - - # get DataFrame - # item - assert_frame_equal(p.loc[item], p[item]) - assert_frame_equal(p.loc[item, :], p[item]) - assert_frame_equal(p.loc[item, :, :], p[item]) - - # major axis, axis=1 - assert_frame_equal(p.loc[:, date], p.major_xs(date)) - assert_frame_equal(p.loc[:, date, :], p.major_xs(date)) - - # minor axis, axis=2 - assert_frame_equal(p.loc[:, :, 'C'], p.minor_xs('C')) - - # get Series - assert_series_equal(p.loc[item, date], p[item].loc[date]) - assert_series_equal(p.loc[item, date, :], p[item].loc[date]) - assert_series_equal(p.loc[item, :, col], p[item][col]) - assert_series_equal(p.loc[:, date, col], p.major_xs(date).loc[col]) - - def test_getitem_fancy_xs_check_view(self): - item = 'ItemB' - date = self.panel.major_axis[5] - - # make sure it's always a view - NS = slice(None, None) - - # DataFrames - comp = assert_frame_equal - self._check_view(item, comp) - self._check_view((item, NS), comp) - self._check_view((item, NS, NS), comp) - self._check_view((NS, date), comp) - self._check_view((NS, date, NS), comp) - self._check_view((NS, NS, 'C'), comp) - - # Series - comp = assert_series_equal - self._check_view((item, date), comp) - self._check_view((item, date, NS), comp) - self._check_view((item, NS, 'C'), comp) - self._check_view((NS, date, 'C'), comp) - - def test_getitem_callable(self): - p = self.panel - # GH 12533 - - assert_frame_equal(p[lambda x: 'ItemB'], p.loc['ItemB']) - assert_panel_equal(p[lambda x: ['ItemB', 'ItemC']], - p.loc[['ItemB', 'ItemC']]) - - def test_ix_setitem_slice_dataframe(self): - a = Panel(items=[1, 2, 3], major_axis=[11, 22, 33], - minor_axis=[111, 222, 333]) - b = DataFrame(np.random.randn(2, 3), index=[111, 333], - columns=[1, 2, 3]) - - a.loc[:, 22, [111, 333]] = b - - assert_frame_equal(a.loc[:, 22, [111, 333]], b) - - def test_ix_align(self): - from pandas import Series - b = Series(np.random.randn(10), name=0) - b.sort_values() - df_orig = Panel(np.random.randn(3, 10, 2)) - df = df_orig.copy() - - df.loc[0, :, 0] = b - assert_series_equal(df.loc[0, :, 0].reindex(b.index), b) - - df = df_orig.swapaxes(0, 1) - df.loc[:, 0, 0] = b - assert_series_equal(df.loc[:, 0, 0].reindex(b.index), b) - - df = df_orig.swapaxes(1, 2) - df.loc[0, 0, :] = b - assert_series_equal(df.loc[0, 0, :].reindex(b.index), b) - - def test_ix_frame_align(self): - p_orig = tm.makePanel() - df = p_orig.iloc[0].copy() - assert_frame_equal(p_orig['ItemA'], df) - - p = p_orig.copy() - p.iloc[0, :, :] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.iloc[0] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.iloc[0, :, :] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.iloc[0] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.loc['ItemA'] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.loc['ItemA', :, :] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p['ItemA'] = df - assert_panel_equal(p, p_orig) - - p = p_orig.copy() - p.iloc[0, [0, 1, 3, 5], -2:] = df - out = p.iloc[0, [0, 1, 3, 5], -2:] - assert_frame_equal(out, df.iloc[[0, 1, 3, 5], [2, 3]]) - - # GH3830, panel assignent by values/frame - for dtype in ['float64', 'int64']: - - panel = Panel(np.arange(40).reshape((2, 4, 5)), - items=['a1', 'a2'], dtype=dtype) - df1 = panel.iloc[0] - df2 = panel.iloc[1] - - tm.assert_frame_equal(panel.loc['a1'], df1) - tm.assert_frame_equal(panel.loc['a2'], df2) - - # Assignment by Value Passes for 'a2' - panel.loc['a2'] = df1.values - tm.assert_frame_equal(panel.loc['a1'], df1) - tm.assert_frame_equal(panel.loc['a2'], df1) - - # Assignment by DataFrame Ok w/o loc 'a2' - panel['a2'] = df2 - tm.assert_frame_equal(panel.loc['a1'], df1) - tm.assert_frame_equal(panel.loc['a2'], df2) - - # Assignment by DataFrame Fails for 'a2' - panel.loc['a2'] = df2 - tm.assert_frame_equal(panel.loc['a1'], df1) - tm.assert_frame_equal(panel.loc['a2'], df2) - - def _check_view(self, indexer, comp): - cp = self.panel.copy() - obj = cp.loc[indexer] - obj.values[:] = 0 - self.assertTrue((obj.values == 0).all()) - comp(cp.loc[indexer].reindex_like(obj), obj) - - def test_logical_with_nas(self): - d = Panel({'ItemA': {'a': [np.nan, False]}, - 'ItemB': {'a': [True, True]}}) - - result = d['ItemA'] | d['ItemB'] - expected = DataFrame({'a': [np.nan, True]}) - assert_frame_equal(result, expected) - - # this is autodowncasted here - result = d['ItemA'].fillna(False) | d['ItemB'] - expected = DataFrame({'a': [True, True]}) - assert_frame_equal(result, expected) - - def test_neg(self): - # what to do? - assert_panel_equal(-self.panel, -1 * self.panel) - - def test_invert(self): - assert_panel_equal(-(self.panel < 0), ~(self.panel < 0)) - - def test_comparisons(self): - p1 = tm.makePanel() - p2 = tm.makePanel() - - tp = p1.reindex(items=p1.items + ['foo']) - df = p1[p1.items[0]] - - def test_comp(func): - - # versus same index - result = func(p1, p2) - self.assert_numpy_array_equal(result.values, - func(p1.values, p2.values)) - - # versus non-indexed same objs - self.assertRaises(Exception, func, p1, tp) - - # versus different objs - self.assertRaises(Exception, func, p1, df) - - # versus scalar - result3 = func(self.panel, 0) - self.assert_numpy_array_equal(result3.values, - func(self.panel.values, 0)) - - with np.errstate(invalid='ignore'): - test_comp(operator.eq) - test_comp(operator.ne) - test_comp(operator.lt) - test_comp(operator.gt) - test_comp(operator.ge) - test_comp(operator.le) - - def test_get_value(self): - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: - result = self.panel.get_value(item, mjr, mnr) - expected = self.panel[item][mnr][mjr] - assert_almost_equal(result, expected) - with tm.assertRaisesRegexp(TypeError, - "There must be an argument for each axis"): - self.panel.get_value('a') - - def test_set_value(self): - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: - self.panel.set_value(item, mjr, mnr, 1.) - assert_almost_equal(self.panel[item][mnr][mjr], 1.) - - # resize - res = self.panel.set_value('ItemE', 'foo', 'bar', 1.5) - tm.assertIsInstance(res, Panel) - self.assertIsNot(res, self.panel) - self.assertEqual(res.get_value('ItemE', 'foo', 'bar'), 1.5) - - res3 = self.panel.set_value('ItemE', 'foobar', 'baz', 5) - self.assertTrue(is_float_dtype(res3['ItemE'].values)) - with tm.assertRaisesRegexp(TypeError, - "There must be an argument for each axis" - " plus the value provided"): - self.panel.set_value('a') - - -_panel = tm.makePanel() -tm.add_nans(_panel) - - -class TestPanel(tm.TestCase, PanelTests, CheckIndexing, SafeForLongAndSparse, - SafeForSparse): - - @classmethod - def assert_panel_equal(cls, x, y): - assert_panel_equal(x, y) - - def setUp(self): - self.panel = _panel.copy() - self.panel.major_axis.name = None - self.panel.minor_axis.name = None - self.panel.items.name = None - - def test_constructor(self): - # with BlockManager - wp = Panel(self.panel._data) - self.assertIs(wp._data, self.panel._data) - - wp = Panel(self.panel._data, copy=True) - self.assertIsNot(wp._data, self.panel._data) - assert_panel_equal(wp, self.panel) - - # strings handled prop - wp = Panel([[['foo', 'foo', 'foo', ], ['foo', 'foo', 'foo']]]) - self.assertEqual(wp.values.dtype, np.object_) - - vals = self.panel.values - - # no copy - wp = Panel(vals) - self.assertIs(wp.values, vals) - - # copy - wp = Panel(vals, copy=True) - self.assertIsNot(wp.values, vals) - - # GH #8285, test when scalar data is used to construct a Panel - # if dtype is not passed, it should be inferred - value_and_dtype = [(1, 'int64'), (3.14, 'float64'), - ('foo', np.object_)] - for (val, dtype) in value_and_dtype: - wp = Panel(val, items=range(2), major_axis=range(3), - minor_axis=range(4)) - vals = np.empty((2, 3, 4), dtype=dtype) - vals.fill(val) - assert_panel_equal(wp, Panel(vals, dtype=dtype)) - - # test the case when dtype is passed - wp = Panel(1, items=range(2), major_axis=range(3), minor_axis=range(4), - dtype='float32') - vals = np.empty((2, 3, 4), dtype='float32') - vals.fill(1) - assert_panel_equal(wp, Panel(vals, dtype='float32')) - - def test_constructor_cast(self): - zero_filled = self.panel.fillna(0) - - casted = Panel(zero_filled._data, dtype=int) - casted2 = Panel(zero_filled.values, dtype=int) - - exp_values = zero_filled.values.astype(int) - assert_almost_equal(casted.values, exp_values) - assert_almost_equal(casted2.values, exp_values) - - casted = Panel(zero_filled._data, dtype=np.int32) - casted2 = Panel(zero_filled.values, dtype=np.int32) - - exp_values = zero_filled.values.astype(np.int32) - assert_almost_equal(casted.values, exp_values) - assert_almost_equal(casted2.values, exp_values) - - # can't cast - data = [[['foo', 'bar', 'baz']]] - self.assertRaises(ValueError, Panel, data, dtype=float) - - def test_constructor_empty_panel(self): - empty = Panel() - self.assertEqual(len(empty.items), 0) - self.assertEqual(len(empty.major_axis), 0) - self.assertEqual(len(empty.minor_axis), 0) - - def test_constructor_observe_dtype(self): - # GH #411 - panel = Panel(items=lrange(3), major_axis=lrange(3), - minor_axis=lrange(3), dtype='O') - self.assertEqual(panel.values.dtype, np.object_) - - def test_constructor_dtypes(self): - # GH #797 - - def _check_dtype(panel, dtype): - for i in panel.items: - self.assertEqual(panel[i].values.dtype.name, dtype) - - # only nan holding types allowed here - for dtype in ['float64', 'float32', 'object']: - panel = Panel(items=lrange(2), major_axis=lrange(10), - minor_axis=lrange(5), dtype=dtype) - _check_dtype(panel, dtype) - - for dtype in ['float64', 'float32', 'int64', 'int32', 'object']: - panel = Panel(np.array(np.random.randn(2, 10, 5), dtype=dtype), - items=lrange(2), - major_axis=lrange(10), - minor_axis=lrange(5), dtype=dtype) - _check_dtype(panel, dtype) - - for dtype in ['float64', 'float32', 'int64', 'int32', 'object']: - panel = Panel(np.array(np.random.randn(2, 10, 5), dtype='O'), - items=lrange(2), - major_axis=lrange(10), - minor_axis=lrange(5), dtype=dtype) - _check_dtype(panel, dtype) - - for dtype in ['float64', 'float32', 'int64', 'int32', 'object']: - panel = Panel(np.random.randn(2, 10, 5), items=lrange( - 2), major_axis=lrange(10), minor_axis=lrange(5), dtype=dtype) - _check_dtype(panel, dtype) - - for dtype in ['float64', 'float32', 'int64', 'int32', 'object']: - df1 = DataFrame(np.random.randn(2, 5), - index=lrange(2), columns=lrange(5)) - df2 = DataFrame(np.random.randn(2, 5), - index=lrange(2), columns=lrange(5)) - panel = Panel.from_dict({'a': df1, 'b': df2}, dtype=dtype) - _check_dtype(panel, dtype) - - def test_constructor_fails_with_not_3d_input(self): - with tm.assertRaisesRegexp(ValueError, - "The number of dimensions required is 3"): - Panel(np.random.randn(10, 2)) - - def test_consolidate(self): - self.assertTrue(self.panel._data.is_consolidated()) - - self.panel['foo'] = 1. - self.assertFalse(self.panel._data.is_consolidated()) - - panel = self.panel._consolidate() - self.assertTrue(panel._data.is_consolidated()) - - def test_ctor_dict(self): - itema = self.panel['ItemA'] - itemb = self.panel['ItemB'] - - d = {'A': itema, 'B': itemb[5:]} - d2 = {'A': itema._series, 'B': itemb[5:]._series} - d3 = {'A': None, - 'B': DataFrame(itemb[5:]._series), - 'C': DataFrame(itema._series)} - - wp = Panel.from_dict(d) - wp2 = Panel.from_dict(d2) # nested Dict - - # TODO: unused? - wp3 = Panel.from_dict(d3) # noqa - - self.assert_index_equal(wp.major_axis, self.panel.major_axis) - assert_panel_equal(wp, wp2) - - # intersect - wp = Panel.from_dict(d, intersect=True) - self.assert_index_equal(wp.major_axis, itemb.index[5:]) - - # use constructor - assert_panel_equal(Panel(d), Panel.from_dict(d)) - assert_panel_equal(Panel(d2), Panel.from_dict(d2)) - assert_panel_equal(Panel(d3), Panel.from_dict(d3)) - - # a pathological case - d4 = {'A': None, 'B': None} - - # TODO: unused? - wp4 = Panel.from_dict(d4) # noqa - - assert_panel_equal(Panel(d4), Panel(items=['A', 'B'])) - - # cast - dcasted = dict((k, v.reindex(wp.major_axis).fillna(0)) - for k, v in compat.iteritems(d)) - result = Panel(dcasted, dtype=int) - expected = Panel(dict((k, v.astype(int)) - for k, v in compat.iteritems(dcasted))) - assert_panel_equal(result, expected) - - result = Panel(dcasted, dtype=np.int32) - expected = Panel(dict((k, v.astype(np.int32)) - for k, v in compat.iteritems(dcasted))) - assert_panel_equal(result, expected) - - def test_constructor_dict_mixed(self): - data = dict((k, v.values) for k, v in self.panel.iteritems()) - result = Panel(data) - exp_major = Index(np.arange(len(self.panel.major_axis))) - self.assert_index_equal(result.major_axis, exp_major) - - result = Panel(data, items=self.panel.items, - major_axis=self.panel.major_axis, - minor_axis=self.panel.minor_axis) - assert_panel_equal(result, self.panel) - - data['ItemC'] = self.panel['ItemC'] - result = Panel(data) - assert_panel_equal(result, self.panel) - - # corner, blow up - data['ItemB'] = data['ItemB'][:-1] - self.assertRaises(Exception, Panel, data) - - data['ItemB'] = self.panel['ItemB'].values[:, :-1] - self.assertRaises(Exception, Panel, data) - - def test_ctor_orderedDict(self): - keys = list(set(np.random.randint(0, 5000, 100)))[ - :50] # unique random int keys - d = OrderedDict([(k, mkdf(10, 5)) for k in keys]) - p = Panel(d) - self.assertTrue(list(p.items) == keys) - - p = Panel.from_dict(d) - self.assertTrue(list(p.items) == keys) - - def test_constructor_resize(self): - data = self.panel._data - items = self.panel.items[:-1] - major = self.panel.major_axis[:-1] - minor = self.panel.minor_axis[:-1] - - result = Panel(data, items=items, major_axis=major, minor_axis=minor) - expected = self.panel.reindex(items=items, major=major, minor=minor) - assert_panel_equal(result, expected) - - result = Panel(data, items=items, major_axis=major) - expected = self.panel.reindex(items=items, major=major) - assert_panel_equal(result, expected) - - result = Panel(data, items=items) - expected = self.panel.reindex(items=items) - assert_panel_equal(result, expected) - - result = Panel(data, minor_axis=minor) - expected = self.panel.reindex(minor=minor) - assert_panel_equal(result, expected) - - def test_from_dict_mixed_orient(self): - df = tm.makeDataFrame() - df['foo'] = 'bar' - - data = {'k1': df, 'k2': df} - - panel = Panel.from_dict(data, orient='minor') - - self.assertEqual(panel['foo'].values.dtype, np.object_) - self.assertEqual(panel['A'].values.dtype, np.float64) - - def test_constructor_error_msgs(self): - def testit(): - Panel(np.random.randn(3, 4, 5), lrange(4), lrange(5), lrange(5)) - - assertRaisesRegexp(ValueError, - r"Shape of passed values is \(3, 4, 5\), " - r"indices imply \(4, 5, 5\)", - testit) - - def testit(): - Panel(np.random.randn(3, 4, 5), lrange(5), lrange(4), lrange(5)) - - assertRaisesRegexp(ValueError, - r"Shape of passed values is \(3, 4, 5\), " - r"indices imply \(5, 4, 5\)", - testit) - - def testit(): - Panel(np.random.randn(3, 4, 5), lrange(5), lrange(5), lrange(4)) - - assertRaisesRegexp(ValueError, - r"Shape of passed values is \(3, 4, 5\), " - r"indices imply \(5, 5, 4\)", - testit) - - def test_conform(self): - df = self.panel['ItemA'][:-5].filter(items=['A', 'B']) - conformed = self.panel.conform(df) - - tm.assert_index_equal(conformed.index, self.panel.major_axis) - tm.assert_index_equal(conformed.columns, self.panel.minor_axis) - - def test_convert_objects(self): - - # GH 4937 - p = Panel(dict(A=dict(a=['1', '1.0']))) - expected = Panel(dict(A=dict(a=[1, 1.0]))) - result = p._convert(numeric=True, coerce=True) - assert_panel_equal(result, expected) - - def test_dtypes(self): - - result = self.panel.dtypes - expected = Series(np.dtype('float64'), index=self.panel.items) - assert_series_equal(result, expected) - - def test_astype(self): - # GH7271 - data = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]) - panel = Panel(data, ['a', 'b'], ['c', 'd'], ['e', 'f']) - - str_data = np.array([[['1', '2'], ['3', '4']], - [['5', '6'], ['7', '8']]]) - expected = Panel(str_data, ['a', 'b'], ['c', 'd'], ['e', 'f']) - assert_panel_equal(panel.astype(str), expected) - - self.assertRaises(NotImplementedError, panel.astype, {0: str}) - - def test_apply(self): - # GH1148 - - # ufunc - applied = self.panel.apply(np.sqrt) - with np.errstate(invalid='ignore'): - expected = np.sqrt(self.panel.values) - assert_almost_equal(applied.values, expected) - - # ufunc same shape - result = self.panel.apply(lambda x: x * 2, axis='items') - expected = self.panel * 2 - assert_panel_equal(result, expected) - result = self.panel.apply(lambda x: x * 2, axis='major_axis') - expected = self.panel * 2 - assert_panel_equal(result, expected) - result = self.panel.apply(lambda x: x * 2, axis='minor_axis') - expected = self.panel * 2 - assert_panel_equal(result, expected) - - # reduction to DataFrame - result = self.panel.apply(lambda x: x.dtype, axis='items') - expected = DataFrame(np.dtype('float64'), index=self.panel.major_axis, - columns=self.panel.minor_axis) - assert_frame_equal(result, expected) - result = self.panel.apply(lambda x: x.dtype, axis='major_axis') - expected = DataFrame(np.dtype('float64'), index=self.panel.minor_axis, - columns=self.panel.items) - assert_frame_equal(result, expected) - result = self.panel.apply(lambda x: x.dtype, axis='minor_axis') - expected = DataFrame(np.dtype('float64'), index=self.panel.major_axis, - columns=self.panel.items) - assert_frame_equal(result, expected) - - # reductions via other dims - expected = self.panel.sum(0) - result = self.panel.apply(lambda x: x.sum(), axis='items') - assert_frame_equal(result, expected) - expected = self.panel.sum(1) - result = self.panel.apply(lambda x: x.sum(), axis='major_axis') - assert_frame_equal(result, expected) - expected = self.panel.sum(2) - result = self.panel.apply(lambda x: x.sum(), axis='minor_axis') - assert_frame_equal(result, expected) - - # pass kwargs - result = self.panel.apply(lambda x, y: x.sum() + y, axis='items', y=5) - expected = self.panel.sum(0) + 5 - assert_frame_equal(result, expected) - - def test_apply_slabs(self): - - # same shape as original - result = self.panel.apply(lambda x: x * 2, - axis=['items', 'major_axis']) - expected = (self.panel * 2).transpose('minor_axis', 'major_axis', - 'items') - assert_panel_equal(result, expected) - result = self.panel.apply(lambda x: x * 2, - axis=['major_axis', 'items']) - assert_panel_equal(result, expected) - - result = self.panel.apply(lambda x: x * 2, - axis=['items', 'minor_axis']) - expected = (self.panel * 2).transpose('major_axis', 'minor_axis', - 'items') - assert_panel_equal(result, expected) - result = self.panel.apply(lambda x: x * 2, - axis=['minor_axis', 'items']) - assert_panel_equal(result, expected) - - result = self.panel.apply(lambda x: x * 2, - axis=['major_axis', 'minor_axis']) - expected = self.panel * 2 - assert_panel_equal(result, expected) - result = self.panel.apply(lambda x: x * 2, - axis=['minor_axis', 'major_axis']) - assert_panel_equal(result, expected) - - # reductions - result = self.panel.apply(lambda x: x.sum(0), axis=[ - 'items', 'major_axis' - ]) - expected = self.panel.sum(1).T - assert_frame_equal(result, expected) - - result = self.panel.apply(lambda x: x.sum(1), axis=[ - 'items', 'major_axis' - ]) - expected = self.panel.sum(0) - assert_frame_equal(result, expected) - - # transforms - f = lambda x: ((x.T - x.mean(1)) / x.std(1)).T - - # make sure that we don't trigger any warnings - with catch_warnings(record=True): - result = self.panel.apply(f, axis=['items', 'major_axis']) - expected = Panel(dict([(ax, f(self.panel.loc[:, :, ax])) - for ax in self.panel.minor_axis])) - assert_panel_equal(result, expected) - - result = self.panel.apply(f, axis=['major_axis', 'minor_axis']) - expected = Panel(dict([(ax, f(self.panel.loc[ax])) - for ax in self.panel.items])) - assert_panel_equal(result, expected) - - result = self.panel.apply(f, axis=['minor_axis', 'items']) - expected = Panel(dict([(ax, f(self.panel.loc[:, ax])) - for ax in self.panel.major_axis])) - assert_panel_equal(result, expected) - - # with multi-indexes - # GH7469 - index = MultiIndex.from_tuples([('one', 'a'), ('one', 'b'), ( - 'two', 'a'), ('two', 'b')]) - dfa = DataFrame(np.array(np.arange(12, dtype='int64')).reshape( - 4, 3), columns=list("ABC"), index=index) - dfb = DataFrame(np.array(np.arange(10, 22, dtype='int64')).reshape( - 4, 3), columns=list("ABC"), index=index) - p = Panel({'f': dfa, 'g': dfb}) - result = p.apply(lambda x: x.sum(), axis=0) - - # on windows this will be in32 - result = result.astype('int64') - expected = p.sum(0) - assert_frame_equal(result, expected) - - def test_apply_no_or_zero_ndim(self): - # GH10332 - self.panel = Panel(np.random.rand(5, 5, 5)) - - result_int = self.panel.apply(lambda df: 0, axis=[1, 2]) - result_float = self.panel.apply(lambda df: 0.0, axis=[1, 2]) - result_int64 = self.panel.apply(lambda df: np.int64(0), axis=[1, 2]) - result_float64 = self.panel.apply(lambda df: np.float64(0.0), - axis=[1, 2]) - - expected_int = expected_int64 = Series([0] * 5) - expected_float = expected_float64 = Series([0.0] * 5) - - assert_series_equal(result_int, expected_int) - assert_series_equal(result_int64, expected_int64) - assert_series_equal(result_float, expected_float) - assert_series_equal(result_float64, expected_float64) - - def test_reindex(self): - ref = self.panel['ItemB'] - - # items - result = self.panel.reindex(items=['ItemA', 'ItemB']) - assert_frame_equal(result['ItemB'], ref) - - # major - new_major = list(self.panel.major_axis[:10]) - result = self.panel.reindex(major=new_major) - assert_frame_equal(result['ItemB'], ref.reindex(index=new_major)) - - # raise exception put both major and major_axis - self.assertRaises(Exception, self.panel.reindex, major_axis=new_major, - major=new_major) - - # minor - new_minor = list(self.panel.minor_axis[:2]) - result = self.panel.reindex(minor=new_minor) - assert_frame_equal(result['ItemB'], ref.reindex(columns=new_minor)) - - # this ok - result = self.panel.reindex() - assert_panel_equal(result, self.panel) - self.assertFalse(result is self.panel) - - # with filling - smaller_major = self.panel.major_axis[::5] - smaller = self.panel.reindex(major=smaller_major) - - larger = smaller.reindex(major=self.panel.major_axis, method='pad') - - assert_frame_equal(larger.major_xs(self.panel.major_axis[1]), - smaller.major_xs(smaller_major[0])) - - # don't necessarily copy - result = self.panel.reindex(major=self.panel.major_axis, copy=False) - assert_panel_equal(result, self.panel) - self.assertTrue(result is self.panel) - - def test_reindex_multi(self): - - # with and without copy full reindexing - result = self.panel.reindex(items=self.panel.items, - major=self.panel.major_axis, - minor=self.panel.minor_axis, copy=False) - - self.assertIs(result.items, self.panel.items) - self.assertIs(result.major_axis, self.panel.major_axis) - self.assertIs(result.minor_axis, self.panel.minor_axis) - - result = self.panel.reindex(items=self.panel.items, - major=self.panel.major_axis, - minor=self.panel.minor_axis, copy=False) - assert_panel_equal(result, self.panel) - - # multi-axis indexing consistency - # GH 5900 - df = DataFrame(np.random.randn(4, 3)) - p = Panel({'Item1': df}) - expected = Panel({'Item1': df}) - expected['Item2'] = np.nan - - items = ['Item1', 'Item2'] - major_axis = np.arange(4) - minor_axis = np.arange(3) - - results = [] - results.append(p.reindex(items=items, major_axis=major_axis, - copy=True)) - results.append(p.reindex(items=items, major_axis=major_axis, - copy=False)) - results.append(p.reindex(items=items, minor_axis=minor_axis, - copy=True)) - results.append(p.reindex(items=items, minor_axis=minor_axis, - copy=False)) - results.append(p.reindex(items=items, major_axis=major_axis, - minor_axis=minor_axis, copy=True)) - results.append(p.reindex(items=items, major_axis=major_axis, - minor_axis=minor_axis, copy=False)) - - for i, r in enumerate(results): - assert_panel_equal(expected, r) - - def test_reindex_like(self): - # reindex_like - smaller = self.panel.reindex(items=self.panel.items[:-1], - major=self.panel.major_axis[:-1], - minor=self.panel.minor_axis[:-1]) - smaller_like = self.panel.reindex_like(smaller) - assert_panel_equal(smaller, smaller_like) - - def test_take(self): - # axis == 0 - result = self.panel.take([2, 0, 1], axis=0) - expected = self.panel.reindex(items=['ItemC', 'ItemA', 'ItemB']) - assert_panel_equal(result, expected) - - # axis >= 1 - result = self.panel.take([3, 0, 1, 2], axis=2) - expected = self.panel.reindex(minor=['D', 'A', 'B', 'C']) - assert_panel_equal(result, expected) - - # neg indicies ok - expected = self.panel.reindex(minor=['D', 'D', 'B', 'C']) - result = self.panel.take([3, -1, 1, 2], axis=2) - assert_panel_equal(result, expected) - - self.assertRaises(Exception, self.panel.take, [4, 0, 1, 2], axis=2) - - def test_sort_index(self): - import random - - ritems = list(self.panel.items) - rmajor = list(self.panel.major_axis) - rminor = list(self.panel.minor_axis) - random.shuffle(ritems) - random.shuffle(rmajor) - random.shuffle(rminor) - - random_order = self.panel.reindex(items=ritems) - sorted_panel = random_order.sort_index(axis=0) - assert_panel_equal(sorted_panel, self.panel) - - # descending - random_order = self.panel.reindex(items=ritems) - sorted_panel = random_order.sort_index(axis=0, ascending=False) - assert_panel_equal(sorted_panel, - self.panel.reindex(items=self.panel.items[::-1])) - - random_order = self.panel.reindex(major=rmajor) - sorted_panel = random_order.sort_index(axis=1) - assert_panel_equal(sorted_panel, self.panel) - - random_order = self.panel.reindex(minor=rminor) - sorted_panel = random_order.sort_index(axis=2) - assert_panel_equal(sorted_panel, self.panel) - - def test_fillna(self): - filled = self.panel.fillna(0) - self.assertTrue(np.isfinite(filled.values).all()) - - filled = self.panel.fillna(method='backfill') - assert_frame_equal(filled['ItemA'], - self.panel['ItemA'].fillna(method='backfill')) - - panel = self.panel.copy() - panel['str'] = 'foo' - - filled = panel.fillna(method='backfill') - assert_frame_equal(filled['ItemA'], - panel['ItemA'].fillna(method='backfill')) - - empty = self.panel.reindex(items=[]) - filled = empty.fillna(0) - assert_panel_equal(filled, empty) - - self.assertRaises(ValueError, self.panel.fillna) - self.assertRaises(ValueError, self.panel.fillna, 5, method='ffill') - - self.assertRaises(TypeError, self.panel.fillna, [1, 2]) - self.assertRaises(TypeError, self.panel.fillna, (1, 2)) - - # limit not implemented when only value is specified - p = Panel(np.random.randn(3, 4, 5)) - p.iloc[0:2, 0:2, 0:2] = np.nan - self.assertRaises(NotImplementedError, lambda: p.fillna(999, limit=1)) - - # Test in place fillNA - # Expected result - expected = Panel([[[0, 1], [2, 1]], [[10, 11], [12, 11]]], - items=['a', 'b'], minor_axis=['x', 'y'], - dtype=np.float64) - # method='ffill' - p1 = Panel([[[0, 1], [2, np.nan]], [[10, 11], [12, np.nan]]], - items=['a', 'b'], minor_axis=['x', 'y'], - dtype=np.float64) - p1.fillna(method='ffill', inplace=True) - assert_panel_equal(p1, expected) - - # method='bfill' - p2 = Panel([[[0, np.nan], [2, 1]], [[10, np.nan], [12, 11]]], - items=['a', 'b'], minor_axis=['x', 'y'], dtype=np.float64) - p2.fillna(method='bfill', inplace=True) - assert_panel_equal(p2, expected) - - def test_ffill_bfill(self): - assert_panel_equal(self.panel.ffill(), - self.panel.fillna(method='ffill')) - assert_panel_equal(self.panel.bfill(), - self.panel.fillna(method='bfill')) - - def test_truncate_fillna_bug(self): - # #1823 - result = self.panel.truncate(before=None, after=None, axis='items') - - # it works! - result.fillna(value=0.0) - - def test_swapaxes(self): - result = self.panel.swapaxes('items', 'minor') - self.assertIs(result.items, self.panel.minor_axis) - - result = self.panel.swapaxes('items', 'major') - self.assertIs(result.items, self.panel.major_axis) - - result = self.panel.swapaxes('major', 'minor') - self.assertIs(result.major_axis, self.panel.minor_axis) - - panel = self.panel.copy() - result = panel.swapaxes('major', 'minor') - panel.values[0, 0, 1] = np.nan - expected = panel.swapaxes('major', 'minor') - assert_panel_equal(result, expected) - - # this should also work - result = self.panel.swapaxes(0, 1) - self.assertIs(result.items, self.panel.major_axis) - - # this works, but return a copy - result = self.panel.swapaxes('items', 'items') - assert_panel_equal(self.panel, result) - self.assertNotEqual(id(self.panel), id(result)) - - def test_transpose(self): - result = self.panel.transpose('minor', 'major', 'items') - expected = self.panel.swapaxes('items', 'minor') - assert_panel_equal(result, expected) - - # test kwargs - result = self.panel.transpose(items='minor', major='major', - minor='items') - expected = self.panel.swapaxes('items', 'minor') - assert_panel_equal(result, expected) - - # text mixture of args - result = self.panel.transpose('minor', major='major', minor='items') - expected = self.panel.swapaxes('items', 'minor') - assert_panel_equal(result, expected) - - result = self.panel.transpose('minor', 'major', minor='items') - expected = self.panel.swapaxes('items', 'minor') - assert_panel_equal(result, expected) - - # duplicate axes - with tm.assertRaisesRegexp(TypeError, - 'not enough/duplicate arguments'): - self.panel.transpose('minor', maj='major', minor='items') - - with tm.assertRaisesRegexp(ValueError, 'repeated axis in transpose'): - self.panel.transpose('minor', 'major', major='minor', - minor='items') - - result = self.panel.transpose(2, 1, 0) - assert_panel_equal(result, expected) - - result = self.panel.transpose('minor', 'items', 'major') - expected = self.panel.swapaxes('items', 'minor') - expected = expected.swapaxes('major', 'minor') - assert_panel_equal(result, expected) - - result = self.panel.transpose(2, 0, 1) - assert_panel_equal(result, expected) - - self.assertRaises(ValueError, self.panel.transpose, 0, 0, 1) - - def test_transpose_copy(self): - panel = self.panel.copy() - result = panel.transpose(2, 0, 1, copy=True) - expected = panel.swapaxes('items', 'minor') - expected = expected.swapaxes('major', 'minor') - assert_panel_equal(result, expected) - - panel.values[0, 1, 1] = np.nan - self.assertTrue(notnull(result.values[1, 0, 1])) - - def test_to_frame(self): - # filtered - filtered = self.panel.to_frame() - expected = self.panel.to_frame().dropna(how='any') - assert_frame_equal(filtered, expected) - - # unfiltered - unfiltered = self.panel.to_frame(filter_observations=False) - assert_panel_equal(unfiltered.to_panel(), self.panel) - - # names - self.assertEqual(unfiltered.index.names, ('major', 'minor')) - - # unsorted, round trip - df = self.panel.to_frame(filter_observations=False) - unsorted = df.take(np.random.permutation(len(df))) - pan = unsorted.to_panel() - assert_panel_equal(pan, self.panel) - - # preserve original index names - df = DataFrame(np.random.randn(6, 2), - index=[['a', 'a', 'b', 'b', 'c', 'c'], - [0, 1, 0, 1, 0, 1]], - columns=['one', 'two']) - df.index.names = ['foo', 'bar'] - df.columns.name = 'baz' - - rdf = df.to_panel().to_frame() - self.assertEqual(rdf.index.names, df.index.names) - self.assertEqual(rdf.columns.names, df.columns.names) - - def test_to_frame_mixed(self): - panel = self.panel.fillna(0) - panel['str'] = 'foo' - panel['bool'] = panel['ItemA'] > 0 - - lp = panel.to_frame() - wp = lp.to_panel() - self.assertEqual(wp['bool'].values.dtype, np.bool_) - # Previously, this was mutating the underlying index and changing its - # name - assert_frame_equal(wp['bool'], panel['bool'], check_names=False) - - # GH 8704 - # with categorical - df = panel.to_frame() - df['category'] = df['str'].astype('category') - - # to_panel - # TODO: this converts back to object - p = df.to_panel() - expected = panel.copy() - expected['category'] = 'foo' - assert_panel_equal(p, expected) - - def test_to_frame_multi_major(self): - idx = MultiIndex.from_tuples([(1, 'one'), (1, 'two'), (2, 'one'), ( - 2, 'two')]) - df = DataFrame([[1, 'a', 1], [2, 'b', 1], [3, 'c', 1], [4, 'd', 1]], - columns=['A', 'B', 'C'], index=idx) - wp = Panel({'i1': df, 'i2': df}) - expected_idx = MultiIndex.from_tuples( - [ - (1, 'one', 'A'), (1, 'one', 'B'), - (1, 'one', 'C'), (1, 'two', 'A'), - (1, 'two', 'B'), (1, 'two', 'C'), - (2, 'one', 'A'), (2, 'one', 'B'), - (2, 'one', 'C'), (2, 'two', 'A'), - (2, 'two', 'B'), (2, 'two', 'C') - ], - names=[None, None, 'minor']) - expected = DataFrame({'i1': [1, 'a', 1, 2, 'b', 1, 3, - 'c', 1, 4, 'd', 1], - 'i2': [1, 'a', 1, 2, 'b', - 1, 3, 'c', 1, 4, 'd', 1]}, - index=expected_idx) - result = wp.to_frame() - assert_frame_equal(result, expected) - - wp.iloc[0, 0].iloc[0] = np.nan # BUG on setting. GH #5773 - result = wp.to_frame() - assert_frame_equal(result, expected[1:]) - - idx = MultiIndex.from_tuples([(1, 'two'), (1, 'one'), (2, 'one'), ( - np.nan, 'two')]) - df = DataFrame([[1, 'a', 1], [2, 'b', 1], [3, 'c', 1], [4, 'd', 1]], - columns=['A', 'B', 'C'], index=idx) - wp = Panel({'i1': df, 'i2': df}) - ex_idx = MultiIndex.from_tuples([(1, 'two', 'A'), (1, 'two', 'B'), - (1, 'two', 'C'), - (1, 'one', 'A'), - (1, 'one', 'B'), - (1, 'one', 'C'), - (2, 'one', 'A'), - (2, 'one', 'B'), - (2, 'one', 'C'), - (np.nan, 'two', 'A'), - (np.nan, 'two', 'B'), - (np.nan, 'two', 'C')], - names=[None, None, 'minor']) - expected.index = ex_idx - result = wp.to_frame() - assert_frame_equal(result, expected) - - def test_to_frame_multi_major_minor(self): - cols = MultiIndex(levels=[['C_A', 'C_B'], ['C_1', 'C_2']], - labels=[[0, 0, 1, 1], [0, 1, 0, 1]]) - idx = MultiIndex.from_tuples([(1, 'one'), (1, 'two'), (2, 'one'), ( - 2, 'two'), (3, 'three'), (4, 'four')]) - df = DataFrame([[1, 2, 11, 12], [3, 4, 13, 14], - ['a', 'b', 'w', 'x'], - ['c', 'd', 'y', 'z'], [-1, -2, -3, -4], - [-5, -6, -7, -8]], columns=cols, index=idx) - wp = Panel({'i1': df, 'i2': df}) - - exp_idx = MultiIndex.from_tuples( - [(1, 'one', 'C_A', 'C_1'), (1, 'one', 'C_A', 'C_2'), - (1, 'one', 'C_B', 'C_1'), (1, 'one', 'C_B', 'C_2'), - (1, 'two', 'C_A', 'C_1'), (1, 'two', 'C_A', 'C_2'), - (1, 'two', 'C_B', 'C_1'), (1, 'two', 'C_B', 'C_2'), - (2, 'one', 'C_A', 'C_1'), (2, 'one', 'C_A', 'C_2'), - (2, 'one', 'C_B', 'C_1'), (2, 'one', 'C_B', 'C_2'), - (2, 'two', 'C_A', 'C_1'), (2, 'two', 'C_A', 'C_2'), - (2, 'two', 'C_B', 'C_1'), (2, 'two', 'C_B', 'C_2'), - (3, 'three', 'C_A', 'C_1'), (3, 'three', 'C_A', 'C_2'), - (3, 'three', 'C_B', 'C_1'), (3, 'three', 'C_B', 'C_2'), - (4, 'four', 'C_A', 'C_1'), (4, 'four', 'C_A', 'C_2'), - (4, 'four', 'C_B', 'C_1'), (4, 'four', 'C_B', 'C_2')], - names=[None, None, None, None]) - exp_val = [[1, 1], [2, 2], [11, 11], [12, 12], [3, 3], [4, 4], - [13, 13], [14, 14], ['a', 'a'], ['b', 'b'], ['w', 'w'], - ['x', 'x'], ['c', 'c'], ['d', 'd'], ['y', 'y'], ['z', 'z'], - [-1, -1], [-2, -2], [-3, -3], [-4, -4], [-5, -5], [-6, -6], - [-7, -7], [-8, -8]] - result = wp.to_frame() - expected = DataFrame(exp_val, columns=['i1', 'i2'], index=exp_idx) - assert_frame_equal(result, expected) - - def test_to_frame_multi_drop_level(self): - idx = MultiIndex.from_tuples([(1, 'one'), (2, 'one'), (2, 'two')]) - df = DataFrame({'A': [np.nan, 1, 2]}, index=idx) - wp = Panel({'i1': df, 'i2': df}) - result = wp.to_frame() - exp_idx = MultiIndex.from_tuples([(2, 'one', 'A'), (2, 'two', 'A')], - names=[None, None, 'minor']) - expected = DataFrame({'i1': [1., 2], 'i2': [1., 2]}, index=exp_idx) - assert_frame_equal(result, expected) - - def test_to_panel_na_handling(self): - df = DataFrame(np.random.randint(0, 10, size=20).reshape((10, 2)), - index=[[0, 0, 0, 0, 0, 0, 1, 1, 1, 1], - [0, 1, 2, 3, 4, 5, 2, 3, 4, 5]]) - - panel = df.to_panel() - self.assertTrue(isnull(panel[0].loc[1, [0, 1]]).all()) - - def test_to_panel_duplicates(self): - # #2441 - df = DataFrame({'a': [0, 0, 1], 'b': [1, 1, 1], 'c': [1, 2, 3]}) - idf = df.set_index(['a', 'b']) - assertRaisesRegexp(ValueError, 'non-uniquely indexed', idf.to_panel) - - def test_panel_dups(self): - - # GH 4960 - # duplicates in an index - - # items - data = np.random.randn(5, 100, 5) - no_dup_panel = Panel(data, items=list("ABCDE")) - panel = Panel(data, items=list("AACDE")) - - expected = no_dup_panel['A'] - result = panel.iloc[0] - assert_frame_equal(result, expected) - - expected = no_dup_panel['E'] - result = panel.loc['E'] - assert_frame_equal(result, expected) - - expected = no_dup_panel.loc[['A', 'B']] - expected.items = ['A', 'A'] - result = panel.loc['A'] - assert_panel_equal(result, expected) - - # major - data = np.random.randn(5, 5, 5) - no_dup_panel = Panel(data, major_axis=list("ABCDE")) - panel = Panel(data, major_axis=list("AACDE")) - - expected = no_dup_panel.loc[:, 'A'] - result = panel.iloc[:, 0] - assert_frame_equal(result, expected) - - expected = no_dup_panel.loc[:, 'E'] - result = panel.loc[:, 'E'] - assert_frame_equal(result, expected) - - expected = no_dup_panel.loc[:, ['A', 'B']] - expected.major_axis = ['A', 'A'] - result = panel.loc[:, 'A'] - assert_panel_equal(result, expected) - - # minor - data = np.random.randn(5, 100, 5) - no_dup_panel = Panel(data, minor_axis=list("ABCDE")) - panel = Panel(data, minor_axis=list("AACDE")) - - expected = no_dup_panel.loc[:, :, 'A'] - result = panel.iloc[:, :, 0] - assert_frame_equal(result, expected) - - expected = no_dup_panel.loc[:, :, 'E'] - result = panel.loc[:, :, 'E'] - assert_frame_equal(result, expected) - - expected = no_dup_panel.loc[:, :, ['A', 'B']] - expected.minor_axis = ['A', 'A'] - result = panel.loc[:, :, 'A'] - assert_panel_equal(result, expected) - - def test_filter(self): - pass - - def test_compound(self): - compounded = self.panel.compound() - - assert_series_equal(compounded['ItemA'], - (1 + self.panel['ItemA']).product(0) - 1, - check_names=False) - - def test_shift(self): - # major - idx = self.panel.major_axis[0] - idx_lag = self.panel.major_axis[1] - shifted = self.panel.shift(1) - assert_frame_equal(self.panel.major_xs(idx), shifted.major_xs(idx_lag)) - - # minor - idx = self.panel.minor_axis[0] - idx_lag = self.panel.minor_axis[1] - shifted = self.panel.shift(1, axis='minor') - assert_frame_equal(self.panel.minor_xs(idx), shifted.minor_xs(idx_lag)) - - # items - idx = self.panel.items[0] - idx_lag = self.panel.items[1] - shifted = self.panel.shift(1, axis='items') - assert_frame_equal(self.panel[idx], shifted[idx_lag]) - - # negative numbers, #2164 - result = self.panel.shift(-1) - expected = Panel(dict((i, f.shift(-1)[:-1]) - for i, f in self.panel.iteritems())) - assert_panel_equal(result, expected) - - # mixed dtypes #6959 - data = [('item ' + ch, makeMixedDataFrame()) for ch in list('abcde')] - data = dict(data) - mixed_panel = Panel.from_dict(data, orient='minor') - shifted = mixed_panel.shift(1) - assert_series_equal(mixed_panel.dtypes, shifted.dtypes) - - def test_tshift(self): - # PeriodIndex - ps = tm.makePeriodPanel() - shifted = ps.tshift(1) - unshifted = shifted.tshift(-1) - - assert_panel_equal(unshifted, ps) - - shifted2 = ps.tshift(freq='B') - assert_panel_equal(shifted, shifted2) - - shifted3 = ps.tshift(freq=BDay()) - assert_panel_equal(shifted, shifted3) - - assertRaisesRegexp(ValueError, 'does not match', ps.tshift, freq='M') - - # DatetimeIndex - panel = _panel - shifted = panel.tshift(1) - unshifted = shifted.tshift(-1) - - assert_panel_equal(panel, unshifted) - - shifted2 = panel.tshift(freq=panel.major_axis.freq) - assert_panel_equal(shifted, shifted2) - - inferred_ts = Panel(panel.values, items=panel.items, - major_axis=Index(np.asarray(panel.major_axis)), - minor_axis=panel.minor_axis) - shifted = inferred_ts.tshift(1) - unshifted = shifted.tshift(-1) - assert_panel_equal(shifted, panel.tshift(1)) - assert_panel_equal(unshifted, inferred_ts) - - no_freq = panel.iloc[:, [0, 5, 7], :] - self.assertRaises(ValueError, no_freq.tshift) - - def test_pct_change(self): - df1 = DataFrame({'c1': [1, 2, 5], 'c2': [3, 4, 6]}) - df2 = df1 + 1 - df3 = DataFrame({'c1': [3, 4, 7], 'c2': [5, 6, 8]}) - wp = Panel({'i1': df1, 'i2': df2, 'i3': df3}) - # major, 1 - result = wp.pct_change() # axis='major' - expected = Panel({'i1': df1.pct_change(), - 'i2': df2.pct_change(), - 'i3': df3.pct_change()}) - assert_panel_equal(result, expected) - result = wp.pct_change(axis=1) - assert_panel_equal(result, expected) - # major, 2 - result = wp.pct_change(periods=2) - expected = Panel({'i1': df1.pct_change(2), - 'i2': df2.pct_change(2), - 'i3': df3.pct_change(2)}) - assert_panel_equal(result, expected) - # minor, 1 - result = wp.pct_change(axis='minor') - expected = Panel({'i1': df1.pct_change(axis=1), - 'i2': df2.pct_change(axis=1), - 'i3': df3.pct_change(axis=1)}) - assert_panel_equal(result, expected) - result = wp.pct_change(axis=2) - assert_panel_equal(result, expected) - # minor, 2 - result = wp.pct_change(periods=2, axis='minor') - expected = Panel({'i1': df1.pct_change(periods=2, axis=1), - 'i2': df2.pct_change(periods=2, axis=1), - 'i3': df3.pct_change(periods=2, axis=1)}) - assert_panel_equal(result, expected) - # items, 1 - result = wp.pct_change(axis='items') - expected = Panel({'i1': DataFrame({'c1': [np.nan, np.nan, np.nan], - 'c2': [np.nan, np.nan, np.nan]}), - 'i2': DataFrame({'c1': [1, 0.5, .2], - 'c2': [1. / 3, 0.25, 1. / 6]}), - 'i3': DataFrame({'c1': [.5, 1. / 3, 1. / 6], - 'c2': [.25, .2, 1. / 7]})}) - assert_panel_equal(result, expected) - result = wp.pct_change(axis=0) - assert_panel_equal(result, expected) - # items, 2 - result = wp.pct_change(periods=2, axis='items') - expected = Panel({'i1': DataFrame({'c1': [np.nan, np.nan, np.nan], - 'c2': [np.nan, np.nan, np.nan]}), - 'i2': DataFrame({'c1': [np.nan, np.nan, np.nan], - 'c2': [np.nan, np.nan, np.nan]}), - 'i3': DataFrame({'c1': [2, 1, .4], - 'c2': [2. / 3, .5, 1. / 3]})}) - assert_panel_equal(result, expected) - - def test_round(self): - values = [[[-3.2, 2.2], [0, -4.8213], [3.123, 123.12], - [-1566.213, 88.88], [-12, 94.5]], - [[-5.82, 3.5], [6.21, -73.272], [-9.087, 23.12], - [272.212, -99.99], [23, -76.5]]] - evalues = [[[float(np.around(i)) for i in j] for j in k] - for k in values] - p = Panel(values, items=['Item1', 'Item2'], - major_axis=pd.date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - expected = Panel(evalues, items=['Item1', 'Item2'], - major_axis=pd.date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - result = p.round() - self.assert_panel_equal(expected, result) - - def test_numpy_round(self): - values = [[[-3.2, 2.2], [0, -4.8213], [3.123, 123.12], - [-1566.213, 88.88], [-12, 94.5]], - [[-5.82, 3.5], [6.21, -73.272], [-9.087, 23.12], - [272.212, -99.99], [23, -76.5]]] - evalues = [[[float(np.around(i)) for i in j] for j in k] - for k in values] - p = Panel(values, items=['Item1', 'Item2'], - major_axis=pd.date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - expected = Panel(evalues, items=['Item1', 'Item2'], - major_axis=pd.date_range('1/1/2000', periods=5), - minor_axis=['A', 'B']) - result = np.round(p) - self.assert_panel_equal(expected, result) - - msg = "the 'out' parameter is not supported" - tm.assertRaisesRegexp(ValueError, msg, np.round, p, out=p) - - def test_multiindex_get(self): - ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1), ('b', 2)], - names=['first', 'second']) - wp = Panel(np.random.random((4, 5, 5)), - items=ind, - major_axis=np.arange(5), - minor_axis=np.arange(5)) - f1 = wp['a'] - f2 = wp.loc['a'] - assert_panel_equal(f1, f2) - - self.assertTrue((f1.items == [1, 2]).all()) - self.assertTrue((f2.items == [1, 2]).all()) - - ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], - names=['first', 'second']) - - def test_multiindex_blocks(self): - ind = MultiIndex.from_tuples([('a', 1), ('a', 2), ('b', 1)], - names=['first', 'second']) - wp = Panel(self.panel._data) - wp.items = ind - f1 = wp['a'] - self.assertTrue((f1.items == [1, 2]).all()) - - f1 = wp[('b', 1)] - self.assertTrue((f1.columns == ['A', 'B', 'C', 'D']).all()) - - def test_repr_empty(self): - empty = Panel() - repr(empty) - - def test_rename(self): - mapper = {'ItemA': 'foo', 'ItemB': 'bar', 'ItemC': 'baz'} - - renamed = self.panel.rename_axis(mapper, axis=0) - exp = Index(['foo', 'bar', 'baz']) - self.assert_index_equal(renamed.items, exp) - - renamed = self.panel.rename_axis(str.lower, axis=2) - exp = Index(['a', 'b', 'c', 'd']) - self.assert_index_equal(renamed.minor_axis, exp) - - # don't copy - renamed_nocopy = self.panel.rename_axis(mapper, axis=0, copy=False) - renamed_nocopy['foo'] = 3. - self.assertTrue((self.panel['ItemA'].values == 3).all()) - - def test_get_attr(self): - assert_frame_equal(self.panel['ItemA'], self.panel.ItemA) - - # specific cases from #3440 - self.panel['a'] = self.panel['ItemA'] - assert_frame_equal(self.panel['a'], self.panel.a) - self.panel['i'] = self.panel['ItemA'] - assert_frame_equal(self.panel['i'], self.panel.i) - - def test_from_frame_level1_unsorted(self): - tuples = [('MSFT', 3), ('MSFT', 2), ('AAPL', 2), ('AAPL', 1), - ('MSFT', 1)] - midx = MultiIndex.from_tuples(tuples) - df = DataFrame(np.random.rand(5, 4), index=midx) - p = df.to_panel() - assert_frame_equal(p.minor_xs(2), df.xs(2, level=1).sort_index()) - - def test_to_excel(self): - try: - import xlwt # noqa - import xlrd # noqa - import openpyxl # noqa - from pandas.io.excel import ExcelFile - except ImportError: - pytest.skip("need xlwt xlrd openpyxl") - - for ext in ['xls', 'xlsx']: - with ensure_clean('__tmp__.' + ext) as path: - self.panel.to_excel(path) - try: - reader = ExcelFile(path) - except ImportError: - pytest.skip("need xlwt xlrd openpyxl") - - for item, df in self.panel.iteritems(): - recdf = reader.parse(str(item), index_col=0) - assert_frame_equal(df, recdf) - - def test_to_excel_xlsxwriter(self): - try: - import xlrd # noqa - import xlsxwriter # noqa - from pandas.io.excel import ExcelFile - except ImportError: - pytest.skip("Requires xlrd and xlsxwriter. Skipping test.") - - with ensure_clean('__tmp__.xlsx') as path: - self.panel.to_excel(path, engine='xlsxwriter') - try: - reader = ExcelFile(path) - except ImportError as e: - pytest.skip("cannot write excel file: %s" % e) - - for item, df in self.panel.iteritems(): - recdf = reader.parse(str(item), index_col=0) - assert_frame_equal(df, recdf) - - def test_dropna(self): - p = Panel(np.random.randn(4, 5, 6), major_axis=list('abcde')) - p.loc[:, ['b', 'd'], 0] = np.nan - - result = p.dropna(axis=1) - exp = p.loc[:, ['a', 'c', 'e'], :] - assert_panel_equal(result, exp) - inp = p.copy() - inp.dropna(axis=1, inplace=True) - assert_panel_equal(inp, exp) - - result = p.dropna(axis=1, how='all') - assert_panel_equal(result, p) - - p.loc[:, ['b', 'd'], :] = np.nan - result = p.dropna(axis=1, how='all') - exp = p.loc[:, ['a', 'c', 'e'], :] - assert_panel_equal(result, exp) - - p = Panel(np.random.randn(4, 5, 6), items=list('abcd')) - p.loc[['b'], :, 0] = np.nan - - result = p.dropna() - exp = p.loc[['a', 'c', 'd']] - assert_panel_equal(result, exp) - - result = p.dropna(how='all') - assert_panel_equal(result, p) - - p.loc['b'] = np.nan - result = p.dropna(how='all') - exp = p.loc[['a', 'c', 'd']] - assert_panel_equal(result, exp) - - def test_drop(self): - df = DataFrame({"A": [1, 2], "B": [3, 4]}) - panel = Panel({"One": df, "Two": df}) - - def check_drop(drop_val, axis_number, aliases, expected): - try: - actual = panel.drop(drop_val, axis=axis_number) - assert_panel_equal(actual, expected) - for alias in aliases: - actual = panel.drop(drop_val, axis=alias) - assert_panel_equal(actual, expected) - except AssertionError: - pprint_thing("Failed with axis_number %d and aliases: %s" % - (axis_number, aliases)) - raise - # Items - expected = Panel({"One": df}) - check_drop('Two', 0, ['items'], expected) - - self.assertRaises(ValueError, panel.drop, 'Three') - - # errors = 'ignore' - dropped = panel.drop('Three', errors='ignore') - assert_panel_equal(dropped, panel) - dropped = panel.drop(['Two', 'Three'], errors='ignore') - expected = Panel({"One": df}) - assert_panel_equal(dropped, expected) - - # Major - exp_df = DataFrame({"A": [2], "B": [4]}, index=[1]) - expected = Panel({"One": exp_df, "Two": exp_df}) - check_drop(0, 1, ['major_axis', 'major'], expected) - - exp_df = DataFrame({"A": [1], "B": [3]}, index=[0]) - expected = Panel({"One": exp_df, "Two": exp_df}) - check_drop([1], 1, ['major_axis', 'major'], expected) - - # Minor - exp_df = df[['B']] - expected = Panel({"One": exp_df, "Two": exp_df}) - check_drop(["A"], 2, ['minor_axis', 'minor'], expected) - - exp_df = df[['A']] - expected = Panel({"One": exp_df, "Two": exp_df}) - check_drop("B", 2, ['minor_axis', 'minor'], expected) - - def test_update(self): - pan = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - other = Panel([[[3.6, 2., np.nan], [np.nan, np.nan, 7]]], items=[1]) - - pan.update(other) - - expected = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]], - [[3.6, 2., 3], [1.5, np.nan, 7], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - assert_panel_equal(pan, expected) - - def test_update_from_dict(self): - pan = Panel({'one': DataFrame([[1.5, np.nan, 3], [1.5, np.nan, 3], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]]), - 'two': DataFrame([[1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]])}) - - other = {'two': DataFrame([[3.6, 2., np.nan], [np.nan, np.nan, 7]])} - - pan.update(other) - - expected = Panel( - {'two': DataFrame([[3.6, 2., 3], [1.5, np.nan, 7], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]]), - 'one': DataFrame([[1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]])}) - - assert_panel_equal(pan, expected) - - def test_update_nooverwrite(self): - pan = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - other = Panel([[[3.6, 2., np.nan], [np.nan, np.nan, 7]]], items=[1]) - - pan.update(other, overwrite=False) - - expected = Panel([[[1.5, np.nan, 3], [1.5, np.nan, 3], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]], - [[1.5, 2., 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - assert_panel_equal(pan, expected) - - def test_update_filtered(self): - pan = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - other = Panel([[[3.6, 2., np.nan], [np.nan, np.nan, 7]]], items=[1]) - - pan.update(other, filter_func=lambda x: x > 2) - - expected = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]], - [[1.5, np.nan, 3], [1.5, np.nan, 7], - [1.5, np.nan, 3.], [1.5, np.nan, 3.]]]) - - assert_panel_equal(pan, expected) - - def test_update_raise(self): - pan = Panel([[[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 3.], [1.5, np.nan, 3.], [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]) - - self.assertRaises(Exception, pan.update, *(pan, ), - **{'raise_conflict': True}) - - def test_all_any(self): - self.assertTrue((self.panel.all(axis=0).values == nanall( - self.panel, axis=0)).all()) - self.assertTrue((self.panel.all(axis=1).values == nanall( - self.panel, axis=1).T).all()) - self.assertTrue((self.panel.all(axis=2).values == nanall( - self.panel, axis=2).T).all()) - self.assertTrue((self.panel.any(axis=0).values == nanany( - self.panel, axis=0)).all()) - self.assertTrue((self.panel.any(axis=1).values == nanany( - self.panel, axis=1).T).all()) - self.assertTrue((self.panel.any(axis=2).values == nanany( - self.panel, axis=2).T).all()) - - def test_all_any_unhandled(self): - self.assertRaises(NotImplementedError, self.panel.all, bool_only=True) - self.assertRaises(NotImplementedError, self.panel.any, bool_only=True) - - -class TestLongPanel(tm.TestCase): - """ - LongPanel no longer exists, but... - """ - - def setUp(self): - import warnings - warnings.filterwarnings(action='ignore', category=FutureWarning) - - panel = tm.makePanel() - tm.add_nans(panel) - - self.panel = panel.to_frame() - self.unfiltered_panel = panel.to_frame(filter_observations=False) - - def test_ops_differently_indexed(self): - # trying to set non-identically indexed panel - wp = self.panel.to_panel() - wp2 = wp.reindex(major=wp.major_axis[:-1]) - lp2 = wp2.to_frame() - - result = self.panel + lp2 - assert_frame_equal(result.reindex(lp2.index), lp2 * 2) - - # careful, mutation - self.panel['foo'] = lp2['ItemA'] - assert_series_equal(self.panel['foo'].reindex(lp2.index), lp2['ItemA'], - check_names=False) - - def test_ops_scalar(self): - result = self.panel.mul(2) - expected = DataFrame.__mul__(self.panel, 2) - assert_frame_equal(result, expected) - - def test_combineFrame(self): - wp = self.panel.to_panel() - result = self.panel.add(wp['ItemA'].stack(), axis=0) - assert_frame_equal(result.to_panel()['ItemA'], wp['ItemA'] * 2) - - def test_combinePanel(self): - wp = self.panel.to_panel() - result = self.panel.add(self.panel) - wide_result = result.to_panel() - assert_frame_equal(wp['ItemA'] * 2, wide_result['ItemA']) - - # one item - result = self.panel.add(self.panel.filter(['ItemA'])) - - def test_combine_scalar(self): - result = self.panel.mul(2) - expected = DataFrame(self.panel._data) * 2 - assert_frame_equal(result, expected) - - def test_combine_series(self): - s = self.panel['ItemA'][:10] - result = self.panel.add(s, axis=0) - expected = DataFrame.add(self.panel, s, axis=0) - assert_frame_equal(result, expected) - - s = self.panel.iloc[5] - result = self.panel + s - expected = DataFrame.add(self.panel, s, axis=1) - assert_frame_equal(result, expected) - - def test_operators(self): - wp = self.panel.to_panel() - result = (self.panel + 1).to_panel() - assert_frame_equal(wp['ItemA'] + 1, result['ItemA']) - - def test_arith_flex_panel(self): - ops = ['add', 'sub', 'mul', 'div', 'truediv', 'pow', 'floordiv', 'mod'] - if not compat.PY3: - aliases = {} - else: - aliases = {'div': 'truediv'} - self.panel = self.panel.to_panel() - - for n in [np.random.randint(-50, -1), np.random.randint(1, 50), 0]: - for op in ops: - alias = aliases.get(op, op) - f = getattr(operator, alias) - exp = f(self.panel, n) - result = getattr(self.panel, op)(n) - assert_panel_equal(result, exp, check_panel_type=True) - - # rops - r_f = lambda x, y: f(y, x) - exp = r_f(self.panel, n) - result = getattr(self.panel, 'r' + op)(n) - assert_panel_equal(result, exp) - - def test_sort(self): - def is_sorted(arr): - return (arr[1:] > arr[:-1]).any() - - sorted_minor = self.panel.sort_index(level=1) - self.assertTrue(is_sorted(sorted_minor.index.labels[1])) - - sorted_major = sorted_minor.sort_index(level=0) - self.assertTrue(is_sorted(sorted_major.index.labels[0])) - - def test_to_string(self): - buf = StringIO() - self.panel.to_string(buf) - - def test_to_sparse(self): - if isinstance(self.panel, Panel): - msg = 'sparsifying is not supported' - tm.assertRaisesRegexp(NotImplementedError, msg, - self.panel.to_sparse) - - def test_truncate(self): - dates = self.panel.index.levels[0] - start, end = dates[1], dates[5] - - trunced = self.panel.truncate(start, end).to_panel() - expected = self.panel.to_panel()['ItemA'].truncate(start, end) - - # TODO trucate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) - - trunced = self.panel.truncate(before=start).to_panel() - expected = self.panel.to_panel()['ItemA'].truncate(before=start) - - # TODO trucate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) - - trunced = self.panel.truncate(after=end).to_panel() - expected = self.panel.to_panel()['ItemA'].truncate(after=end) - - # TODO trucate drops index.names - assert_frame_equal(trunced['ItemA'], expected, check_names=False) - - # truncate on dates that aren't in there - wp = self.panel.to_panel() - new_index = wp.major_axis[::5] - - wp2 = wp.reindex(major=new_index) - - lp2 = wp2.to_frame() - lp_trunc = lp2.truncate(wp.major_axis[2], wp.major_axis[-2]) - - wp_trunc = wp2.truncate(wp.major_axis[2], wp.major_axis[-2]) - - assert_panel_equal(wp_trunc, lp_trunc.to_panel()) - - # throw proper exception - self.assertRaises(Exception, lp2.truncate, wp.major_axis[-2], - wp.major_axis[2]) - - def test_axis_dummies(self): - from pandas.core.reshape import make_axis_dummies - - minor_dummies = make_axis_dummies(self.panel, 'minor').astype(np.uint8) - self.assertEqual(len(minor_dummies.columns), - len(self.panel.index.levels[1])) - - major_dummies = make_axis_dummies(self.panel, 'major').astype(np.uint8) - self.assertEqual(len(major_dummies.columns), - len(self.panel.index.levels[0])) - - mapping = {'A': 'one', 'B': 'one', 'C': 'two', 'D': 'two'} - - transformed = make_axis_dummies(self.panel, 'minor', - transform=mapping.get).astype(np.uint8) - self.assertEqual(len(transformed.columns), 2) - self.assert_index_equal(transformed.columns, Index(['one', 'two'])) - - # TODO: test correctness - - def test_get_dummies(self): - from pandas.core.reshape import get_dummies, make_axis_dummies - - self.panel['Label'] = self.panel.index.labels[1] - minor_dummies = make_axis_dummies(self.panel, 'minor').astype(np.uint8) - dummies = get_dummies(self.panel['Label']) - self.assert_numpy_array_equal(dummies.values, minor_dummies.values) - - def test_mean(self): - means = self.panel.mean(level='minor') - - # test versus Panel version - wide_means = self.panel.to_panel().mean('major') - assert_frame_equal(means, wide_means) - - def test_sum(self): - sums = self.panel.sum(level='minor') - - # test versus Panel version - wide_sums = self.panel.to_panel().sum('major') - assert_frame_equal(sums, wide_sums) - - def test_count(self): - index = self.panel.index - - major_count = self.panel.count(level=0)['ItemA'] - labels = index.labels[0] - for i, idx in enumerate(index.levels[0]): - self.assertEqual(major_count[i], (labels == i).sum()) - - minor_count = self.panel.count(level=1)['ItemA'] - labels = index.labels[1] - for i, idx in enumerate(index.levels[1]): - self.assertEqual(minor_count[i], (labels == i).sum()) - - def test_join(self): - lp1 = self.panel.filter(['ItemA', 'ItemB']) - lp2 = self.panel.filter(['ItemC']) - - joined = lp1.join(lp2) - - self.assertEqual(len(joined.columns), 3) - - self.assertRaises(Exception, lp1.join, - self.panel.filter(['ItemB', 'ItemC'])) - - def test_pivot(self): - from pandas.core.reshape import _slow_pivot - - one, two, three = (np.array([1, 2, 3, 4, 5]), - np.array(['a', 'b', 'c', 'd', 'e']), - np.array([1, 2, 3, 5, 4.])) - df = pivot(one, two, three) - self.assertEqual(df['a'][1], 1) - self.assertEqual(df['b'][2], 2) - self.assertEqual(df['c'][3], 3) - self.assertEqual(df['d'][4], 5) - self.assertEqual(df['e'][5], 4) - assert_frame_equal(df, _slow_pivot(one, two, three)) - - # weird overlap, TODO: test? - a, b, c = (np.array([1, 2, 3, 4, 4]), - np.array(['a', 'a', 'a', 'a', 'a']), - np.array([1., 2., 3., 4., 5.])) - self.assertRaises(Exception, pivot, a, b, c) - - # corner case, empty - df = pivot(np.array([]), np.array([]), np.array([])) - - -def test_monotonic(): - pos = np.array([1, 2, 3, 5]) - - def _monotonic(arr): - return not (arr[1:] < arr[:-1]).any() - - assert _monotonic(pos) - - neg = np.array([1, 2, 3, 4, 3]) - - assert not _monotonic(neg) - - neg2 = np.array([5, 1, 2, 3, 4, 5]) - - assert not _monotonic(neg2) - - -def test_panel_index(): - index = panelm.panel_index([1, 2, 3, 4], [1, 2, 3]) - expected = MultiIndex.from_arrays([np.tile([1, 2, 3, 4], 3), - np.repeat([1, 2, 3], 4)], - names=['time', 'panel']) - tm.assert_index_equal(index, expected) diff --git a/pandas/tests/test_panel4d.py b/pandas/tests/test_panel4d.py deleted file mode 100644 index c0511581cd299..0000000000000 --- a/pandas/tests/test_panel4d.py +++ /dev/null @@ -1,954 +0,0 @@ -# -*- coding: utf-8 -*- -from datetime import datetime -from pandas.compat import range, lrange -import operator -import pytest -from warnings import catch_warnings -import numpy as np - -from pandas.types.common import is_float_dtype -from pandas import Series, Index, isnull, notnull -from pandas.core.panel import Panel -from pandas.core.panel4d import Panel4D -from pandas.core.series import remove_na -from pandas.tseries.offsets import BDay - -from pandas.util.testing import (assert_panel_equal, - assert_panel4d_equal, - assert_frame_equal, - assert_series_equal, - assert_almost_equal) -import pandas.util.testing as tm - - -def add_nans(panel4d): - for l, label in enumerate(panel4d.labels): - panel = panel4d[label] - tm.add_nans(panel) - - -class SafeForLongAndSparse(object): - - def test_repr(self): - repr(self.panel4d) - - def test_iter(self): - tm.equalContents(list(self.panel4d), self.panel4d.labels) - - def test_count(self): - f = lambda s: notnull(s).sum() - self._check_stat_op('count', f, obj=self.panel4d, has_skipna=False) - - def test_sum(self): - self._check_stat_op('sum', np.sum) - - def test_mean(self): - self._check_stat_op('mean', np.mean) - - def test_prod(self): - self._check_stat_op('prod', np.prod) - - def test_median(self): - def wrapper(x): - if isnull(x).any(): - return np.nan - return np.median(x) - - self._check_stat_op('median', wrapper) - - def test_min(self): - self._check_stat_op('min', np.min) - - def test_max(self): - self._check_stat_op('max', np.max) - - def test_skew(self): - try: - from scipy.stats import skew - except ImportError: - pytest.skip("no scipy.stats.skew") - - def this_skew(x): - if len(x) < 3: - return np.nan - return skew(x, bias=False) - self._check_stat_op('skew', this_skew) - - # def test_mad(self): - # f = lambda x: np.abs(x - x.mean()).mean() - # self._check_stat_op('mad', f) - - def test_var(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.var(x, ddof=1) - self._check_stat_op('var', alt) - - def test_std(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.std(x, ddof=1) - self._check_stat_op('std', alt) - - def test_sem(self): - def alt(x): - if len(x) < 2: - return np.nan - return np.std(x, ddof=1) / np.sqrt(len(x)) - self._check_stat_op('sem', alt) - - # def test_skew(self): - # from scipy.stats import skew - - # def alt(x): - # if len(x) < 3: - # return np.nan - # return skew(x, bias=False) - - # self._check_stat_op('skew', alt) - - def _check_stat_op(self, name, alternative, obj=None, has_skipna=True): - if obj is None: - obj = self.panel4d - - # # set some NAs - # obj.loc[5:10] = np.nan - # obj.loc[15:20, -2:] = np.nan - - f = getattr(obj, name) - - if has_skipna: - def skipna_wrapper(x): - nona = remove_na(x) - if len(nona) == 0: - return np.nan - return alternative(nona) - - def wrapper(x): - return alternative(np.asarray(x)) - - with catch_warnings(record=True): - for i in range(obj.ndim): - result = f(axis=i, skipna=False) - expected = obj.apply(wrapper, axis=i) - assert_panel_equal(result, expected) - else: - skipna_wrapper = alternative - wrapper = alternative - - with catch_warnings(record=True): - for i in range(obj.ndim): - result = f(axis=i) - if not tm._incompat_bottleneck_version(name): - expected = obj.apply(skipna_wrapper, axis=i) - assert_panel_equal(result, expected) - - self.assertRaises(Exception, f, axis=obj.ndim) - - -class SafeForSparse(object): - - @classmethod - def assert_panel_equal(cls, x, y): - assert_panel_equal(x, y) - - @classmethod - def assert_panel4d_equal(cls, x, y): - assert_panel4d_equal(x, y) - - def test_get_axis(self): - assert(self.panel4d._get_axis(0) is self.panel4d.labels) - assert(self.panel4d._get_axis(1) is self.panel4d.items) - assert(self.panel4d._get_axis(2) is self.panel4d.major_axis) - assert(self.panel4d._get_axis(3) is self.panel4d.minor_axis) - - def test_set_axis(self): - with catch_warnings(record=True): - new_labels = Index(np.arange(len(self.panel4d.labels))) - - # TODO: unused? - # new_items = Index(np.arange(len(self.panel4d.items))) - - new_major = Index(np.arange(len(self.panel4d.major_axis))) - new_minor = Index(np.arange(len(self.panel4d.minor_axis))) - - # ensure propagate to potentially prior-cached items too - - # TODO: unused? - # label = self.panel4d['l1'] - - self.panel4d.labels = new_labels - - if hasattr(self.panel4d, '_item_cache'): - self.assertNotIn('l1', self.panel4d._item_cache) - self.assertIs(self.panel4d.labels, new_labels) - - self.panel4d.major_axis = new_major - self.assertIs(self.panel4d[0].major_axis, new_major) - self.assertIs(self.panel4d.major_axis, new_major) - - self.panel4d.minor_axis = new_minor - self.assertIs(self.panel4d[0].minor_axis, new_minor) - self.assertIs(self.panel4d.minor_axis, new_minor) - - def test_get_axis_number(self): - self.assertEqual(self.panel4d._get_axis_number('labels'), 0) - self.assertEqual(self.panel4d._get_axis_number('items'), 1) - self.assertEqual(self.panel4d._get_axis_number('major'), 2) - self.assertEqual(self.panel4d._get_axis_number('minor'), 3) - - def test_get_axis_name(self): - self.assertEqual(self.panel4d._get_axis_name(0), 'labels') - self.assertEqual(self.panel4d._get_axis_name(1), 'items') - self.assertEqual(self.panel4d._get_axis_name(2), 'major_axis') - self.assertEqual(self.panel4d._get_axis_name(3), 'minor_axis') - - def test_arith(self): - with catch_warnings(record=True): - self._test_op(self.panel4d, operator.add) - self._test_op(self.panel4d, operator.sub) - self._test_op(self.panel4d, operator.mul) - self._test_op(self.panel4d, operator.truediv) - self._test_op(self.panel4d, operator.floordiv) - self._test_op(self.panel4d, operator.pow) - - self._test_op(self.panel4d, lambda x, y: y + x) - self._test_op(self.panel4d, lambda x, y: y - x) - self._test_op(self.panel4d, lambda x, y: y * x) - self._test_op(self.panel4d, lambda x, y: y / x) - self._test_op(self.panel4d, lambda x, y: y ** x) - - self.assertRaises(Exception, self.panel4d.__add__, - self.panel4d['l1']) - - @staticmethod - def _test_op(panel4d, op): - result = op(panel4d, 1) - assert_panel_equal(result['l1'], op(panel4d['l1'], 1)) - - def test_keys(self): - tm.equalContents(list(self.panel4d.keys()), self.panel4d.labels) - - def test_iteritems(self): - """Test panel4d.iteritems()""" - - self.assertEqual(len(list(self.panel4d.iteritems())), - len(self.panel4d.labels)) - - def test_combinePanel4d(self): - with catch_warnings(record=True): - result = self.panel4d.add(self.panel4d) - self.assert_panel4d_equal(result, self.panel4d * 2) - - def test_neg(self): - with catch_warnings(record=True): - self.assert_panel4d_equal(-self.panel4d, self.panel4d * -1) - - def test_select(self): - with catch_warnings(record=True): - - p = self.panel4d - - # select labels - result = p.select(lambda x: x in ('l1', 'l3'), axis='labels') - expected = p.reindex(labels=['l1', 'l3']) - self.assert_panel4d_equal(result, expected) - - # select items - result = p.select(lambda x: x in ('ItemA', 'ItemC'), axis='items') - expected = p.reindex(items=['ItemA', 'ItemC']) - self.assert_panel4d_equal(result, expected) - - # select major_axis - result = p.select(lambda x: x >= datetime(2000, 1, 15), - axis='major') - new_major = p.major_axis[p.major_axis >= datetime(2000, 1, 15)] - expected = p.reindex(major=new_major) - self.assert_panel4d_equal(result, expected) - - # select minor_axis - result = p.select(lambda x: x in ('D', 'A'), axis=3) - expected = p.reindex(minor=['A', 'D']) - self.assert_panel4d_equal(result, expected) - - # corner case, empty thing - result = p.select(lambda x: x in ('foo',), axis='items') - self.assert_panel4d_equal(result, p.reindex(items=[])) - - def test_get_value(self): - - for item in self.panel.items: - for mjr in self.panel.major_axis[::2]: - for mnr in self.panel.minor_axis: - result = self.panel.get_value(item, mjr, mnr) - expected = self.panel[item][mnr][mjr] - assert_almost_equal(result, expected) - - def test_abs(self): - - with catch_warnings(record=True): - result = self.panel4d.abs() - expected = np.abs(self.panel4d) - self.assert_panel4d_equal(result, expected) - - p = self.panel4d['l1'] - result = p.abs() - expected = np.abs(p) - assert_panel_equal(result, expected) - - df = p['ItemA'] - result = df.abs() - expected = np.abs(df) - assert_frame_equal(result, expected) - - -class CheckIndexing(object): - - def test_getitem(self): - self.assertRaises(Exception, self.panel4d.__getitem__, 'ItemQ') - - def test_delitem_and_pop(self): - - with catch_warnings(record=True): - expected = self.panel4d['l2'] - result = self.panel4d.pop('l2') - assert_panel_equal(expected, result) - self.assertNotIn('l2', self.panel4d.labels) - - del self.panel4d['l3'] - self.assertNotIn('l3', self.panel4d.labels) - self.assertRaises(Exception, self.panel4d.__delitem__, 'l3') - - values = np.empty((4, 4, 4, 4)) - values[0] = 0 - values[1] = 1 - values[2] = 2 - values[3] = 3 - - panel4d = Panel4D(values, lrange(4), lrange(4), - lrange(4), lrange(4)) - - # did we delete the right row? - panel4dc = panel4d.copy() - del panel4dc[0] - assert_panel_equal(panel4dc[1], panel4d[1]) - assert_panel_equal(panel4dc[2], panel4d[2]) - assert_panel_equal(panel4dc[3], panel4d[3]) - - panel4dc = panel4d.copy() - del panel4dc[1] - assert_panel_equal(panel4dc[0], panel4d[0]) - assert_panel_equal(panel4dc[2], panel4d[2]) - assert_panel_equal(panel4dc[3], panel4d[3]) - - panel4dc = panel4d.copy() - del panel4dc[2] - assert_panel_equal(panel4dc[1], panel4d[1]) - assert_panel_equal(panel4dc[0], panel4d[0]) - assert_panel_equal(panel4dc[3], panel4d[3]) - - panel4dc = panel4d.copy() - del panel4dc[3] - assert_panel_equal(panel4dc[1], panel4d[1]) - assert_panel_equal(panel4dc[2], panel4d[2]) - assert_panel_equal(panel4dc[0], panel4d[0]) - - def test_setitem(self): - with catch_warnings(record=True): - - # Panel - p = Panel(dict( - ItemA=self.panel4d['l1']['ItemA'][2:].filter( - items=['A', 'B']))) - self.panel4d['l4'] = p - self.panel4d['l5'] = p - - p2 = self.panel4d['l4'] - - assert_panel_equal(p, p2.reindex(items=p.items, - major_axis=p.major_axis, - minor_axis=p.minor_axis)) - - # scalar - self.panel4d['lG'] = 1 - self.panel4d['lE'] = True - self.assertEqual(self.panel4d['lG'].values.dtype, np.int64) - self.assertEqual(self.panel4d['lE'].values.dtype, np.bool_) - - # object dtype - self.panel4d['lQ'] = 'foo' - self.assertEqual(self.panel4d['lQ'].values.dtype, np.object_) - - # boolean dtype - self.panel4d['lP'] = self.panel4d['l1'] > 0 - self.assertEqual(self.panel4d['lP'].values.dtype, np.bool_) - - def test_setitem_by_indexer(self): - - with catch_warnings(record=True): - - # Panel - panel4dc = self.panel4d.copy() - p = panel4dc.iloc[0] - - def func(): - self.panel4d.iloc[0] = p - self.assertRaises(NotImplementedError, func) - - # DataFrame - panel4dc = self.panel4d.copy() - df = panel4dc.iloc[0, 0] - df.iloc[:] = 1 - panel4dc.iloc[0, 0] = df - self.assertTrue((panel4dc.iloc[0, 0].values == 1).all()) - - # Series - panel4dc = self.panel4d.copy() - s = panel4dc.iloc[0, 0, :, 0] - s.iloc[:] = 1 - panel4dc.iloc[0, 0, :, 0] = s - self.assertTrue((panel4dc.iloc[0, 0, :, 0].values == 1).all()) - - # scalar - panel4dc = self.panel4d.copy() - panel4dc.iloc[0] = 1 - panel4dc.iloc[1] = True - panel4dc.iloc[2] = 'foo' - self.assertTrue((panel4dc.iloc[0].values == 1).all()) - self.assertTrue(panel4dc.iloc[1].values.all()) - self.assertTrue((panel4dc.iloc[2].values == 'foo').all()) - - def test_setitem_by_indexer_mixed_type(self): - - with catch_warnings(record=True): - # GH 8702 - self.panel4d['foo'] = 'bar' - - # scalar - panel4dc = self.panel4d.copy() - panel4dc.iloc[0] = 1 - panel4dc.iloc[1] = True - panel4dc.iloc[2] = 'foo' - self.assertTrue((panel4dc.iloc[0].values == 1).all()) - self.assertTrue(panel4dc.iloc[1].values.all()) - self.assertTrue((panel4dc.iloc[2].values == 'foo').all()) - - def test_comparisons(self): - with catch_warnings(record=True): - p1 = tm.makePanel4D() - p2 = tm.makePanel4D() - - tp = p1.reindex(labels=p1.labels.tolist() + ['foo']) - p = p1[p1.labels[0]] - - def test_comp(func): - result = func(p1, p2) - self.assert_numpy_array_equal(result.values, - func(p1.values, p2.values)) - - # versus non-indexed same objs - self.assertRaises(Exception, func, p1, tp) - - # versus different objs - self.assertRaises(Exception, func, p1, p) - - result3 = func(self.panel4d, 0) - self.assert_numpy_array_equal(result3.values, - func(self.panel4d.values, 0)) - - with np.errstate(invalid='ignore'): - test_comp(operator.eq) - test_comp(operator.ne) - test_comp(operator.lt) - test_comp(operator.gt) - test_comp(operator.ge) - test_comp(operator.le) - - def test_major_xs(self): - ref = self.panel4d['l1']['ItemA'] - - idx = self.panel4d.major_axis[5] - with catch_warnings(record=True): - xs = self.panel4d.major_xs(idx) - - assert_series_equal(xs['l1'].T['ItemA'], - ref.xs(idx), check_names=False) - - # not contained - idx = self.panel4d.major_axis[0] - BDay() - self.assertRaises(Exception, self.panel4d.major_xs, idx) - - def test_major_xs_mixed(self): - self.panel4d['l4'] = 'foo' - with catch_warnings(record=True): - xs = self.panel4d.major_xs(self.panel4d.major_axis[0]) - self.assertEqual(xs['l1']['A'].dtype, np.float64) - self.assertEqual(xs['l4']['A'].dtype, np.object_) - - def test_minor_xs(self): - ref = self.panel4d['l1']['ItemA'] - - with catch_warnings(record=True): - idx = self.panel4d.minor_axis[1] - xs = self.panel4d.minor_xs(idx) - - assert_series_equal(xs['l1'].T['ItemA'], ref[idx], check_names=False) - - # not contained - self.assertRaises(Exception, self.panel4d.minor_xs, 'E') - - def test_minor_xs_mixed(self): - self.panel4d['l4'] = 'foo' - - with catch_warnings(record=True): - xs = self.panel4d.minor_xs('D') - self.assertEqual(xs['l1'].T['ItemA'].dtype, np.float64) - self.assertEqual(xs['l4'].T['ItemA'].dtype, np.object_) - - def test_xs(self): - l1 = self.panel4d.xs('l1', axis=0) - expected = self.panel4d['l1'] - assert_panel_equal(l1, expected) - - # view if possible - l1_view = self.panel4d.xs('l1', axis=0) - l1_view.values[:] = np.nan - self.assertTrue(np.isnan(self.panel4d['l1'].values).all()) - - # mixed-type - self.panel4d['strings'] = 'foo' - with catch_warnings(record=True): - result = self.panel4d.xs('D', axis=3) - self.assertIsNotNone(result.is_copy) - - def test_getitem_fancy_labels(self): - with catch_warnings(record=True): - panel4d = self.panel4d - - labels = panel4d.labels[[1, 0]] - items = panel4d.items[[1, 0]] - dates = panel4d.major_axis[::2] - cols = ['D', 'C', 'F'] - - # all 4 specified - assert_panel4d_equal(panel4d.loc[labels, items, dates, cols], - panel4d.reindex(labels=labels, items=items, - major=dates, minor=cols)) - - # 3 specified - assert_panel4d_equal(panel4d.loc[:, items, dates, cols], - panel4d.reindex(items=items, major=dates, - minor=cols)) - - # 2 specified - assert_panel4d_equal(panel4d.loc[:, :, dates, cols], - panel4d.reindex(major=dates, minor=cols)) - - assert_panel4d_equal(panel4d.loc[:, items, :, cols], - panel4d.reindex(items=items, minor=cols)) - - assert_panel4d_equal(panel4d.loc[:, items, dates, :], - panel4d.reindex(items=items, major=dates)) - - # only 1 - assert_panel4d_equal(panel4d.loc[:, items, :, :], - panel4d.reindex(items=items)) - - assert_panel4d_equal(panel4d.loc[:, :, dates, :], - panel4d.reindex(major=dates)) - - assert_panel4d_equal(panel4d.loc[:, :, :, cols], - panel4d.reindex(minor=cols)) - - def test_getitem_fancy_slice(self): - pass - - def test_getitem_fancy_ints(self): - pass - - def test_get_value(self): - for label in self.panel4d.labels: - for item in self.panel4d.items: - for mjr in self.panel4d.major_axis[::2]: - for mnr in self.panel4d.minor_axis: - result = self.panel4d.get_value( - label, item, mjr, mnr) - expected = self.panel4d[label][item][mnr][mjr] - assert_almost_equal(result, expected) - - def test_set_value(self): - - with catch_warnings(record=True): - - for label in self.panel4d.labels: - for item in self.panel4d.items: - for mjr in self.panel4d.major_axis[::2]: - for mnr in self.panel4d.minor_axis: - self.panel4d.set_value(label, item, mjr, mnr, 1.) - assert_almost_equal( - self.panel4d[label][item][mnr][mjr], 1.) - - res3 = self.panel4d.set_value('l4', 'ItemE', 'foobar', 'baz', 5) - self.assertTrue(is_float_dtype(res3['l4'].values)) - - # resize - res = self.panel4d.set_value('l4', 'ItemE', 'foo', 'bar', 1.5) - tm.assertIsInstance(res, Panel4D) - self.assertIsNot(res, self.panel4d) - self.assertEqual(res.get_value('l4', 'ItemE', 'foo', 'bar'), 1.5) - - res3 = self.panel4d.set_value('l4', 'ItemE', 'foobar', 'baz', 5) - self.assertTrue(is_float_dtype(res3['l4'].values)) - - -class TestPanel4d(tm.TestCase, CheckIndexing, SafeForSparse, - SafeForLongAndSparse): - - @classmethod - def assert_panel4d_equal(cls, x, y): - assert_panel4d_equal(x, y) - - def setUp(self): - with catch_warnings(record=True): - self.panel4d = tm.makePanel4D(nper=8) - add_nans(self.panel4d) - - def test_constructor(self): - - with catch_warnings(record=True): - panel4d = Panel4D(self.panel4d._data) - self.assertIs(panel4d._data, self.panel4d._data) - - panel4d = Panel4D(self.panel4d._data, copy=True) - self.assertIsNot(panel4d._data, self.panel4d._data) - assert_panel4d_equal(panel4d, self.panel4d) - - vals = self.panel4d.values - - # no copy - panel4d = Panel4D(vals) - self.assertIs(panel4d.values, vals) - - # copy - panel4d = Panel4D(vals, copy=True) - self.assertIsNot(panel4d.values, vals) - - # GH #8285, test when scalar data is used to construct a Panel4D - # if dtype is not passed, it should be inferred - value_and_dtype = [(1, 'int64'), (3.14, 'float64'), - ('foo', np.object_)] - for (val, dtype) in value_and_dtype: - panel4d = Panel4D(val, labels=range(2), items=range( - 3), major_axis=range(4), minor_axis=range(5)) - vals = np.empty((2, 3, 4, 5), dtype=dtype) - vals.fill(val) - expected = Panel4D(vals, dtype=dtype) - assert_panel4d_equal(panel4d, expected) - - # test the case when dtype is passed - panel4d = Panel4D(1, labels=range(2), items=range( - 3), major_axis=range(4), minor_axis=range(5), dtype='float32') - vals = np.empty((2, 3, 4, 5), dtype='float32') - vals.fill(1) - - expected = Panel4D(vals, dtype='float32') - assert_panel4d_equal(panel4d, expected) - - def test_constructor_cast(self): - with catch_warnings(record=True): - zero_filled = self.panel4d.fillna(0) - - casted = Panel4D(zero_filled._data, dtype=int) - casted2 = Panel4D(zero_filled.values, dtype=int) - - exp_values = zero_filled.values.astype(int) - assert_almost_equal(casted.values, exp_values) - assert_almost_equal(casted2.values, exp_values) - - casted = Panel4D(zero_filled._data, dtype=np.int32) - casted2 = Panel4D(zero_filled.values, dtype=np.int32) - - exp_values = zero_filled.values.astype(np.int32) - assert_almost_equal(casted.values, exp_values) - assert_almost_equal(casted2.values, exp_values) - - # can't cast - data = [[['foo', 'bar', 'baz']]] - self.assertRaises(ValueError, Panel, data, dtype=float) - - def test_consolidate(self): - with catch_warnings(record=True): - self.assertTrue(self.panel4d._data.is_consolidated()) - - self.panel4d['foo'] = 1. - self.assertFalse(self.panel4d._data.is_consolidated()) - - panel4d = self.panel4d._consolidate() - self.assertTrue(panel4d._data.is_consolidated()) - - def test_ctor_dict(self): - with catch_warnings(record=True): - l1 = self.panel4d['l1'] - l2 = self.panel4d['l2'] - - d = {'A': l1, 'B': l2.loc[['ItemB'], :, :]} - panel4d = Panel4D(d) - - assert_panel_equal(panel4d['A'], self.panel4d['l1']) - assert_frame_equal(panel4d.loc['B', 'ItemB', :, :], - self.panel4d.loc['l2', ['ItemB'], - :, :]['ItemB']) - - def test_constructor_dict_mixed(self): - with catch_warnings(record=True): - data = dict((k, v.values) for k, v in self.panel4d.iteritems()) - result = Panel4D(data) - - exp_major = Index(np.arange(len(self.panel4d.major_axis))) - self.assert_index_equal(result.major_axis, exp_major) - - result = Panel4D(data, - labels=self.panel4d.labels, - items=self.panel4d.items, - major_axis=self.panel4d.major_axis, - minor_axis=self.panel4d.minor_axis) - assert_panel4d_equal(result, self.panel4d) - - data['l2'] = self.panel4d['l2'] - - result = Panel4D(data) - assert_panel4d_equal(result, self.panel4d) - - # corner, blow up - data['l2'] = data['l2']['ItemB'] - self.assertRaises(Exception, Panel4D, data) - - data['l2'] = self.panel4d['l2'].values[:, :, :-1] - self.assertRaises(Exception, Panel4D, data) - - def test_constructor_resize(self): - with catch_warnings(record=True): - data = self.panel4d._data - labels = self.panel4d.labels[:-1] - items = self.panel4d.items[:-1] - major = self.panel4d.major_axis[:-1] - minor = self.panel4d.minor_axis[:-1] - - result = Panel4D(data, labels=labels, items=items, - major_axis=major, minor_axis=minor) - expected = self.panel4d.reindex( - labels=labels, items=items, major=major, minor=minor) - assert_panel4d_equal(result, expected) - - result = Panel4D(data, items=items, major_axis=major) - expected = self.panel4d.reindex(items=items, major=major) - assert_panel4d_equal(result, expected) - - result = Panel4D(data, items=items) - expected = self.panel4d.reindex(items=items) - assert_panel4d_equal(result, expected) - - result = Panel4D(data, minor_axis=minor) - expected = self.panel4d.reindex(minor=minor) - assert_panel4d_equal(result, expected) - - def test_conform(self): - with catch_warnings(record=True): - - p = self.panel4d['l1'].filter(items=['ItemA', 'ItemB']) - conformed = self.panel4d.conform(p) - - tm.assert_index_equal(conformed.items, self.panel4d.labels) - tm.assert_index_equal(conformed.major_axis, - self.panel4d.major_axis) - tm.assert_index_equal(conformed.minor_axis, - self.panel4d.minor_axis) - - def test_reindex(self): - with catch_warnings(record=True): - ref = self.panel4d['l2'] - - # labels - result = self.panel4d.reindex(labels=['l1', 'l2']) - assert_panel_equal(result['l2'], ref) - - # items - result = self.panel4d.reindex(items=['ItemA', 'ItemB']) - assert_frame_equal(result['l2']['ItemB'], ref['ItemB']) - - # major - new_major = list(self.panel4d.major_axis[:10]) - result = self.panel4d.reindex(major=new_major) - assert_frame_equal( - result['l2']['ItemB'], ref['ItemB'].reindex(index=new_major)) - - # raise exception put both major and major_axis - self.assertRaises(Exception, self.panel4d.reindex, - major_axis=new_major, major=new_major) - - # minor - new_minor = list(self.panel4d.minor_axis[:2]) - result = self.panel4d.reindex(minor=new_minor) - assert_frame_equal( - result['l2']['ItemB'], ref['ItemB'].reindex(columns=new_minor)) - - result = self.panel4d.reindex(labels=self.panel4d.labels, - items=self.panel4d.items, - major=self.panel4d.major_axis, - minor=self.panel4d.minor_axis) - - # don't necessarily copy - result = self.panel4d.reindex() - assert_panel4d_equal(result, self.panel4d) - self.assertFalse(result is self.panel4d) - - # with filling - smaller_major = self.panel4d.major_axis[::5] - smaller = self.panel4d.reindex(major=smaller_major) - - larger = smaller.reindex(major=self.panel4d.major_axis, - method='pad') - - assert_panel_equal(larger.loc[:, :, self.panel4d.major_axis[1], :], - smaller.loc[:, :, smaller_major[0], :]) - - # don't necessarily copy - result = self.panel4d.reindex( - major=self.panel4d.major_axis, copy=False) - assert_panel4d_equal(result, self.panel4d) - self.assertTrue(result is self.panel4d) - - def test_not_hashable(self): - with catch_warnings(record=True): - p4D_empty = Panel4D() - self.assertRaises(TypeError, hash, p4D_empty) - self.assertRaises(TypeError, hash, self.panel4d) - - def test_reindex_like(self): - # reindex_like - with catch_warnings(record=True): - smaller = self.panel4d.reindex(labels=self.panel4d.labels[:-1], - items=self.panel4d.items[:-1], - major=self.panel4d.major_axis[:-1], - minor=self.panel4d.minor_axis[:-1]) - smaller_like = self.panel4d.reindex_like(smaller) - assert_panel4d_equal(smaller, smaller_like) - - def test_sort_index(self): - with catch_warnings(record=True): - import random - - rlabels = list(self.panel4d.labels) - ritems = list(self.panel4d.items) - rmajor = list(self.panel4d.major_axis) - rminor = list(self.panel4d.minor_axis) - random.shuffle(rlabels) - random.shuffle(ritems) - random.shuffle(rmajor) - random.shuffle(rminor) - - random_order = self.panel4d.reindex(labels=rlabels) - sorted_panel4d = random_order.sort_index(axis=0) - assert_panel4d_equal(sorted_panel4d, self.panel4d) - - def test_fillna(self): - - with catch_warnings(record=True): - self.assertFalse(np.isfinite(self.panel4d.values).all()) - filled = self.panel4d.fillna(0) - self.assertTrue(np.isfinite(filled.values).all()) - - self.assertRaises(NotImplementedError, - self.panel4d.fillna, method='pad') - - def test_swapaxes(self): - with catch_warnings(record=True): - result = self.panel4d.swapaxes('labels', 'items') - self.assertIs(result.items, self.panel4d.labels) - - result = self.panel4d.swapaxes('labels', 'minor') - self.assertIs(result.labels, self.panel4d.minor_axis) - - result = self.panel4d.swapaxes('items', 'minor') - self.assertIs(result.items, self.panel4d.minor_axis) - - result = self.panel4d.swapaxes('items', 'major') - self.assertIs(result.items, self.panel4d.major_axis) - - result = self.panel4d.swapaxes('major', 'minor') - self.assertIs(result.major_axis, self.panel4d.minor_axis) - - # this should also work - result = self.panel4d.swapaxes(0, 1) - self.assertIs(result.labels, self.panel4d.items) - - # this works, but return a copy - result = self.panel4d.swapaxes('items', 'items') - assert_panel4d_equal(self.panel4d, result) - self.assertNotEqual(id(self.panel4d), id(result)) - - def test_update(self): - - with catch_warnings(record=True): - p4d = Panel4D([[[[1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]]) - - other = Panel4D([[[[3.6, 2., np.nan]], - [[np.nan, np.nan, 7]]]]) - - p4d.update(other) - - expected = Panel4D([[[[3.6, 2, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]], - [[1.5, np.nan, 7], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.], - [1.5, np.nan, 3.]]]]) - - assert_panel4d_equal(p4d, expected) - - def test_dtypes(self): - - result = self.panel4d.dtypes - expected = Series(np.dtype('float64'), index=self.panel4d.labels) - assert_series_equal(result, expected) - - def test_repr_empty(self): - with catch_warnings(record=True): - empty = Panel4D() - repr(empty) - - def test_rename(self): - with catch_warnings(record=True): - - mapper = {'l1': 'foo', - 'l2': 'bar', - 'l3': 'baz'} - - renamed = self.panel4d.rename_axis(mapper, axis=0) - exp = Index(['foo', 'bar', 'baz']) - self.assert_index_equal(renamed.labels, exp) - - renamed = self.panel4d.rename_axis(str.lower, axis=3) - exp = Index(['a', 'b', 'c', 'd']) - self.assert_index_equal(renamed.minor_axis, exp) - - # don't copy - renamed_nocopy = self.panel4d.rename_axis(mapper, - axis=0, - copy=False) - renamed_nocopy['foo'] = 3. - self.assertTrue((self.panel4d['l1'].values == 3).all()) - - def test_get_attr(self): - assert_panel_equal(self.panel4d['l1'], self.panel4d.l1) diff --git a/pandas/tests/test_panelnd.py b/pandas/tests/test_panelnd.py deleted file mode 100644 index 7ecc773cd7bea..0000000000000 --- a/pandas/tests/test_panelnd.py +++ /dev/null @@ -1,102 +0,0 @@ -# -*- coding: utf-8 -*- -from warnings import catch_warnings -from pandas.core import panelnd -from pandas.core.panel import Panel - -from pandas.util.testing import assert_panel_equal -import pandas.util.testing as tm - - -class TestPanelnd(tm.TestCase): - - def setUp(self): - pass - - def test_4d_construction(self): - - with catch_warnings(record=True): - - # create a 4D - Panel4D = panelnd.create_nd_panel_factory( - klass_name='Panel4D', - orders=['labels', 'items', 'major_axis', 'minor_axis'], - slices={'items': 'items', 'major_axis': 'major_axis', - 'minor_axis': 'minor_axis'}, - slicer=Panel, - aliases={'major': 'major_axis', 'minor': 'minor_axis'}, - stat_axis=2) - - p4d = Panel4D(dict(L1=tm.makePanel(), L2=tm.makePanel())) # noqa - - def test_4d_construction_alt(self): - - with catch_warnings(record=True): - - # create a 4D - Panel4D = panelnd.create_nd_panel_factory( - klass_name='Panel4D', - orders=['labels', 'items', 'major_axis', 'minor_axis'], - slices={'items': 'items', 'major_axis': 'major_axis', - 'minor_axis': 'minor_axis'}, - slicer='Panel', - aliases={'major': 'major_axis', 'minor': 'minor_axis'}, - stat_axis=2) - - p4d = Panel4D(dict(L1=tm.makePanel(), L2=tm.makePanel())) # noqa - - def test_4d_construction_error(self): - - # create a 4D - self.assertRaises(Exception, - panelnd.create_nd_panel_factory, - klass_name='Panel4D', - orders=['labels', 'items', 'major_axis', - 'minor_axis'], - slices={'items': 'items', - 'major_axis': 'major_axis', - 'minor_axis': 'minor_axis'}, - slicer='foo', - aliases={'major': 'major_axis', - 'minor': 'minor_axis'}, - stat_axis=2) - - def test_5d_construction(self): - - with catch_warnings(record=True): - - # create a 4D - Panel4D = panelnd.create_nd_panel_factory( - klass_name='Panel4D', - orders=['labels1', 'items', 'major_axis', 'minor_axis'], - slices={'items': 'items', 'major_axis': 'major_axis', - 'minor_axis': 'minor_axis'}, - slicer=Panel, - aliases={'major': 'major_axis', 'minor': 'minor_axis'}, - stat_axis=2) - - # deprecation GH13564 - p4d = Panel4D(dict(L1=tm.makePanel(), L2=tm.makePanel())) - - # create a 5D - Panel5D = panelnd.create_nd_panel_factory( - klass_name='Panel5D', - orders=['cool1', 'labels1', 'items', 'major_axis', - 'minor_axis'], - slices={'labels1': 'labels1', 'items': 'items', - 'major_axis': 'major_axis', - 'minor_axis': 'minor_axis'}, - slicer=Panel4D, - aliases={'major': 'major_axis', 'minor': 'minor_axis'}, - stat_axis=2) - - # deprecation GH13564 - p5d = Panel5D(dict(C1=p4d)) - - # slice back to 4d - results = p5d.iloc[p5d.cool1.get_loc('C1'), :, :, 0:3, :] - expected = p4d.iloc[:, :, 0:3, :] - assert_panel_equal(results['L1'], expected['L1']) - - # test a transpose - # results = p5d.transpose(1,2,3,4,0) - # expected = diff --git a/pandas/tests/test_register_accessor.py b/pandas/tests/test_register_accessor.py new file mode 100644 index 0000000000000..acc18ed7ad049 --- /dev/null +++ b/pandas/tests/test_register_accessor.py @@ -0,0 +1,89 @@ +import contextlib + +import pytest + +import pandas as pd +import pandas.util.testing as tm + + +@contextlib.contextmanager +def ensure_removed(obj, attr): + """Ensure that an attribute added to 'obj' during the test is + removed when we're done""" + try: + yield + finally: + try: + delattr(obj, attr) + except AttributeError: + pass + obj._accessors.discard(attr) + + +class MyAccessor(object): + + def __init__(self, obj): + self.obj = obj + self.item = 'item' + + @property + def prop(self): + return self.item + + def method(self): + return self.item + + +@pytest.mark.parametrize('obj, registrar', [ + (pd.Series, pd.api.extensions.register_series_accessor), + (pd.DataFrame, pd.api.extensions.register_dataframe_accessor), + (pd.Index, pd.api.extensions.register_index_accessor) +]) +def test_register(obj, registrar): + with ensure_removed(obj, 'mine'): + before = set(dir(obj)) + registrar('mine')(MyAccessor) + assert obj([]).mine.prop == 'item' + after = set(dir(obj)) + assert (before ^ after) == {'mine'} + assert 'mine' in obj._accessors + + +def test_accessor_works(): + with ensure_removed(pd.Series, 'mine'): + pd.api.extensions.register_series_accessor('mine')(MyAccessor) + + s = pd.Series([1, 2]) + assert s.mine.obj is s + + assert s.mine.prop == 'item' + assert s.mine.method() == 'item' + + +def test_overwrite_warns(): + # Need to restore mean + mean = pd.Series.mean + try: + with tm.assert_produces_warning(UserWarning) as w: + pd.api.extensions.register_series_accessor('mean')(MyAccessor) + s = pd.Series([1, 2]) + assert s.mean.prop == 'item' + msg = str(w[0].message) + assert 'mean' in msg + assert 'MyAccessor' in msg + assert 'Series' in msg + finally: + pd.Series.mean = mean + + +def test_raises_attribute_error(): + + with ensure_removed(pd.Series, 'bad'): + + @pd.api.extensions.register_series_accessor("bad") + class Bad(object): + def __init__(self, data): + raise AttributeError("whoops") + + with pytest.raises(AttributeError, match="whoops"): + pd.Series([]).bad diff --git a/pandas/tests/test_reshape.py b/pandas/tests/test_reshape.py deleted file mode 100644 index 7ba743a6c425c..0000000000000 --- a/pandas/tests/test_reshape.py +++ /dev/null @@ -1,957 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable-msg=W0612,E1101 - -from pandas import DataFrame, Series -import pandas as pd - -from numpy import nan -import numpy as np - -from pandas.util.testing import assert_frame_equal - -from pandas.core.reshape import (melt, lreshape, get_dummies, wide_to_long) -import pandas.util.testing as tm -from pandas.compat import range, u - - -class TestMelt(tm.TestCase): - - def setUp(self): - self.df = tm.makeTimeDataFrame()[:10] - self.df['id1'] = (self.df['A'] > 0).astype(np.int64) - self.df['id2'] = (self.df['B'] > 0).astype(np.int64) - - self.var_name = 'var' - self.value_name = 'val' - - self.df1 = pd.DataFrame([[1.067683, -1.110463, 0.20867 - ], [-1.321405, 0.368915, -1.055342], - [-0.807333, 0.08298, -0.873361]]) - self.df1.columns = [list('ABC'), list('abc')] - self.df1.columns.names = ['CAP', 'low'] - - def test_default_col_names(self): - result = melt(self.df) - self.assertEqual(result.columns.tolist(), ['variable', 'value']) - - result1 = melt(self.df, id_vars=['id1']) - self.assertEqual(result1.columns.tolist(), ['id1', 'variable', 'value' - ]) - - result2 = melt(self.df, id_vars=['id1', 'id2']) - self.assertEqual(result2.columns.tolist(), ['id1', 'id2', 'variable', - 'value']) - - def test_value_vars(self): - result3 = melt(self.df, id_vars=['id1', 'id2'], value_vars='A') - self.assertEqual(len(result3), 10) - - result4 = melt(self.df, id_vars=['id1', 'id2'], value_vars=['A', 'B']) - expected4 = DataFrame({'id1': self.df['id1'].tolist() * 2, - 'id2': self.df['id2'].tolist() * 2, - 'variable': ['A'] * 10 + ['B'] * 10, - 'value': (self.df['A'].tolist() + - self.df['B'].tolist())}, - columns=['id1', 'id2', 'variable', 'value']) - tm.assert_frame_equal(result4, expected4) - - def test_value_vars_types(self): - # GH 15348 - expected = DataFrame({'id1': self.df['id1'].tolist() * 2, - 'id2': self.df['id2'].tolist() * 2, - 'variable': ['A'] * 10 + ['B'] * 10, - 'value': (self.df['A'].tolist() + - self.df['B'].tolist())}, - columns=['id1', 'id2', 'variable', 'value']) - - for type_ in (tuple, list, np.array): - result = melt(self.df, id_vars=['id1', 'id2'], - value_vars=type_(('A', 'B'))) - tm.assert_frame_equal(result, expected) - - def test_vars_work_with_multiindex(self): - expected = DataFrame({ - ('A', 'a'): self.df1[('A', 'a')], - 'CAP': ['B'] * len(self.df1), - 'low': ['b'] * len(self.df1), - 'value': self.df1[('B', 'b')], - }, columns=[('A', 'a'), 'CAP', 'low', 'value']) - - result = melt(self.df1, id_vars=[('A', 'a')], value_vars=[('B', 'b')]) - tm.assert_frame_equal(result, expected) - - def test_tuple_vars_fail_with_multiindex(self): - # melt should fail with an informative error message if - # the columns have a MultiIndex and a tuple is passed - # for id_vars or value_vars. - tuple_a = ('A', 'a') - list_a = [tuple_a] - tuple_b = ('B', 'b') - list_b = [tuple_b] - - for id_vars, value_vars in ((tuple_a, list_b), (list_a, tuple_b), - (tuple_a, tuple_b)): - with tm.assertRaisesRegexp(ValueError, r'MultiIndex'): - melt(self.df1, id_vars=id_vars, value_vars=value_vars) - - def test_custom_var_name(self): - result5 = melt(self.df, var_name=self.var_name) - self.assertEqual(result5.columns.tolist(), ['var', 'value']) - - result6 = melt(self.df, id_vars=['id1'], var_name=self.var_name) - self.assertEqual(result6.columns.tolist(), ['id1', 'var', 'value']) - - result7 = melt(self.df, id_vars=['id1', 'id2'], var_name=self.var_name) - self.assertEqual(result7.columns.tolist(), ['id1', 'id2', 'var', - 'value']) - - result8 = melt(self.df, id_vars=['id1', 'id2'], value_vars='A', - var_name=self.var_name) - self.assertEqual(result8.columns.tolist(), ['id1', 'id2', 'var', - 'value']) - - result9 = melt(self.df, id_vars=['id1', 'id2'], value_vars=['A', 'B'], - var_name=self.var_name) - expected9 = DataFrame({'id1': self.df['id1'].tolist() * 2, - 'id2': self.df['id2'].tolist() * 2, - self.var_name: ['A'] * 10 + ['B'] * 10, - 'value': (self.df['A'].tolist() + - self.df['B'].tolist())}, - columns=['id1', 'id2', self.var_name, 'value']) - tm.assert_frame_equal(result9, expected9) - - def test_custom_value_name(self): - result10 = melt(self.df, value_name=self.value_name) - self.assertEqual(result10.columns.tolist(), ['variable', 'val']) - - result11 = melt(self.df, id_vars=['id1'], value_name=self.value_name) - self.assertEqual(result11.columns.tolist(), ['id1', 'variable', 'val']) - - result12 = melt(self.df, id_vars=['id1', 'id2'], - value_name=self.value_name) - self.assertEqual(result12.columns.tolist(), ['id1', 'id2', 'variable', - 'val']) - - result13 = melt(self.df, id_vars=['id1', 'id2'], value_vars='A', - value_name=self.value_name) - self.assertEqual(result13.columns.tolist(), ['id1', 'id2', 'variable', - 'val']) - - result14 = melt(self.df, id_vars=['id1', 'id2'], value_vars=['A', 'B'], - value_name=self.value_name) - expected14 = DataFrame({'id1': self.df['id1'].tolist() * 2, - 'id2': self.df['id2'].tolist() * 2, - 'variable': ['A'] * 10 + ['B'] * 10, - self.value_name: (self.df['A'].tolist() + - self.df['B'].tolist())}, - columns=['id1', 'id2', 'variable', - self.value_name]) - tm.assert_frame_equal(result14, expected14) - - def test_custom_var_and_value_name(self): - - result15 = melt(self.df, var_name=self.var_name, - value_name=self.value_name) - self.assertEqual(result15.columns.tolist(), ['var', 'val']) - - result16 = melt(self.df, id_vars=['id1'], var_name=self.var_name, - value_name=self.value_name) - self.assertEqual(result16.columns.tolist(), ['id1', 'var', 'val']) - - result17 = melt(self.df, id_vars=['id1', 'id2'], - var_name=self.var_name, value_name=self.value_name) - self.assertEqual(result17.columns.tolist(), ['id1', 'id2', 'var', 'val' - ]) - - result18 = melt(self.df, id_vars=['id1', 'id2'], value_vars='A', - var_name=self.var_name, value_name=self.value_name) - self.assertEqual(result18.columns.tolist(), ['id1', 'id2', 'var', 'val' - ]) - - result19 = melt(self.df, id_vars=['id1', 'id2'], value_vars=['A', 'B'], - var_name=self.var_name, value_name=self.value_name) - expected19 = DataFrame({'id1': self.df['id1'].tolist() * 2, - 'id2': self.df['id2'].tolist() * 2, - self.var_name: ['A'] * 10 + ['B'] * 10, - self.value_name: (self.df['A'].tolist() + - self.df['B'].tolist())}, - columns=['id1', 'id2', self.var_name, - self.value_name]) - tm.assert_frame_equal(result19, expected19) - - df20 = self.df.copy() - df20.columns.name = 'foo' - result20 = melt(df20) - self.assertEqual(result20.columns.tolist(), ['foo', 'value']) - - def test_col_level(self): - res1 = melt(self.df1, col_level=0) - res2 = melt(self.df1, col_level='CAP') - self.assertEqual(res1.columns.tolist(), ['CAP', 'value']) - self.assertEqual(res2.columns.tolist(), ['CAP', 'value']) - - def test_multiindex(self): - res = pd.melt(self.df1) - self.assertEqual(res.columns.tolist(), ['CAP', 'low', 'value']) - - -class TestGetDummies(tm.TestCase): - - sparse = False - - def setUp(self): - self.df = DataFrame({'A': ['a', 'b', 'a'], - 'B': ['b', 'b', 'c'], - 'C': [1, 2, 3]}) - - def test_basic(self): - s_list = list('abc') - s_series = Series(s_list) - s_series_index = Series(s_list, list('ABC')) - - expected = DataFrame({'a': {0: 1, - 1: 0, - 2: 0}, - 'b': {0: 0, - 1: 1, - 2: 0}, - 'c': {0: 0, - 1: 0, - 2: 1}}, dtype=np.uint8) - assert_frame_equal(get_dummies(s_list, sparse=self.sparse), expected) - assert_frame_equal(get_dummies(s_series, sparse=self.sparse), expected) - - expected.index = list('ABC') - assert_frame_equal( - get_dummies(s_series_index, sparse=self.sparse), expected) - - def test_basic_types(self): - # GH 10531 - s_list = list('abc') - s_series = Series(s_list) - s_df = DataFrame({'a': [0, 1, 0, 1, 2], - 'b': ['A', 'A', 'B', 'C', 'C'], - 'c': [2, 3, 3, 3, 2]}) - - expected = DataFrame({'a': [1, 0, 0], - 'b': [0, 1, 0], - 'c': [0, 0, 1]}, - dtype='uint8', - columns=list('abc')) - if not self.sparse: - compare = tm.assert_frame_equal - else: - expected = expected.to_sparse(fill_value=0, kind='integer') - compare = tm.assert_sp_frame_equal - - result = get_dummies(s_list, sparse=self.sparse) - compare(result, expected) - - result = get_dummies(s_series, sparse=self.sparse) - compare(result, expected) - - result = get_dummies(s_df, sparse=self.sparse, columns=s_df.columns) - tm.assert_series_equal(result.get_dtype_counts(), - Series({'uint8': 8})) - - result = get_dummies(s_df, sparse=self.sparse, columns=['a']) - expected = Series({'uint8': 3, 'int64': 1, 'object': 1}).sort_values() - tm.assert_series_equal(result.get_dtype_counts().sort_values(), - expected) - - def test_just_na(self): - just_na_list = [np.nan] - just_na_series = Series(just_na_list) - just_na_series_index = Series(just_na_list, index=['A']) - - res_list = get_dummies(just_na_list, sparse=self.sparse) - res_series = get_dummies(just_na_series, sparse=self.sparse) - res_series_index = get_dummies(just_na_series_index, - sparse=self.sparse) - - self.assertEqual(res_list.empty, True) - self.assertEqual(res_series.empty, True) - self.assertEqual(res_series_index.empty, True) - - self.assertEqual(res_list.index.tolist(), [0]) - self.assertEqual(res_series.index.tolist(), [0]) - self.assertEqual(res_series_index.index.tolist(), ['A']) - - def test_include_na(self): - s = ['a', 'b', np.nan] - res = get_dummies(s, sparse=self.sparse) - exp = DataFrame({'a': {0: 1, 1: 0, 2: 0}, - 'b': {0: 0, 1: 1, 2: 0}}, dtype=np.uint8) - assert_frame_equal(res, exp) - - # Sparse dataframes do not allow nan labelled columns, see #GH8822 - res_na = get_dummies(s, dummy_na=True, sparse=self.sparse) - exp_na = DataFrame({nan: {0: 0, 1: 0, 2: 1}, - 'a': {0: 1, 1: 0, 2: 0}, - 'b': {0: 0, 1: 1, 2: 0}}, - dtype=np.uint8) - exp_na = exp_na.reindex_axis(['a', 'b', nan], 1) - # hack (NaN handling in assert_index_equal) - exp_na.columns = res_na.columns - assert_frame_equal(res_na, exp_na) - - res_just_na = get_dummies([nan], dummy_na=True, sparse=self.sparse) - exp_just_na = DataFrame(Series(1, index=[0]), columns=[nan], - dtype=np.uint8) - tm.assert_numpy_array_equal(res_just_na.values, exp_just_na.values) - - def test_unicode(self - ): # See GH 6885 - get_dummies chokes on unicode values - import unicodedata - e = 'e' - eacute = unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE') - s = [e, eacute, eacute] - res = get_dummies(s, prefix='letter', sparse=self.sparse) - exp = DataFrame({'letter_e': {0: 1, - 1: 0, - 2: 0}, - u('letter_%s') % eacute: {0: 0, - 1: 1, - 2: 1}}, - dtype=np.uint8) - assert_frame_equal(res, exp) - - def test_dataframe_dummies_all_obj(self): - df = self.df[['A', 'B']] - result = get_dummies(df, sparse=self.sparse) - expected = DataFrame({'A_a': [1, 0, 1], - 'A_b': [0, 1, 0], - 'B_b': [1, 1, 0], - 'B_c': [0, 0, 1]}, dtype=np.uint8) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_mix_default(self): - df = self.df - result = get_dummies(df, sparse=self.sparse) - expected = DataFrame({'C': [1, 2, 3], - 'A_a': [1, 0, 1], - 'A_b': [0, 1, 0], - 'B_b': [1, 1, 0], - 'B_c': [0, 0, 1]}) - cols = ['A_a', 'A_b', 'B_b', 'B_c'] - expected[cols] = expected[cols].astype(np.uint8) - expected = expected[['C', 'A_a', 'A_b', 'B_b', 'B_c']] - assert_frame_equal(result, expected) - - def test_dataframe_dummies_prefix_list(self): - prefixes = ['from_A', 'from_B'] - df = DataFrame({'A': ['a', 'b', 'a'], - 'B': ['b', 'b', 'c'], - 'C': [1, 2, 3]}) - result = get_dummies(df, prefix=prefixes, sparse=self.sparse) - expected = DataFrame({'C': [1, 2, 3], - 'from_A_a': [1, 0, 1], - 'from_A_b': [0, 1, 0], - 'from_B_b': [1, 1, 0], - 'from_B_c': [0, 0, 1]}) - cols = expected.columns[1:] - expected[cols] = expected[cols].astype(np.uint8) - expected = expected[['C', 'from_A_a', 'from_A_b', 'from_B_b', - 'from_B_c']] - assert_frame_equal(result, expected) - - def test_dataframe_dummies_prefix_str(self): - # not that you should do this... - df = self.df - result = get_dummies(df, prefix='bad', sparse=self.sparse) - expected = DataFrame([[1, 1, 0, 1, 0], - [2, 0, 1, 1, 0], - [3, 1, 0, 0, 1]], - columns=['C', 'bad_a', 'bad_b', 'bad_b', 'bad_c'], - dtype=np.uint8) - expected = expected.astype({"C": np.int64}) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_subset(self): - df = self.df - result = get_dummies(df, prefix=['from_A'], columns=['A'], - sparse=self.sparse) - expected = DataFrame({'from_A_a': [1, 0, 1], - 'from_A_b': [0, 1, 0], - 'B': ['b', 'b', 'c'], - 'C': [1, 2, 3]}) - cols = ['from_A_a', 'from_A_b'] - expected[cols] = expected[cols].astype(np.uint8) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_prefix_sep(self): - df = self.df - result = get_dummies(df, prefix_sep='..', sparse=self.sparse) - expected = DataFrame({'C': [1, 2, 3], - 'A..a': [1, 0, 1], - 'A..b': [0, 1, 0], - 'B..b': [1, 1, 0], - 'B..c': [0, 0, 1]}) - expected = expected[['C', 'A..a', 'A..b', 'B..b', 'B..c']] - cols = expected.columns[1:] - expected[cols] = expected[cols].astype(np.uint8) - assert_frame_equal(result, expected) - - result = get_dummies(df, prefix_sep=['..', '__'], sparse=self.sparse) - expected = expected.rename(columns={'B..b': 'B__b', 'B..c': 'B__c'}) - assert_frame_equal(result, expected) - - result = get_dummies(df, prefix_sep={'A': '..', - 'B': '__'}, sparse=self.sparse) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_prefix_bad_length(self): - with tm.assertRaises(ValueError): - get_dummies(self.df, prefix=['too few'], sparse=self.sparse) - - def test_dataframe_dummies_prefix_sep_bad_length(self): - with tm.assertRaises(ValueError): - get_dummies(self.df, prefix_sep=['bad'], sparse=self.sparse) - - def test_dataframe_dummies_prefix_dict(self): - prefixes = {'A': 'from_A', 'B': 'from_B'} - df = DataFrame({'A': ['a', 'b', 'a'], - 'B': ['b', 'b', 'c'], - 'C': [1, 2, 3]}) - result = get_dummies(df, prefix=prefixes, sparse=self.sparse) - expected = DataFrame({'from_A_a': [1, 0, 1], - 'from_A_b': [0, 1, 0], - 'from_B_b': [1, 1, 0], - 'from_B_c': [0, 0, 1], - 'C': [1, 2, 3]}) - cols = ['from_A_a', 'from_A_b', 'from_B_b', 'from_B_c'] - expected[cols] = expected[cols].astype(np.uint8) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_with_na(self): - df = self.df - df.loc[3, :] = [np.nan, np.nan, np.nan] - result = get_dummies(df, dummy_na=True, sparse=self.sparse) - expected = DataFrame({'C': [1, 2, 3, np.nan], - 'A_a': [1, 0, 1, 0], - 'A_b': [0, 1, 0, 0], - 'A_nan': [0, 0, 0, 1], - 'B_b': [1, 1, 0, 0], - 'B_c': [0, 0, 1, 0], - 'B_nan': [0, 0, 0, 1]}) - cols = ['A_a', 'A_b', 'A_nan', 'B_b', 'B_c', 'B_nan'] - expected[cols] = expected[cols].astype(np.uint8) - expected = expected[['C', 'A_a', 'A_b', 'A_nan', - 'B_b', 'B_c', 'B_nan']] - assert_frame_equal(result, expected) - - result = get_dummies(df, dummy_na=False, sparse=self.sparse) - expected = expected[['C', 'A_a', 'A_b', 'B_b', 'B_c']] - assert_frame_equal(result, expected) - - def test_dataframe_dummies_with_categorical(self): - df = self.df - df['cat'] = pd.Categorical(['x', 'y', 'y']) - result = get_dummies(df, sparse=self.sparse) - expected = DataFrame({'C': [1, 2, 3], - 'A_a': [1, 0, 1], - 'A_b': [0, 1, 0], - 'B_b': [1, 1, 0], - 'B_c': [0, 0, 1], - 'cat_x': [1, 0, 0], - 'cat_y': [0, 1, 1]}) - cols = ['A_a', 'A_b', 'B_b', 'B_c', 'cat_x', 'cat_y'] - expected[cols] = expected[cols].astype(np.uint8) - expected = expected[['C', 'A_a', 'A_b', 'B_b', 'B_c', - 'cat_x', 'cat_y']] - assert_frame_equal(result, expected) - - # GH12402 Add a new parameter `drop_first` to avoid collinearity - def test_basic_drop_first(self): - # Basic case - s_list = list('abc') - s_series = Series(s_list) - s_series_index = Series(s_list, list('ABC')) - - expected = DataFrame({'b': {0: 0, - 1: 1, - 2: 0}, - 'c': {0: 0, - 1: 0, - 2: 1}}, dtype=np.uint8) - - result = get_dummies(s_list, sparse=self.sparse, drop_first=True) - assert_frame_equal(result, expected) - - result = get_dummies(s_series, sparse=self.sparse, drop_first=True) - assert_frame_equal(result, expected) - - expected.index = list('ABC') - result = get_dummies(s_series_index, sparse=self.sparse, - drop_first=True) - assert_frame_equal(result, expected) - - def test_basic_drop_first_one_level(self): - # Test the case that categorical variable only has one level. - s_list = list('aaa') - s_series = Series(s_list) - s_series_index = Series(s_list, list('ABC')) - - expected = DataFrame(index=np.arange(3)) - - result = get_dummies(s_list, sparse=self.sparse, drop_first=True) - assert_frame_equal(result, expected) - - result = get_dummies(s_series, sparse=self.sparse, drop_first=True) - assert_frame_equal(result, expected) - - expected = DataFrame(index=list('ABC')) - result = get_dummies(s_series_index, sparse=self.sparse, - drop_first=True) - assert_frame_equal(result, expected) - - def test_basic_drop_first_NA(self): - # Test NA hadling together with drop_first - s_NA = ['a', 'b', np.nan] - res = get_dummies(s_NA, sparse=self.sparse, drop_first=True) - exp = DataFrame({'b': {0: 0, - 1: 1, - 2: 0}}, dtype=np.uint8) - assert_frame_equal(res, exp) - - res_na = get_dummies(s_NA, dummy_na=True, sparse=self.sparse, - drop_first=True) - exp_na = DataFrame({'b': {0: 0, - 1: 1, - 2: 0}, - nan: {0: 0, - 1: 0, - 2: 1}}, dtype=np.uint8).reindex_axis( - ['b', nan], 1) - assert_frame_equal(res_na, exp_na) - - res_just_na = get_dummies([nan], dummy_na=True, sparse=self.sparse, - drop_first=True) - exp_just_na = DataFrame(index=np.arange(1)) - assert_frame_equal(res_just_na, exp_just_na) - - def test_dataframe_dummies_drop_first(self): - df = self.df[['A', 'B']] - result = get_dummies(df, sparse=self.sparse, drop_first=True) - expected = DataFrame({'A_b': [0, 1, 0], - 'B_c': [0, 0, 1]}, dtype=np.uint8) - assert_frame_equal(result, expected) - - def test_dataframe_dummies_drop_first_with_categorical(self): - df = self.df - df['cat'] = pd.Categorical(['x', 'y', 'y']) - result = get_dummies(df, sparse=self.sparse, drop_first=True) - expected = DataFrame({'C': [1, 2, 3], - 'A_b': [0, 1, 0], - 'B_c': [0, 0, 1], - 'cat_y': [0, 1, 1]}) - cols = ['A_b', 'B_c', 'cat_y'] - expected[cols] = expected[cols].astype(np.uint8) - expected = expected[['C', 'A_b', 'B_c', 'cat_y']] - assert_frame_equal(result, expected) - - def test_dataframe_dummies_drop_first_with_na(self): - df = self.df - df.loc[3, :] = [np.nan, np.nan, np.nan] - result = get_dummies(df, dummy_na=True, sparse=self.sparse, - drop_first=True) - expected = DataFrame({'C': [1, 2, 3, np.nan], - 'A_b': [0, 1, 0, 0], - 'A_nan': [0, 0, 0, 1], - 'B_c': [0, 0, 1, 0], - 'B_nan': [0, 0, 0, 1]}) - cols = ['A_b', 'A_nan', 'B_c', 'B_nan'] - expected[cols] = expected[cols].astype(np.uint8) - - expected = expected[['C', 'A_b', 'A_nan', 'B_c', 'B_nan']] - assert_frame_equal(result, expected) - - result = get_dummies(df, dummy_na=False, sparse=self.sparse, - drop_first=True) - expected = expected[['C', 'A_b', 'B_c']] - assert_frame_equal(result, expected) - - def test_int_int(self): - data = Series([1, 2, 1]) - result = pd.get_dummies(data) - expected = DataFrame([[1, 0], [0, 1], [1, 0]], columns=[1, 2], - dtype=np.uint8) - tm.assert_frame_equal(result, expected) - - data = Series(pd.Categorical(['a', 'b', 'a'])) - result = pd.get_dummies(data) - expected = DataFrame([[1, 0], [0, 1], [1, 0]], - columns=pd.Categorical(['a', 'b']), - dtype=np.uint8) - tm.assert_frame_equal(result, expected) - - def test_int_df(self): - data = DataFrame( - {'A': [1, 2, 1], - 'B': pd.Categorical(['a', 'b', 'a']), - 'C': [1, 2, 1], - 'D': [1., 2., 1.] - } - ) - columns = ['C', 'D', 'A_1', 'A_2', 'B_a', 'B_b'] - expected = DataFrame([ - [1, 1., 1, 0, 1, 0], - [2, 2., 0, 1, 0, 1], - [1, 1., 1, 0, 1, 0] - ], columns=columns) - expected[columns[2:]] = expected[columns[2:]].astype(np.uint8) - result = pd.get_dummies(data, columns=['A', 'B']) - tm.assert_frame_equal(result, expected) - - def test_dataframe_dummies_preserve_categorical_dtype(self): - # GH13854 - for ordered in [False, True]: - cat = pd.Categorical(list("xy"), categories=list("xyz"), - ordered=ordered) - result = get_dummies(cat) - - data = np.array([[1, 0, 0], [0, 1, 0]], dtype=np.uint8) - cols = pd.CategoricalIndex(cat.categories, - categories=cat.categories, - ordered=ordered) - expected = DataFrame(data, columns=cols) - - tm.assert_frame_equal(result, expected) - - -class TestGetDummiesSparse(TestGetDummies): - sparse = True - - -class TestMakeAxisDummies(tm.TestCase): - - def test_preserve_categorical_dtype(self): - # GH13854 - for ordered in [False, True]: - cidx = pd.CategoricalIndex(list("xyz"), ordered=ordered) - midx = pd.MultiIndex(levels=[['a'], cidx], - labels=[[0, 0], [0, 1]]) - df = DataFrame([[10, 11]], index=midx) - - expected = DataFrame([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], - index=midx, columns=cidx) - - from pandas.core.reshape import make_axis_dummies - result = make_axis_dummies(df) - tm.assert_frame_equal(result, expected) - - result = make_axis_dummies(df, transform=lambda x: x) - tm.assert_frame_equal(result, expected) - - -class TestLreshape(tm.TestCase): - - def test_pairs(self): - data = {'birthdt': ['08jan2009', '20dec2008', '30dec2008', '21dec2008', - '11jan2009'], - 'birthwt': [1766, 3301, 1454, 3139, 4133], - 'id': [101, 102, 103, 104, 105], - 'sex': ['Male', 'Female', 'Female', 'Female', 'Female'], - 'visitdt1': ['11jan2009', '22dec2008', '04jan2009', - '29dec2008', '20jan2009'], - 'visitdt2': - ['21jan2009', nan, '22jan2009', '31dec2008', '03feb2009'], - 'visitdt3': ['05feb2009', nan, nan, '02jan2009', '15feb2009'], - 'wt1': [1823, 3338, 1549, 3298, 4306], - 'wt2': [2011.0, nan, 1892.0, 3338.0, 4575.0], - 'wt3': [2293.0, nan, nan, 3377.0, 4805.0]} - - df = DataFrame(data) - - spec = {'visitdt': ['visitdt%d' % i for i in range(1, 4)], - 'wt': ['wt%d' % i for i in range(1, 4)]} - result = lreshape(df, spec) - - exp_data = {'birthdt': - ['08jan2009', '20dec2008', '30dec2008', '21dec2008', - '11jan2009', '08jan2009', '30dec2008', '21dec2008', - '11jan2009', '08jan2009', '21dec2008', '11jan2009'], - 'birthwt': [1766, 3301, 1454, 3139, 4133, 1766, 1454, 3139, - 4133, 1766, 3139, 4133], - 'id': [101, 102, 103, 104, 105, 101, 103, 104, 105, 101, - 104, 105], - 'sex': ['Male', 'Female', 'Female', 'Female', 'Female', - 'Male', 'Female', 'Female', 'Female', 'Male', - 'Female', 'Female'], - 'visitdt': ['11jan2009', '22dec2008', '04jan2009', - '29dec2008', '20jan2009', '21jan2009', - '22jan2009', '31dec2008', '03feb2009', - '05feb2009', '02jan2009', '15feb2009'], - 'wt': [1823.0, 3338.0, 1549.0, 3298.0, 4306.0, 2011.0, - 1892.0, 3338.0, 4575.0, 2293.0, 3377.0, 4805.0]} - exp = DataFrame(exp_data, columns=result.columns) - tm.assert_frame_equal(result, exp) - - result = lreshape(df, spec, dropna=False) - exp_data = {'birthdt': - ['08jan2009', '20dec2008', '30dec2008', '21dec2008', - '11jan2009', '08jan2009', '20dec2008', '30dec2008', - '21dec2008', '11jan2009', '08jan2009', '20dec2008', - '30dec2008', '21dec2008', '11jan2009'], - 'birthwt': [1766, 3301, 1454, 3139, 4133, 1766, 3301, 1454, - 3139, 4133, 1766, 3301, 1454, 3139, 4133], - 'id': [101, 102, 103, 104, 105, 101, 102, 103, 104, 105, - 101, 102, 103, 104, 105], - 'sex': ['Male', 'Female', 'Female', 'Female', 'Female', - 'Male', 'Female', 'Female', 'Female', 'Female', - 'Male', 'Female', 'Female', 'Female', 'Female'], - 'visitdt': ['11jan2009', '22dec2008', '04jan2009', - '29dec2008', '20jan2009', '21jan2009', nan, - '22jan2009', '31dec2008', '03feb2009', - '05feb2009', nan, nan, '02jan2009', - '15feb2009'], - 'wt': [1823.0, 3338.0, 1549.0, 3298.0, 4306.0, 2011.0, nan, - 1892.0, 3338.0, 4575.0, 2293.0, nan, nan, 3377.0, - 4805.0]} - exp = DataFrame(exp_data, columns=result.columns) - tm.assert_frame_equal(result, exp) - - spec = {'visitdt': ['visitdt%d' % i for i in range(1, 3)], - 'wt': ['wt%d' % i for i in range(1, 4)]} - self.assertRaises(ValueError, lreshape, df, spec) - - -class TestWideToLong(tm.TestCase): - - def test_simple(self): - np.random.seed(123) - x = np.random.randn(3) - df = pd.DataFrame({"A1970": {0: "a", - 1: "b", - 2: "c"}, - "A1980": {0: "d", - 1: "e", - 2: "f"}, - "B1970": {0: 2.5, - 1: 1.2, - 2: .7}, - "B1980": {0: 3.2, - 1: 1.3, - 2: .1}, - "X": dict(zip( - range(3), x))}) - df["id"] = df.index - exp_data = {"X": x.tolist() + x.tolist(), - "A": ['a', 'b', 'c', 'd', 'e', 'f'], - "B": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], - "year": ['1970', '1970', '1970', '1980', '1980', '1980'], - "id": [0, 1, 2, 0, 1, 2]} - exp_frame = DataFrame(exp_data) - exp_frame = exp_frame.set_index(['id', 'year'])[["X", "A", "B"]] - long_frame = wide_to_long(df, ["A", "B"], i="id", j="year") - tm.assert_frame_equal(long_frame, exp_frame) - - def test_stubs(self): - # GH9204 - df = pd.DataFrame([[0, 1, 2, 3, 8], [4, 5, 6, 7, 9]]) - df.columns = ['id', 'inc1', 'inc2', 'edu1', 'edu2'] - stubs = ['inc', 'edu'] - - # TODO: unused? - df_long = pd.wide_to_long(df, stubs, i='id', j='age') # noqa - - self.assertEqual(stubs, ['inc', 'edu']) - - def test_separating_character(self): - # GH14779 - np.random.seed(123) - x = np.random.randn(3) - df = pd.DataFrame({"A.1970": {0: "a", - 1: "b", - 2: "c"}, - "A.1980": {0: "d", - 1: "e", - 2: "f"}, - "B.1970": {0: 2.5, - 1: 1.2, - 2: .7}, - "B.1980": {0: 3.2, - 1: 1.3, - 2: .1}, - "X": dict(zip( - range(3), x))}) - df["id"] = df.index - exp_data = {"X": x.tolist() + x.tolist(), - "A": ['a', 'b', 'c', 'd', 'e', 'f'], - "B": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], - "year": ['1970', '1970', '1970', '1980', '1980', '1980'], - "id": [0, 1, 2, 0, 1, 2]} - exp_frame = DataFrame(exp_data) - exp_frame = exp_frame.set_index(['id', 'year'])[["X", "A", "B"]] - long_frame = wide_to_long(df, ["A", "B"], i="id", j="year", sep=".") - tm.assert_frame_equal(long_frame, exp_frame) - - def test_escapable_characters(self): - np.random.seed(123) - x = np.random.randn(3) - df = pd.DataFrame({"A(quarterly)1970": {0: "a", - 1: "b", - 2: "c"}, - "A(quarterly)1980": {0: "d", - 1: "e", - 2: "f"}, - "B(quarterly)1970": {0: 2.5, - 1: 1.2, - 2: .7}, - "B(quarterly)1980": {0: 3.2, - 1: 1.3, - 2: .1}, - "X": dict(zip( - range(3), x))}) - df["id"] = df.index - exp_data = {"X": x.tolist() + x.tolist(), - "A(quarterly)": ['a', 'b', 'c', 'd', 'e', 'f'], - "B(quarterly)": [2.5, 1.2, 0.7, 3.2, 1.3, 0.1], - "year": ['1970', '1970', '1970', '1980', '1980', '1980'], - "id": [0, 1, 2, 0, 1, 2]} - exp_frame = DataFrame(exp_data) - exp_frame = exp_frame.set_index( - ['id', 'year'])[["X", "A(quarterly)", "B(quarterly)"]] - long_frame = wide_to_long(df, ["A(quarterly)", "B(quarterly)"], - i="id", j="year") - tm.assert_frame_equal(long_frame, exp_frame) - - def test_unbalanced(self): - # test that we can have a varying amount of time variables - df = pd.DataFrame({'A2010': [1.0, 2.0], - 'A2011': [3.0, 4.0], - 'B2010': [5.0, 6.0], - 'X': ['X1', 'X2']}) - df['id'] = df.index - exp_data = {'X': ['X1', 'X1', 'X2', 'X2'], - 'A': [1.0, 3.0, 2.0, 4.0], - 'B': [5.0, np.nan, 6.0, np.nan], - 'id': [0, 0, 1, 1], - 'year': ['2010', '2011', '2010', '2011']} - exp_frame = pd.DataFrame(exp_data) - exp_frame = exp_frame.set_index(['id', 'year'])[["X", "A", "B"]] - long_frame = wide_to_long(df, ['A', 'B'], i='id', j='year') - tm.assert_frame_equal(long_frame, exp_frame) - - def test_character_overlap(self): - # Test we handle overlapping characters in both id_vars and value_vars - df = pd.DataFrame({ - 'A11': ['a11', 'a22', 'a33'], - 'A12': ['a21', 'a22', 'a23'], - 'B11': ['b11', 'b12', 'b13'], - 'B12': ['b21', 'b22', 'b23'], - 'BB11': [1, 2, 3], - 'BB12': [4, 5, 6], - 'BBBX': [91, 92, 93], - 'BBBZ': [91, 92, 93] - }) - df['id'] = df.index - exp_frame = pd.DataFrame({ - 'BBBX': [91, 92, 93, 91, 92, 93], - 'BBBZ': [91, 92, 93, 91, 92, 93], - 'A': ['a11', 'a22', 'a33', 'a21', 'a22', 'a23'], - 'B': ['b11', 'b12', 'b13', 'b21', 'b22', 'b23'], - 'BB': [1, 2, 3, 4, 5, 6], - 'id': [0, 1, 2, 0, 1, 2], - 'year': ['11', '11', '11', '12', '12', '12']}) - exp_frame = exp_frame.set_index(['id', 'year'])[ - ['BBBX', 'BBBZ', 'A', 'B', 'BB']] - long_frame = wide_to_long(df, ['A', 'B', 'BB'], i='id', j='year') - tm.assert_frame_equal(long_frame.sort_index(axis=1), - exp_frame.sort_index(axis=1)) - - def test_invalid_separator(self): - # if an invalid separator is supplied a empty data frame is returned - sep = 'nope!' - df = pd.DataFrame({'A2010': [1.0, 2.0], - 'A2011': [3.0, 4.0], - 'B2010': [5.0, 6.0], - 'X': ['X1', 'X2']}) - df['id'] = df.index - exp_data = {'X': '', - 'A2010': [], - 'A2011': [], - 'B2010': [], - 'id': [], - 'year': [], - 'A': [], - 'B': []} - exp_frame = pd.DataFrame(exp_data) - exp_frame = exp_frame.set_index(['id', 'year'])[[ - 'X', 'A2010', 'A2011', 'B2010', 'A', 'B']] - exp_frame.index.set_levels([[0, 1], []], inplace=True) - long_frame = wide_to_long(df, ['A', 'B'], i='id', j='year', sep=sep) - tm.assert_frame_equal(long_frame.sort_index(axis=1), - exp_frame.sort_index(axis=1)) - - def test_num_string_disambiguation(self): - # Test that we can disambiguate number value_vars from - # string value_vars - df = pd.DataFrame({ - 'A11': ['a11', 'a22', 'a33'], - 'A12': ['a21', 'a22', 'a23'], - 'B11': ['b11', 'b12', 'b13'], - 'B12': ['b21', 'b22', 'b23'], - 'BB11': [1, 2, 3], - 'BB12': [4, 5, 6], - 'Arating': [91, 92, 93], - 'Arating_old': [91, 92, 93] - }) - df['id'] = df.index - exp_frame = pd.DataFrame({ - 'Arating': [91, 92, 93, 91, 92, 93], - 'Arating_old': [91, 92, 93, 91, 92, 93], - 'A': ['a11', 'a22', 'a33', 'a21', 'a22', 'a23'], - 'B': ['b11', 'b12', 'b13', 'b21', 'b22', 'b23'], - 'BB': [1, 2, 3, 4, 5, 6], - 'id': [0, 1, 2, 0, 1, 2], - 'year': ['11', '11', '11', '12', '12', '12']}) - exp_frame = exp_frame.set_index(['id', 'year'])[ - ['Arating', 'Arating_old', 'A', 'B', 'BB']] - long_frame = wide_to_long(df, ['A', 'B', 'BB'], i='id', j='year') - tm.assert_frame_equal(long_frame.sort_index(axis=1), - exp_frame.sort_index(axis=1)) - - def test_invalid_suffixtype(self): - # If all stubs names end with a string, but a numeric suffix is - # assumed, an empty data frame is returned - df = pd.DataFrame({'Aone': [1.0, 2.0], - 'Atwo': [3.0, 4.0], - 'Bone': [5.0, 6.0], - 'X': ['X1', 'X2']}) - df['id'] = df.index - exp_data = {'X': '', - 'Aone': [], - 'Atwo': [], - 'Bone': [], - 'id': [], - 'year': [], - 'A': [], - 'B': []} - exp_frame = pd.DataFrame(exp_data) - exp_frame = exp_frame.set_index(['id', 'year'])[[ - 'X', 'Aone', 'Atwo', 'Bone', 'A', 'B']] - exp_frame.index.set_levels([[0, 1], []], inplace=True) - long_frame = wide_to_long(df, ['A', 'B'], i='id', j='year') - tm.assert_frame_equal(long_frame.sort_index(axis=1), - exp_frame.sort_index(axis=1)) - - def test_multiple_id_columns(self): - # Taken from http://www.ats.ucla.edu/stat/stata/modules/reshapel.htm - df = pd.DataFrame({ - 'famid': [1, 1, 1, 2, 2, 2, 3, 3, 3], - 'birth': [1, 2, 3, 1, 2, 3, 1, 2, 3], - 'ht1': [2.8, 2.9, 2.2, 2, 1.8, 1.9, 2.2, 2.3, 2.1], - 'ht2': [3.4, 3.8, 2.9, 3.2, 2.8, 2.4, 3.3, 3.4, 2.9] - }) - exp_frame = pd.DataFrame({ - 'ht': [2.8, 3.4, 2.9, 3.8, 2.2, 2.9, 2.0, 3.2, 1.8, - 2.8, 1.9, 2.4, 2.2, 3.3, 2.3, 3.4, 2.1, 2.9], - 'famid': [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3], - 'birth': [1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3], - 'age': ['1', '2', '1', '2', '1', '2', '1', '2', '1', - '2', '1', '2', '1', '2', '1', '2', '1', '2'] - }) - exp_frame = exp_frame.set_index(['famid', 'birth', 'age'])[['ht']] - long_frame = wide_to_long(df, 'ht', i=['famid', 'birth'], j='age') - tm.assert_frame_equal(long_frame, exp_frame) diff --git a/pandas/tests/test_sorting.py b/pandas/tests/test_sorting.py index 99361695b2371..7528566e8326e 100644 --- a/pandas/tests/test_sorting.py +++ b/pandas/tests/test_sorting.py @@ -1,23 +1,26 @@ -import pytest -from itertools import product from collections import defaultdict +from datetime import datetime +from itertools import product +import warnings import numpy as np from numpy import nan -import pandas as pd +import pytest + +from pandas.compat import PY2 + +from pandas import DataFrame, MultiIndex, Series, compat, concat, merge from pandas.core import common as com -from pandas import DataFrame, MultiIndex, merge, concat, Series, compat +from pandas.core.sorting import ( + decons_group_index, get_group_index, is_int64_overflow_possible, + lexsort_indexer, nargsort, safe_sort) from pandas.util import testing as tm from pandas.util.testing import assert_frame_equal, assert_series_equal -from pandas.core.sorting import (is_int64_overflow_possible, - decons_group_index, - get_group_index, - nargsort, - lexsort_indexer) -class TestSorting(tm.TestCase): +class TestSorting(object): + @pytest.mark.slow def test_int64_overflow(self): B = np.concatenate((np.arange(1000), np.arange(1000), np.arange(500))) @@ -39,30 +42,30 @@ def test_int64_overflow(self): right = rg.sum()['values'] exp_index, _ = left.index.sortlevel() - self.assert_index_equal(left.index, exp_index) + tm.assert_index_equal(left.index, exp_index) exp_index, _ = right.index.sortlevel(0) - self.assert_index_equal(right.index, exp_index) + tm.assert_index_equal(right.index, exp_index) tups = list(map(tuple, df[['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' ]].values)) - tups = com._asarray_tuplesafe(tups) + tups = com.asarray_tuplesafe(tups) expected = df.groupby(tups).sum()['values'] for k, v in compat.iteritems(expected): - self.assertEqual(left[k], right[k[::-1]]) - self.assertEqual(left[k], v) - self.assertEqual(len(left), len(right)) + assert left[k] == right[k[::-1]] + assert left[k] == v + assert len(left) == len(right) + + def test_int64_overflow_moar(self): # GH9096 values = range(55109) - data = pd.DataFrame.from_dict({'a': values, - 'b': values, - 'c': values, - 'd': values}) + data = DataFrame.from_dict( + {'a': values, 'b': values, 'c': values, 'd': values}) grouped = data.groupby(['a', 'b', 'c', 'd']) - self.assertEqual(len(grouped), len(values)) + assert len(grouped) == len(values) arr = np.random.randint(-1 << 12, 1 << 12, (1 << 15, 5)) i = np.random.choice(len(arr), len(arr) * 4) @@ -76,15 +79,15 @@ def test_int64_overflow(self): gr = df.groupby(list('abcde')) # verify this is testing what it is supposed to test! - self.assertTrue(is_int64_overflow_possible(gr.grouper.shape)) + assert is_int64_overflow_possible(gr.grouper.shape) - # mannually compute groupings + # manually compute groupings jim, joe = defaultdict(list), defaultdict(list) for key, a, b in zip(map(tuple, arr), df['jim'], df['joe']): jim[key].append(a) joe[key].append(b) - self.assertEqual(len(gr), len(jim)) + assert len(gr) == len(jim) mi = MultiIndex.from_tuples(jim.keys(), names=list('abcde')) def aggr(func): @@ -124,13 +127,6 @@ def test_nargsort(self): # np.argsort(items2) may not place NaNs first items2 = np.array(items, dtype='O') - try: - # GH 2785; due to a regression in NumPy1.6.2 - np.argsort(np.array([[1, 2], [1, 3], [1, 2]], dtype='i')) - np.argsort(items2, kind='mergesort') - except TypeError: - pytest.skip('requested sort not available for type') - # mergesort is the most difficult to get right because we want it to be # stable. @@ -188,7 +184,7 @@ def test_nargsort(self): tm.assert_numpy_array_equal(result, np.array(exp), check_dtype=False) -class TestMerge(tm.TestCase): +class TestMerge(object): @pytest.mark.slow def test_int64_overflow_issues(self): @@ -201,7 +197,7 @@ def test_int64_overflow_issues(self): # it works! result = merge(df1, df2, how='outer') - self.assertTrue(len(result) == 2000) + assert len(result) == 2000 low, high, n = -1 << 10, 1 << 10, 1 << 20 left = DataFrame(np.random.randint(low, high, (n, 7)), @@ -216,11 +212,11 @@ def test_int64_overflow_issues(self): right['right'] *= -1 out = merge(left, right, how='outer') - self.assertEqual(len(out), len(left)) + assert len(out) == len(left) assert_series_equal(out['left'], - out['right'], check_names=False) result = out.iloc[:, :-2].sum(axis=1) assert_series_equal(out['left'], result, check_names=False) - self.assertTrue(result.name is None) + assert result.name is None out.sort_values(out.columns.tolist(), inplace=True) out.index = np.arange(len(out)) @@ -241,7 +237,7 @@ def test_int64_overflow_issues(self): # confirm that this is checking what it is supposed to check shape = left.apply(Series.nunique).values - self.assertTrue(is_int64_overflow_possible(shape)) + assert is_int64_overflow_possible(shape) # add duplicates to left frame left = concat([left, left], ignore_index=True) @@ -299,15 +295,15 @@ def verify_order(df): out = DataFrame(vals, columns=list('ABCDEFG') + ['left', 'right']) out = align(out) - jmask = {'left': out['left'].notnull(), - 'right': out['right'].notnull(), - 'inner': out['left'].notnull() & out['right'].notnull(), + jmask = {'left': out['left'].notna(), + 'right': out['right'].notna(), + 'inner': out['left'].notna() & out['right'].notna(), 'outer': np.ones(len(out), dtype='bool')} for how in 'left', 'right', 'outer', 'inner': mask = jmask[how] frame = align(out[mask].copy()) - self.assertTrue(mask.all() ^ mask.any() or how == 'outer') + assert mask.all() ^ mask.any() or how == 'outer' for sort in [False, True]: res = merge(left, right, how=how, sort=sort) @@ -326,14 +322,115 @@ def testit(label_list, shape): label_list2 = decons_group_index(group_index, shape) for a, b in zip(label_list, label_list2): - assert (np.array_equal(a, b)) + tm.assert_numpy_array_equal(a, b) shape = (4, 5, 6) - label_list = [np.tile([0, 1, 2, 3, 0, 1, 2, 3], 100), np.tile( - [0, 2, 4, 3, 0, 1, 2, 3], 100), np.tile( - [5, 1, 0, 2, 3, 0, 5, 4], 100)] + label_list = [np.tile([0, 1, 2, 3, 0, 1, 2, 3], 100).astype(np.int64), + np.tile([0, 2, 4, 3, 0, 1, 2, 3], 100).astype(np.int64), + np.tile([5, 1, 0, 2, 3, 0, 5, 4], 100).astype(np.int64)] testit(label_list, shape) shape = (10000, 10000) - label_list = [np.tile(np.arange(10000), 5), np.tile(np.arange(10000), 5)] + label_list = [np.tile(np.arange(10000, dtype=np.int64), 5), + np.tile(np.arange(10000, dtype=np.int64), 5)] testit(label_list, shape) + + +class TestSafeSort(object): + + def test_basic_sort(self): + values = [3, 1, 2, 0, 4] + result = safe_sort(values) + expected = np.array([0, 1, 2, 3, 4]) + tm.assert_numpy_array_equal(result, expected) + + values = list("baaacb") + result = safe_sort(values) + expected = np.array(list("aaabbc"), dtype='object') + tm.assert_numpy_array_equal(result, expected) + + values = [] + result = safe_sort(values) + expected = np.array([]) + tm.assert_numpy_array_equal(result, expected) + + def test_labels(self): + values = [3, 1, 2, 0, 4] + expected = np.array([0, 1, 2, 3, 4]) + + labels = [0, 1, 1, 2, 3, 0, -1, 4] + result, result_labels = safe_sort(values, labels) + expected_labels = np.array([3, 1, 1, 2, 0, 3, -1, 4], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result_labels, expected_labels) + + # na_sentinel + labels = [0, 1, 1, 2, 3, 0, 99, 4] + result, result_labels = safe_sort(values, labels, + na_sentinel=99) + expected_labels = np.array([3, 1, 1, 2, 0, 3, 99, 4], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result_labels, expected_labels) + + # out of bound indices + labels = [0, 101, 102, 2, 3, 0, 99, 4] + result, result_labels = safe_sort(values, labels) + expected_labels = np.array([3, -1, -1, 2, 0, 3, -1, 4], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result_labels, expected_labels) + + labels = [] + result, result_labels = safe_sort(values, labels) + expected_labels = np.array([], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result_labels, expected_labels) + + def test_mixed_integer(self): + values = np.array(['b', 1, 0, 'a', 0, 'b'], dtype=object) + result = safe_sort(values) + expected = np.array([0, 0, 1, 'a', 'b', 'b'], dtype=object) + tm.assert_numpy_array_equal(result, expected) + + values = np.array(['b', 1, 0, 'a'], dtype=object) + labels = [0, 1, 2, 3, 0, -1, 1] + result, result_labels = safe_sort(values, labels) + expected = np.array([0, 1, 'a', 'b'], dtype=object) + expected_labels = np.array([3, 1, 0, 2, 3, -1, 1], dtype=np.intp) + tm.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result_labels, expected_labels) + + def test_mixed_integer_from_list(self): + values = ['b', 1, 0, 'a', 0, 'b'] + result = safe_sort(values) + expected = np.array([0, 0, 1, 'a', 'b', 'b'], dtype=object) + tm.assert_numpy_array_equal(result, expected) + + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") + def test_unsortable(self): + # GH 13714 + arr = np.array([1, 2, datetime.now(), 0, 3], dtype=object) + msg = (r"'(<|>)' not supported between instances of ('" + r"datetime\.datetime' and 'int'|'int' and 'datetime\.datetime" + r"')|" + r"unorderable types: int\(\) > datetime\.datetime\(\)") + if compat.PY2: + # RuntimeWarning: tp_compare didn't return -1 or -2 for exception + with warnings.catch_warnings(): + with pytest.raises(TypeError, match=msg): + safe_sort(arr) + else: + with pytest.raises(TypeError, match=msg): + safe_sort(arr) + + def test_exceptions(self): + with pytest.raises(TypeError, + match="Only list-like objects are allowed"): + safe_sort(values=1) + + with pytest.raises(TypeError, + match="Only list-like objects or None"): + safe_sort(values=[0, 1, 2], labels=1) + + with pytest.raises(ValueError, + match="values should be unique"): + safe_sort(values=[0, 1, 2, 1], labels=[0, 1]) diff --git a/pandas/tests/test_strings.py b/pandas/tests/test_strings.py index 7a68ec8f368ae..40a83f90c8dfd 100644 --- a/pandas/tests/test_strings.py +++ b/pandas/tests/test_strings.py @@ -4,33 +4,291 @@ from datetime import datetime, timedelta import re -from numpy import nan as NA import numpy as np +from numpy import nan as NA from numpy.random import randint +import pytest -from pandas.compat import range, u import pandas.compat as compat -from pandas import (Index, Series, DataFrame, isnull, MultiIndex, notnull) - -from pandas.util.testing import assert_series_equal -import pandas.util.testing as tm +from pandas.compat import PY2, PY3, range, u +from pandas import DataFrame, Index, MultiIndex, Series, concat, isna, notna import pandas.core.strings as strings - - -class TestStringMethods(tm.TestCase): +import pandas.util.testing as tm +from pandas.util.testing import assert_index_equal, assert_series_equal + + +def assert_series_or_index_equal(left, right): + if isinstance(left, Series): + assert_series_equal(left, right) + else: # Index + assert_index_equal(left, right) + + +_any_string_method = [ + ('cat', (), {'sep': ','}), # noqa: E241 + ('cat', (Series(list('zyx')),), {'sep': ',', # noqa: E241 + 'join': 'left'}), + ('center', (10,), {}), # noqa: E241 + ('contains', ('a',), {}), # noqa: E241 + ('count', ('a',), {}), # noqa: E241 + ('decode', ('UTF-8',), {}), # noqa: E241 + ('encode', ('UTF-8',), {}), # noqa: E241 + ('endswith', ('a',), {}), # noqa: E241 + ('extract', ('([a-z]*)',), {'expand': False}), # noqa: E241 + ('extract', ('([a-z]*)',), {'expand': True}), # noqa: E241 + ('extractall', ('([a-z]*)',), {}), # noqa: E241 + ('find', ('a',), {}), # noqa: E241 + ('findall', ('a',), {}), # noqa: E241 + ('get', (0,), {}), # noqa: E241 + # because "index" (and "rindex") fail intentionally + # if the string is not found, search only for empty string + ('index', ('',), {}), # noqa: E241 + ('join', (',',), {}), # noqa: E241 + ('ljust', (10,), {}), # noqa: E241 + ('match', ('a',), {}), # noqa: E241 + ('normalize', ('NFC',), {}), # noqa: E241 + ('pad', (10,), {}), # noqa: E241 + ('partition', (' ',), {'expand': False}), # noqa: E241 + ('partition', (' ',), {'expand': True}), # noqa: E241 + ('repeat', (3,), {}), # noqa: E241 + ('replace', ('a', 'z',), {}), # noqa: E241 + ('rfind', ('a',), {}), # noqa: E241 + ('rindex', ('',), {}), # noqa: E241 + ('rjust', (10,), {}), # noqa: E241 + ('rpartition', (' ',), {'expand': False}), # noqa: E241 + ('rpartition', (' ',), {'expand': True}), # noqa: E241 + ('slice', (0, 1,), {}), # noqa: E241 + ('slice_replace', (0, 1, 'z',), {}), # noqa: E241 + ('split', (' ',), {'expand': False}), # noqa: E241 + ('split', (' ',), {'expand': True}), # noqa: E241 + ('startswith', ('a',), {}), # noqa: E241 + # translating unicode points of "a" to "d" + ('translate', ({97: 100},), {}), # noqa: E241 + ('wrap', (2,), {}), # noqa: E241 + ('zfill', (10,), {}) # noqa: E241 +] + list(zip([ + # methods without positional arguments: zip with empty tuple and empty dict + 'capitalize', 'cat', 'get_dummies', + 'isalnum', 'isalpha', 'isdecimal', + 'isdigit', 'islower', 'isnumeric', + 'isspace', 'istitle', 'isupper', + 'len', 'lower', 'lstrip', 'partition', + 'rpartition', 'rsplit', 'rstrip', + 'slice', 'slice_replace', 'split', + 'strip', 'swapcase', 'title', 'upper', 'casefold' +], [()] * 100, [{}] * 100)) +ids, _, _ = zip(*_any_string_method) # use method name as fixture-id + + +# test that the above list captures all methods of StringMethods +missing_methods = {f for f in dir(strings.StringMethods) + if not f.startswith('_')} - set(ids) +assert not missing_methods + + +@pytest.fixture(params=_any_string_method, ids=ids) +def any_string_method(request): + """ + Fixture for all public methods of `StringMethods` + + This fixture returns a tuple of the method name and sample arguments + necessary to call the method. + + Returns + ------- + method_name : str + The name of the method in `StringMethods` + args : tuple + Sample values for the positional arguments + kwargs : dict + Sample values for the keyword arguments + + Examples + -------- + >>> def test_something(any_string_method): + ... s = pd.Series(['a', 'b', np.nan, 'd']) + ... + ... method_name, args, kwargs = any_string_method + ... method = getattr(s.str, method_name) + ... # will not raise + ... method(*args, **kwargs) + """ + return request.param + + +# subset of the full set from pandas/conftest.py +_any_allowed_skipna_inferred_dtype = [ + ('string', ['a', np.nan, 'c']), + ('unicode' if not PY3 else 'string', [u('a'), np.nan, u('c')]), + ('bytes' if PY3 else 'string', [b'a', np.nan, b'c']), + ('empty', [np.nan, np.nan, np.nan]), + ('empty', []), + ('mixed-integer', ['a', np.nan, 2]) +] +ids, _ = zip(*_any_allowed_skipna_inferred_dtype) # use inferred type as id + + +@pytest.fixture(params=_any_allowed_skipna_inferred_dtype, ids=ids) +def any_allowed_skipna_inferred_dtype(request): + """ + Fixture for all (inferred) dtypes allowed in StringMethods.__init__ + + The covered (inferred) types are: + * 'string' + * 'unicode' (if PY2) + * 'empty' + * 'bytes' (if PY3) + * 'mixed' + * 'mixed-integer' + + Returns + ------- + inferred_dtype : str + The string for the inferred dtype from _libs.lib.infer_dtype + values : np.ndarray + An array of object dtype that will be inferred to have + `inferred_dtype` + + Examples + -------- + >>> import pandas._libs.lib as lib + >>> + >>> def test_something(any_allowed_skipna_inferred_dtype): + ... inferred_dtype, values = any_allowed_skipna_inferred_dtype + ... # will pass + ... assert lib.infer_dtype(values, skipna=True) == inferred_dtype + """ + inferred_dtype, values = request.param + values = np.array(values, dtype=object) # object dtype to avoid casting + + # correctness of inference tested in tests/dtypes/test_inference.py + return inferred_dtype, values + + +class TestStringMethods(object): def test_api(self): # GH 6106, GH 9322 - self.assertIs(Series.str, strings.StringMethods) - self.assertIsInstance(Series(['']).str, strings.StringMethods) - - # GH 9184 - invalid = Series([1]) - with tm.assertRaisesRegexp(AttributeError, "only use .str accessor"): - invalid.str - self.assertFalse(hasattr(invalid, 'str')) + assert Series.str is strings.StringMethods + assert isinstance(Series(['']).str, strings.StringMethods) + + @pytest.mark.parametrize('dtype', [object, 'category']) + @pytest.mark.parametrize('box', [Series, Index]) + def test_api_per_dtype(self, box, dtype, any_skipna_inferred_dtype): + # one instance of parametrized fixture + inferred_dtype, values = any_skipna_inferred_dtype + + t = box(values, dtype=dtype) # explicit dtype to avoid casting + + # TODO: get rid of these xfails + if dtype == 'category' and inferred_dtype in ['period', 'interval']: + pytest.xfail(reason='Conversion to numpy array fails because ' + 'the ._values-attribute is not a numpy array for ' + 'PeriodArray/IntervalArray; see GH 23553') + if box == Index and inferred_dtype in ['empty', 'bytes']: + pytest.xfail(reason='Raising too restrictively; ' + 'solved by GH 23167') + if (box == Index and dtype == object + and inferred_dtype in ['boolean', 'date', 'time']): + pytest.xfail(reason='Inferring incorrectly because of NaNs; ' + 'solved by GH 23167') + if (box == Series + and (dtype == object and inferred_dtype not in [ + 'string', 'unicode', 'empty', + 'bytes', 'mixed', 'mixed-integer']) + or (dtype == 'category' + and inferred_dtype in ['decimal', 'boolean', 'time'])): + pytest.xfail(reason='Not raising correctly; solved by GH 23167') + + types_passing_constructor = ['string', 'unicode', 'empty', + 'bytes', 'mixed', 'mixed-integer'] + if inferred_dtype in types_passing_constructor: + # GH 6106 + assert isinstance(t.str, strings.StringMethods) + else: + # GH 9184, GH 23011, GH 23163 + with pytest.raises(AttributeError, match='Can only use .str ' + 'accessor with string values.*'): + t.str + assert not hasattr(t, 'str') + + @pytest.mark.parametrize('dtype', [object, 'category']) + @pytest.mark.parametrize('box', [Series, Index]) + def test_api_per_method(self, box, dtype, + any_allowed_skipna_inferred_dtype, + any_string_method): + # this test does not check correctness of the different methods, + # just that the methods work on the specified (inferred) dtypes, + # and raise on all others + + # one instance of each parametrized fixture + inferred_dtype, values = any_allowed_skipna_inferred_dtype + method_name, args, kwargs = any_string_method + + # TODO: get rid of these xfails + if (method_name not in ['encode', 'decode', 'len'] + and inferred_dtype == 'bytes'): + pytest.xfail(reason='Not raising for "bytes", see GH 23011;' + 'Also: malformed method names, see GH 23551; ' + 'solved by GH 23167') + if (method_name == 'cat' + and inferred_dtype in ['mixed', 'mixed-integer']): + pytest.xfail(reason='Bad error message; should raise better; ' + 'solved by GH 23167') + if box == Index and inferred_dtype in ['empty', 'bytes']: + pytest.xfail(reason='Raising too restrictively; ' + 'solved by GH 23167') + if (box == Index and dtype == object + and inferred_dtype in ['boolean', 'date', 'time']): + pytest.xfail(reason='Inferring incorrectly because of NaNs; ' + 'solved by GH 23167') + + t = box(values, dtype=dtype) # explicit dtype to avoid casting + method = getattr(t.str, method_name) + + bytes_allowed = method_name in ['encode', 'decode', 'len'] + # as of v0.23.4, all methods except 'cat' are very lenient with the + # allowed data types, just returning NaN for entries that error. + # This could be changed with an 'errors'-kwarg to the `str`-accessor, + # see discussion in GH 13877 + mixed_allowed = method_name not in ['cat'] + + allowed_types = (['string', 'unicode', 'empty'] + + ['bytes'] * bytes_allowed + + ['mixed', 'mixed-integer'] * mixed_allowed) + + if inferred_dtype in allowed_types: + # xref GH 23555, GH 23556 + method(*args, **kwargs) # works! + else: + # GH 23011, GH 23163 + msg = ('Cannot use .str.{name} with values of inferred dtype ' + '{inferred_dtype!r}.'.format(name=method_name, + inferred_dtype=inferred_dtype)) + with pytest.raises(TypeError, match=msg): + method(*args, **kwargs) + + def test_api_for_categorical(self, any_string_method): + # https://github.com/pandas-dev/pandas/issues/10661 + s = Series(list('aabb')) + s = s + " " + s + c = s.astype('category') + assert isinstance(c.str, strings.StringMethods) + + method_name, args, kwargs = any_string_method + + result = getattr(c.str, method_name)(*args, **kwargs) + expected = getattr(s.str, method_name)(*args, **kwargs) + + if isinstance(result, DataFrame): + tm.assert_frame_equal(result, expected) + elif isinstance(result, Series): + tm.assert_series_equal(result, expected) + else: + # str.cat(others=None) returns string, for example + assert result == expected def test_iter(self): # GH3638 @@ -39,7 +297,7 @@ def test_iter(self): for s in ds.str: # iter must yield a Series - tm.assertIsInstance(s, Series) + assert isinstance(s, Series) # indices of each yielded Series should be equal to the index of # the original Series @@ -47,13 +305,12 @@ def test_iter(self): for el in s: # each element of the series is either a basestring/str or nan - self.assertTrue(isinstance(el, compat.string_types) or - isnull(el)) + assert isinstance(el, compat.string_types) or isna(el) # desired behavior is to iterate until everything would be nan on the # next iter so make sure the last element of the iterator was 'l' in # this case since 'wikitravel' is the longest string - self.assertEqual(s.dropna().values.item(), 'l') + assert s.dropna().values.item() == 'l' def test_iter_empty(self): ds = Series([], dtype=object) @@ -65,8 +322,8 @@ def test_iter_empty(self): # nothing to iterate over so nothing defined values should remain # unchanged - self.assertEqual(i, 100) - self.assertEqual(s, 1) + assert i == 100 + assert s == 1 def test_iter_single_element(self): ds = Series(['a']) @@ -74,7 +331,7 @@ def test_iter_single_element(self): for i, s in enumerate(ds.str): pass - self.assertFalse(i) + assert not i assert_series_equal(ds, s) def test_iter_object_try_string(self): @@ -86,43 +343,346 @@ def test_iter_object_try_string(self): for i, s in enumerate(ds.str): pass - self.assertEqual(i, 100) - self.assertEqual(s, 'h') + assert i == 100 + assert s == 'h' + + @pytest.mark.parametrize('box', [Series, Index]) + @pytest.mark.parametrize('other', [None, Series, Index]) + def test_str_cat_name(self, box, other): + # GH 21053 + values = ['a', 'b'] + if other: + other = other(values) + else: + other = values + result = box(values, name='name').str.cat(other, sep=',', join='left') + assert result.name == 'name' - def test_cat(self): - one = np.array(['a', 'a', 'b', 'b', 'c', NA], dtype=np.object_) - two = np.array(['a', NA, 'b', 'd', 'foo', NA], dtype=np.object_) + @pytest.mark.parametrize('box', [Series, Index]) + def test_str_cat(self, box): + # test_cat above tests "str_cat" from ndarray; + # here testing "str.cat" from Series/Indext to ndarray/list + s = box(['a', 'a', 'b', 'b', 'c', np.nan]) # single array - result = strings.str_cat(one) - exp = 'aabbc' - self.assertEqual(result, exp) - - result = strings.str_cat(one, na_rep='NA') - exp = 'aabbcNA' - self.assertEqual(result, exp) - - result = strings.str_cat(one, na_rep='-') - exp = 'aabbc-' - self.assertEqual(result, exp) - - result = strings.str_cat(one, sep='_', na_rep='NA') - exp = 'a_a_b_b_c_NA' - self.assertEqual(result, exp) - - result = strings.str_cat(two, sep='-') - exp = 'a-b-d-foo' - self.assertEqual(result, exp) - - # Multiple arrays - result = strings.str_cat(one, [two], na_rep='NA') - exp = np.array(['aa', 'aNA', 'bb', 'bd', 'cfoo', 'NANA'], - dtype=np.object_) - self.assert_numpy_array_equal(result, exp) - - result = strings.str_cat(one, two) - exp = np.array(['aa', NA, 'bb', 'bd', 'cfoo', NA], dtype=np.object_) - tm.assert_almost_equal(result, exp) + result = s.str.cat() + expected = 'aabbc' + assert result == expected + + result = s.str.cat(na_rep='-') + expected = 'aabbc-' + assert result == expected + + result = s.str.cat(sep='_', na_rep='NA') + expected = 'a_a_b_b_c_NA' + assert result == expected + + t = np.array(['a', np.nan, 'b', 'd', 'foo', np.nan], dtype=object) + expected = box(['aa', 'a-', 'bb', 'bd', 'cfoo', '--']) + + # Series/Index with array + result = s.str.cat(t, na_rep='-') + assert_series_or_index_equal(result, expected) + + # Series/Index with list + result = s.str.cat(list(t), na_rep='-') + assert_series_or_index_equal(result, expected) + + # errors for incorrect lengths + rgx = 'All arrays must be same length, except those having an index.*' + z = Series(['1', '2', '3']) + + with pytest.raises(ValueError, match=rgx): + s.str.cat(z) + + with pytest.raises(ValueError, match=rgx): + s.str.cat(z.values) + + with pytest.raises(ValueError, match=rgx): + s.str.cat(list(z)) + + @pytest.mark.parametrize('box', [Series, Index]) + def test_str_cat_raises_intuitive_error(self, box): + # GH 11334 + s = box(['a', 'b', 'c', 'd']) + message = "Did you mean to supply a `sep` keyword?" + with pytest.raises(ValueError, match=message): + s.str.cat('|') + with pytest.raises(ValueError, match=message): + s.str.cat(' ') + + @pytest.mark.parametrize('sep', ['', None]) + @pytest.mark.parametrize('dtype_target', ['object', 'category']) + @pytest.mark.parametrize('dtype_caller', ['object', 'category']) + @pytest.mark.parametrize('box', [Series, Index]) + def test_str_cat_categorical(self, box, dtype_caller, dtype_target, sep): + s = Index(['a', 'a', 'b', 'a'], dtype=dtype_caller) + s = s if box == Index else Series(s, index=s) + t = Index(['b', 'a', 'b', 'c'], dtype=dtype_target) + + expected = Index(['ab', 'aa', 'bb', 'ac']) + expected = expected if box == Index else Series(expected, index=s) + + # Series/Index with unaligned Index + with tm.assert_produces_warning(expected_warning=FutureWarning): + # FutureWarning to switch to alignment by default + result = s.str.cat(t, sep=sep) + assert_series_or_index_equal(result, expected) + + # Series/Index with Series having matching Index + t = Series(t, index=s) + result = s.str.cat(t, sep=sep) + assert_series_or_index_equal(result, expected) + + # Series/Index with Series.values + result = s.str.cat(t.values, sep=sep) + assert_series_or_index_equal(result, expected) + + # Series/Index with Series having different Index + t = Series(t.values, index=t) + with tm.assert_produces_warning(expected_warning=FutureWarning): + # FutureWarning to switch to alignment by default + result = s.str.cat(t, sep=sep) + assert_series_or_index_equal(result, expected) + + @pytest.mark.parametrize('box', [Series, Index]) + def test_str_cat_mixed_inputs(self, box): + s = Index(['a', 'b', 'c', 'd']) + s = s if box == Index else Series(s, index=s) + + t = Series(['A', 'B', 'C', 'D'], index=s.values) + d = concat([t, Series(s, index=s)], axis=1) + + expected = Index(['aAa', 'bBb', 'cCc', 'dDd']) + expected = expected if box == Index else Series(expected.values, + index=s.values) + + # Series/Index with DataFrame + result = s.str.cat(d) + assert_series_or_index_equal(result, expected) + + # Series/Index with two-dimensional ndarray + result = s.str.cat(d.values) + assert_series_or_index_equal(result, expected) + + # Series/Index with list of Series + result = s.str.cat([t, s]) + assert_series_or_index_equal(result, expected) + + # Series/Index with mixed list of Series/array + result = s.str.cat([t, s.values]) + assert_series_or_index_equal(result, expected) + + # Series/Index with list of list-likes + with tm.assert_produces_warning(expected_warning=FutureWarning): + # nested list-likes will be deprecated + result = s.str.cat([t.values, list(s)]) + assert_series_or_index_equal(result, expected) + + # Series/Index with list of Series; different indexes + t.index = ['b', 'c', 'd', 'a'] + with tm.assert_produces_warning(expected_warning=FutureWarning): + # FutureWarning to switch to alignment by default + result = s.str.cat([t, s]) + assert_series_or_index_equal(result, expected) + + # Series/Index with mixed list; different indexes + with tm.assert_produces_warning(expected_warning=FutureWarning): + # FutureWarning to switch to alignment by default + result = s.str.cat([t, s.values]) + assert_series_or_index_equal(result, expected) + + # Series/Index with DataFrame; different indexes + d.index = ['b', 'c', 'd', 'a'] + with tm.assert_produces_warning(expected_warning=FutureWarning): + # FutureWarning to switch to alignment by default + result = s.str.cat(d) + assert_series_or_index_equal(result, expected) + + # Series/Index with iterator of list-likes + with tm.assert_produces_warning(expected_warning=FutureWarning): + # nested list-likes will be deprecated + result = s.str.cat(iter([t.values, list(s)])) + assert_series_or_index_equal(result, expected) + + # errors for incorrect lengths + rgx = 'All arrays must be same length, except those having an index.*' + z = Series(['1', '2', '3']) + e = concat([z, z], axis=1) + + # DataFrame + with pytest.raises(ValueError, match=rgx): + s.str.cat(e) + + # two-dimensional ndarray + with pytest.raises(ValueError, match=rgx): + s.str.cat(e.values) + + # list of Series + with pytest.raises(ValueError, match=rgx): + s.str.cat([z, s]) + + # list of list-likes + with pytest.raises(ValueError, match=rgx): + s.str.cat([z.values, s.values]) + + # mixed list of Series/list-like + with pytest.raises(ValueError, match=rgx): + s.str.cat([z.values, s]) + + # errors for incorrect arguments in list-like + rgx = 'others must be Series, Index, DataFrame,.*' + # make sure None/NaN do not crash checks in _get_series_list + u = Series(['a', np.nan, 'c', None]) + + # mix of string and Series + with pytest.raises(TypeError, match=rgx): + s.str.cat([u, 'u']) + + # DataFrame in list + with pytest.raises(TypeError, match=rgx): + s.str.cat([u, d]) + + # 2-dim ndarray in list + with pytest.raises(TypeError, match=rgx): + s.str.cat([u, d.values]) + + # nested lists + with pytest.raises(TypeError, match=rgx): + s.str.cat([u, [u, d]]) + + # forbidden input type: set + # GH 23009 + with pytest.raises(TypeError, match=rgx): + s.str.cat(set(u)) + + # forbidden input type: set in list + # GH 23009 + with pytest.raises(TypeError, match=rgx): + s.str.cat([u, set(u)]) + + # other forbidden input type, e.g. int + with pytest.raises(TypeError, match=rgx): + s.str.cat(1) + + @pytest.mark.parametrize('join', ['left', 'outer', 'inner', 'right']) + @pytest.mark.parametrize('box', [Series, Index]) + def test_str_cat_align_indexed(self, box, join): + # https://github.com/pandas-dev/pandas/issues/18657 + s = Series(['a', 'b', 'c', 'd'], index=['a', 'b', 'c', 'd']) + t = Series(['D', 'A', 'E', 'B'], index=['d', 'a', 'e', 'b']) + sa, ta = s.align(t, join=join) + # result after manual alignment of inputs + expected = sa.str.cat(ta, na_rep='-') + + if box == Index: + s = Index(s) + sa = Index(sa) + expected = Index(expected) + + result = s.str.cat(t, join=join, na_rep='-') + assert_series_or_index_equal(result, expected) + + @pytest.mark.parametrize('join', ['left', 'outer', 'inner', 'right']) + def test_str_cat_align_mixed_inputs(self, join): + s = Series(['a', 'b', 'c', 'd']) + t = Series(['d', 'a', 'e', 'b'], index=[3, 0, 4, 1]) + d = concat([t, t], axis=1) + + expected_outer = Series(['aaa', 'bbb', 'c--', 'ddd', '-ee']) + expected = expected_outer.loc[s.index.join(t.index, how=join)] + + # list of Series + result = s.str.cat([t, t], join=join, na_rep='-') + tm.assert_series_equal(result, expected) + + # DataFrame + result = s.str.cat(d, join=join, na_rep='-') + tm.assert_series_equal(result, expected) + + # mixed list of indexed/unindexed + u = np.array(['A', 'B', 'C', 'D']) + expected_outer = Series(['aaA', 'bbB', 'c-C', 'ddD', '-e-']) + # joint index of rhs [t, u]; u will be forced have index of s + rhs_idx = t.index & s.index if join == 'inner' else t.index | s.index + + expected = expected_outer.loc[s.index.join(rhs_idx, how=join)] + result = s.str.cat([t, u], join=join, na_rep='-') + tm.assert_series_equal(result, expected) + + with tm.assert_produces_warning(expected_warning=FutureWarning): + # nested list-likes will be deprecated + result = s.str.cat([t, list(u)], join=join, na_rep='-') + tm.assert_series_equal(result, expected) + + # errors for incorrect lengths + rgx = r'If `others` contains arrays or lists \(or other list-likes.*' + z = Series(['1', '2', '3']).values + + # unindexed object of wrong length + with pytest.raises(ValueError, match=rgx): + s.str.cat(z, join=join) + + # unindexed object of wrong length in list + with pytest.raises(ValueError, match=rgx): + s.str.cat([t, z], join=join) + + @pytest.mark.parametrize('box', [Series, Index]) + @pytest.mark.parametrize('other', [Series, Index]) + def test_str_cat_all_na(self, box, other): + # GH 24044 + + # check that all NaNs in caller / target work + s = Index(['a', 'b', 'c', 'd']) + s = s if box == Index else Series(s, index=s) + t = other([np.nan] * 4, dtype=object) + # add index of s for alignment + t = t if other == Index else Series(t, index=s) + + # all-NA target + if box == Series: + expected = Series([np.nan] * 4, index=s.index, dtype=object) + else: # box == Index + expected = Index([np.nan] * 4, dtype=object) + result = s.str.cat(t, join='left') + assert_series_or_index_equal(result, expected) + + # all-NA caller (only for Series) + if other == Series: + expected = Series([np.nan] * 4, dtype=object, index=t.index) + result = t.str.cat(s, join='left') + tm.assert_series_equal(result, expected) + + def test_str_cat_special_cases(self): + s = Series(['a', 'b', 'c', 'd']) + t = Series(['d', 'a', 'e', 'b'], index=[3, 0, 4, 1]) + + # iterator of elements with different types + expected = Series(['aaa', 'bbb', 'c-c', 'ddd', '-e-']) + result = s.str.cat(iter([t, s.values]), join='outer', na_rep='-') + tm.assert_series_equal(result, expected) + + # right-align with different indexes in others + expected = Series(['aa-', 'd-d'], index=[0, 3]) + result = s.str.cat([t.loc[[0]], t.loc[[3]]], join='right', na_rep='-') + tm.assert_series_equal(result, expected) + + def test_cat_on_filtered_index(self): + df = DataFrame(index=MultiIndex.from_product( + [[2011, 2012], [1, 2, 3]], names=['year', 'month'])) + + df = df.reset_index() + df = df[df.month > 1] + + str_year = df.year.astype('str') + str_month = df.month.astype('str') + str_both = str_year.str.cat(str_month, sep=' ') + + assert str_both.loc[1] == '2011 2' + + str_multiple = str_year.str.cat([str_month, str_month], sep=' ') + + assert str_multiple.loc[1] == '2011 2 2' def test_count(self): values = np.array(['foo', 'foofoo', NA, 'foooofooofommmfoo'], @@ -134,7 +694,7 @@ def test_count(self): result = Series(values).str.count('f[o]+') exp = Series([1, 2, NA, 4]) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_series_equal(result, exp) # mixed @@ -145,7 +705,7 @@ def test_count(self): rs = Series(mixed).str.count('a') xp = Series([1, NA, 0, NA, NA, 0, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -157,7 +717,7 @@ def test_count(self): result = Series(values).str.count('f[o]+') exp = Series([1, 2, NA, 4]) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_series_equal(result, exp) def test_contains(self): @@ -176,7 +736,7 @@ def test_contains(self): values = ['foo', 'xyz', 'fooommm__foo', 'mmm_'] result = strings.str_contains(values, pat) expected = np.array([False, False, True, True]) - self.assertEqual(result.dtype, np.bool_) + assert result.dtype == np.bool_ tm.assert_numpy_array_equal(result, expected) # case insensitive using regex @@ -199,7 +759,7 @@ def test_contains(self): rs = Series(mixed).str.contains('o') xp = Series([False, NA, False, NA, NA, True, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -219,13 +779,31 @@ def test_contains(self): dtype=np.object_) result = strings.str_contains(values, pat) expected = np.array([False, False, True, True]) - self.assertEqual(result.dtype, np.bool_) + assert result.dtype == np.bool_ tm.assert_numpy_array_equal(result, expected) - # na - values = Series(['om', 'foo', np.nan]) - res = values.str.contains('foo', na="foo") - self.assertEqual(res.loc[2], "foo") + def test_contains_for_object_category(self): + # gh 22158 + + # na for category + values = Series(["a", "b", "c", "a", np.nan], dtype="category") + result = values.str.contains('a', na=True) + expected = Series([True, False, False, True, True]) + tm.assert_series_equal(result, expected) + + result = values.str.contains('a', na=False) + expected = Series([True, False, False, True, False]) + tm.assert_series_equal(result, expected) + + # na for objects + values = Series(["a", "b", "c", "a", np.nan]) + result = values.str.contains('a', na=True) + expected = Series([True, False, False, True, True]) + tm.assert_series_equal(result, expected) + + result = values.str.contains('a', na=False) + expected = Series([True, False, False, True, False]) + tm.assert_series_equal(result, expected) def test_startswith(self): values = Series(['om', NA, 'foo_nom', 'nom', 'bar_foo', NA, 'foo']) @@ -243,7 +821,7 @@ def test_startswith(self): tm.assert_numpy_array_equal(rs, xp) rs = Series(mixed).str.startswith('f') - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) xp = Series([False, NA, False, NA, NA, True, NA, NA, NA]) tm.assert_series_equal(rs, xp) @@ -274,7 +852,7 @@ def test_endswith(self): rs = Series(mixed).str.endswith('f') xp = Series([False, NA, False, NA, NA, False, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -326,7 +904,7 @@ def test_lower_upper(self): mixed = mixed.str.upper() rs = Series(mixed).str.lower() xp = Series(['a', NA, 'b', NA, NA, 'foo', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -380,13 +958,11 @@ def test_swapcase(self): def test_casemethods(self): values = ['aaa', 'bbb', 'CCC', 'Dddd', 'eEEE'] s = Series(values) - self.assertEqual(s.str.lower().tolist(), [v.lower() for v in values]) - self.assertEqual(s.str.upper().tolist(), [v.upper() for v in values]) - self.assertEqual(s.str.title().tolist(), [v.title() for v in values]) - self.assertEqual(s.str.capitalize().tolist(), [ - v.capitalize() for v in values]) - self.assertEqual(s.str.swapcase().tolist(), [ - v.swapcase() for v in values]) + assert s.str.lower().tolist() == [v.lower() for v in values] + assert s.str.upper().tolist() == [v.upper() for v in values] + assert s.str.title().tolist() == [v.title() for v in values] + assert s.str.capitalize().tolist() == [v.capitalize() for v in values] + assert s.str.swapcase().tolist() == [v.swapcase() for v in values] def test_replace(self): values = Series(['fooBAD__barBAD', NA]) @@ -405,7 +981,7 @@ def test_replace(self): rs = Series(mixed).str.replace('BAD[_]*', '') xp = Series(['a', NA, 'b', NA, NA, 'foo', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -426,11 +1002,13 @@ def test_replace(self): tm.assert_series_equal(result, exp) # GH 13438 + msg = "repl must be a string or callable" for klass in (Series, Index): for repl in (None, 3, {'a': 'b'}): for data in (['a', 'b', None], ['a', 'b', 'c', 'ad']): values = klass(data) - self.assertRaises(TypeError, values.str.replace, 'a', repl) + with pytest.raises(TypeError, match=msg): + values.str.replace('a', repl) def test_replace_callable(self): # GH 15055 @@ -450,15 +1028,15 @@ def test_replace_callable(self): r'(?(3)required )positional arguments?') repl = lambda: None - with tm.assertRaisesRegexp(TypeError, p_err): + with pytest.raises(TypeError, match=p_err): values.str.replace('a', repl) repl = lambda m, x: None - with tm.assertRaisesRegexp(TypeError, p_err): + with pytest.raises(TypeError, match=p_err): values.str.replace('a', repl) repl = lambda m, x, y=None: None - with tm.assertRaisesRegexp(TypeError, p_err): + with pytest.raises(TypeError, match=p_err): values.str.replace('a', repl) # test regex named groups @@ -485,7 +1063,7 @@ def test_replace_compiled_regex(self): rs = Series(mixed).str.replace(pat, '') xp = Series(['a', NA, 'b', NA, NA, 'foo', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -511,13 +1089,16 @@ def test_replace_compiled_regex(self): values = Series(['fooBAD__barBAD__bad', NA]) pat = re.compile(r'BAD[_]*') - with tm.assertRaisesRegexp(ValueError, "case and flags cannot be"): + with pytest.raises(ValueError, + match="case and flags cannot be"): result = values.str.replace(pat, '', flags=re.IGNORECASE) - with tm.assertRaisesRegexp(ValueError, "case and flags cannot be"): + with pytest.raises(ValueError, + match="case and flags cannot be"): result = values.str.replace(pat, '', case=False) - with tm.assertRaisesRegexp(ValueError, "case and flags cannot be"): + with pytest.raises(ValueError, + match="case and flags cannot be"): result = values.str.replace(pat, '', case=True) # test with callable @@ -528,6 +1109,31 @@ def test_replace_compiled_regex(self): exp = Series(['foObaD__baRbaD', NA]) tm.assert_series_equal(result, exp) + def test_replace_literal(self): + # GH16808 literal replace (regex=False vs regex=True) + values = Series(['f.o', 'foo', NA]) + exp = Series(['bao', 'bao', NA]) + result = values.str.replace('f.', 'ba') + tm.assert_series_equal(result, exp) + + exp = Series(['bao', 'foo', NA]) + result = values.str.replace('f.', 'ba', regex=False) + tm.assert_series_equal(result, exp) + + # Cannot do a literal replace if given a callable repl or compiled + # pattern + callable_repl = lambda m: m.group(0).swapcase() + compiled_pat = re.compile('[a-z][A-Z]{2}') + + msg = "Cannot use a callable replacement when regex=False" + with pytest.raises(ValueError, match=msg): + values.str.replace('abc', callable_repl, regex=False) + + msg = ("Cannot use a compiled regex as replacement pattern with" + " regex=False") + with pytest.raises(ValueError, match=msg): + values.str.replace(compiled_pat, '', regex=False) + def test_repeat(self): values = Series(['a', 'b', NA, 'c', NA, 'd']) @@ -545,7 +1151,7 @@ def test_repeat(self): rs = Series(mixed).str.repeat(3) xp = Series(['aaa', NA, 'bbb', NA, NA, 'foofoofoo', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -571,27 +1177,12 @@ def test_match(self): exp = Series([True, NA, False]) tm.assert_series_equal(result, exp) - # test passing as_indexer still works but is ignored - values = Series(['fooBAD__barBAD', NA, 'foo']) - exp = Series([True, NA, False]) - with tm.assert_produces_warning(FutureWarning): - result = values.str.match('.*BAD[_]+.*BAD', as_indexer=True) - tm.assert_series_equal(result, exp) - with tm.assert_produces_warning(FutureWarning): - result = values.str.match('.*BAD[_]+.*BAD', as_indexer=False) - tm.assert_series_equal(result, exp) - with tm.assert_produces_warning(FutureWarning): - result = values.str.match('.*(BAD[_]+).*(BAD)', as_indexer=True) - tm.assert_series_equal(result, exp) - self.assertRaises(ValueError, values.str.match, '.*(BAD[_]+).*(BAD)', - as_indexer=False) - # mixed mixed = Series(['aBAD_BAD', NA, 'BAD_b_BAD', True, datetime.today(), 'foo', None, 1, 2.]) rs = Series(mixed).str.match('.*(BAD[_]+).*(BAD)') xp = Series([True, NA, True, NA, NA, False, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_series_equal(rs, xp) # unicode @@ -610,13 +1201,16 @@ def test_match(self): def test_extract_expand_None(self): values = Series(['fooBAD__barBAD', NA, 'foo']) - with tm.assert_produces_warning(FutureWarning): + with pytest.raises(ValueError, + match='expand must be True or False'): values.str.extract('.*(BAD[_]+).*(BAD)', expand=None) def test_extract_expand_unspecified(self): values = Series(['fooBAD__barBAD', NA, 'foo']) - with tm.assert_produces_warning(FutureWarning): - values.str.extract('.*(BAD[_]+).*(BAD)') + result_unspecified = values.str.extract('.*(BAD[_]+).*') + assert isinstance(result_unspecified, DataFrame) + result_true = values.str.extract('.*(BAD[_]+).*', expand=True) + tm.assert_frame_equal(result_unspecified, result_true) def test_extract_expand_False(self): # Contains tests like those in test_match and some others. @@ -647,24 +1241,25 @@ def test_extract_expand_False(self): # Index only works with one regex group since # multi-group would expand to a frame idx = Index(['A1', 'A2', 'A3', 'A4', 'B5']) - with tm.assertRaisesRegexp(ValueError, "supported"): + with pytest.raises(ValueError, match="supported"): idx.str.extract('([AB])([123])', expand=False) # these should work for both Series and Index for klass in [Series, Index]: # no groups s_or_idx = klass(['A1', 'B2', 'C3']) - f = lambda: s_or_idx.str.extract('[ABC][123]', expand=False) - self.assertRaises(ValueError, f) + msg = "pattern contains no capture groups" + with pytest.raises(ValueError, match=msg): + s_or_idx.str.extract('[ABC][123]', expand=False) # only non-capturing groups - f = lambda: s_or_idx.str.extract('(?:[AB]).*', expand=False) - self.assertRaises(ValueError, f) + with pytest.raises(ValueError, match=msg): + s_or_idx.str.extract('(?:[AB]).*', expand=False) # single group renames series/index properly s_or_idx = klass(['A1', 'A2']) result = s_or_idx.str.extract(r'(?PA)\d', expand=False) - self.assertEqual(result.name, 'uno') + assert result.name == 'uno' exp = klass(['A', 'A'], name='uno') if klass == Series: @@ -768,7 +1363,7 @@ def check_index(index): r = s.str.extract(r'(?P[a-z])', expand=False) e = Series(['a', 'b', 'c'], name='sue') tm.assert_series_equal(r, e) - self.assertEqual(r.name, e.name) + assert r.name == e.name def test_extract_expand_True(self): # Contains tests like those in test_match and some others. @@ -799,17 +1394,18 @@ def test_extract_expand_True(self): for klass in [Series, Index]: # no groups s_or_idx = klass(['A1', 'B2', 'C3']) - f = lambda: s_or_idx.str.extract('[ABC][123]', expand=True) - self.assertRaises(ValueError, f) + msg = "pattern contains no capture groups" + with pytest.raises(ValueError, match=msg): + s_or_idx.str.extract('[ABC][123]', expand=True) # only non-capturing groups - f = lambda: s_or_idx.str.extract('(?:[AB]).*', expand=True) - self.assertRaises(ValueError, f) + with pytest.raises(ValueError, match=msg): + s_or_idx.str.extract('(?:[AB]).*', expand=True) # single group renames series/index properly s_or_idx = klass(['A1', 'A2']) result_df = s_or_idx.str.extract(r'(?PA)\d', expand=True) - tm.assertIsInstance(result_df, DataFrame) + assert isinstance(result_df, DataFrame) result_series = result_df['uno'] assert_series_equal(result_series, Series(['A', 'A'], name='uno')) @@ -1070,28 +1666,50 @@ def test_extractall_single_group_with_quantifier(self): e = DataFrame(['ab', 'abc', 'd', 'cd'], i) tm.assert_frame_equal(r, e) - def test_extractall_no_matches(self): - s = Series(['a3', 'b3', 'd4c2'], name='series_name') + @pytest.mark.parametrize('data, names', [ + ([], (None, )), + ([], ('i1', )), + ([], (None, 'i2')), + ([], ('i1', 'i2')), + (['a3', 'b3', 'd4c2'], (None, )), + (['a3', 'b3', 'd4c2'], ('i1', 'i2')), + (['a3', 'b3', 'd4c2'], (None, 'i2')), + (['a3', 'b3', 'd4c2'], ('i1', 'i2')), + ]) + def test_extractall_no_matches(self, data, names): + # GH19075 extractall with no matches should return a valid MultiIndex + n = len(data) + if len(names) == 1: + i = Index(range(n), name=names[0]) + else: + a = (tuple([i] * (n - 1)) for i in range(n)) + i = MultiIndex.from_tuples(a, names=names) + s = Series(data, name='series_name', index=i, dtype='object') + ei = MultiIndex.from_tuples([], names=(names + ('match',))) + # one un-named group. r = s.str.extractall('(z)') - e = DataFrame(columns=[0]) + e = DataFrame(columns=[0], index=ei) tm.assert_frame_equal(r, e) + # two un-named groups. r = s.str.extractall('(z)(z)') - e = DataFrame(columns=[0, 1]) + e = DataFrame(columns=[0, 1], index=ei) tm.assert_frame_equal(r, e) + # one named group. r = s.str.extractall('(?Pz)') - e = DataFrame(columns=["first"]) + e = DataFrame(columns=["first"], index=ei) tm.assert_frame_equal(r, e) + # two named groups. r = s.str.extractall('(?Pz)(?Pz)') - e = DataFrame(columns=["first", "second"]) + e = DataFrame(columns=["first", "second"], index=ei) tm.assert_frame_equal(r, e) + # one named, one un-named. r = s.str.extractall('(z)(?Pz)') - e = DataFrame(columns=[0, - "second"]) + e = DataFrame(columns=[0, "second"], index=ei) tm.assert_frame_equal(r, e) def test_extractall_stringindex(self): @@ -1123,7 +1741,7 @@ def test_extractall_errors(self): # no capture groups. (it returns DataFrame with one column for # each capture group) s = Series(['a3', 'b3', 'd4c2'], name='series_name') - with tm.assertRaisesRegexp(ValueError, "no capture groups"): + with pytest.raises(ValueError, match="no capture groups"): s.str.extractall(r'[a-z]') def test_extract_index_one_two_groups(self): @@ -1207,17 +1825,16 @@ def test_extractall_same_as_extract_subject_index(self): tm.assert_frame_equal(extract_one_noname, no_match_index) def test_empty_str_methods(self): - empty_str = empty = Series(dtype=str) + empty_str = empty = Series(dtype=object) empty_int = Series(dtype=int) empty_bool = Series(dtype=bool) - empty_list = Series(dtype=list) empty_bytes = Series(dtype=object) # GH7241 # (extract) on empty series tm.assert_series_equal(empty_str, empty.str.cat(empty)) - self.assertEqual('', empty.str.cat()) + assert '' == empty.str.cat() tm.assert_series_equal(empty_str, empty.str.title()) tm.assert_series_equal(empty_int, empty.str.count('a')) tm.assert_series_equal(empty_bool, empty.str.contains('a')) @@ -1241,25 +1858,24 @@ def test_empty_str_methods(self): DataFrame(columns=[0, 1], dtype=str), empty.str.extract('()()', expand=False)) tm.assert_frame_equal(DataFrame(dtype=str), empty.str.get_dummies()) - tm.assert_series_equal(empty_str, empty_list.str.join('')) + tm.assert_series_equal(empty_str, empty_str.str.join('')) tm.assert_series_equal(empty_int, empty.str.len()) - tm.assert_series_equal(empty_list, empty_list.str.findall('a')) + tm.assert_series_equal(empty_str, empty_str.str.findall('a')) tm.assert_series_equal(empty_int, empty.str.find('a')) tm.assert_series_equal(empty_int, empty.str.rfind('a')) tm.assert_series_equal(empty_str, empty.str.pad(42)) tm.assert_series_equal(empty_str, empty.str.center(42)) - tm.assert_series_equal(empty_list, empty.str.split('a')) - tm.assert_series_equal(empty_list, empty.str.rsplit('a')) - tm.assert_series_equal(empty_list, + tm.assert_series_equal(empty_str, empty.str.split('a')) + tm.assert_series_equal(empty_str, empty.str.rsplit('a')) + tm.assert_series_equal(empty_str, empty.str.partition('a', expand=False)) - tm.assert_series_equal(empty_list, + tm.assert_series_equal(empty_str, empty.str.rpartition('a', expand=False)) tm.assert_series_equal(empty_str, empty.str.slice(stop=1)) tm.assert_series_equal(empty_str, empty.str.slice(step=1)) tm.assert_series_equal(empty_str, empty.str.strip()) tm.assert_series_equal(empty_str, empty.str.lstrip()) tm.assert_series_equal(empty_str, empty.str.rstrip()) - tm.assert_series_equal(empty_str, empty.str.rstrip()) tm.assert_series_equal(empty_str, empty.str.wrap(42)) tm.assert_series_equal(empty_str, empty.str.get(0)) tm.assert_series_equal(empty_str, empty_bytes.str.decode('ascii')) @@ -1320,20 +1936,13 @@ def test_ismethods(self): tm.assert_series_equal(str_s.str.isupper(), Series(upper_e)) tm.assert_series_equal(str_s.str.istitle(), Series(title_e)) - self.assertEqual(str_s.str.isalnum().tolist(), [v.isalnum() - for v in values]) - self.assertEqual(str_s.str.isalpha().tolist(), [v.isalpha() - for v in values]) - self.assertEqual(str_s.str.isdigit().tolist(), [v.isdigit() - for v in values]) - self.assertEqual(str_s.str.isspace().tolist(), [v.isspace() - for v in values]) - self.assertEqual(str_s.str.islower().tolist(), [v.islower() - for v in values]) - self.assertEqual(str_s.str.isupper().tolist(), [v.isupper() - for v in values]) - self.assertEqual(str_s.str.istitle().tolist(), [v.istitle() - for v in values]) + assert str_s.str.isalnum().tolist() == [v.isalnum() for v in values] + assert str_s.str.isalpha().tolist() == [v.isalpha() for v in values] + assert str_s.str.isdigit().tolist() == [v.isdigit() for v in values] + assert str_s.str.isspace().tolist() == [v.isspace() for v in values] + assert str_s.str.islower().tolist() == [v.islower() for v in values] + assert str_s.str.isupper().tolist() == [v.isupper() for v in values] + assert str_s.str.istitle().tolist() == [v.istitle() for v in values] def test_isnumeric(self): # 0x00bc: ¼ VULGAR FRACTION ONE QUARTER @@ -1348,10 +1957,8 @@ def test_isnumeric(self): tm.assert_series_equal(s.str.isdecimal(), Series(decimal_e)) unicodes = [u'A', u'3', u'¼', u'★', u'፸', u'3', u'four'] - self.assertEqual(s.str.isnumeric().tolist(), [ - v.isnumeric() for v in unicodes]) - self.assertEqual(s.str.isdecimal().tolist(), [ - v.isdecimal() for v in unicodes]) + assert s.str.isnumeric().tolist() == [v.isnumeric() for v in unicodes] + assert s.str.isdecimal().tolist() == [v.isdecimal() for v in unicodes] values = ['A', np.nan, u'¼', u'★', np.nan, u'3', 'four'] s = Series(values) @@ -1410,7 +2017,7 @@ def test_join(self): rs = Series(mixed).str.split('_').str.join('_') xp = Series(['a_b', NA, 'asdf_cas_asdf', NA, NA, 'foo', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -1422,7 +2029,7 @@ def test_len(self): values = Series(['foo', 'fooo', 'fooooo', np.nan, 'fooooooo']) result = values.str.len() - exp = values.map(lambda x: len(x) if notnull(x) else NA) + exp = values.map(lambda x: len(x) if notna(x) else NA) tm.assert_series_equal(result, exp) # mixed @@ -1432,7 +2039,7 @@ def test_len(self): rs = Series(mixed).str.len() xp = Series([3, NA, 13, NA, NA, 3, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -1440,7 +2047,7 @@ def test_len(self): 'fooooooo')]) result = values.str.len() - exp = values.map(lambda x: len(x) if notnull(x) else NA) + exp = values.map(lambda x: len(x) if notna(x) else NA) tm.assert_series_equal(result, exp) def test_findall(self): @@ -1457,7 +2064,7 @@ def test_findall(self): rs = Series(mixed).str.findall('BAD[_]*') xp = Series([['BAD__', 'BAD'], NA, [], NA, NA, ['BAD'], NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -1505,12 +2112,12 @@ def test_find(self): dtype=np.int64) tm.assert_numpy_array_equal(result.values, expected) - with tm.assertRaisesRegexp(TypeError, - "expected a string object, not int"): + with pytest.raises(TypeError, + match="expected a string object, not int"): result = values.str.find(0) - with tm.assertRaisesRegexp(TypeError, - "expected a string object, not int"): + with pytest.raises(TypeError, + match="expected a string object, not int"): result = values.str.rfind(0) def test_find_nan(self): @@ -1580,11 +2187,11 @@ def _check(result, expected): dtype=np.int64) tm.assert_numpy_array_equal(result.values, expected) - with tm.assertRaisesRegexp(ValueError, "substring not found"): + with pytest.raises(ValueError, match="substring not found"): result = s.str.index('DE') - with tm.assertRaisesRegexp(TypeError, - "expected a string object, not int"): + msg = "expected a string object, not int" + with pytest.raises(TypeError, match=msg): result = s.str.index(0) # test with nan @@ -1616,7 +2223,7 @@ def test_pad(self): rs = Series(mixed).str.pad(5, side='left') xp = Series([' a', NA, ' b', NA, NA, ' ee', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) mixed = Series(['a', NA, 'b', True, datetime.today(), 'ee', None, 1, 2. @@ -1625,7 +2232,7 @@ def test_pad(self): rs = Series(mixed).str.pad(5, side='right') xp = Series(['a ', NA, 'b ', NA, NA, 'ee ', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) mixed = Series(['a', NA, 'b', True, datetime.today(), 'ee', None, 1, 2. @@ -1634,7 +2241,7 @@ def test_pad(self): rs = Series(mixed).str.pad(5, side='both') xp = Series([' a ', NA, ' b ', NA, NA, ' ee ', NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -1668,22 +2275,22 @@ def test_pad_fillchar(self): exp = Series(['XXaXX', 'XXbXX', NA, 'XXcXX', NA, 'eeeeee']) tm.assert_almost_equal(result, exp) - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not str"): + msg = "fillchar must be a character, not str" + with pytest.raises(TypeError, match=msg): result = values.str.pad(5, fillchar='XY') - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not int"): + msg = "fillchar must be a character, not int" + with pytest.raises(TypeError, match=msg): result = values.str.pad(5, fillchar=5) - def test_pad_width(self): - # GH 13598 + @pytest.mark.parametrize("f", ['center', 'ljust', 'rjust', 'zfill', 'pad']) + def test_pad_width(self, f): + # see gh-13598 s = Series(['1', '22', 'a', 'bb']) + msg = "width must be of integer type, not*" - for f in ['center', 'ljust', 'rjust', 'zfill', 'pad']: - with tm.assertRaisesRegexp(TypeError, - "width must be of integer type, not*"): - getattr(s.str, f)('f') + with pytest.raises(TypeError, match=msg): + getattr(s.str, f)('f') def test_translate(self): @@ -1714,8 +2321,8 @@ def _check(result, expected): expected = klass(['abcde', 'abcc', 'cddd', 'cde']) _check(result, expected) else: - with tm.assertRaisesRegexp( - ValueError, "deletechars is not a valid argument"): + msg = "deletechars is not a valid argument" + with pytest.raises(ValueError, match=msg): result = s.str.translate(table, deletechars='fg') # Series with non-string values @@ -1746,19 +2353,19 @@ def test_center_ljust_rjust(self): rs = Series(mixed).str.center(5) xp = Series([' a ', NA, ' b ', NA, NA, ' c ', ' eee ', NA, NA, NA ]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) rs = Series(mixed).str.ljust(5) xp = Series(['a ', NA, 'b ', NA, NA, 'c ', 'eee ', NA, NA, NA ]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) rs = Series(mixed).str.rjust(5) xp = Series([' a', NA, ' b', NA, NA, ' c', ' eee', NA, NA, NA ]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -1803,29 +2410,25 @@ def test_center_ljust_rjust_fillchar(self): # If fillchar is not a charatter, normal str raises TypeError # 'aaa'.ljust(5, 'XY') # TypeError: must be char, not str - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not str"): - result = values.str.center(5, fillchar='XY') + template = "fillchar must be a character, not {dtype}" + + with pytest.raises(TypeError, match=template.format(dtype="str")): + values.str.center(5, fillchar='XY') - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not str"): - result = values.str.ljust(5, fillchar='XY') + with pytest.raises(TypeError, match=template.format(dtype="str")): + values.str.ljust(5, fillchar='XY') - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not str"): - result = values.str.rjust(5, fillchar='XY') + with pytest.raises(TypeError, match=template.format(dtype="str")): + values.str.rjust(5, fillchar='XY') - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not int"): - result = values.str.center(5, fillchar=1) + with pytest.raises(TypeError, match=template.format(dtype="int")): + values.str.center(5, fillchar=1) - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not int"): - result = values.str.ljust(5, fillchar=1) + with pytest.raises(TypeError, match=template.format(dtype="int")): + values.str.ljust(5, fillchar=1) - with tm.assertRaisesRegexp(TypeError, - "fillchar must be a character, not int"): - result = values.str.rjust(5, fillchar=1) + with pytest.raises(TypeError, match=template.format(dtype="int")): + values.str.rjust(5, fillchar=1) def test_zfill(self): values = Series(['1', '22', 'aaa', '333', '45678']) @@ -1870,11 +2473,11 @@ def test_split(self): result = mixed.str.split('_') exp = Series([['a', 'b', 'c'], NA, ['d', 'e', 'f'], NA, NA, NA, NA, NA ]) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_almost_equal(result, exp) result = mixed.str.split('_', expand=False) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_almost_equal(result, exp) # unicode @@ -1915,11 +2518,11 @@ def test_rsplit(self): result = mixed.str.rsplit('_') exp = Series([['a', 'b', 'c'], NA, ['d', 'e', 'f'], NA, NA, NA, NA, NA ]) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_almost_equal(result, exp) result = mixed.str.rsplit('_', expand=False) - tm.assertIsInstance(result, Series) + assert isinstance(result, Series) tm.assert_almost_equal(result, exp) # unicode @@ -1944,14 +2547,27 @@ def test_rsplit(self): exp = Series([['a_b', 'c'], ['c_d', 'e'], NA, ['f_g', 'h']]) tm.assert_series_equal(result, exp) + def test_split_blank_string(self): + # expand blank split GH 20067 + values = Series([''], name='test') + result = values.str.split(expand=True) + exp = DataFrame([[]]) + tm.assert_frame_equal(result, exp) + + values = Series(['a b c', 'a b', '', ' '], name='test') + result = values.str.split(expand=True) + exp = DataFrame([['a', 'b', 'c'], ['a', 'b', np.nan], + [np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]]) + tm.assert_frame_equal(result, exp) + def test_split_noargs(self): # #1859 s = Series(['Wes McKinney', 'Travis Oliphant']) result = s.str.split() expected = ['Travis', 'Oliphant'] - self.assertEqual(result[1], expected) + assert result[1] == expected result = s.str.rsplit() - self.assertEqual(result[1], expected) + assert result[1] == expected def test_split_maxsplit(self): # re.split 0, str.split -1 @@ -2006,32 +2622,43 @@ def test_split_to_dataframe(self): index=['preserve', 'me']) tm.assert_frame_equal(result, exp) - with tm.assertRaisesRegexp(ValueError, "expand must be"): + with pytest.raises(ValueError, match="expand must be"): s.str.split('_', expand="not_a_boolean") def test_split_to_multiindex_expand(self): - idx = Index(['nosplit', 'alsonosplit']) + # https://github.com/pandas-dev/pandas/issues/23677 + + idx = Index(['nosplit', 'alsonosplit', np.nan]) result = idx.str.split('_', expand=True) exp = idx tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 1) + assert result.nlevels == 1 - idx = Index(['some_equal_splits', 'with_no_nans']) + idx = Index(['some_equal_splits', 'with_no_nans', np.nan, None]) result = idx.str.split('_', expand=True) - exp = MultiIndex.from_tuples([('some', 'equal', 'splits'), ( - 'with', 'no', 'nans')]) + exp = MultiIndex.from_tuples([('some', 'equal', 'splits'), + ('with', 'no', 'nans'), + [np.nan, np.nan, np.nan], + [None, None, None]]) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 3) + assert result.nlevels == 3 - idx = Index(['some_unequal_splits', 'one_of_these_things_is_not']) + idx = Index(['some_unequal_splits', + 'one_of_these_things_is_not', + np.nan, None]) result = idx.str.split('_', expand=True) - exp = MultiIndex.from_tuples([('some', 'unequal', 'splits', NA, NA, NA - ), ('one', 'of', 'these', 'things', - 'is', 'not')]) + exp = MultiIndex.from_tuples([('some', 'unequal', 'splits', + NA, NA, NA), + ('one', 'of', 'these', + 'things', 'is', 'not'), + (np.nan, np.nan, np.nan, + np.nan, np.nan, np.nan), + (None, None, None, + None, None, None)]) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 6) + assert result.nlevels == 6 - with tm.assertRaisesRegexp(ValueError, "expand must be"): + with pytest.raises(ValueError, match="expand must be"): idx.str.split('_', expand="not_a_boolean") def test_rsplit_to_dataframe_expand(self): @@ -2068,21 +2695,33 @@ def test_rsplit_to_multiindex_expand(self): result = idx.str.rsplit('_', expand=True) exp = idx tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 1) + assert result.nlevels == 1 idx = Index(['some_equal_splits', 'with_no_nans']) result = idx.str.rsplit('_', expand=True) exp = MultiIndex.from_tuples([('some', 'equal', 'splits'), ( 'with', 'no', 'nans')]) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 3) + assert result.nlevels == 3 idx = Index(['some_equal_splits', 'with_no_nans']) result = idx.str.rsplit('_', expand=True, n=1) exp = MultiIndex.from_tuples([('some_equal', 'splits'), ('with_no', 'nans')]) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 2) + assert result.nlevels == 2 + + def test_split_nan_expand(self): + # gh-18450 + s = Series(["foo,bar,baz", NA]) + result = s.str.split(",", expand=True) + exp = DataFrame([["foo", "bar", "baz"], [NA, NA, NA]]) + tm.assert_frame_equal(result, exp) + + # check that these are actually np.nan and not None + # TODO see GH 18463 + # tm.assert_frame_equal does not differentiate + assert all(np.isnan(x) for x in result.iloc[1]) def test_split_with_name(self): # GH 12617 @@ -2100,59 +2739,63 @@ def test_split_with_name(self): idx = Index(['a,b', 'c,d'], name='xxx') res = idx.str.split(',') exp = Index([['a', 'b'], ['c', 'd']], name='xxx') - self.assertTrue(res.nlevels, 1) + assert res.nlevels == 1 tm.assert_index_equal(res, exp) res = idx.str.split(',', expand=True) exp = MultiIndex.from_tuples([('a', 'b'), ('c', 'd')]) - self.assertTrue(res.nlevels, 2) + assert res.nlevels == 2 tm.assert_index_equal(res, exp) def test_partition_series(self): - values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h']) + # https://github.com/pandas-dev/pandas/issues/23558 + + values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h', None]) result = values.str.partition('_', expand=False) exp = Series([('a', '_', 'b_c'), ('c', '_', 'd_e'), NA, - ('f', '_', 'g_h')]) + ('f', '_', 'g_h'), None]) tm.assert_series_equal(result, exp) result = values.str.rpartition('_', expand=False) exp = Series([('a_b', '_', 'c'), ('c_d', '_', 'e'), NA, - ('f_g', '_', 'h')]) + ('f_g', '_', 'h'), None]) tm.assert_series_equal(result, exp) # more than one char - values = Series(['a__b__c', 'c__d__e', NA, 'f__g__h']) + values = Series(['a__b__c', 'c__d__e', NA, 'f__g__h', None]) result = values.str.partition('__', expand=False) exp = Series([('a', '__', 'b__c'), ('c', '__', 'd__e'), NA, - ('f', '__', 'g__h')]) + ('f', '__', 'g__h'), None]) tm.assert_series_equal(result, exp) result = values.str.rpartition('__', expand=False) exp = Series([('a__b', '__', 'c'), ('c__d', '__', 'e'), NA, - ('f__g', '__', 'h')]) + ('f__g', '__', 'h'), None]) tm.assert_series_equal(result, exp) # None - values = Series(['a b c', 'c d e', NA, 'f g h']) + values = Series(['a b c', 'c d e', NA, 'f g h', None]) result = values.str.partition(expand=False) exp = Series([('a', ' ', 'b c'), ('c', ' ', 'd e'), NA, - ('f', ' ', 'g h')]) + ('f', ' ', 'g h'), None]) tm.assert_series_equal(result, exp) result = values.str.rpartition(expand=False) exp = Series([('a b', ' ', 'c'), ('c d', ' ', 'e'), NA, - ('f g', ' ', 'h')]) + ('f g', ' ', 'h'), None]) tm.assert_series_equal(result, exp) - # Not splited - values = Series(['abc', 'cde', NA, 'fgh']) + # Not split + values = Series(['abc', 'cde', NA, 'fgh', None]) result = values.str.partition('_', expand=False) - exp = Series([('abc', '', ''), ('cde', '', ''), NA, ('fgh', '', '')]) + exp = Series([('abc', '', ''), ('cde', '', ''), NA, + ('fgh', '', ''), None]) tm.assert_series_equal(result, exp) result = values.str.rpartition('_', expand=False) - exp = Series([('', '', 'abc'), ('', '', 'cde'), NA, ('', '', 'fgh')]) + exp = Series([('', '', 'abc'), ('', '', 'cde'), NA, + ('', '', 'fgh'), None]) tm.assert_series_equal(result, exp) # unicode @@ -2171,62 +2814,70 @@ def test_partition_series(self): # compare to standard lib values = Series(['A_B_C', 'B_C_D', 'E_F_G', 'EFGHEF']) result = values.str.partition('_', expand=False).tolist() - self.assertEqual(result, [v.partition('_') for v in values]) + assert result == [v.partition('_') for v in values] result = values.str.rpartition('_', expand=False).tolist() - self.assertEqual(result, [v.rpartition('_') for v in values]) + assert result == [v.rpartition('_') for v in values] def test_partition_index(self): - values = Index(['a_b_c', 'c_d_e', 'f_g_h']) + # https://github.com/pandas-dev/pandas/issues/23558 + + values = Index(['a_b_c', 'c_d_e', 'f_g_h', np.nan, None]) result = values.str.partition('_', expand=False) - exp = Index(np.array([('a', '_', 'b_c'), ('c', '_', 'd_e'), ('f', '_', - 'g_h')])) + exp = Index(np.array([('a', '_', 'b_c'), ('c', '_', 'd_e'), + ('f', '_', 'g_h'), np.nan, None])) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 1) + assert result.nlevels == 1 result = values.str.rpartition('_', expand=False) - exp = Index(np.array([('a_b', '_', 'c'), ('c_d', '_', 'e'), ( - 'f_g', '_', 'h')])) + exp = Index(np.array([('a_b', '_', 'c'), ('c_d', '_', 'e'), + ('f_g', '_', 'h'), np.nan, None])) tm.assert_index_equal(result, exp) - self.assertEqual(result.nlevels, 1) + assert result.nlevels == 1 result = values.str.partition('_') - exp = Index([('a', '_', 'b_c'), ('c', '_', 'd_e'), ('f', '_', 'g_h')]) + exp = Index([('a', '_', 'b_c'), ('c', '_', 'd_e'), + ('f', '_', 'g_h'), (np.nan, np.nan, np.nan), + (None, None, None)]) tm.assert_index_equal(result, exp) - self.assertTrue(isinstance(result, MultiIndex)) - self.assertEqual(result.nlevels, 3) + assert isinstance(result, MultiIndex) + assert result.nlevels == 3 result = values.str.rpartition('_') - exp = Index([('a_b', '_', 'c'), ('c_d', '_', 'e'), ('f_g', '_', 'h')]) + exp = Index([('a_b', '_', 'c'), ('c_d', '_', 'e'), + ('f_g', '_', 'h'), (np.nan, np.nan, np.nan), + (None, None, None)]) tm.assert_index_equal(result, exp) - self.assertTrue(isinstance(result, MultiIndex)) - self.assertEqual(result.nlevels, 3) + assert isinstance(result, MultiIndex) + assert result.nlevels == 3 def test_partition_to_dataframe(self): - values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h']) + # https://github.com/pandas-dev/pandas/issues/23558 + + values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h', None]) result = values.str.partition('_') - exp = DataFrame({0: ['a', 'c', np.nan, 'f'], - 1: ['_', '_', np.nan, '_'], - 2: ['b_c', 'd_e', np.nan, 'g_h']}) + exp = DataFrame({0: ['a', 'c', np.nan, 'f', None], + 1: ['_', '_', np.nan, '_', None], + 2: ['b_c', 'd_e', np.nan, 'g_h', None]}) tm.assert_frame_equal(result, exp) result = values.str.rpartition('_') - exp = DataFrame({0: ['a_b', 'c_d', np.nan, 'f_g'], - 1: ['_', '_', np.nan, '_'], - 2: ['c', 'e', np.nan, 'h']}) + exp = DataFrame({0: ['a_b', 'c_d', np.nan, 'f_g', None], + 1: ['_', '_', np.nan, '_', None], + 2: ['c', 'e', np.nan, 'h', None]}) tm.assert_frame_equal(result, exp) - values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h']) + values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h', None]) result = values.str.partition('_', expand=True) - exp = DataFrame({0: ['a', 'c', np.nan, 'f'], - 1: ['_', '_', np.nan, '_'], - 2: ['b_c', 'd_e', np.nan, 'g_h']}) + exp = DataFrame({0: ['a', 'c', np.nan, 'f', None], + 1: ['_', '_', np.nan, '_', None], + 2: ['b_c', 'd_e', np.nan, 'g_h', None]}) tm.assert_frame_equal(result, exp) result = values.str.rpartition('_', expand=True) - exp = DataFrame({0: ['a_b', 'c_d', np.nan, 'f_g'], - 1: ['_', '_', np.nan, '_'], - 2: ['c', 'e', np.nan, 'h']}) + exp = DataFrame({0: ['a_b', 'c_d', np.nan, 'f_g', None], + 1: ['_', '_', np.nan, '_', None], + 2: ['c', 'e', np.nan, 'h', None]}) tm.assert_frame_equal(result, exp) def test_partition_with_name(self): @@ -2245,15 +2896,33 @@ def test_partition_with_name(self): idx = Index(['a,b', 'c,d'], name='xxx') res = idx.str.partition(',') exp = MultiIndex.from_tuples([('a', ',', 'b'), ('c', ',', 'd')]) - self.assertTrue(res.nlevels, 3) + assert res.nlevels == 3 tm.assert_index_equal(res, exp) # should preserve name res = idx.str.partition(',', expand=False) exp = Index(np.array([('a', ',', 'b'), ('c', ',', 'd')]), name='xxx') - self.assertTrue(res.nlevels, 1) + assert res.nlevels == 1 tm.assert_index_equal(res, exp) + def test_partition_deprecation(self): + # GH 22676; depr kwarg "pat" in favor of "sep" + values = Series(['a_b_c', 'c_d_e', NA, 'f_g_h']) + + # str.partition + # using sep -> no warning + expected = values.str.partition(sep='_') + with tm.assert_produces_warning(FutureWarning): + result = values.str.partition(pat='_') + tm.assert_frame_equal(result, expected) + + # str.rpartition + # using sep -> no warning + expected = values.str.rpartition(sep='_') + with tm.assert_produces_warning(FutureWarning): + result = values.str.rpartition(pat='_') + tm.assert_frame_equal(result, expected) + def test_pipe_failures(self): # #2119 s = Series(['A|B|C']) @@ -2279,10 +2948,10 @@ def test_slice(self): (3, 0, -1)]: try: result = values.str.slice(start, stop, step) - expected = Series([s[start:stop:step] if not isnull(s) else NA + expected = Series([s[start:stop:step] if not isna(s) else NA for s in values]) tm.assert_series_equal(result, expected) - except: + except IndexError: print('failed on %s:%s:%s' % (start, stop, step)) raise @@ -2293,7 +2962,7 @@ def test_slice(self): rs = Series(mixed).str.slice(2, 5) xp = Series(['foo', NA, 'bar', NA, NA, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) rs = Series(mixed).str.slice(2, 5, -1) @@ -2371,19 +3040,19 @@ def test_strip_lstrip_rstrip_mixed(self): rs = Series(mixed).str.strip() xp = Series(['aa', NA, 'bb', NA, NA, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) rs = Series(mixed).str.lstrip() xp = Series(['aa ', NA, 'bb \t\n', NA, NA, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) rs = Series(mixed).str.rstrip() xp = Series([' aa', NA, ' bb', NA, NA, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) def test_strip_lstrip_rstrip_unicode(self): @@ -2472,7 +3141,7 @@ def test_get(self): rs = Series(mixed).str.split('_').str.get(1) xp = Series(['b', NA, 'd', NA, NA, NA, NA, NA]) - tm.assertIsInstance(rs, Series) + assert isinstance(rs, Series) tm.assert_almost_equal(rs, xp) # unicode @@ -2482,7 +3151,45 @@ def test_get(self): expected = Series([u('b'), u('d'), np.nan, u('g')]) tm.assert_series_equal(result, expected) - def test_more_contains(self): + # bounds testing + values = Series(['1_2_3_4_5', '6_7_8_9_10', '11_12']) + + # positive index + result = values.str.split('_').str.get(2) + expected = Series(['3', '8', np.nan]) + tm.assert_series_equal(result, expected) + + # negative index + result = values.str.split('_').str.get(-3) + expected = Series(['3', '8', np.nan]) + tm.assert_series_equal(result, expected) + + def test_get_complex(self): + # GH 20671, getting value not in dict raising `KeyError` + values = Series([(1, 2, 3), [1, 2, 3], {1, 2, 3}, + {1: 'a', 2: 'b', 3: 'c'}]) + + result = values.str.get(1) + expected = Series([2, 2, np.nan, 'a']) + tm.assert_series_equal(result, expected) + + result = values.str.get(-1) + expected = Series([3, 3, np.nan, np.nan]) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('to_type', [tuple, list, np.array]) + def test_get_complex_nested(self, to_type): + values = Series([to_type([to_type([1, 2])])]) + + result = values.str.get(0) + expected = Series([to_type([1, 2])]) + tm.assert_series_equal(result, expected) + + result = values.str.get(1) + expected = Series([np.nan]) + tm.assert_series_equal(result, expected) + + def test_contains_moar(self): # PR #1179 s = Series(['A', 'B', 'C', 'Aaba', 'Baca', '', NA, 'CABA', 'dog', 'cat']) @@ -2532,7 +3239,7 @@ def test_contains_nan(self): expected = Series([np.nan, np.nan, np.nan], dtype=np.object_) assert_series_equal(result, expected) - def test_more_replace(self): + def test_replace_moar(self): # PR #1179 s = Series(['A', 'B', 'C', 'Aaba', 'Baca', '', NA, 'CABA', 'dog', 'cat']) @@ -2591,20 +3298,20 @@ def test_match_findall_flags(self): pat = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})' result = data.str.extract(pat, flags=re.IGNORECASE, expand=True) - self.assertEqual(result.iloc[0].tolist(), ['dave', 'google', 'com']) + assert result.iloc[0].tolist() == ['dave', 'google', 'com'] result = data.str.match(pat, flags=re.IGNORECASE) - self.assertEqual(result[0], True) + assert result[0] result = data.str.findall(pat, flags=re.IGNORECASE) - self.assertEqual(result[0][0], ('dave', 'google', 'com')) + assert result[0][0] == ('dave', 'google', 'com') result = data.str.count(pat, flags=re.IGNORECASE) - self.assertEqual(result[0], 1) + assert result[0] == 1 with tm.assert_produces_warning(UserWarning): result = data.str.contains(pat, flags=re.IGNORECASE) - self.assertEqual(result[0], True) + assert result[0] def test_encode_decode(self): base = Series([u('a'), u('b'), u('a\xe4')]) @@ -2616,10 +3323,14 @@ def test_encode_decode(self): tm.assert_series_equal(result, exp) + @pytest.mark.skipif(PY2, reason="pytest.raises match regex fails") def test_encode_decode_errors(self): encodeBase = Series([u('a'), u('b'), u('a\x9d')]) - self.assertRaises(UnicodeEncodeError, encodeBase.str.encode, 'cp1252') + msg = (r"'charmap' codec can't encode character '\\x9d' in position 1:" + " character maps to ") + with pytest.raises(UnicodeEncodeError, match=msg): + encodeBase.str.encode('cp1252') f = lambda x: x.encode('cp1252', 'ignore') result = encodeBase.str.encode('cp1252', 'ignore') @@ -2628,7 +3339,10 @@ def test_encode_decode_errors(self): decodeBase = Series([b'a', b'b', b'a\x9d']) - self.assertRaises(UnicodeDecodeError, decodeBase.str.decode, 'cp1252') + msg = ("'charmap' codec can't decode byte 0x9d in position 1:" + " character maps to ") + with pytest.raises(UnicodeDecodeError, match=msg): + decodeBase.str.decode('cp1252') f = lambda x: x.decode('cp1252', 'ignore') result = decodeBase.str.decode('cp1252', 'ignore') @@ -2652,7 +3366,7 @@ def test_normalize(self): result = s.str.normalize('NFC') tm.assert_series_equal(result, expected) - with tm.assertRaisesRegexp(ValueError, "invalid normalization form"): + with pytest.raises(ValueError, match="invalid normalization form"): s.str.normalize('xxx') s = Index([u'ABC', u'123', u'アイエ']) @@ -2660,32 +3374,6 @@ def test_normalize(self): result = s.str.normalize('NFKC') tm.assert_index_equal(result, expected) - def test_cat_on_filtered_index(self): - df = DataFrame(index=MultiIndex.from_product( - [[2011, 2012], [1, 2, 3]], names=['year', 'month'])) - - df = df.reset_index() - df = df[df.month > 1] - - str_year = df.year.astype('str') - str_month = df.month.astype('str') - str_both = str_year.str.cat(str_month, sep=' ') - - self.assertEqual(str_both.loc[1], '2011 2') - - str_multiple = str_year.str.cat([str_month, str_month], sep=' ') - - self.assertEqual(str_multiple.loc[1], '2011 2 2') - - def test_str_cat_raises_intuitive_error(self): - # https://github.com/pandas-dev/pandas/issues/11334 - s = Series(['a', 'b', 'c', 'd']) - message = "Did you mean to supply a `sep` keyword?" - with tm.assertRaisesRegexp(ValueError, message): - s.str.cat('|') - with tm.assertRaisesRegexp(ValueError, message): - s.str.cat(' ') - def test_index_str_accessor_visibility(self): from pandas.core.strings import StringMethods @@ -2705,15 +3393,15 @@ def test_index_str_accessor_visibility(self): (['aa', datetime(2011, 1, 1)], 'mixed')] for values, tp in cases: idx = Index(values) - self.assertTrue(isinstance(Series(values).str, StringMethods)) - self.assertTrue(isinstance(idx.str, StringMethods)) - self.assertEqual(idx.inferred_type, tp) + assert isinstance(Series(values).str, StringMethods) + assert isinstance(idx.str, StringMethods) + assert idx.inferred_type == tp for values, tp in cases: idx = Index(values) - self.assertTrue(isinstance(Series(values).str, StringMethods)) - self.assertTrue(isinstance(idx.str, StringMethods)) - self.assertEqual(idx.inferred_type, tp) + assert isinstance(Series(values).str, StringMethods) + assert isinstance(idx.str, StringMethods) + assert idx.inferred_type == tp cases = [([1, np.nan], 'floating'), ([datetime(2011, 1, 1)], 'datetime64'), @@ -2721,33 +3409,43 @@ def test_index_str_accessor_visibility(self): for values, tp in cases: idx = Index(values) message = 'Can only use .str accessor with string values' - with self.assertRaisesRegexp(AttributeError, message): + with pytest.raises(AttributeError, match=message): Series(values).str - with self.assertRaisesRegexp(AttributeError, message): + with pytest.raises(AttributeError, match=message): idx.str - self.assertEqual(idx.inferred_type, tp) + assert idx.inferred_type == tp # MultiIndex has mixed dtype, but not allow to use accessor idx = MultiIndex.from_tuples([('a', 'b'), ('a', 'b')]) - self.assertEqual(idx.inferred_type, 'mixed') + assert idx.inferred_type == 'mixed' message = 'Can only use .str accessor with Index, not MultiIndex' - with self.assertRaisesRegexp(AttributeError, message): + with pytest.raises(AttributeError, match=message): idx.str def test_str_accessor_no_new_attributes(self): # https://github.com/pandas-dev/pandas/issues/10673 s = Series(list('aabbcde')) - with tm.assertRaisesRegexp(AttributeError, - "You cannot add any new attribute"): + with pytest.raises(AttributeError, + match="You cannot add any new attribute"): s.str.xlabel = "a" def test_method_on_bytes(self): lhs = Series(np.array(list('abc'), 'S1').astype(object)) rhs = Series(np.array(list('def'), 'S1').astype(object)) if compat.PY3: - self.assertRaises(TypeError, lhs.str.cat, rhs) + with pytest.raises(TypeError, match="can't concat str to bytes"): + lhs.str.cat(rhs) else: result = lhs.str.cat(rhs) expected = Series(np.array( ['ad', 'be', 'cf'], 'S2').astype(object)) tm.assert_series_equal(result, expected) + + @pytest.mark.skipif(compat.PY2, reason='not in python2') + def test_casefold(self): + # GH25405 + expected = Series(['ss', NA, 'case', 'ssd']) + s = Series(['ß', NA, 'case', 'ßd']) + result = s.str.casefold() + + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/test_take.py b/pandas/tests/test_take.py index 0bc1d0dcd0532..c9e4ed90b1dea 100644 --- a/pandas/tests/test_take.py +++ b/pandas/tests/test_take.py @@ -1,321 +1,277 @@ # -*- coding: utf-8 -*- -import re from datetime import datetime +import re import numpy as np +import pytest + +from pandas._libs.tslib import iNaT from pandas.compat import long + import pandas.core.algorithms as algos import pandas.util.testing as tm -from pandas._libs.tslib import iNaT -class TestTake(tm.TestCase): - # standard incompatible fill error +@pytest.fixture(params=[True, False]) +def writeable(request): + return request.param + + +# Check that take_nd works both with writeable arrays +# (in which case fast typed memory-views implementation) +# and read-only arrays alike. +@pytest.fixture(params=[ + (np.float64, True), + (np.float32, True), + (np.uint64, False), + (np.uint32, False), + (np.uint16, False), + (np.uint8, False), + (np.int64, False), + (np.int32, False), + (np.int16, False), + (np.int8, False), + (np.object_, True), + (np.bool, False), +]) +def dtype_can_hold_na(request): + return request.param + + +@pytest.fixture(params=[ + (np.int8, np.int16(127), np.int8), + (np.int8, np.int16(128), np.int16), + (np.int32, 1, np.int32), + (np.int32, 2.0, np.float64), + (np.int32, 3.0 + 4.0j, np.complex128), + (np.int32, True, np.object_), + (np.int32, "", np.object_), + (np.float64, 1, np.float64), + (np.float64, 2.0, np.float64), + (np.float64, 3.0 + 4.0j, np.complex128), + (np.float64, True, np.object_), + (np.float64, "", np.object_), + (np.complex128, 1, np.complex128), + (np.complex128, 2.0, np.complex128), + (np.complex128, 3.0 + 4.0j, np.complex128), + (np.complex128, True, np.object_), + (np.complex128, "", np.object_), + (np.bool_, 1, np.object_), + (np.bool_, 2.0, np.object_), + (np.bool_, 3.0 + 4.0j, np.object_), + (np.bool_, True, np.bool_), + (np.bool_, '', np.object_), +]) +def dtype_fill_out_dtype(request): + return request.param + + +class TestTake(object): + # Standard incompatible fill error. fill_error = re.compile("Incompatible type for fill_value") - def test_1d_with_out(self): - def _test_dtype(dtype, can_hold_na, writeable=True): - data = np.random.randint(0, 2, 4).astype(dtype) - data.flags.writeable = writeable + def test_1d_with_out(self, dtype_can_hold_na, writeable): + dtype, can_hold_na = dtype_can_hold_na + + data = np.random.randint(0, 2, 4).astype(dtype) + data.flags.writeable = writeable + + indexer = [2, 1, 0, 1] + out = np.empty(4, dtype=dtype) + algos.take_1d(data, indexer, out=out) - indexer = [2, 1, 0, 1] - out = np.empty(4, dtype=dtype) + expected = data.take(indexer) + tm.assert_almost_equal(out, expected) + + indexer = [2, 1, 0, -1] + out = np.empty(4, dtype=dtype) + + if can_hold_na: algos.take_1d(data, indexer, out=out) expected = data.take(indexer) + expected[3] = np.nan tm.assert_almost_equal(out, expected) - - indexer = [2, 1, 0, -1] - out = np.empty(4, dtype=dtype) - if can_hold_na: + else: + with pytest.raises(TypeError, match=self.fill_error): algos.take_1d(data, indexer, out=out) - expected = data.take(indexer) - expected[3] = np.nan - tm.assert_almost_equal(out, expected) - else: - with tm.assertRaisesRegexp(TypeError, self.fill_error): - algos.take_1d(data, indexer, out=out) - # no exception o/w - data.take(indexer, out=out) - - for writeable in [True, False]: - # Check that take_nd works both with writeable arrays (in which - # case fast typed memoryviews implementation) and read-only - # arrays alike. - _test_dtype(np.float64, True, writeable=writeable) - _test_dtype(np.float32, True, writeable=writeable) - _test_dtype(np.uint64, False, writeable=writeable) - _test_dtype(np.uint32, False, writeable=writeable) - _test_dtype(np.uint16, False, writeable=writeable) - _test_dtype(np.uint8, False, writeable=writeable) - _test_dtype(np.int64, False, writeable=writeable) - _test_dtype(np.int32, False, writeable=writeable) - _test_dtype(np.int16, False, writeable=writeable) - _test_dtype(np.int8, False, writeable=writeable) - _test_dtype(np.object_, True, writeable=writeable) - _test_dtype(np.bool, False, writeable=writeable) - - def test_1d_fill_nonna(self): - def _test_dtype(dtype, fill_value, out_dtype): - data = np.random.randint(0, 2, 4).astype(dtype) - - indexer = [2, 1, 0, -1] - - result = algos.take_1d(data, indexer, fill_value=fill_value) - assert ((result[[0, 1, 2]] == data[[2, 1, 0]]).all()) - assert (result[3] == fill_value) - assert (result.dtype == out_dtype) - - indexer = [2, 1, 0, 1] - - result = algos.take_1d(data, indexer, fill_value=fill_value) - assert ((result[[0, 1, 2, 3]] == data[indexer]).all()) - assert (result.dtype == dtype) - - _test_dtype(np.int8, np.int16(127), np.int8) - _test_dtype(np.int8, np.int16(128), np.int16) - _test_dtype(np.int32, 1, np.int32) - _test_dtype(np.int32, 2.0, np.float64) - _test_dtype(np.int32, 3.0 + 4.0j, np.complex128) - _test_dtype(np.int32, True, np.object_) - _test_dtype(np.int32, '', np.object_) - _test_dtype(np.float64, 1, np.float64) - _test_dtype(np.float64, 2.0, np.float64) - _test_dtype(np.float64, 3.0 + 4.0j, np.complex128) - _test_dtype(np.float64, True, np.object_) - _test_dtype(np.float64, '', np.object_) - _test_dtype(np.complex128, 1, np.complex128) - _test_dtype(np.complex128, 2.0, np.complex128) - _test_dtype(np.complex128, 3.0 + 4.0j, np.complex128) - _test_dtype(np.complex128, True, np.object_) - _test_dtype(np.complex128, '', np.object_) - _test_dtype(np.bool_, 1, np.object_) - _test_dtype(np.bool_, 2.0, np.object_) - _test_dtype(np.bool_, 3.0 + 4.0j, np.object_) - _test_dtype(np.bool_, True, np.bool_) - _test_dtype(np.bool_, '', np.object_) - - def test_2d_with_out(self): - def _test_dtype(dtype, can_hold_na, writeable=True): - data = np.random.randint(0, 2, (5, 3)).astype(dtype) - data.flags.writeable = writeable - - indexer = [2, 1, 0, 1] - out0 = np.empty((4, 3), dtype=dtype) - out1 = np.empty((5, 4), dtype=dtype) + + # No Exception otherwise. + data.take(indexer, out=out) + + def test_1d_fill_nonna(self, dtype_fill_out_dtype): + dtype, fill_value, out_dtype = dtype_fill_out_dtype + data = np.random.randint(0, 2, 4).astype(dtype) + indexer = [2, 1, 0, -1] + + result = algos.take_1d(data, indexer, fill_value=fill_value) + assert ((result[[0, 1, 2]] == data[[2, 1, 0]]).all()) + assert (result[3] == fill_value) + assert (result.dtype == out_dtype) + + indexer = [2, 1, 0, 1] + + result = algos.take_1d(data, indexer, fill_value=fill_value) + assert ((result[[0, 1, 2, 3]] == data[indexer]).all()) + assert (result.dtype == dtype) + + def test_2d_with_out(self, dtype_can_hold_na, writeable): + dtype, can_hold_na = dtype_can_hold_na + + data = np.random.randint(0, 2, (5, 3)).astype(dtype) + data.flags.writeable = writeable + + indexer = [2, 1, 0, 1] + out0 = np.empty((4, 3), dtype=dtype) + out1 = np.empty((5, 4), dtype=dtype) + algos.take_nd(data, indexer, out=out0, axis=0) + algos.take_nd(data, indexer, out=out1, axis=1) + + expected0 = data.take(indexer, axis=0) + expected1 = data.take(indexer, axis=1) + tm.assert_almost_equal(out0, expected0) + tm.assert_almost_equal(out1, expected1) + + indexer = [2, 1, 0, -1] + out0 = np.empty((4, 3), dtype=dtype) + out1 = np.empty((5, 4), dtype=dtype) + + if can_hold_na: algos.take_nd(data, indexer, out=out0, axis=0) algos.take_nd(data, indexer, out=out1, axis=1) + expected0 = data.take(indexer, axis=0) expected1 = data.take(indexer, axis=1) + expected0[3, :] = np.nan + expected1[:, 3] = np.nan + tm.assert_almost_equal(out0, expected0) tm.assert_almost_equal(out1, expected1) - - indexer = [2, 1, 0, -1] - out0 = np.empty((4, 3), dtype=dtype) - out1 = np.empty((5, 4), dtype=dtype) - if can_hold_na: - algos.take_nd(data, indexer, out=out0, axis=0) - algos.take_nd(data, indexer, out=out1, axis=1) - expected0 = data.take(indexer, axis=0) - expected1 = data.take(indexer, axis=1) - expected0[3, :] = np.nan - expected1[:, 3] = np.nan - tm.assert_almost_equal(out0, expected0) - tm.assert_almost_equal(out1, expected1) - else: - for i, out in enumerate([out0, out1]): - with tm.assertRaisesRegexp(TypeError, self.fill_error): - algos.take_nd(data, indexer, out=out, axis=i) - # no exception o/w - data.take(indexer, out=out, axis=i) - - for writeable in [True, False]: - # Check that take_nd works both with writeable arrays (in which - # case fast typed memoryviews implementation) and read-only - # arrays alike. - _test_dtype(np.float64, True, writeable=writeable) - _test_dtype(np.float32, True, writeable=writeable) - _test_dtype(np.uint64, False, writeable=writeable) - _test_dtype(np.uint32, False, writeable=writeable) - _test_dtype(np.uint16, False, writeable=writeable) - _test_dtype(np.uint8, False, writeable=writeable) - _test_dtype(np.int64, False, writeable=writeable) - _test_dtype(np.int32, False, writeable=writeable) - _test_dtype(np.int16, False, writeable=writeable) - _test_dtype(np.int8, False, writeable=writeable) - _test_dtype(np.object_, True, writeable=writeable) - _test_dtype(np.bool, False, writeable=writeable) - - def test_2d_fill_nonna(self): - def _test_dtype(dtype, fill_value, out_dtype): - data = np.random.randint(0, 2, (5, 3)).astype(dtype) - - indexer = [2, 1, 0, -1] - - result = algos.take_nd(data, indexer, axis=0, - fill_value=fill_value) - assert ((result[[0, 1, 2], :] == data[[2, 1, 0], :]).all()) - assert ((result[3, :] == fill_value).all()) - assert (result.dtype == out_dtype) - - result = algos.take_nd(data, indexer, axis=1, - fill_value=fill_value) - assert ((result[:, [0, 1, 2]] == data[:, [2, 1, 0]]).all()) - assert ((result[:, 3] == fill_value).all()) - assert (result.dtype == out_dtype) - - indexer = [2, 1, 0, 1] - - result = algos.take_nd(data, indexer, axis=0, - fill_value=fill_value) - assert ((result[[0, 1, 2, 3], :] == data[indexer, :]).all()) - assert (result.dtype == dtype) - - result = algos.take_nd(data, indexer, axis=1, - fill_value=fill_value) - assert ((result[:, [0, 1, 2, 3]] == data[:, indexer]).all()) - assert (result.dtype == dtype) - - _test_dtype(np.int8, np.int16(127), np.int8) - _test_dtype(np.int8, np.int16(128), np.int16) - _test_dtype(np.int32, 1, np.int32) - _test_dtype(np.int32, 2.0, np.float64) - _test_dtype(np.int32, 3.0 + 4.0j, np.complex128) - _test_dtype(np.int32, True, np.object_) - _test_dtype(np.int32, '', np.object_) - _test_dtype(np.float64, 1, np.float64) - _test_dtype(np.float64, 2.0, np.float64) - _test_dtype(np.float64, 3.0 + 4.0j, np.complex128) - _test_dtype(np.float64, True, np.object_) - _test_dtype(np.float64, '', np.object_) - _test_dtype(np.complex128, 1, np.complex128) - _test_dtype(np.complex128, 2.0, np.complex128) - _test_dtype(np.complex128, 3.0 + 4.0j, np.complex128) - _test_dtype(np.complex128, True, np.object_) - _test_dtype(np.complex128, '', np.object_) - _test_dtype(np.bool_, 1, np.object_) - _test_dtype(np.bool_, 2.0, np.object_) - _test_dtype(np.bool_, 3.0 + 4.0j, np.object_) - _test_dtype(np.bool_, True, np.bool_) - _test_dtype(np.bool_, '', np.object_) - - def test_3d_with_out(self): - def _test_dtype(dtype, can_hold_na): - data = np.random.randint(0, 2, (5, 4, 3)).astype(dtype) - - indexer = [2, 1, 0, 1] - out0 = np.empty((4, 4, 3), dtype=dtype) - out1 = np.empty((5, 4, 3), dtype=dtype) - out2 = np.empty((5, 4, 4), dtype=dtype) + else: + for i, out in enumerate([out0, out1]): + with pytest.raises(TypeError, match=self.fill_error): + algos.take_nd(data, indexer, out=out, axis=i) + + # No Exception otherwise. + data.take(indexer, out=out, axis=i) + + def test_2d_fill_nonna(self, dtype_fill_out_dtype): + dtype, fill_value, out_dtype = dtype_fill_out_dtype + data = np.random.randint(0, 2, (5, 3)).astype(dtype) + indexer = [2, 1, 0, -1] + + result = algos.take_nd(data, indexer, axis=0, + fill_value=fill_value) + assert ((result[[0, 1, 2], :] == data[[2, 1, 0], :]).all()) + assert ((result[3, :] == fill_value).all()) + assert (result.dtype == out_dtype) + + result = algos.take_nd(data, indexer, axis=1, + fill_value=fill_value) + assert ((result[:, [0, 1, 2]] == data[:, [2, 1, 0]]).all()) + assert ((result[:, 3] == fill_value).all()) + assert (result.dtype == out_dtype) + + indexer = [2, 1, 0, 1] + result = algos.take_nd(data, indexer, axis=0, + fill_value=fill_value) + assert ((result[[0, 1, 2, 3], :] == data[indexer, :]).all()) + assert (result.dtype == dtype) + + result = algos.take_nd(data, indexer, axis=1, + fill_value=fill_value) + assert ((result[:, [0, 1, 2, 3]] == data[:, indexer]).all()) + assert (result.dtype == dtype) + + def test_3d_with_out(self, dtype_can_hold_na): + dtype, can_hold_na = dtype_can_hold_na + + data = np.random.randint(0, 2, (5, 4, 3)).astype(dtype) + indexer = [2, 1, 0, 1] + + out0 = np.empty((4, 4, 3), dtype=dtype) + out1 = np.empty((5, 4, 3), dtype=dtype) + out2 = np.empty((5, 4, 4), dtype=dtype) + + algos.take_nd(data, indexer, out=out0, axis=0) + algos.take_nd(data, indexer, out=out1, axis=1) + algos.take_nd(data, indexer, out=out2, axis=2) + + expected0 = data.take(indexer, axis=0) + expected1 = data.take(indexer, axis=1) + expected2 = data.take(indexer, axis=2) + + tm.assert_almost_equal(out0, expected0) + tm.assert_almost_equal(out1, expected1) + tm.assert_almost_equal(out2, expected2) + + indexer = [2, 1, 0, -1] + out0 = np.empty((4, 4, 3), dtype=dtype) + out1 = np.empty((5, 4, 3), dtype=dtype) + out2 = np.empty((5, 4, 4), dtype=dtype) + + if can_hold_na: algos.take_nd(data, indexer, out=out0, axis=0) algos.take_nd(data, indexer, out=out1, axis=1) algos.take_nd(data, indexer, out=out2, axis=2) + expected0 = data.take(indexer, axis=0) expected1 = data.take(indexer, axis=1) expected2 = data.take(indexer, axis=2) + + expected0[3, :, :] = np.nan + expected1[:, 3, :] = np.nan + expected2[:, :, 3] = np.nan + tm.assert_almost_equal(out0, expected0) tm.assert_almost_equal(out1, expected1) tm.assert_almost_equal(out2, expected2) - - indexer = [2, 1, 0, -1] - out0 = np.empty((4, 4, 3), dtype=dtype) - out1 = np.empty((5, 4, 3), dtype=dtype) - out2 = np.empty((5, 4, 4), dtype=dtype) - if can_hold_na: - algos.take_nd(data, indexer, out=out0, axis=0) - algos.take_nd(data, indexer, out=out1, axis=1) - algos.take_nd(data, indexer, out=out2, axis=2) - expected0 = data.take(indexer, axis=0) - expected1 = data.take(indexer, axis=1) - expected2 = data.take(indexer, axis=2) - expected0[3, :, :] = np.nan - expected1[:, 3, :] = np.nan - expected2[:, :, 3] = np.nan - tm.assert_almost_equal(out0, expected0) - tm.assert_almost_equal(out1, expected1) - tm.assert_almost_equal(out2, expected2) - else: - for i, out in enumerate([out0, out1, out2]): - with tm.assertRaisesRegexp(TypeError, self.fill_error): - algos.take_nd(data, indexer, out=out, axis=i) - # no exception o/w - data.take(indexer, out=out, axis=i) - - _test_dtype(np.float64, True) - _test_dtype(np.float32, True) - _test_dtype(np.uint64, False) - _test_dtype(np.uint32, False) - _test_dtype(np.uint16, False) - _test_dtype(np.uint8, False) - _test_dtype(np.int64, False) - _test_dtype(np.int32, False) - _test_dtype(np.int16, False) - _test_dtype(np.int8, False) - _test_dtype(np.object_, True) - _test_dtype(np.bool, False) - - def test_3d_fill_nonna(self): - def _test_dtype(dtype, fill_value, out_dtype): - data = np.random.randint(0, 2, (5, 4, 3)).astype(dtype) - - indexer = [2, 1, 0, -1] - - result = algos.take_nd(data, indexer, axis=0, - fill_value=fill_value) - assert ((result[[0, 1, 2], :, :] == data[[2, 1, 0], :, :]).all()) - assert ((result[3, :, :] == fill_value).all()) - assert (result.dtype == out_dtype) - - result = algos.take_nd(data, indexer, axis=1, - fill_value=fill_value) - assert ((result[:, [0, 1, 2], :] == data[:, [2, 1, 0], :]).all()) - assert ((result[:, 3, :] == fill_value).all()) - assert (result.dtype == out_dtype) - - result = algos.take_nd(data, indexer, axis=2, - fill_value=fill_value) - assert ((result[:, :, [0, 1, 2]] == data[:, :, [2, 1, 0]]).all()) - assert ((result[:, :, 3] == fill_value).all()) - assert (result.dtype == out_dtype) - - indexer = [2, 1, 0, 1] - - result = algos.take_nd(data, indexer, axis=0, - fill_value=fill_value) - assert ((result[[0, 1, 2, 3], :, :] == data[indexer, :, :]).all()) - assert (result.dtype == dtype) - - result = algos.take_nd(data, indexer, axis=1, - fill_value=fill_value) - assert ((result[:, [0, 1, 2, 3], :] == data[:, indexer, :]).all()) - assert (result.dtype == dtype) - - result = algos.take_nd(data, indexer, axis=2, - fill_value=fill_value) - assert ((result[:, :, [0, 1, 2, 3]] == data[:, :, indexer]).all()) - assert (result.dtype == dtype) - - _test_dtype(np.int8, np.int16(127), np.int8) - _test_dtype(np.int8, np.int16(128), np.int16) - _test_dtype(np.int32, 1, np.int32) - _test_dtype(np.int32, 2.0, np.float64) - _test_dtype(np.int32, 3.0 + 4.0j, np.complex128) - _test_dtype(np.int32, True, np.object_) - _test_dtype(np.int32, '', np.object_) - _test_dtype(np.float64, 1, np.float64) - _test_dtype(np.float64, 2.0, np.float64) - _test_dtype(np.float64, 3.0 + 4.0j, np.complex128) - _test_dtype(np.float64, True, np.object_) - _test_dtype(np.float64, '', np.object_) - _test_dtype(np.complex128, 1, np.complex128) - _test_dtype(np.complex128, 2.0, np.complex128) - _test_dtype(np.complex128, 3.0 + 4.0j, np.complex128) - _test_dtype(np.complex128, True, np.object_) - _test_dtype(np.complex128, '', np.object_) - _test_dtype(np.bool_, 1, np.object_) - _test_dtype(np.bool_, 2.0, np.object_) - _test_dtype(np.bool_, 3.0 + 4.0j, np.object_) - _test_dtype(np.bool_, True, np.bool_) - _test_dtype(np.bool_, '', np.object_) + else: + for i, out in enumerate([out0, out1, out2]): + with pytest.raises(TypeError, match=self.fill_error): + algos.take_nd(data, indexer, out=out, axis=i) + + # No Exception otherwise. + data.take(indexer, out=out, axis=i) + + def test_3d_fill_nonna(self, dtype_fill_out_dtype): + dtype, fill_value, out_dtype = dtype_fill_out_dtype + + data = np.random.randint(0, 2, (5, 4, 3)).astype(dtype) + indexer = [2, 1, 0, -1] + + result = algos.take_nd(data, indexer, axis=0, + fill_value=fill_value) + assert ((result[[0, 1, 2], :, :] == data[[2, 1, 0], :, :]).all()) + assert ((result[3, :, :] == fill_value).all()) + assert (result.dtype == out_dtype) + + result = algos.take_nd(data, indexer, axis=1, + fill_value=fill_value) + assert ((result[:, [0, 1, 2], :] == data[:, [2, 1, 0], :]).all()) + assert ((result[:, 3, :] == fill_value).all()) + assert (result.dtype == out_dtype) + + result = algos.take_nd(data, indexer, axis=2, + fill_value=fill_value) + assert ((result[:, :, [0, 1, 2]] == data[:, :, [2, 1, 0]]).all()) + assert ((result[:, :, 3] == fill_value).all()) + assert (result.dtype == out_dtype) + + indexer = [2, 1, 0, 1] + result = algos.take_nd(data, indexer, axis=0, + fill_value=fill_value) + assert ((result[[0, 1, 2, 3], :, :] == data[indexer, :, :]).all()) + assert (result.dtype == dtype) + + result = algos.take_nd(data, indexer, axis=1, + fill_value=fill_value) + assert ((result[:, [0, 1, 2, 3], :] == data[:, indexer, :]).all()) + assert (result.dtype == dtype) + + result = algos.take_nd(data, indexer, axis=2, + fill_value=fill_value) + assert ((result[:, :, [0, 1, 2, 3]] == data[:, :, indexer]).all()) + assert (result.dtype == dtype) def test_1d_other_dtypes(self): arr = np.random.randn(10).astype(np.float32) @@ -348,24 +304,24 @@ def test_1d_bool(self): result = algos.take_1d(arr, [0, 2, 2, 1]) expected = arr.take([0, 2, 2, 1]) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = algos.take_1d(arr, [0, 2, -1]) - self.assertEqual(result.dtype, np.object_) + assert result.dtype == np.object_ def test_2d_bool(self): arr = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=bool) result = algos.take_nd(arr, [0, 2, 2, 1]) expected = arr.take([0, 2, 2, 1], axis=0) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = algos.take_nd(arr, [0, 2, 2, 1], axis=1) expected = arr.take([0, 2, 2, 1], axis=1) - self.assert_numpy_array_equal(result, expected) + tm.assert_numpy_array_equal(result, expected) result = algos.take_nd(arr, [0, 2, -1]) - self.assertEqual(result.dtype, np.object_) + assert result.dtype == np.object_ def test_2d_float32(self): arr = np.random.randn(4, 3).astype(np.float32) @@ -443,3 +399,70 @@ def test_2d_datetime64(self): expected = arr.take(indexer, axis=1) expected[:, [2, 4]] = datetime(2007, 1, 1) tm.assert_almost_equal(result, expected) + + def test_take_axis_0(self): + arr = np.arange(12).reshape(4, 3) + result = algos.take(arr, [0, -1]) + expected = np.array([[0, 1, 2], [9, 10, 11]]) + tm.assert_numpy_array_equal(result, expected) + + # allow_fill=True + result = algos.take(arr, [0, -1], allow_fill=True, fill_value=0) + expected = np.array([[0, 1, 2], [0, 0, 0]]) + tm.assert_numpy_array_equal(result, expected) + + def test_take_axis_1(self): + arr = np.arange(12).reshape(4, 3) + result = algos.take(arr, [0, -1], axis=1) + expected = np.array([[0, 2], [3, 5], [6, 8], [9, 11]]) + tm.assert_numpy_array_equal(result, expected) + + # allow_fill=True + result = algos.take(arr, [0, -1], axis=1, allow_fill=True, + fill_value=0) + expected = np.array([[0, 0], [3, 0], [6, 0], [9, 0]]) + tm.assert_numpy_array_equal(result, expected) + + +class TestExtensionTake(object): + # The take method found in pd.api.extensions + + def test_bounds_check_large(self): + arr = np.array([1, 2]) + with pytest.raises(IndexError): + algos.take(arr, [2, 3], allow_fill=True) + + with pytest.raises(IndexError): + algos.take(arr, [2, 3], allow_fill=False) + + def test_bounds_check_small(self): + arr = np.array([1, 2, 3], dtype=np.int64) + indexer = [0, -1, -2] + with pytest.raises(ValueError): + algos.take(arr, indexer, allow_fill=True) + + result = algos.take(arr, indexer) + expected = np.array([1, 3, 2], dtype=np.int64) + tm.assert_numpy_array_equal(result, expected) + + @pytest.mark.parametrize('allow_fill', [True, False]) + def test_take_empty(self, allow_fill): + arr = np.array([], dtype=np.int64) + # empty take is ok + result = algos.take(arr, [], allow_fill=allow_fill) + tm.assert_numpy_array_equal(arr, result) + + with pytest.raises(IndexError): + algos.take(arr, [0], allow_fill=allow_fill) + + def test_take_na_empty(self): + result = algos.take(np.array([]), [-1, -1], allow_fill=True, + fill_value=0.0) + expected = np.array([0., 0.]) + tm.assert_numpy_array_equal(result, expected) + + def test_take_coerces_list(self): + arr = [1, 2, 3] + result = algos.take(arr, [0, 0]) + expected = np.array([1, 1]) + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/test_testing.py b/pandas/tests/test_testing.py deleted file mode 100644 index e5cb953cb35a5..0000000000000 --- a/pandas/tests/test_testing.py +++ /dev/null @@ -1,778 +0,0 @@ -# -*- coding: utf-8 -*- -import pandas as pd -import unittest -import pytest -import numpy as np -import sys -from pandas import Series, DataFrame -import pandas.util.testing as tm -from pandas.util.testing import (assert_almost_equal, assertRaisesRegexp, - raise_with_traceback, assert_index_equal, - assert_series_equal, assert_frame_equal, - assert_numpy_array_equal, - RNGContext) -from pandas.compat import is_platform_windows - - -class TestAssertAlmostEqual(tm.TestCase): - - def _assert_almost_equal_both(self, a, b, **kwargs): - assert_almost_equal(a, b, **kwargs) - assert_almost_equal(b, a, **kwargs) - - def _assert_not_almost_equal_both(self, a, b, **kwargs): - self.assertRaises(AssertionError, assert_almost_equal, a, b, **kwargs) - self.assertRaises(AssertionError, assert_almost_equal, b, a, **kwargs) - - def test_assert_almost_equal_numbers(self): - self._assert_almost_equal_both(1.1, 1.1) - self._assert_almost_equal_both(1.1, 1.100001) - self._assert_almost_equal_both(np.int16(1), 1.000001) - self._assert_almost_equal_both(np.float64(1.1), 1.1) - self._assert_almost_equal_both(np.uint32(5), 5) - - self._assert_not_almost_equal_both(1.1, 1) - self._assert_not_almost_equal_both(1.1, True) - self._assert_not_almost_equal_both(1, 2) - self._assert_not_almost_equal_both(1.0001, np.int16(1)) - - def test_assert_almost_equal_numbers_with_zeros(self): - self._assert_almost_equal_both(0, 0) - self._assert_almost_equal_both(0, 0.0) - self._assert_almost_equal_both(0, np.float64(0)) - self._assert_almost_equal_both(0.000001, 0) - - self._assert_not_almost_equal_both(0.001, 0) - self._assert_not_almost_equal_both(1, 0) - - def test_assert_almost_equal_numbers_with_mixed(self): - self._assert_not_almost_equal_both(1, 'abc') - self._assert_not_almost_equal_both(1, [1, ]) - self._assert_not_almost_equal_both(1, object()) - - def test_assert_almost_equal_edge_case_ndarrays(self): - self._assert_almost_equal_both(np.array([], dtype='M8[ns]'), - np.array([], dtype='float64'), - check_dtype=False) - self._assert_almost_equal_both(np.array([], dtype=str), - np.array([], dtype='int64'), - check_dtype=False) - - def test_assert_almost_equal_dicts(self): - self._assert_almost_equal_both({'a': 1, 'b': 2}, {'a': 1, 'b': 2}) - - self._assert_not_almost_equal_both({'a': 1, 'b': 2}, {'a': 1, 'b': 3}) - self._assert_not_almost_equal_both({'a': 1, 'b': 2}, - {'a': 1, 'b': 2, 'c': 3}) - self._assert_not_almost_equal_both({'a': 1}, 1) - self._assert_not_almost_equal_both({'a': 1}, 'abc') - self._assert_not_almost_equal_both({'a': 1}, [1, ]) - - def test_assert_almost_equal_dict_like_object(self): - class DictLikeObj(object): - - def keys(self): - return ('a', ) - - def __getitem__(self, item): - if item == 'a': - return 1 - - self._assert_almost_equal_both({'a': 1}, DictLikeObj(), - check_dtype=False) - - self._assert_not_almost_equal_both({'a': 2}, DictLikeObj(), - check_dtype=False) - - def test_assert_almost_equal_strings(self): - self._assert_almost_equal_both('abc', 'abc') - - self._assert_not_almost_equal_both('abc', 'abcd') - self._assert_not_almost_equal_both('abc', 'abd') - self._assert_not_almost_equal_both('abc', 1) - self._assert_not_almost_equal_both('abc', [1, ]) - - def test_assert_almost_equal_iterables(self): - self._assert_almost_equal_both([1, 2, 3], [1, 2, 3]) - self._assert_almost_equal_both(np.array([1, 2, 3]), - np.array([1, 2, 3])) - - # class / dtype are different - self._assert_not_almost_equal_both(np.array([1, 2, 3]), [1, 2, 3]) - self._assert_not_almost_equal_both(np.array([1, 2, 3]), - np.array([1., 2., 3.])) - - # Can't compare generators - self._assert_not_almost_equal_both(iter([1, 2, 3]), [1, 2, 3]) - - self._assert_not_almost_equal_both([1, 2, 3], [1, 2, 4]) - self._assert_not_almost_equal_both([1, 2, 3], [1, 2, 3, 4]) - self._assert_not_almost_equal_both([1, 2, 3], 1) - - def test_assert_almost_equal_null(self): - self._assert_almost_equal_both(None, None) - - self._assert_not_almost_equal_both(None, np.NaN) - self._assert_not_almost_equal_both(None, 0) - self._assert_not_almost_equal_both(np.NaN, 0) - - def test_assert_almost_equal_inf(self): - self._assert_almost_equal_both(np.inf, np.inf) - self._assert_almost_equal_both(np.inf, float("inf")) - self._assert_not_almost_equal_both(np.inf, 0) - self._assert_almost_equal_both(np.array([np.inf, np.nan, -np.inf]), - np.array([np.inf, np.nan, -np.inf])) - self._assert_almost_equal_both(np.array([np.inf, None, -np.inf], - dtype=np.object_), - np.array([np.inf, np.nan, -np.inf], - dtype=np.object_)) - - def test_assert_almost_equal_pandas(self): - self.assert_almost_equal(pd.Index([1., 1.1]), - pd.Index([1., 1.100001])) - self.assert_almost_equal(pd.Series([1., 1.1]), - pd.Series([1., 1.100001])) - self.assert_almost_equal(pd.DataFrame({'a': [1., 1.1]}), - pd.DataFrame({'a': [1., 1.100001]})) - - def test_assert_almost_equal_object(self): - a = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-01')] - b = [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-01')] - self._assert_almost_equal_both(a, b) - - -class TestUtilTesting(tm.TestCase): - - def test_raise_with_traceback(self): - with assertRaisesRegexp(LookupError, "error_text"): - try: - raise ValueError("THIS IS AN ERROR") - except ValueError as e: - e = LookupError("error_text") - raise_with_traceback(e) - with assertRaisesRegexp(LookupError, "error_text"): - try: - raise ValueError("This is another error") - except ValueError: - e = LookupError("error_text") - _, _, traceback = sys.exc_info() - raise_with_traceback(e, traceback) - - -class TestAssertNumpyArrayEqual(tm.TestCase): - - def test_numpy_array_equal_message(self): - - if is_platform_windows(): - pytest.skip("windows has incomparable line-endings " - "and uses L on the shape") - - expected = """numpy array are different - -numpy array shapes are different -\\[left\\]: \\(2,\\) -\\[right\\]: \\(3,\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([1, 2]), np.array([3, 4, 5])) - - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([1, 2]), np.array([3, 4, 5])) - - # scalar comparison - expected = """Expected type """ - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(1, 2) - expected = """expected 2\\.00000 but got 1\\.00000, with decimal 5""" - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(1, 2) - - # array / scalar array comparison - expected = """numpy array are different - -numpy array classes are different -\\[left\\]: ndarray -\\[right\\]: int""" - - with assertRaisesRegexp(AssertionError, expected): - # numpy_array_equal only accepts np.ndarray - assert_numpy_array_equal(np.array([1]), 1) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([1]), 1) - - # scalar / array comparison - expected = """numpy array are different - -numpy array classes are different -\\[left\\]: int -\\[right\\]: ndarray""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(1, np.array([1])) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(1, np.array([1])) - - expected = """numpy array are different - -numpy array values are different \\(66\\.66667 %\\) -\\[left\\]: \\[nan, 2\\.0, 3\\.0\\] -\\[right\\]: \\[1\\.0, nan, 3\\.0\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([np.nan, 2, 3]), - np.array([1, np.nan, 3])) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([np.nan, 2, 3]), - np.array([1, np.nan, 3])) - - expected = """numpy array are different - -numpy array values are different \\(50\\.0 %\\) -\\[left\\]: \\[1, 2\\] -\\[right\\]: \\[1, 3\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([1, 2]), np.array([1, 3])) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([1, 2]), np.array([1, 3])) - - expected = """numpy array are different - -numpy array values are different \\(50\\.0 %\\) -\\[left\\]: \\[1\\.1, 2\\.000001\\] -\\[right\\]: \\[1\\.1, 2.0\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal( - np.array([1.1, 2.000001]), np.array([1.1, 2.0])) - - # must pass - assert_almost_equal(np.array([1.1, 2.000001]), np.array([1.1, 2.0])) - - expected = """numpy array are different - -numpy array values are different \\(16\\.66667 %\\) -\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\], \\[5, 6\\]\\] -\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\], \\[5, 6\\]\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([[1, 2], [3, 4], [5, 6]]), - np.array([[1, 3], [3, 4], [5, 6]])) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([[1, 2], [3, 4], [5, 6]]), - np.array([[1, 3], [3, 4], [5, 6]])) - - expected = """numpy array are different - -numpy array values are different \\(25\\.0 %\\) -\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\]\\] -\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\]\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([[1, 2], [3, 4]]), - np.array([[1, 3], [3, 4]])) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([[1, 2], [3, 4]]), - np.array([[1, 3], [3, 4]])) - - # allow to overwrite message - expected = """Index are different - -Index shapes are different -\\[left\\]: \\(2,\\) -\\[right\\]: \\(3,\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(np.array([1, 2]), np.array([3, 4, 5]), - obj='Index') - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(np.array([1, 2]), np.array([3, 4, 5]), - obj='Index') - - def test_numpy_array_equal_object_message(self): - - if is_platform_windows(): - pytest.skip("windows has incomparable line-endings " - "and uses L on the shape") - - a = np.array([pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-01')]) - b = np.array([pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02')]) - - expected = """numpy array are different - -numpy array values are different \\(50\\.0 %\\) -\\[left\\]: \\[2011-01-01 00:00:00, 2011-01-01 00:00:00\\] -\\[right\\]: \\[2011-01-01 00:00:00, 2011-01-02 00:00:00\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(a, b) - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal(a, b) - - def test_numpy_array_equal_copy_flag(self): - a = np.array([1, 2, 3]) - b = a.copy() - c = a.view() - expected = r'array\(\[1, 2, 3\]\) is not array\(\[1, 2, 3\]\)' - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(a, b, check_same='same') - expected = r'array\(\[1, 2, 3\]\) is array\(\[1, 2, 3\]\)' - with assertRaisesRegexp(AssertionError, expected): - assert_numpy_array_equal(a, c, check_same='copy') - - def test_assert_almost_equal_iterable_message(self): - - expected = """Iterable are different - -Iterable length are different -\\[left\\]: 2 -\\[right\\]: 3""" - - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal([1, 2], [3, 4, 5]) - - expected = """Iterable are different - -Iterable values are different \\(50\\.0 %\\) -\\[left\\]: \\[1, 2\\] -\\[right\\]: \\[1, 3\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_almost_equal([1, 2], [1, 3]) - - -class TestAssertIndexEqual(unittest.TestCase): - - def test_index_equal_message(self): - - expected = """Index are different - -Index levels are different -\\[left\\]: 1, Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) -\\[right\\]: 2, MultiIndex\\(levels=\\[\\[u?'A', u?'B'\\], \\[1, 2, 3, 4\\]\\], - labels=\\[\\[0, 0, 1, 1\\], \\[0, 1, 2, 3\\]\\]\\)""" - - idx1 = pd.Index([1, 2, 3]) - idx2 = pd.MultiIndex.from_tuples([('A', 1), ('A', 2), - ('B', 3), ('B', 4)]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, exact=False) - - expected = """MultiIndex level \\[1\\] are different - -MultiIndex level \\[1\\] values are different \\(25\\.0 %\\) -\\[left\\]: Int64Index\\(\\[2, 2, 3, 4\\], dtype='int64'\\) -\\[right\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" - - idx1 = pd.MultiIndex.from_tuples([('A', 2), ('A', 2), - ('B', 3), ('B', 4)]) - idx2 = pd.MultiIndex.from_tuples([('A', 1), ('A', 2), - ('B', 3), ('B', 4)]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, check_exact=False) - - expected = """Index are different - -Index length are different -\\[left\\]: 3, Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) -\\[right\\]: 4, Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" - - idx1 = pd.Index([1, 2, 3]) - idx2 = pd.Index([1, 2, 3, 4]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, check_exact=False) - - expected = """Index are different - -Index classes are different -\\[left\\]: Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) -\\[right\\]: Float64Index\\(\\[1\\.0, 2\\.0, 3\\.0\\], dtype='float64'\\)""" - - idx1 = pd.Index([1, 2, 3]) - idx2 = pd.Index([1, 2, 3.0]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, exact=True) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, exact=True, check_exact=False) - - expected = """Index are different - -Index values are different \\(33\\.33333 %\\) -\\[left\\]: Float64Index\\(\\[1.0, 2.0, 3.0], dtype='float64'\\) -\\[right\\]: Float64Index\\(\\[1.0, 2.0, 3.0000000001\\], dtype='float64'\\)""" - - idx1 = pd.Index([1, 2, 3.]) - idx2 = pd.Index([1, 2, 3.0000000001]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - - # must success - assert_index_equal(idx1, idx2, check_exact=False) - - expected = """Index are different - -Index values are different \\(33\\.33333 %\\) -\\[left\\]: Float64Index\\(\\[1.0, 2.0, 3.0], dtype='float64'\\) -\\[right\\]: Float64Index\\(\\[1.0, 2.0, 3.0001\\], dtype='float64'\\)""" - - idx1 = pd.Index([1, 2, 3.]) - idx2 = pd.Index([1, 2, 3.0001]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, check_exact=False) - # must success - assert_index_equal(idx1, idx2, check_exact=False, - check_less_precise=True) - - expected = """Index are different - -Index values are different \\(33\\.33333 %\\) -\\[left\\]: Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) -\\[right\\]: Int64Index\\(\\[1, 2, 4\\], dtype='int64'\\)""" - - idx1 = pd.Index([1, 2, 3]) - idx2 = pd.Index([1, 2, 4]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, check_less_precise=True) - - expected = """MultiIndex level \\[1\\] are different - -MultiIndex level \\[1\\] values are different \\(25\\.0 %\\) -\\[left\\]: Int64Index\\(\\[2, 2, 3, 4\\], dtype='int64'\\) -\\[right\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" - - idx1 = pd.MultiIndex.from_tuples([('A', 2), ('A', 2), - ('B', 3), ('B', 4)]) - idx2 = pd.MultiIndex.from_tuples([('A', 1), ('A', 2), - ('B', 3), ('B', 4)]) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2, check_exact=False) - - def test_index_equal_metadata_message(self): - - expected = """Index are different - -Attribute "names" are different -\\[left\\]: \\[None\\] -\\[right\\]: \\[u?'x'\\]""" - - idx1 = pd.Index([1, 2, 3]) - idx2 = pd.Index([1, 2, 3], name='x') - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - - # same name, should pass - assert_index_equal(pd.Index([1, 2, 3], name=np.nan), - pd.Index([1, 2, 3], name=np.nan)) - assert_index_equal(pd.Index([1, 2, 3], name=pd.NaT), - pd.Index([1, 2, 3], name=pd.NaT)) - - expected = """Index are different - -Attribute "names" are different -\\[left\\]: \\[nan\\] -\\[right\\]: \\[NaT\\]""" - - idx1 = pd.Index([1, 2, 3], name=np.nan) - idx2 = pd.Index([1, 2, 3], name=pd.NaT) - with assertRaisesRegexp(AssertionError, expected): - assert_index_equal(idx1, idx2) - - -class TestAssertSeriesEqual(tm.TestCase): - - def _assert_equal(self, x, y, **kwargs): - assert_series_equal(x, y, **kwargs) - assert_series_equal(y, x, **kwargs) - - def _assert_not_equal(self, a, b, **kwargs): - self.assertRaises(AssertionError, assert_series_equal, a, b, **kwargs) - self.assertRaises(AssertionError, assert_series_equal, b, a, **kwargs) - - def test_equal(self): - self._assert_equal(Series(range(3)), Series(range(3))) - self._assert_equal(Series(list('abc')), Series(list('abc'))) - - def test_not_equal(self): - self._assert_not_equal(Series(range(3)), Series(range(3)) + 1) - self._assert_not_equal(Series(list('abc')), Series(list('xyz'))) - self._assert_not_equal(Series(range(3)), Series(range(4))) - self._assert_not_equal( - Series(range(3)), Series( - range(3), dtype='float64')) - self._assert_not_equal( - Series(range(3)), Series( - range(3), index=[1, 2, 4])) - - # ATM meta data is not checked in assert_series_equal - # self._assert_not_equal(Series(range(3)),Series(range(3),name='foo'),check_names=True) - - def test_less_precise(self): - s1 = Series([0.12345], dtype='float64') - s2 = Series([0.12346], dtype='float64') - - self.assertRaises(AssertionError, assert_series_equal, s1, s2) - self._assert_equal(s1, s2, check_less_precise=True) - for i in range(4): - self._assert_equal(s1, s2, check_less_precise=i) - self.assertRaises(AssertionError, assert_series_equal, s1, s2, 10) - - s1 = Series([0.12345], dtype='float32') - s2 = Series([0.12346], dtype='float32') - - self.assertRaises(AssertionError, assert_series_equal, s1, s2) - self._assert_equal(s1, s2, check_less_precise=True) - for i in range(4): - self._assert_equal(s1, s2, check_less_precise=i) - self.assertRaises(AssertionError, assert_series_equal, s1, s2, 10) - - # even less than less precise - s1 = Series([0.1235], dtype='float32') - s2 = Series([0.1236], dtype='float32') - - self.assertRaises(AssertionError, assert_series_equal, s1, s2) - self.assertRaises(AssertionError, assert_series_equal, s1, s2, True) - - def test_index_dtype(self): - df1 = DataFrame.from_records( - {'a': [1, 2], 'c': ['l1', 'l2']}, index=['a']) - df2 = DataFrame.from_records( - {'a': [1.0, 2.0], 'c': ['l1', 'l2']}, index=['a']) - self._assert_not_equal(df1.c, df2.c, check_index_type=True) - - def test_multiindex_dtype(self): - df1 = DataFrame.from_records( - {'a': [1, 2], 'b': [2.1, 1.5], - 'c': ['l1', 'l2']}, index=['a', 'b']) - df2 = DataFrame.from_records( - {'a': [1.0, 2.0], 'b': [2.1, 1.5], - 'c': ['l1', 'l2']}, index=['a', 'b']) - self._assert_not_equal(df1.c, df2.c, check_index_type=True) - - def test_series_equal_message(self): - - expected = """Series are different - -Series length are different -\\[left\\]: 3, RangeIndex\\(start=0, stop=3, step=1\\) -\\[right\\]: 4, RangeIndex\\(start=0, stop=4, step=1\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_series_equal(pd.Series([1, 2, 3]), pd.Series([1, 2, 3, 4])) - - expected = """Series are different - -Series values are different \\(33\\.33333 %\\) -\\[left\\]: \\[1, 2, 3\\] -\\[right\\]: \\[1, 2, 4\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_series_equal(pd.Series([1, 2, 3]), pd.Series([1, 2, 4])) - with assertRaisesRegexp(AssertionError, expected): - assert_series_equal(pd.Series([1, 2, 3]), pd.Series([1, 2, 4]), - check_less_precise=True) - - -class TestAssertFrameEqual(tm.TestCase): - - def _assert_equal(self, x, y, **kwargs): - assert_frame_equal(x, y, **kwargs) - assert_frame_equal(y, x, **kwargs) - - def _assert_not_equal(self, a, b, **kwargs): - self.assertRaises(AssertionError, assert_frame_equal, a, b, **kwargs) - self.assertRaises(AssertionError, assert_frame_equal, b, a, **kwargs) - - def test_equal_with_different_row_order(self): - # check_like=True ignores row-column orderings - df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, - index=['a', 'b', 'c']) - df2 = pd.DataFrame({'A': [3, 2, 1], 'B': [6, 5, 4]}, - index=['c', 'b', 'a']) - - self._assert_equal(df1, df2, check_like=True) - self._assert_not_equal(df1, df2) - - def test_not_equal_with_different_shape(self): - self._assert_not_equal(pd.DataFrame({'A': [1, 2, 3]}), - pd.DataFrame({'A': [1, 2, 3, 4]})) - - def test_index_dtype(self): - df1 = DataFrame.from_records( - {'a': [1, 2], 'c': ['l1', 'l2']}, index=['a']) - df2 = DataFrame.from_records( - {'a': [1.0, 2.0], 'c': ['l1', 'l2']}, index=['a']) - self._assert_not_equal(df1, df2, check_index_type=True) - - def test_multiindex_dtype(self): - df1 = DataFrame.from_records( - {'a': [1, 2], 'b': [2.1, 1.5], - 'c': ['l1', 'l2']}, index=['a', 'b']) - df2 = DataFrame.from_records( - {'a': [1.0, 2.0], 'b': [2.1, 1.5], - 'c': ['l1', 'l2']}, index=['a', 'b']) - self._assert_not_equal(df1, df2, check_index_type=True) - - def test_empty_dtypes(self): - df1 = pd.DataFrame(columns=["col1", "col2"]) - df1["col1"] = df1["col1"].astype('int64') - df2 = pd.DataFrame(columns=["col1", "col2"]) - self._assert_equal(df1, df2, check_dtype=False) - self._assert_not_equal(df1, df2, check_dtype=True) - - def test_frame_equal_message(self): - - expected = """DataFrame are different - -DataFrame shape mismatch -\\[left\\]: \\(3, 2\\) -\\[right\\]: \\(3, 1\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_frame_equal(pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}), - pd.DataFrame({'A': [1, 2, 3]})) - - expected = """DataFrame\\.index are different - -DataFrame\\.index values are different \\(33\\.33333 %\\) -\\[left\\]: Index\\(\\[u?'a', u?'b', u?'c'\\], dtype='object'\\) -\\[right\\]: Index\\(\\[u?'a', u?'b', u?'d'\\], dtype='object'\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_frame_equal(pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, - index=['a', 'b', 'c']), - pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, - index=['a', 'b', 'd'])) - - expected = """DataFrame\\.columns are different - -DataFrame\\.columns values are different \\(50\\.0 %\\) -\\[left\\]: Index\\(\\[u?'A', u?'B'\\], dtype='object'\\) -\\[right\\]: Index\\(\\[u?'A', u?'b'\\], dtype='object'\\)""" - - with assertRaisesRegexp(AssertionError, expected): - assert_frame_equal(pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, - index=['a', 'b', 'c']), - pd.DataFrame({'A': [1, 2, 3], 'b': [4, 5, 6]}, - index=['a', 'b', 'c'])) - - expected = """DataFrame\\.iloc\\[:, 1\\] are different - -DataFrame\\.iloc\\[:, 1\\] values are different \\(33\\.33333 %\\) -\\[left\\]: \\[4, 5, 6\\] -\\[right\\]: \\[4, 5, 7\\]""" - - with assertRaisesRegexp(AssertionError, expected): - assert_frame_equal(pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}), - pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 7]})) - - with assertRaisesRegexp(AssertionError, expected): - assert_frame_equal(pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}), - pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 7]}), - by_blocks=True) - - -class TestIsInstance(tm.TestCase): - - def test_isinstance(self): - - expected = "Expected type " - with assertRaisesRegexp(AssertionError, expected): - tm.assertIsInstance(1, pd.Series) - - def test_notisinstance(self): - - expected = "Input must not be type " - with assertRaisesRegexp(AssertionError, expected): - tm.assertNotIsInstance(pd.Series([1]), pd.Series) - - -class TestAssertCategoricalEqual(unittest.TestCase): - - def test_categorical_equal_message(self): - - expected = """Categorical\\.categories are different - -Categorical\\.categories values are different \\(25\\.0 %\\) -\\[left\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\) -\\[right\\]: Int64Index\\(\\[1, 2, 3, 5\\], dtype='int64'\\)""" - - a = pd.Categorical([1, 2, 3, 4]) - b = pd.Categorical([1, 2, 3, 5]) - with assertRaisesRegexp(AssertionError, expected): - tm.assert_categorical_equal(a, b) - - expected = """Categorical\\.codes are different - -Categorical\\.codes values are different \\(50\\.0 %\\) -\\[left\\]: \\[0, 1, 3, 2\\] -\\[right\\]: \\[0, 1, 2, 3\\]""" - - a = pd.Categorical([1, 2, 4, 3], categories=[1, 2, 3, 4]) - b = pd.Categorical([1, 2, 3, 4], categories=[1, 2, 3, 4]) - with assertRaisesRegexp(AssertionError, expected): - tm.assert_categorical_equal(a, b) - - expected = """Categorical are different - -Attribute "ordered" are different -\\[left\\]: False -\\[right\\]: True""" - - a = pd.Categorical([1, 2, 3, 4], ordered=False) - b = pd.Categorical([1, 2, 3, 4], ordered=True) - with assertRaisesRegexp(AssertionError, expected): - tm.assert_categorical_equal(a, b) - - -class TestRNGContext(unittest.TestCase): - - def test_RNGContext(self): - expected0 = 1.764052345967664 - expected1 = 1.6243453636632417 - - with RNGContext(0): - with RNGContext(1): - self.assertEqual(np.random.randn(), expected1) - self.assertEqual(np.random.randn(), expected0) - - -class TestDeprecatedTests(tm.TestCase): - - def test_warning(self): - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertEquals(1, 1) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertNotEquals(1, 2) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assert_(True) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertAlmostEquals(1.0, 1.0000000001) - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertNotAlmostEquals(1, 2) - - -class TestLocale(tm.TestCase): - - def test_locale(self): - if sys.platform == 'win32': - pytest.skip( - "skipping on win platforms as locale not available") - - # GH9744 - locales = tm.get_locales() - self.assertTrue(len(locales) >= 1) diff --git a/pandas/tests/test_util.py b/pandas/tests/test_util.py deleted file mode 100644 index 1bf9f4da45bff..0000000000000 --- a/pandas/tests/test_util.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8 -*- -from collections import OrderedDict -import sys -import unittest -from uuid import uuid4 -from pandas.util._move import move_into_mutable_buffer, BadMove, stolenbuf -from pandas.util.decorators import deprecate_kwarg -from pandas.util.validators import (validate_args, validate_kwargs, - validate_args_and_kwargs, - validate_bool_kwarg) - -import pandas.util.testing as tm - - -class TestDecorators(tm.TestCase): - - def setUp(self): - @deprecate_kwarg('old', 'new') - def _f1(new=False): - return new - - @deprecate_kwarg('old', 'new', {'yes': True, 'no': False}) - def _f2(new=False): - return new - - @deprecate_kwarg('old', 'new', lambda x: x + 1) - def _f3(new=0): - return new - - self.f1 = _f1 - self.f2 = _f2 - self.f3 = _f3 - - def test_deprecate_kwarg(self): - x = 78 - with tm.assert_produces_warning(FutureWarning): - result = self.f1(old=x) - self.assertIs(result, x) - with tm.assert_produces_warning(None): - self.f1(new=x) - - def test_dict_deprecate_kwarg(self): - x = 'yes' - with tm.assert_produces_warning(FutureWarning): - result = self.f2(old=x) - self.assertEqual(result, True) - - def test_missing_deprecate_kwarg(self): - x = 'bogus' - with tm.assert_produces_warning(FutureWarning): - result = self.f2(old=x) - self.assertEqual(result, 'bogus') - - def test_callable_deprecate_kwarg(self): - x = 5 - with tm.assert_produces_warning(FutureWarning): - result = self.f3(old=x) - self.assertEqual(result, x + 1) - with tm.assertRaises(TypeError): - self.f3(old='hello') - - def test_bad_deprecate_kwarg(self): - with tm.assertRaises(TypeError): - @deprecate_kwarg('old', 'new', 0) - def f4(new=None): - pass - - -def test_rands(): - r = tm.rands(10) - assert(len(r) == 10) - - -def test_rands_array(): - arr = tm.rands_array(5, size=10) - assert(arr.shape == (10,)) - assert(len(arr[0]) == 5) - - arr = tm.rands_array(7, size=(10, 10)) - assert(arr.shape == (10, 10)) - assert(len(arr[1, 1]) == 7) - - -class TestValidateArgs(tm.TestCase): - fname = 'func' - - def test_bad_min_fname_arg_count(self): - msg = "'max_fname_arg_count' must be non-negative" - with tm.assertRaisesRegexp(ValueError, msg): - validate_args(self.fname, (None,), -1, 'foo') - - def test_bad_arg_length_max_value_single(self): - args = (None, None) - compat_args = ('foo',) - - min_fname_arg_count = 0 - max_length = len(compat_args) + min_fname_arg_count - actual_length = len(args) + min_fname_arg_count - msg = (r"{fname}\(\) takes at most {max_length} " - r"argument \({actual_length} given\)" - .format(fname=self.fname, max_length=max_length, - actual_length=actual_length)) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_args(self.fname, args, - min_fname_arg_count, - compat_args) - - def test_bad_arg_length_max_value_multiple(self): - args = (None, None) - compat_args = dict(foo=None) - - min_fname_arg_count = 2 - max_length = len(compat_args) + min_fname_arg_count - actual_length = len(args) + min_fname_arg_count - msg = (r"{fname}\(\) takes at most {max_length} " - r"arguments \({actual_length} given\)" - .format(fname=self.fname, max_length=max_length, - actual_length=actual_length)) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_args(self.fname, args, - min_fname_arg_count, - compat_args) - - def test_not_all_defaults(self): - bad_arg = 'foo' - msg = ("the '{arg}' parameter is not supported " - r"in the pandas implementation of {func}\(\)". - format(arg=bad_arg, func=self.fname)) - - compat_args = OrderedDict() - compat_args['foo'] = 2 - compat_args['bar'] = -1 - compat_args['baz'] = 3 - - arg_vals = (1, -1, 3) - - for i in range(1, 3): - with tm.assertRaisesRegexp(ValueError, msg): - validate_args(self.fname, arg_vals[:i], 2, compat_args) - - def test_validation(self): - # No exceptions should be thrown - validate_args(self.fname, (None,), 2, dict(out=None)) - - compat_args = OrderedDict() - compat_args['axis'] = 1 - compat_args['out'] = None - - validate_args(self.fname, (1, None), 2, compat_args) - - -class TestValidateKwargs(tm.TestCase): - fname = 'func' - - def test_bad_kwarg(self): - goodarg = 'f' - badarg = goodarg + 'o' - - compat_args = OrderedDict() - compat_args[goodarg] = 'foo' - compat_args[badarg + 'o'] = 'bar' - kwargs = {goodarg: 'foo', badarg: 'bar'} - msg = (r"{fname}\(\) got an unexpected " - r"keyword argument '{arg}'".format( - fname=self.fname, arg=badarg)) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_kwargs(self.fname, kwargs, compat_args) - - def test_not_all_none(self): - bad_arg = 'foo' - msg = (r"the '{arg}' parameter is not supported " - r"in the pandas implementation of {func}\(\)". - format(arg=bad_arg, func=self.fname)) - - compat_args = OrderedDict() - compat_args['foo'] = 1 - compat_args['bar'] = 's' - compat_args['baz'] = None - - kwarg_keys = ('foo', 'bar', 'baz') - kwarg_vals = (2, 's', None) - - for i in range(1, 3): - kwargs = dict(zip(kwarg_keys[:i], - kwarg_vals[:i])) - - with tm.assertRaisesRegexp(ValueError, msg): - validate_kwargs(self.fname, kwargs, compat_args) - - def test_validation(self): - # No exceptions should be thrown - compat_args = OrderedDict() - compat_args['f'] = None - compat_args['b'] = 1 - compat_args['ba'] = 's' - kwargs = dict(f=None, b=1) - validate_kwargs(self.fname, kwargs, compat_args) - - def test_validate_bool_kwarg(self): - arg_names = ['inplace', 'copy'] - invalid_values = [1, "True", [1, 2, 3], 5.0] - valid_values = [True, False, None] - - for name in arg_names: - for value in invalid_values: - with tm.assertRaisesRegexp(ValueError, - ("For argument \"%s\" expected " - "type bool, received type %s") % - (name, type(value).__name__)): - validate_bool_kwarg(value, name) - - for value in valid_values: - tm.assert_equal(validate_bool_kwarg(value, name), value) - - -class TestValidateKwargsAndArgs(tm.TestCase): - fname = 'func' - - def test_invalid_total_length_max_length_one(self): - compat_args = ('foo',) - kwargs = {'foo': 'FOO'} - args = ('FoO', 'BaZ') - - min_fname_arg_count = 0 - max_length = len(compat_args) + min_fname_arg_count - actual_length = len(kwargs) + len(args) + min_fname_arg_count - msg = (r"{fname}\(\) takes at most {max_length} " - r"argument \({actual_length} given\)" - .format(fname=self.fname, max_length=max_length, - actual_length=actual_length)) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_args_and_kwargs(self.fname, args, kwargs, - min_fname_arg_count, - compat_args) - - def test_invalid_total_length_max_length_multiple(self): - compat_args = ('foo', 'bar', 'baz') - kwargs = {'foo': 'FOO', 'bar': 'BAR'} - args = ('FoO', 'BaZ') - - min_fname_arg_count = 2 - max_length = len(compat_args) + min_fname_arg_count - actual_length = len(kwargs) + len(args) + min_fname_arg_count - msg = (r"{fname}\(\) takes at most {max_length} " - r"arguments \({actual_length} given\)" - .format(fname=self.fname, max_length=max_length, - actual_length=actual_length)) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_args_and_kwargs(self.fname, args, kwargs, - min_fname_arg_count, - compat_args) - - def test_no_args_with_kwargs(self): - bad_arg = 'bar' - min_fname_arg_count = 2 - - compat_args = OrderedDict() - compat_args['foo'] = -5 - compat_args[bad_arg] = 1 - - msg = (r"the '{arg}' parameter is not supported " - r"in the pandas implementation of {func}\(\)". - format(arg=bad_arg, func=self.fname)) - - args = () - kwargs = {'foo': -5, bad_arg: 2} - tm.assertRaisesRegexp(ValueError, msg, - validate_args_and_kwargs, - self.fname, args, kwargs, - min_fname_arg_count, compat_args) - - args = (-5, 2) - kwargs = {} - tm.assertRaisesRegexp(ValueError, msg, - validate_args_and_kwargs, - self.fname, args, kwargs, - min_fname_arg_count, compat_args) - - def test_duplicate_argument(self): - min_fname_arg_count = 2 - compat_args = OrderedDict() - compat_args['foo'] = None - compat_args['bar'] = None - compat_args['baz'] = None - kwargs = {'foo': None, 'bar': None} - args = (None,) # duplicate value for 'foo' - - msg = (r"{fname}\(\) got multiple values for keyword " - r"argument '{arg}'".format(fname=self.fname, arg='foo')) - - with tm.assertRaisesRegexp(TypeError, msg): - validate_args_and_kwargs(self.fname, args, kwargs, - min_fname_arg_count, - compat_args) - - def test_validation(self): - # No exceptions should be thrown - compat_args = OrderedDict() - compat_args['foo'] = 1 - compat_args['bar'] = None - compat_args['baz'] = -2 - kwargs = {'baz': -2} - args = (1, None) - - min_fname_arg_count = 2 - validate_args_and_kwargs(self.fname, args, kwargs, - min_fname_arg_count, - compat_args) - - -class TestMove(tm.TestCase): - - def test_cannot_create_instance_of_stolenbuffer(self): - """Stolen buffers need to be created through the smart constructor - ``move_into_mutable_buffer`` which has a bunch of checks in it. - """ - msg = "cannot create 'pandas.util._move.stolenbuf' instances" - with tm.assertRaisesRegexp(TypeError, msg): - stolenbuf() - - def test_more_than_one_ref(self): - """Test case for when we try to use ``move_into_mutable_buffer`` when - the object being moved has other references. - """ - b = b'testing' - - with tm.assertRaises(BadMove) as e: - def handle_success(type_, value, tb): - self.assertIs(value.args[0], b) - return type(e).handle_success(e, type_, value, tb) # super - - e.handle_success = handle_success - move_into_mutable_buffer(b) - - def test_exactly_one_ref(self): - """Test case for when the object being moved has exactly one reference. - """ - b = b'testing' - - # We need to pass an expression on the stack to ensure that there are - # not extra references hanging around. We cannot rewrite this test as - # buf = b[:-3] - # as_stolen_buf = move_into_mutable_buffer(buf) - # because then we would have more than one reference to buf. - as_stolen_buf = move_into_mutable_buffer(b[:-3]) - - # materialize as bytearray to show that it is mutable - self.assertEqual(bytearray(as_stolen_buf), b'test') - - @unittest.skipIf( - sys.version_info[0] > 2, - 'bytes objects cannot be interned in py3', - ) - def test_interned(self): - salt = uuid4().hex - - def make_string(): - # We need to actually create a new string so that it has refcount - # one. We use a uuid so that we know the string could not already - # be in the intern table. - return ''.join(('testing: ', salt)) - - # This should work, the string has one reference on the stack. - move_into_mutable_buffer(make_string()) - - refcount = [None] # nonlocal - - def ref_capture(ob): - # Subtract two because those are the references owned by this - # frame: - # 1. The local variables of this stack frame. - # 2. The python data stack of this stack frame. - refcount[0] = sys.getrefcount(ob) - 2 - return ob - - with tm.assertRaises(BadMove): - # If we intern the string it will still have one reference but now - # it is in the intern table so if other people intern the same - # string while the mutable buffer holds the first string they will - # be the same instance. - move_into_mutable_buffer(ref_capture(intern(make_string()))) # noqa - - self.assertEqual( - refcount[0], - 1, - msg='The BadMove was probably raised for refcount reasons instead' - ' of interning reasons', - ) - - -def test_numpy_errstate_is_default(): - # The defaults since numpy 1.6.0 - expected = {'over': 'warn', 'divide': 'warn', 'invalid': 'warn', - 'under': 'ignore'} - import numpy as np - from pandas.compat import numpy # noqa - # The errstate should be unchanged after that import. - tm.assert_equal(np.geterr(), expected) diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index fe03d7886e661..ce9d1888b8e96 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -1,24 +1,26 @@ +from collections import OrderedDict +from datetime import datetime, timedelta from itertools import product -import pytest -import sys import warnings from warnings import catch_warnings -from datetime import datetime, timedelta -from numpy.random import randn import numpy as np -from distutils.version import LooseVersion +from numpy.random import randn +import pytest + +from pandas.compat import range, zip +from pandas.errors import UnsupportedFunctionCall +import pandas.util._test_decorators as td import pandas as pd -from pandas import (Series, DataFrame, Panel, bdate_range, isnull, - notnull, concat, Timestamp) -import pandas.stats.moments as mom -import pandas.core.window as rwindow -import pandas.tseries.offsets as offsets +from pandas import ( + DataFrame, Index, Series, Timestamp, bdate_range, concat, isna, notna) from pandas.core.base import SpecificationError -from pandas.core.common import UnsupportedFunctionCall +from pandas.core.sorting import safe_sort +import pandas.core.window as rwindow import pandas.util.testing as tm -from pandas.compat import range, zip, PY3 + +import pandas.tseries.offsets as offsets N, K = 100, 10 @@ -30,7 +32,23 @@ def assert_equal(left, right): tm.assert_frame_equal(left, right) -class Base(tm.TestCase): +@pytest.fixture(params=[True, False]) +def raw(request): + return request.param + + +@pytest.fixture(params=['triang', 'blackman', 'hamming', 'bartlett', 'bohman', + 'blackmanharris', 'nuttall', 'barthann']) +def win_types(request): + return request.param + + +@pytest.fixture(params=['kaiser', 'gaussian', 'general_gaussian']) +def win_types_special(request): + return request.param + + +class Base(object): _nan_locs = np.arange(20, 40) _inf_locs = np.array([]) @@ -48,7 +66,7 @@ def _create_data(self): class TestApi(Base): - def setUp(self): + def setup_method(self, method): self._create_data() def test_getitem(self): @@ -57,7 +75,7 @@ def test_getitem(self): tm.assert_index_equal(r._selected_obj.columns, self.frame.columns) r = self.frame.rolling(window=5)[1] - self.assertEqual(r._selected_obj.name, self.frame.columns[1]) + assert r._selected_obj.name == self.frame.columns[1] # technically this is allowed r = self.frame.rolling(window=5)[1, 3] @@ -71,10 +89,9 @@ def test_getitem(self): def test_select_bad_cols(self): df = DataFrame([[1, 2]], columns=['A', 'B']) g = df.rolling(window=5) - self.assertRaises(KeyError, g.__getitem__, ['C']) # g[['C']] - - self.assertRaises(KeyError, g.__getitem__, ['A', 'C']) # g[['A', 'C']] - with tm.assertRaisesRegexp(KeyError, '^[^A]+$'): + with pytest.raises(KeyError, match="Columns not found: 'C'"): + g[['C']] + with pytest.raises(KeyError, match='^[^A]+$'): # A should not be referenced as a bad column... # will have to rethink regex if you change message! g[['A', 'C']] @@ -84,12 +101,13 @@ def test_attribute_access(self): df = DataFrame([[1, 2]], columns=['A', 'B']) r = df.rolling(window=5) tm.assert_series_equal(r.A.sum(), r['A'].sum()) - self.assertRaises(AttributeError, lambda: r.F) + msg = "'Rolling' object has no attribute 'F'" + with pytest.raises(AttributeError, match=msg): + r.F def tests_skip_nuisance(self): df = DataFrame({'A': range(5), 'B': range(5, 10), 'C': 'foo'}) - r = df.rolling(window=3) result = r[['A', 'B']].sum() expected = DataFrame({'A': [np.nan, np.nan, 3, 6, 9], @@ -97,9 +115,12 @@ def tests_skip_nuisance(self): columns=list('AB')) tm.assert_frame_equal(result, expected) - expected = pd.concat([r[['A', 'B']].sum(), df[['C']]], axis=1) - result = r.sum() - tm.assert_frame_equal(result, expected, check_like=True) + def test_skip_sum_object_raises(self): + df = DataFrame({'A': range(5), 'B': range(5, 10), 'C': 'foo'}) + r = df.rolling(window=3) + + with pytest.raises(TypeError, match='cannot handle this type'): + r.sum() def test_agg(self): df = DataFrame({'A': range(5), 'B': range(0, 10, 2)}) @@ -113,53 +134,65 @@ def test_agg(self): b_sum = r['B'].sum() result = r.aggregate([np.mean, np.std]) - expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) + expected = concat([a_mean, a_std, b_mean, b_std], axis=1) expected.columns = pd.MultiIndex.from_product([['A', 'B'], ['mean', 'std']]) tm.assert_frame_equal(result, expected) result = r.aggregate({'A': np.mean, 'B': np.std}) - expected = pd.concat([a_mean, b_std], axis=1) + expected = concat([a_mean, b_std], axis=1) tm.assert_frame_equal(result, expected, check_like=True) result = r.aggregate({'A': ['mean', 'std']}) - expected = pd.concat([a_mean, a_std], axis=1) + expected = concat([a_mean, a_std], axis=1) expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), ('A', 'std')]) tm.assert_frame_equal(result, expected) result = r['A'].aggregate(['mean', 'sum']) - expected = pd.concat([a_mean, a_sum], axis=1) + expected = concat([a_mean, a_sum], axis=1) expected.columns = ['mean', 'sum'] tm.assert_frame_equal(result, expected) - result = r.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}}) - expected = pd.concat([a_mean, a_sum], axis=1) + with catch_warnings(record=True): + # using a dict with renaming + warnings.simplefilter("ignore", FutureWarning) + result = r.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}}) + expected = concat([a_mean, a_sum], axis=1) expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), ('A', 'sum')]) tm.assert_frame_equal(result, expected, check_like=True) - result = r.aggregate({'A': {'mean': 'mean', - 'sum': 'sum'}, - 'B': {'mean2': 'mean', - 'sum2': 'sum'}}) - expected = pd.concat([a_mean, a_sum, b_mean, b_sum], axis=1) + with catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) + result = r.aggregate({'A': {'mean': 'mean', + 'sum': 'sum'}, + 'B': {'mean2': 'mean', + 'sum2': 'sum'}}) + expected = concat([a_mean, a_sum, b_mean, b_sum], axis=1) exp_cols = [('A', 'mean'), ('A', 'sum'), ('B', 'mean2'), ('B', 'sum2')] expected.columns = pd.MultiIndex.from_tuples(exp_cols) tm.assert_frame_equal(result, expected, check_like=True) result = r.aggregate({'A': ['mean', 'std'], 'B': ['mean', 'std']}) - expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) + expected = concat([a_mean, a_std, b_mean, b_std], axis=1) exp_cols = [('A', 'mean'), ('A', 'std'), ('B', 'mean'), ('B', 'std')] expected.columns = pd.MultiIndex.from_tuples(exp_cols) tm.assert_frame_equal(result, expected, check_like=True) + def test_agg_apply(self, raw): + # passed lambda + df = DataFrame({'A': range(5), 'B': range(0, 10, 2)}) + + r = df.rolling(window=3) + a_sum = r['A'].sum() + result = r.agg({'A': np.sum, 'B': lambda x: np.std(x, ddof=1)}) - rcustom = r['B'].apply(lambda x: np.std(x, ddof=1)) - expected = pd.concat([a_sum, rcustom], axis=1) + rcustom = r['B'].apply(lambda x: np.std(x, ddof=1), raw=raw) + expected = concat([a_sum, rcustom], axis=1) tm.assert_frame_equal(result, expected, check_like=True) def test_agg_consistency(self): @@ -172,7 +205,7 @@ def test_agg_consistency(self): tm.assert_index_equal(result, expected) result = r['A'].agg([np.sum, np.mean]).columns - expected = pd.Index(['sum', 'mean']) + expected = Index(['sum', 'mean']) tm.assert_index_equal(result, expected) result = r.agg({'A': [np.sum, np.mean]}).columns @@ -185,22 +218,25 @@ def test_agg_nested_dicts(self): df = DataFrame({'A': range(5), 'B': range(0, 10, 2)}) r = df.rolling(window=3) - def f(): + msg = r"cannot perform renaming for (r1|r2) with a nested dictionary" + with pytest.raises(SpecificationError, match=msg): r.aggregate({'r1': {'A': ['mean', 'sum']}, 'r2': {'B': ['mean', 'sum']}}) - self.assertRaises(SpecificationError, f) - - expected = pd.concat([r['A'].mean(), r['A'].std(), r['B'].mean(), - r['B'].std()], axis=1) + expected = concat([r['A'].mean(), r['A'].std(), + r['B'].mean(), r['B'].std()], axis=1) expected.columns = pd.MultiIndex.from_tuples([('ra', 'mean'), ( 'ra', 'std'), ('rb', 'mean'), ('rb', 'std')]) - result = r[['A', 'B']].agg({'A': {'ra': ['mean', 'std']}, - 'B': {'rb': ['mean', 'std']}}) + with catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) + result = r[['A', 'B']].agg({'A': {'ra': ['mean', 'std']}, + 'B': {'rb': ['mean', 'std']}}) tm.assert_frame_equal(result, expected, check_like=True) - result = r.agg({'A': {'ra': ['mean', 'std']}, - 'B': {'rb': ['mean', 'std']}}) + with catch_warnings(record=True): + warnings.simplefilter("ignore", FutureWarning) + result = r.agg({'A': {'ra': ['mean', 'std']}, + 'B': {'rb': ['mean', 'std']}}) expected.columns = pd.MultiIndex.from_tuples([('A', 'ra', 'mean'), ( 'A', 'ra', 'std'), ('B', 'rb', 'mean'), ('B', 'rb', 'std')]) tm.assert_frame_equal(result, expected, check_like=True) @@ -221,8 +257,8 @@ def test_count_nonnumeric_types(self): 'fl_inf': [1., 2., np.Inf], 'fl_nan': [1., 2., np.NaN], 'str_nan': ['aa', 'bb', np.NaN], - 'dt_nat': [pd.Timestamp('20170101'), pd.Timestamp('20170203'), - pd.Timestamp(None)], + 'dt_nat': [Timestamp('20170101'), Timestamp('20170203'), + Timestamp(None)], 'periods_nat': [pd.Period('2012-01'), pd.Period('2012-02'), pd.Period(None)]}, columns=cols) @@ -245,16 +281,16 @@ def test_count_nonnumeric_types(self): tm.assert_frame_equal(result, expected) result = df.rolling(1).count() - expected = df.notnull().astype(float) + expected = df.notna().astype(float) tm.assert_frame_equal(result, expected) + @td.skip_if_no_scipy + @pytest.mark.filterwarnings("ignore:can't resolve:ImportWarning") def test_window_with_args(self): - tm._skip_if_no_scipy() - # make sure that we are aggregating window functions correctly with arg r = Series(np.random.randn(100)).rolling(window=10, min_periods=1, win_type='gaussian') - expected = pd.concat([r.mean(std=10), r.mean(std=.01)], axis=1) + expected = concat([r.mean(std=10), r.mean(std=.01)], axis=1) expected.columns = ['', ''] result = r.aggregate([lambda x: x.mean(std=10), lambda x: x.mean(std=.01)]) @@ -266,7 +302,7 @@ def a(x): def b(x): return x.mean(std=0.01) - expected = pd.concat([r.mean(std=10), r.mean(std=.01)], axis=1) + expected = concat([r.mean(std=10), r.mean(std=.01)], axis=1) expected.columns = ['a', 'b'] result = r.aggregate([a, b]) tm.assert_frame_equal(result, expected) @@ -277,86 +313,114 @@ def test_preserve_metadata(self): s2 = s.rolling(30).sum() s3 = s.rolling(20).sum() - self.assertEqual(s2.name, 'foo') - self.assertEqual(s3.name, 'foo') - - def test_how_compat(self): - # in prior versions, we would allow how to be used in the resample - # now that its deprecated, we need to handle this in the actual - # aggregation functions - s = pd.Series( - np.random.randn(20), - index=pd.date_range('1/1/2000', periods=20, freq='12H')) - - for how in ['min', 'max', 'median']: - for op in ['mean', 'sum', 'std', 'var', 'kurt', 'skew']: - for t in ['rolling', 'expanding']: + assert s2.name == 'foo' + assert s3.name == 'foo' + + @pytest.mark.parametrize("func,window_size,expected_vals", [ + ('rolling', 2, [[np.nan, np.nan, np.nan, np.nan], + [15., 20., 25., 20.], + [25., 30., 35., 30.], + [np.nan, np.nan, np.nan, np.nan], + [20., 30., 35., 30.], + [35., 40., 60., 40.], + [60., 80., 85., 80]]), + ('expanding', None, [[10., 10., 20., 20.], + [15., 20., 25., 20.], + [20., 30., 30., 20.], + [10., 10., 30., 30.], + [20., 30., 35., 30.], + [26.666667, 40., 50., 30.], + [40., 80., 60., 30.]])]) + def test_multiple_agg_funcs(self, func, window_size, expected_vals): + # GH 15072 + df = pd.DataFrame([ + ['A', 10, 20], + ['A', 20, 30], + ['A', 30, 40], + ['B', 10, 30], + ['B', 30, 40], + ['B', 40, 80], + ['B', 80, 90]], columns=['stock', 'low', 'high']) + + f = getattr(df.groupby('stock'), func) + if window_size: + window = f(window_size) + else: + window = f() - with catch_warnings(record=True): + index = pd.MultiIndex.from_tuples([ + ('A', 0), ('A', 1), ('A', 2), + ('B', 3), ('B', 4), ('B', 5), ('B', 6)], names=['stock', None]) + columns = pd.MultiIndex.from_tuples([ + ('low', 'mean'), ('low', 'max'), ('high', 'mean'), + ('high', 'min')]) + expected = pd.DataFrame(expected_vals, index=index, columns=columns) - dfunc = getattr(pd, "{0}_{1}".format(t, op)) - if dfunc is None: - continue + result = window.agg(OrderedDict(( + ('low', ['mean', 'max']), + ('high', ['mean', 'min']), + ))) - if t == 'rolling': - kwargs = {'window': 5} - else: - kwargs = {} - result = dfunc(s, freq='D', how=how, **kwargs) - - expected = getattr( - getattr(s, t)(freq='D', **kwargs), op)(how=how) - tm.assert_series_equal(result, expected) + tm.assert_frame_equal(result, expected) +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") class TestWindow(Base): - def setUp(self): + def setup_method(self, method): self._create_data() - def test_constructor(self): + @td.skip_if_no_scipy + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor(self, which): # GH 12669 - tm._skip_if_no_scipy() - for o in [self.series, self.frame]: - c = o.rolling + o = getattr(self, which) + c = o.rolling - # valid - c(win_type='boxcar', window=2, min_periods=1) - c(win_type='boxcar', window=2, min_periods=1, center=True) - c(win_type='boxcar', window=2, min_periods=1, center=False) + # valid + c(win_type='boxcar', window=2, min_periods=1) + c(win_type='boxcar', window=2, min_periods=1, center=True) + c(win_type='boxcar', window=2, min_periods=1, center=False) - for wt in ['boxcar', 'triang', 'blackman', 'hamming', 'bartlett', - 'bohman', 'blackmanharris', 'nuttall', 'barthann']: - c(win_type=wt, window=2) + # not valid + for w in [2., 'foo', np.array([2])]: + with pytest.raises(ValueError): + c(win_type='boxcar', window=2, min_periods=w) + with pytest.raises(ValueError): + c(win_type='boxcar', window=2, min_periods=1, center=w) - # not valid - for w in [2., 'foo', np.array([2])]: - with self.assertRaises(ValueError): - c(win_type='boxcar', window=2, min_periods=w) - with self.assertRaises(ValueError): - c(win_type='boxcar', window=2, min_periods=1, center=w) + for wt in ['foobar', 1]: + with pytest.raises(ValueError): + c(win_type=wt, window=2) - for wt in ['foobar', 1]: - with self.assertRaises(ValueError): - c(win_type=wt, window=2) + @td.skip_if_no_scipy + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor_with_win_type(self, which, win_types): + # GH 12669 + o = getattr(self, which) + c = o.rolling + c(win_type=win_types, window=2) - def test_numpy_compat(self): + @pytest.mark.parametrize( + 'method', ['sum', 'mean']) + def test_numpy_compat(self, method): # see gh-12811 w = rwindow.Window(Series([2, 4, 6]), window=[0, 2]) msg = "numpy operations are not valid with window objects" - for func in ('sum', 'mean'): - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(w, func), 1, 2, 3) - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(w, func), dtype=np.float64) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(w, method)(1, 2, 3) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(w, method)(dtype=np.float64) class TestRolling(Base): - def setUp(self): + def setup_method(self, method): self._create_data() def test_doc_string(self): @@ -366,75 +430,248 @@ def test_doc_string(self): df.rolling(2).sum() df.rolling(2, min_periods=1).sum() - def test_constructor(self): + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor(self, which): # GH 12669 - for o in [self.series, self.frame]: - c = o.rolling + o = getattr(self, which) + c = o.rolling - # valid - c(window=2) - c(window=2, min_periods=1) - c(window=2, min_periods=1, center=True) - c(window=2, min_periods=1, center=False) + # valid + c(window=2) + c(window=2, min_periods=1) + c(window=2, min_periods=1, center=True) + c(window=2, min_periods=1, center=False) - # GH 13383 + # GH 13383 + with pytest.raises(ValueError): c(0) - with self.assertRaises(ValueError): - c(-1) - - # not valid - for w in [2., 'foo', np.array([2])]: - with self.assertRaises(ValueError): - c(window=w) - with self.assertRaises(ValueError): - c(window=2, min_periods=w) - with self.assertRaises(ValueError): - c(window=2, min_periods=1, center=w) - - def test_constructor_with_win_type(self): + c(-1) + + # not valid + for w in [2., 'foo', np.array([2])]: + with pytest.raises(ValueError): + c(window=w) + with pytest.raises(ValueError): + c(window=2, min_periods=w) + with pytest.raises(ValueError): + c(window=2, min_periods=1, center=w) + + @td.skip_if_no_scipy + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor_with_win_type(self, which): # GH 13383 - tm._skip_if_no_scipy() - for o in [self.series, self.frame]: - c = o.rolling - c(0, win_type='boxcar') - with self.assertRaises(ValueError): - c(-1, win_type='boxcar') - - def test_constructor_with_timedelta_window(self): + o = getattr(self, which) + c = o.rolling + with pytest.raises(ValueError): + c(-1, win_type='boxcar') + + @pytest.mark.parametrize( + 'window', [timedelta(days=3), pd.Timedelta(days=3)]) + def test_constructor_with_timedelta_window(self, window): # GH 15440 n = 10 - df = pd.DataFrame({'value': np.arange(n)}, - index=pd.date_range('2015-12-24', - periods=n, - freq="D")) + df = DataFrame({'value': np.arange(n)}, + index=pd.date_range('2015-12-24', periods=n, freq="D")) expected_data = np.append([0., 1.], np.arange(3., 27., 3)) - for window in [timedelta(days=3), pd.Timedelta(days=3)]: - result = df.rolling(window=window).sum() - expected = pd.DataFrame({'value': expected_data}, - index=pd.date_range('2015-12-24', - periods=n, - freq="D")) - tm.assert_frame_equal(result, expected) - expected = df.rolling('3D').sum() - tm.assert_frame_equal(result, expected) - def test_numpy_compat(self): + result = df.rolling(window=window).sum() + expected = DataFrame({'value': expected_data}, + index=pd.date_range('2015-12-24', periods=n, + freq="D")) + tm.assert_frame_equal(result, expected) + expected = df.rolling('3D').sum() + tm.assert_frame_equal(result, expected) + + @pytest.mark.parametrize( + 'window', [timedelta(days=3), pd.Timedelta(days=3), '3D']) + def test_constructor_timedelta_window_and_minperiods(self, window, raw): + # GH 15305 + n = 10 + df = DataFrame({'value': np.arange(n)}, + index=pd.date_range('2017-08-08', periods=n, freq="D")) + expected = DataFrame( + {'value': np.append([np.NaN, 1.], np.arange(3., 27., 3))}, + index=pd.date_range('2017-08-08', periods=n, freq="D")) + result_roll_sum = df.rolling(window=window, min_periods=2).sum() + result_roll_generic = df.rolling(window=window, + min_periods=2).apply(sum, raw=raw) + tm.assert_frame_equal(result_roll_sum, expected) + tm.assert_frame_equal(result_roll_generic, expected) + + @pytest.mark.parametrize( + 'method', ['std', 'mean', 'sum', 'max', 'min', 'var']) + def test_numpy_compat(self, method): # see gh-12811 r = rwindow.Rolling(Series([2, 4, 6]), window=2) msg = "numpy operations are not valid with window objects" - for func in ('std', 'mean', 'sum', 'max', 'min', 'var'): - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(r, func), 1, 2, 3) - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(r, func), dtype=np.float64) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(r, method)(1, 2, 3) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(r, method)(dtype=np.float64) + + def test_closed(self): + df = DataFrame({'A': [0, 1, 2, 3, 4]}) + # closed only allowed for datetimelike + with pytest.raises(ValueError): + df.rolling(window=3, closed='neither') + + @pytest.mark.parametrize("func", ['min', 'max']) + def test_closed_one_entry(self, func): + # GH24718 + ser = pd.Series(data=[2], index=pd.date_range('2000', periods=1)) + result = getattr(ser.rolling('10D', closed='left'), func)() + tm.assert_series_equal(result, pd.Series([np.nan], index=ser.index)) + + @pytest.mark.parametrize("func", ['min', 'max']) + def test_closed_one_entry_groupby(self, func): + # GH24718 + ser = pd.DataFrame(data={'A': [1, 1, 2], 'B': [3, 2, 1]}, + index=pd.date_range('2000', periods=3)) + result = getattr( + ser.groupby('A', sort=False)['B'].rolling('10D', closed='left'), + func)() + exp_idx = pd.MultiIndex.from_arrays(arrays=[[1, 1, 2], ser.index], + names=('A', None)) + expected = pd.Series(data=[np.nan, 3, np.nan], index=exp_idx, name='B') + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("input_dtype", ['int', 'float']) + @pytest.mark.parametrize("func,closed,expected", [ + ('min', 'right', [0.0, 0, 0, 1, 2, 3, 4, 5, 6, 7]), + ('min', 'both', [0.0, 0, 0, 0, 1, 2, 3, 4, 5, 6]), + ('min', 'neither', [np.nan, 0, 0, 1, 2, 3, 4, 5, 6, 7]), + ('min', 'left', [np.nan, 0, 0, 0, 1, 2, 3, 4, 5, 6]), + ('max', 'right', [0.0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + ('max', 'both', [0.0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), + ('max', 'neither', [np.nan, 0, 1, 2, 3, 4, 5, 6, 7, 8]), + ('max', 'left', [np.nan, 0, 1, 2, 3, 4, 5, 6, 7, 8]) + ]) + def test_closed_min_max_datetime(self, input_dtype, + func, closed, + expected): + # see gh-21704 + ser = pd.Series(data=np.arange(10).astype(input_dtype), + index=pd.date_range('2000', periods=10)) + + result = getattr(ser.rolling('3D', closed=closed), func)() + expected = pd.Series(expected, index=ser.index) + tm.assert_series_equal(result, expected) + + def test_closed_uneven(self): + # see gh-21704 + ser = pd.Series(data=np.arange(10), + index=pd.date_range('2000', periods=10)) + + # uneven + ser = ser.drop(index=ser.index[[1, 5]]) + result = ser.rolling('3D', closed='left').min() + expected = pd.Series([np.nan, 0, 0, 2, 3, 4, 6, 6], + index=ser.index) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize("func,closed,expected", [ + ('min', 'right', [np.nan, 0, 0, 1, 2, 3, 4, 5, np.nan, np.nan]), + ('min', 'both', [np.nan, 0, 0, 0, 1, 2, 3, 4, 5, np.nan]), + ('min', 'neither', [np.nan, np.nan, 0, 1, 2, 3, 4, 5, np.nan, np.nan]), + ('min', 'left', [np.nan, np.nan, 0, 0, 1, 2, 3, 4, 5, np.nan]), + ('max', 'right', [np.nan, 1, 2, 3, 4, 5, 6, 6, np.nan, np.nan]), + ('max', 'both', [np.nan, 1, 2, 3, 4, 5, 6, 6, 6, np.nan]), + ('max', 'neither', [np.nan, np.nan, 1, 2, 3, 4, 5, 6, np.nan, np.nan]), + ('max', 'left', [np.nan, np.nan, 1, 2, 3, 4, 5, 6, 6, np.nan]) + ]) + def test_closed_min_max_minp(self, func, closed, expected): + # see gh-21704 + ser = pd.Series(data=np.arange(10), + index=pd.date_range('2000', periods=10)) + ser[ser.index[-3:]] = np.nan + result = getattr(ser.rolling('3D', min_periods=2, closed=closed), + func)() + expected = pd.Series(expected, index=ser.index) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('roller', ['1s', 1]) + def tests_empty_df_rolling(self, roller): + # GH 15819 Verifies that datetime and integer rolling windows can be + # applied to empty DataFrames + expected = DataFrame() + result = DataFrame().rolling(roller).sum() + tm.assert_frame_equal(result, expected) + + # Verifies that datetime and integer rolling windows can be applied to + # empty DataFrames with datetime index + expected = DataFrame(index=pd.DatetimeIndex([])) + result = DataFrame(index=pd.DatetimeIndex([])).rolling(roller).sum() + tm.assert_frame_equal(result, expected) + + def test_missing_minp_zero(self): + # https://github.com/pandas-dev/pandas/pull/18921 + # minp=0 + x = pd.Series([np.nan]) + result = x.rolling(1, min_periods=0).sum() + expected = pd.Series([0.0]) + tm.assert_series_equal(result, expected) + + # minp=1 + result = x.rolling(1, min_periods=1).sum() + expected = pd.Series([np.nan]) + tm.assert_series_equal(result, expected) + + def test_missing_minp_zero_variable(self): + # https://github.com/pandas-dev/pandas/pull/18921 + x = pd.Series([np.nan] * 4, + index=pd.DatetimeIndex(['2017-01-01', '2017-01-04', + '2017-01-06', '2017-01-07'])) + result = x.rolling(pd.Timedelta("2d"), min_periods=0).sum() + expected = pd.Series(0.0, index=x.index) + tm.assert_series_equal(result, expected) + + def test_multi_index_names(self): + + # GH 16789, 16825 + cols = pd.MultiIndex.from_product([['A', 'B'], ['C', 'D', 'E']], + names=['1', '2']) + df = DataFrame(np.ones((10, 6)), columns=cols) + result = df.rolling(3).cov() + + tm.assert_index_equal(result.columns, df.columns) + assert result.index.names == [None, '1', '2'] + + @pytest.mark.parametrize('klass', [pd.Series, pd.DataFrame]) + def test_iter_raises(self, klass): + # https://github.com/pandas-dev/pandas/issues/11704 + # Iteration over a Window + obj = klass([1, 2, 3, 4]) + with pytest.raises(NotImplementedError): + iter(obj.rolling(2)) + + def test_rolling_axis(self, axis_frame): + # see gh-23372. + df = DataFrame(np.ones((10, 20))) + axis = df._get_axis_number(axis_frame) + + if axis == 0: + expected = DataFrame({ + i: [np.nan] * 2 + [3.0] * 8 + for i in range(20) + }) + else: + # axis == 1 + expected = DataFrame([ + [np.nan] * 2 + [3.0] * 18 + ] * 10) + + result = df.rolling(3, axis=axis_frame).sum() + tm.assert_frame_equal(result, expected) class TestExpanding(Base): - def setUp(self): + def setup_method(self, method): self._create_data() def test_doc_string(self): @@ -443,40 +680,103 @@ def test_doc_string(self): df df.expanding(2).sum() - def test_constructor(self): + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor(self, which): # GH 12669 - for o in [self.series, self.frame]: - c = o.expanding + o = getattr(self, which) + c = o.expanding - # valid - c(min_periods=1) - c(min_periods=1, center=True) - c(min_periods=1, center=False) + # valid + c(min_periods=1) + c(min_periods=1, center=True) + c(min_periods=1, center=False) - # not valid - for w in [2., 'foo', np.array([2])]: - with self.assertRaises(ValueError): - c(min_periods=w) - with self.assertRaises(ValueError): - c(min_periods=1, center=w) + # not valid + for w in [2., 'foo', np.array([2])]: + with pytest.raises(ValueError): + c(min_periods=w) + with pytest.raises(ValueError): + c(min_periods=1, center=w) - def test_numpy_compat(self): + @pytest.mark.parametrize( + 'method', ['std', 'mean', 'sum', 'max', 'min', 'var']) + def test_numpy_compat(self, method): # see gh-12811 e = rwindow.Expanding(Series([2, 4, 6]), window=2) msg = "numpy operations are not valid with window objects" - for func in ('std', 'mean', 'sum', 'max', 'min', 'var'): - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(e, func), 1, 2, 3) - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(e, func), dtype=np.float64) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(e, method)(1, 2, 3) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(e, method)(dtype=np.float64) + + @pytest.mark.parametrize( + 'expander', + [1, pytest.param('ls', marks=pytest.mark.xfail( + reason='GH#16425 expanding with ' + 'offset not supported'))]) + def test_empty_df_expanding(self, expander): + # GH 15819 Verifies that datetime and integer expanding windows can be + # applied to empty DataFrames + + expected = DataFrame() + result = DataFrame().expanding(expander).sum() + tm.assert_frame_equal(result, expected) + + # Verifies that datetime and integer expanding windows can be applied + # to empty DataFrames with datetime index + expected = DataFrame(index=pd.DatetimeIndex([])) + result = DataFrame( + index=pd.DatetimeIndex([])).expanding(expander).sum() + tm.assert_frame_equal(result, expected) + + def test_missing_minp_zero(self): + # https://github.com/pandas-dev/pandas/pull/18921 + # minp=0 + x = pd.Series([np.nan]) + result = x.expanding(min_periods=0).sum() + expected = pd.Series([0.0]) + tm.assert_series_equal(result, expected) + + # minp=1 + result = x.expanding(min_periods=1).sum() + expected = pd.Series([np.nan]) + tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize('klass', [pd.Series, pd.DataFrame]) + def test_iter_raises(self, klass): + # https://github.com/pandas-dev/pandas/issues/11704 + # Iteration over a Window + obj = klass([1, 2, 3, 4]) + with pytest.raises(NotImplementedError): + iter(obj.expanding(2)) + + def test_expanding_axis(self, axis_frame): + # see gh-23372. + df = DataFrame(np.ones((10, 20))) + axis = df._get_axis_number(axis_frame) + + if axis == 0: + expected = DataFrame({ + i: [np.nan] * 2 + [float(j) for j in range(3, 11)] + for i in range(20) + }) + else: + # axis == 1 + expected = DataFrame([ + [np.nan] * 2 + [float(i) for i in range(3, 21)] + ] * 10) + + result = df.expanding(3, axis=axis_frame).sum() + tm.assert_frame_equal(result, expected) class TestEWM(Base): - def setUp(self): + def setup_method(self, method): self._create_data() def test_doc_string(self): @@ -485,75 +785,65 @@ def test_doc_string(self): df df.ewm(com=0.5).mean() - def test_constructor(self): - for o in [self.series, self.frame]: - c = o.ewm - - # valid - c(com=0.5) - c(span=1.5) - c(alpha=0.5) - c(halflife=0.75) - c(com=0.5, span=None) - c(alpha=0.5, com=None) - c(halflife=0.75, alpha=None) - - # not valid: mutually exclusive - with self.assertRaises(ValueError): - c(com=0.5, alpha=0.5) - with self.assertRaises(ValueError): - c(span=1.5, halflife=0.75) - with self.assertRaises(ValueError): - c(alpha=0.5, span=1.5) - - # not valid: com < 0 - with self.assertRaises(ValueError): - c(com=-0.5) - - # not valid: span < 1 - with self.assertRaises(ValueError): - c(span=0.5) - - # not valid: halflife <= 0 - with self.assertRaises(ValueError): - c(halflife=0) - - # not valid: alpha <= 0 or alpha > 1 - for alpha in (-0.5, 1.5): - with self.assertRaises(ValueError): - c(alpha=alpha) - - def test_numpy_compat(self): + @pytest.mark.parametrize( + 'which', ['series', 'frame']) + def test_constructor(self, which): + o = getattr(self, which) + c = o.ewm + + # valid + c(com=0.5) + c(span=1.5) + c(alpha=0.5) + c(halflife=0.75) + c(com=0.5, span=None) + c(alpha=0.5, com=None) + c(halflife=0.75, alpha=None) + + # not valid: mutually exclusive + with pytest.raises(ValueError): + c(com=0.5, alpha=0.5) + with pytest.raises(ValueError): + c(span=1.5, halflife=0.75) + with pytest.raises(ValueError): + c(alpha=0.5, span=1.5) + + # not valid: com < 0 + with pytest.raises(ValueError): + c(com=-0.5) + + # not valid: span < 1 + with pytest.raises(ValueError): + c(span=0.5) + + # not valid: halflife <= 0 + with pytest.raises(ValueError): + c(halflife=0) + + # not valid: alpha <= 0 or alpha > 1 + for alpha in (-0.5, 1.5): + with pytest.raises(ValueError): + c(alpha=alpha) + + @pytest.mark.parametrize( + 'method', ['std', 'mean', 'var']) + def test_numpy_compat(self, method): # see gh-12811 e = rwindow.EWM(Series([2, 4, 6]), alpha=0.5) msg = "numpy operations are not valid with window objects" - for func in ('std', 'mean', 'var'): - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(e, func), 1, 2, 3) - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(e, func), dtype=np.float64) - - -class TestDeprecations(Base): - """ test that we are catching deprecation warnings """ - - def setUp(self): - self._create_data() - - def test_deprecations(self): - - with catch_warnings(record=True): - mom.rolling_mean(np.ones(10), 3, center=True, axis=0) - mom.rolling_mean(Series(np.ones(10)), 3, center=True, axis=0) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(e, method)(1, 2, 3) + with pytest.raises(UnsupportedFunctionCall, match=msg): + getattr(e, method)(dtype=np.float64) -# GH #12373 : rolling functions error on float32 data +# gh-12373 : rolling functions error on float32 data # make sure rolling functions works for different dtypes # -# NOTE that these are yielded tests and so _create_data is -# explicity called, nor do these inherit from unittest.TestCase +# NOTE that these are yielded tests and so _create_data +# is explicitly called. # # further note that we are only checking rolling for fully dtype # compliance (though both expanding and ewm inherit) @@ -623,8 +913,8 @@ def get_expects(self): return expects def _create_dtype_data(self, dtype): - sr1 = Series(range(5), dtype=dtype) - sr2 = Series(range(10, 0, -2), dtype=dtype) + sr1 = Series(np.arange(5), dtype=dtype) + sr2 = Series(np.arange(10, 0, -2), dtype=dtype) df = DataFrame(np.arange(10).reshape((5, 2)), dtype=dtype) data = { @@ -763,9 +1053,10 @@ def _create_data(self): "datetime64[ns, UTC] is not supported ATM") +@pytest.mark.filterwarnings("ignore:can't resolve package:ImportWarning") class TestMoments(Base): - def setUp(self): + def setup_method(self, method): self._create_data() def test_centered_axis_validation(self): @@ -774,7 +1065,7 @@ def test_centered_axis_validation(self): Series(np.ones(10)).rolling(window=3, center=True, axis=0).mean() # bad axis - with self.assertRaises(ValueError): + with pytest.raises(ValueError): Series(np.ones(10)).rolling(window=3, center=True, axis=1).mean() # ok ok @@ -784,84 +1075,64 @@ def test_centered_axis_validation(self): axis=1).mean() # bad axis - with self.assertRaises(ValueError): + with pytest.raises(ValueError): (DataFrame(np.ones((10, 10))) .rolling(window=3, center=True, axis=2).mean()) def test_rolling_sum(self): - self._check_moment_func(mom.rolling_sum, np.sum, name='sum') + self._check_moment_func(np.nansum, name='sum', + zero_min_periods_equal=False) def test_rolling_count(self): counter = lambda x: np.isfinite(x).astype(float).sum() - self._check_moment_func(mom.rolling_count, counter, name='count', - has_min_periods=False, preserve_nan=False, + self._check_moment_func(counter, name='count', has_min_periods=False, fill_value=0) def test_rolling_mean(self): - self._check_moment_func(mom.rolling_mean, np.mean, name='mean') + self._check_moment_func(np.mean, name='mean') + @td.skip_if_no_scipy def test_cmov_mean(self): # GH 8238 - tm._skip_if_no_scipy() - vals = np.array([6.95, 15.21, 4.72, 9.12, 13.81, 13.49, 16.68, 9.48, 10.63, 14.48]) - xp = np.array([np.nan, np.nan, 9.962, 11.27, 11.564, 12.516, 12.818, - 12.952, np.nan, np.nan]) - - with catch_warnings(record=True): - rs = mom.rolling_mean(vals, 5, center=True) - tm.assert_almost_equal(xp, rs) - - xp = Series(rs) - rs = Series(vals).rolling(5, center=True).mean() - tm.assert_series_equal(xp, rs) + result = Series(vals).rolling(5, center=True).mean() + expected = Series([np.nan, np.nan, 9.962, 11.27, 11.564, 12.516, + 12.818, 12.952, np.nan, np.nan]) + tm.assert_series_equal(expected, result) + @td.skip_if_no_scipy def test_cmov_window(self): # GH 8238 - tm._skip_if_no_scipy() - vals = np.array([6.95, 15.21, 4.72, 9.12, 13.81, 13.49, 16.68, 9.48, 10.63, 14.48]) - xp = np.array([np.nan, np.nan, 9.962, 11.27, 11.564, 12.516, 12.818, - 12.952, np.nan, np.nan]) - - with catch_warnings(record=True): - rs = mom.rolling_window(vals, 5, 'boxcar', center=True) - tm.assert_almost_equal(xp, rs) - - xp = Series(rs) - rs = Series(vals).rolling(5, win_type='boxcar', center=True).mean() - tm.assert_series_equal(xp, rs) + result = Series(vals).rolling(5, win_type='boxcar', center=True).mean() + expected = Series([np.nan, np.nan, 9.962, 11.27, 11.564, 12.516, + 12.818, 12.952, np.nan, np.nan]) + tm.assert_series_equal(expected, result) + @td.skip_if_no_scipy def test_cmov_window_corner(self): # GH 8238 - tm._skip_if_no_scipy() - # all nan - vals = np.empty(10, dtype=float) - vals.fill(np.nan) - with catch_warnings(record=True): - rs = mom.rolling_window(vals, 5, 'boxcar', center=True) - self.assertTrue(np.isnan(rs).all()) + vals = pd.Series([np.nan] * 10) + result = vals.rolling(5, center=True, win_type='boxcar').mean() + assert np.isnan(result).all() # empty - vals = np.array([]) - with catch_warnings(record=True): - rs = mom.rolling_window(vals, 5, 'boxcar', center=True) - self.assertEqual(len(rs), 0) + vals = pd.Series([]) + result = vals.rolling(5, center=True, win_type='boxcar').mean() + assert len(result) == 0 # shorter than window - vals = np.random.randn(5) - with catch_warnings(record=True): - rs = mom.rolling_window(vals, 10, 'boxcar') - self.assertTrue(np.isnan(rs).all()) - self.assertEqual(len(rs), 5) + vals = pd.Series(np.random.randn(5)) + result = vals.rolling(10, win_type='boxcar').mean() + assert np.isnan(result).all() + assert len(result) == 5 + @td.skip_if_no_scipy def test_cmov_window_frame(self): # Gh 8238 - tm._skip_if_no_scipy() - vals = np.array([[12.18, 3.64], [10.18, 9.16], [13.24, 14.61], [4.51, 8.11], [6.15, 11.44], [9.14, 6.21], [11.31, 10.67], [2.94, 6.51], [9.42, 8.39], [12.44, @@ -877,7 +1148,7 @@ def test_cmov_window_frame(self): tm.assert_frame_equal(DataFrame(xp), rs) # invalid method - with self.assertRaises(AttributeError): + with pytest.raises(AttributeError): (DataFrame(vals).rolling(5, win_type='boxcar', center=True) .std()) @@ -890,9 +1161,8 @@ def test_cmov_window_frame(self): rs = DataFrame(vals).rolling(5, win_type='boxcar', center=True).sum() tm.assert_frame_equal(DataFrame(xp), rs) + @td.skip_if_no_scipy def test_cmov_window_na_min_periods(self): - tm._skip_if_no_scipy() - # min_periods vals = Series(np.random.randn(10)) vals[4] = np.nan @@ -903,13 +1173,9 @@ def test_cmov_window_na_min_periods(self): center=True).mean() tm.assert_series_equal(xp, rs) - def test_cmov_window_regular(self): + @td.skip_if_no_scipy + def test_cmov_window_regular(self, win_types): # GH 8238 - tm._skip_if_no_scipy() - - win_types = ['triang', 'blackman', 'hamming', 'bartlett', 'bohman', - 'blackmanharris', 'nuttall', 'barthann'] - vals = np.array([6.95, 15.21, 4.72, 9.12, 13.81, 13.49, 16.68, 9.48, 10.63, 14.48]) xps = { @@ -931,35 +1197,25 @@ def test_cmov_window_regular(self): 14.0825, 11.5675, np.nan, np.nan] } - for wt in win_types: - xp = Series(xps[wt]) - rs = Series(vals).rolling(5, win_type=wt, center=True).mean() - tm.assert_series_equal(xp, rs) + xp = Series(xps[win_types]) + rs = Series(vals).rolling(5, win_type=win_types, center=True).mean() + tm.assert_series_equal(xp, rs) - def test_cmov_window_regular_linear_range(self): + @td.skip_if_no_scipy + def test_cmov_window_regular_linear_range(self, win_types): # GH 8238 - tm._skip_if_no_scipy() - - win_types = ['triang', 'blackman', 'hamming', 'bartlett', 'bohman', - 'blackmanharris', 'nuttall', 'barthann'] - vals = np.array(range(10), dtype=np.float) xp = vals.copy() xp[:2] = np.nan xp[-2:] = np.nan xp = Series(xp) - for wt in win_types: - rs = Series(vals).rolling(5, win_type=wt, center=True).mean() - tm.assert_series_equal(xp, rs) + rs = Series(vals).rolling(5, win_type=win_types, center=True).mean() + tm.assert_series_equal(xp, rs) - def test_cmov_window_regular_missing_data(self): + @td.skip_if_no_scipy + def test_cmov_window_regular_missing_data(self, win_types): # GH 8238 - tm._skip_if_no_scipy() - - win_types = ['triang', 'blackman', 'hamming', 'bartlett', 'bohman', - 'blackmanharris', 'nuttall', 'barthann'] - vals = np.array([6.95, 15.21, 4.72, 9.12, 13.81, 13.49, 16.68, np.nan, 10.63, 14.48]) xps = { @@ -981,18 +1237,17 @@ def test_cmov_window_regular_missing_data(self): 9.16438, 13.05052, 14.02175, 16.1098, 13.65509] } - for wt in win_types: - xp = Series(xps[wt]) - rs = Series(vals).rolling(5, win_type=wt, min_periods=3).mean() - tm.assert_series_equal(xp, rs) + xp = Series(xps[win_types]) + rs = Series(vals).rolling(5, win_type=win_types, min_periods=3).mean() + tm.assert_series_equal(xp, rs) - def test_cmov_window_special(self): + @td.skip_if_no_scipy + def test_cmov_window_special(self, win_types_special): # GH 8238 - tm._skip_if_no_scipy() - - win_types = ['kaiser', 'gaussian', 'general_gaussian', 'slepian'] - kwds = [{'beta': 1.}, {'std': 1.}, {'power': 2., - 'width': 2.}, {'width': 0.5}] + kwds = { + 'kaiser': {'beta': 1.}, + 'gaussian': {'std': 1.}, + 'general_gaussian': {'power': 2., 'width': 2.}} vals = np.array([6.95, 15.21, 4.72, 9.12, 13.81, 13.49, 16.68, 9.48, 10.63, 14.48]) @@ -1002,24 +1257,24 @@ def test_cmov_window_special(self): 13.65671, 12.01002, np.nan, np.nan], 'general_gaussian': [np.nan, np.nan, 9.85011, 10.71589, 11.73161, 13.08516, 12.95111, 12.74577, np.nan, np.nan], - 'slepian': [np.nan, np.nan, 9.81073, 10.89359, 11.70284, 12.88331, - 12.96079, 12.77008, np.nan, np.nan], 'kaiser': [np.nan, np.nan, 9.86851, 11.02969, 11.65161, 12.75129, 12.90702, 12.83757, np.nan, np.nan] } - for wt, k in zip(win_types, kwds): - xp = Series(xps[wt]) - rs = Series(vals).rolling(5, win_type=wt, center=True).mean(**k) - tm.assert_series_equal(xp, rs) + xp = Series(xps[win_types_special]) + rs = Series(vals).rolling( + 5, win_type=win_types_special, center=True).mean( + **kwds[win_types_special]) + tm.assert_series_equal(xp, rs) - def test_cmov_window_special_linear_range(self): + @td.skip_if_no_scipy + def test_cmov_window_special_linear_range(self, win_types_special): # GH 8238 - tm._skip_if_no_scipy() - - win_types = ['kaiser', 'gaussian', 'general_gaussian', 'slepian'] - kwds = [{'beta': 1.}, {'std': 1.}, {'power': 2., - 'width': 2.}, {'width': 0.5}] + kwds = { + 'kaiser': {'beta': 1.}, + 'gaussian': {'std': 1.}, + 'general_gaussian': {'power': 2., 'width': 2.}, + 'slepian': {'width': 0.5}} vals = np.array(range(10), dtype=np.float) xp = vals.copy() @@ -1027,367 +1282,278 @@ def test_cmov_window_special_linear_range(self): xp[-2:] = np.nan xp = Series(xp) - for wt, k in zip(win_types, kwds): - rs = Series(vals).rolling(5, win_type=wt, center=True).mean(**k) - tm.assert_series_equal(xp, rs) + rs = Series(vals).rolling( + 5, win_type=win_types_special, center=True).mean( + **kwds[win_types_special]) + tm.assert_series_equal(xp, rs) def test_rolling_median(self): - with catch_warnings(record=True): - self._check_moment_func(mom.rolling_median, np.median, - name='median') + self._check_moment_func(np.median, name='median') def test_rolling_min(self): + self._check_moment_func(np.min, name='min') - with catch_warnings(record=True): - self._check_moment_func(mom.rolling_min, np.min, name='min') - - with catch_warnings(record=True): - a = np.array([1, 2, 3, 4, 5]) - b = mom.rolling_min(a, window=100, min_periods=1) - tm.assert_almost_equal(b, np.ones(len(a))) + a = pd.Series([1, 2, 3, 4, 5]) + result = a.rolling(window=100, min_periods=1).min() + expected = pd.Series(np.ones(len(a))) + tm.assert_series_equal(result, expected) - self.assertRaises(ValueError, mom.rolling_min, np.array([1, 2, 3]), - window=3, min_periods=5) + with pytest.raises(ValueError): + pd.Series([1, 2, 3]).rolling(window=3, min_periods=5).min() def test_rolling_max(self): + self._check_moment_func(np.max, name='max') - with catch_warnings(record=True): - self._check_moment_func(mom.rolling_max, np.max, name='max') - - with catch_warnings(record=True): - a = np.array([1, 2, 3, 4, 5], dtype=np.float64) - b = mom.rolling_max(a, window=100, min_periods=1) - tm.assert_almost_equal(a, b) + a = pd.Series([1, 2, 3, 4, 5], dtype=np.float64) + b = a.rolling(window=100, min_periods=1).max() + tm.assert_almost_equal(a, b) - self.assertRaises(ValueError, mom.rolling_max, np.array([1, 2, 3]), - window=3, min_periods=5) + with pytest.raises(ValueError): + pd.Series([1, 2, 3]).rolling(window=3, min_periods=5).max() - def test_rolling_quantile(self): - qs = [0.0, .1, .5, .9, 1.0] + @pytest.mark.parametrize('q', [0.0, .1, .5, .9, 1.0]) + def test_rolling_quantile(self, q): def scoreatpercentile(a, per): values = np.sort(a, axis=0) - idx = per / 1. * (values.shape[0] - 1) - return values[int(idx)] + idx = int(per / 1. * (values.shape[0] - 1)) - for q in qs: + if idx == values.shape[0] - 1: + retval = values[-1] - def f(x, window, quantile, min_periods=None, freq=None, - center=False): - return mom.rolling_quantile(x, window, quantile, - min_periods=min_periods, freq=freq, - center=center) + else: + qlow = float(idx) / float(values.shape[0] - 1) + qhig = float(idx + 1) / float(values.shape[0] - 1) + vlow = values[idx] + vhig = values[idx + 1] + retval = vlow + (vhig - vlow) * (per - qlow) / (qhig - qlow) + + return retval + + def quantile_func(x): + return scoreatpercentile(x, q) + + self._check_moment_func(quantile_func, name='quantile', + quantile=q) + + def test_rolling_quantile_np_percentile(self): + # #9413: Tests that rolling window's quantile default behavior + # is analogus to Numpy's percentile + row = 10 + col = 5 + idx = pd.date_range('20100101', periods=row, freq='B') + df = DataFrame(np.random.rand(row * col).reshape((row, -1)), index=idx) + + df_quantile = df.quantile([0.25, 0.5, 0.75], axis=0) + np_percentile = np.percentile(df, [25, 50, 75], axis=0) + + tm.assert_almost_equal(df_quantile.values, np.array(np_percentile)) + + @pytest.mark.parametrize('quantile', [0.0, 0.1, 0.45, 0.5, 1]) + @pytest.mark.parametrize('interpolation', ['linear', 'lower', 'higher', + 'nearest', 'midpoint']) + @pytest.mark.parametrize('data', [[1., 2., 3., 4., 5., 6., 7.], + [8., 1., 3., 4., 5., 2., 6., 7.], + [0., np.nan, 0.2, np.nan, 0.4], + [np.nan, np.nan, np.nan, np.nan], + [np.nan, 0.1, np.nan, 0.3, 0.4, 0.5], + [0.5], [np.nan, 0.7, 0.6]]) + def test_rolling_quantile_interpolation_options(self, quantile, + interpolation, data): + # Tests that rolling window's quantile behavior is analogous to + # Series' quantile for each interpolation option + s = Series(data) + + q1 = s.quantile(quantile, interpolation) + q2 = s.expanding(min_periods=1).quantile( + quantile, interpolation).iloc[-1] + + if np.isnan(q1): + assert np.isnan(q2) + else: + assert q1 == q2 - def alt(x): - return scoreatpercentile(x, q) + def test_invalid_quantile_value(self): + data = np.arange(5) + s = Series(data) - self._check_moment_func(f, alt, name='quantile', quantile=q) + with pytest.raises(ValueError, match="Interpolation 'invalid'" + " is not supported"): + s.rolling(len(data), min_periods=1).quantile( + 0.5, interpolation='invalid') def test_rolling_quantile_param(self): ser = Series([0.0, .1, .5, .9, 1.0]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ser.rolling(3).quantile(-0.1) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): ser.rolling(3).quantile(10.0) - with self.assertRaises(TypeError): + with pytest.raises(TypeError): ser.rolling(3).quantile('foo') - def test_rolling_apply(self): + def test_rolling_apply(self, raw): # suppress warnings about empty slices, as we are deliberately testing # with a 0-length Series + with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=".*(empty slice|0 for slice).*", category=RuntimeWarning) - ser = Series([]) - tm.assert_series_equal(ser, - ser.rolling(10).apply(lambda x: x.mean())) + def f(x): + return x[np.isfinite(x)].mean() - f = lambda x: x[np.isfinite(x)].mean() + self._check_moment_func(np.mean, name='apply', func=f, raw=raw) - def roll_mean(x, window, min_periods=None, freq=None, center=False, - **kwargs): - return mom.rolling_apply(x, window, func=f, - min_periods=min_periods, freq=freq, - center=center) - - self._check_moment_func(roll_mean, np.mean, name='apply', func=f) + expected = Series([]) + result = expected.rolling(10).apply(lambda x: x.mean(), raw=raw) + tm.assert_series_equal(result, expected) - # GH 8080 + # gh-8080 s = Series([None, None, None]) - result = s.rolling(2, min_periods=0).apply(lambda x: len(x)) + result = s.rolling(2, min_periods=0).apply(lambda x: len(x), raw=raw) expected = Series([1., 2., 2.]) tm.assert_series_equal(result, expected) - result = s.rolling(2, min_periods=0).apply(len) + result = s.rolling(2, min_periods=0).apply(len, raw=raw) tm.assert_series_equal(result, expected) - def test_rolling_apply_out_of_bounds(self): - # #1850 - arr = np.arange(4) + @pytest.mark.parametrize('klass', [Series, DataFrame]) + @pytest.mark.parametrize( + 'method', [lambda x: x.rolling(window=2), lambda x: x.expanding()]) + def test_apply_future_warning(self, klass, method): - # it works! - with catch_warnings(record=True): - result = mom.rolling_apply(arr, 10, np.sum) - self.assertTrue(isnull(result).all()) + # gh-5071 + s = klass(np.arange(3)) - with catch_warnings(record=True): - result = mom.rolling_apply(arr, 10, np.sum, min_periods=1) - tm.assert_almost_equal(result, result) + with tm.assert_produces_warning(FutureWarning): + method(s).apply(lambda x: len(x)) + + def test_rolling_apply_out_of_bounds(self, raw): + # gh-1850 + vals = pd.Series([1, 2, 3, 4]) + + result = vals.rolling(10).apply(np.sum, raw=raw) + assert result.isna().all() + + result = vals.rolling(10, min_periods=1).apply(np.sum, raw=raw) + expected = pd.Series([1, 3, 6, 10], dtype=float) + tm.assert_almost_equal(result, expected) + + @pytest.mark.parametrize('window', [2, '2s']) + def test_rolling_apply_with_pandas_objects(self, window): + # 5071 + df = pd.DataFrame({'A': np.random.randn(5), + 'B': np.random.randint(0, 10, size=5)}, + index=pd.date_range('20130101', periods=5, freq='s')) + + # we have an equal spaced timeseries index + # so simulate removing the first period + def f(x): + if x.index[0] == df.index[0]: + return np.nan + return x.iloc[-1] + + result = df.rolling(window).apply(f, raw=False) + expected = df.iloc[2:].reindex_like(df) + tm.assert_frame_equal(result, expected) + + with pytest.raises(AttributeError): + df.rolling(window).apply(f, raw=True) def test_rolling_std(self): - self._check_moment_func(mom.rolling_std, lambda x: np.std(x, ddof=1), + self._check_moment_func(lambda x: np.std(x, ddof=1), name='std') - self._check_moment_func(mom.rolling_std, lambda x: np.std(x, ddof=0), + self._check_moment_func(lambda x: np.std(x, ddof=0), name='std', ddof=0) def test_rolling_std_1obs(self): - with catch_warnings(record=True): - result = mom.rolling_std(np.array([1., 2., 3., 4., 5.]), - 1, min_periods=1) - expected = np.array([np.nan] * 5) - tm.assert_almost_equal(result, expected) + vals = pd.Series([1., 2., 3., 4., 5.]) - with catch_warnings(record=True): - result = mom.rolling_std(np.array([1., 2., 3., 4., 5.]), - 1, min_periods=1, ddof=0) - expected = np.zeros(5) - tm.assert_almost_equal(result, expected) + result = vals.rolling(1, min_periods=1).std() + expected = pd.Series([np.nan] * 5) + tm.assert_series_equal(result, expected) - with catch_warnings(record=True): - result = mom.rolling_std(np.array([np.nan, np.nan, 3., 4., 5.]), - 3, min_periods=2) - self.assertTrue(np.isnan(result[2])) + result = vals.rolling(1, min_periods=1).std(ddof=0) + expected = pd.Series([0.] * 5) + tm.assert_series_equal(result, expected) + + result = (pd.Series([np.nan, np.nan, 3, 4, 5]) + .rolling(3, min_periods=2).std()) + assert np.isnan(result[2]) def test_rolling_std_neg_sqrt(self): # unit test from Bottleneck # Test move_nanstd for neg sqrt. - a = np.array([0.0011448196318903589, 0.00028718669878572767, - 0.00028718669878572767, 0.00028718669878572767, - 0.00028718669878572767]) - with catch_warnings(record=True): - b = mom.rolling_std(a, window=3) - self.assertTrue(np.isfinite(b[2:]).all()) + a = pd.Series([0.0011448196318903589, 0.00028718669878572767, + 0.00028718669878572767, 0.00028718669878572767, + 0.00028718669878572767]) + b = a.rolling(window=3).std() + assert np.isfinite(b[2:]).all() - with catch_warnings(record=True): - b = mom.ewmstd(a, span=3) - self.assertTrue(np.isfinite(b[2:]).all()) + b = a.ewm(span=3).std() + assert np.isfinite(b[2:]).all() def test_rolling_var(self): - self._check_moment_func(mom.rolling_var, lambda x: np.var(x, ddof=1), - test_stable=True, name='var') - self._check_moment_func(mom.rolling_var, lambda x: np.var(x, ddof=0), + self._check_moment_func(lambda x: np.var(x, ddof=1), + name='var') + self._check_moment_func(lambda x: np.var(x, ddof=0), name='var', ddof=0) + @td.skip_if_no_scipy def test_rolling_skew(self): - try: - from scipy.stats import skew - except ImportError: - pytest.skip('no scipy') - self._check_moment_func(mom.rolling_skew, - lambda x: skew(x, bias=False), name='skew') + from scipy.stats import skew + self._check_moment_func(lambda x: skew(x, bias=False), name='skew') + @td.skip_if_no_scipy def test_rolling_kurt(self): - try: - from scipy.stats import kurtosis - except ImportError: - pytest.skip('no scipy') - self._check_moment_func(mom.rolling_kurt, - lambda x: kurtosis(x, bias=False), name='kurt') - - def test_fperr_robustness(self): - # TODO: remove this once python 2.5 out of picture - if PY3: - pytest.skip("doesn't work on python 3") + from scipy.stats import kurtosis + self._check_moment_func(lambda x: kurtosis(x, bias=False), + name='kurt') - # #2114 - data = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a@\xaa\xaa\xaa\xaa\xaa\xaa\x02@8\x8e\xe38\x8e\xe3\xe8?z\t\xed%\xb4\x97\xd0?\xa2\x0c<\xdd\x9a\x1f\xb6?\x82\xbb\xfa&y\x7f\x9d?\xac\'\xa7\xc4P\xaa\x83?\x90\xdf\xde\xb0k8j?`\xea\xe9u\xf2zQ?*\xe37\x9d\x98N7?\xe2.\xf5&v\x13\x1f?\xec\xc9\xf8\x19\xa4\xb7\x04?\x90b\xf6w\x85\x9f\xeb>\xb5A\xa4\xfaXj\xd2>F\x02\xdb\xf8\xcb\x8d\xb8>.\xac<\xfb\x87^\xa0>\xe8:\xa6\xf9_\xd3\x85>\xfb?\xe2cUU\xfd?\xfc\x7fA\xed8\x8e\xe3?\xa5\xaa\xac\x91\xf6\x12\xca?n\x1cs\xb6\xf9a\xb1?\xe8%D\xf3L-\x97?5\xddZD\x11\xe7~?#>\xe7\x82\x0b\x9ad?\xd9R4Y\x0fxK?;7x;\nP2?N\xf4JO\xb8j\x18?4\xf81\x8a%G\x00?\x9a\xf5\x97\r2\xb4\xe5>\xcd\x9c\xca\xbcB\xf0\xcc>3\x13\x87(\xd7J\xb3>\x99\x19\xb4\xe0\x1e\xb9\x99>ff\xcd\x95\x14&\x81>\x88\x88\xbc\xc7p\xddf>`\x0b\xa6_\x96|N>@\xb2n\xea\x0eS4>U\x98\x938i\x19\x1b>\x8eeb\xd0\xf0\x10\x02>\xbd\xdc-k\x96\x16\xe8=(\x93\x1e\xf2\x0e\x0f\xd0=\xe0n\xd3Bii\xb5=*\xe9\x19Y\x8c\x8c\x9c=\xc6\xf0\xbb\x90]\x08\x83=]\x96\xfa\xc0|`i=>d\xfc\xd5\xfd\xeaP=R0\xfb\xc7\xa7\x8e6=\xc2\x95\xf9_\x8a\x13\x1e=\xd6c\xa6\xea\x06\r\x04=r\xda\xdd8\t\xbc\xea<\xf6\xe6\x93\xd0\xb0\xd2\xd1<\x9d\xdeok\x96\xc3\xb7<&~\xea9s\xaf\x9f\xb8\x02@\xc6\xd2&\xfd\xa8\xf5\xe8?\xd9\xe1\x19\xfe\xc5\xa3\xd0?v\x82"\xa8\xb2/\xb6?\x9dX\x835\xee\x94\x9d?h\x90W\xce\x9e\xb8\x83?\x8a\xc0th~Kj?\\\x80\xf8\x9a\xa9\x87Q?%\xab\xa0\xce\x8c_7?1\xe4\x80\x13\x11*\x1f? \x98\x00\r\xb6\xc6\x04?\x80u\xabf\x9d\xb3\xeb>UNrD\xbew\xd2>\x1c\x13C[\xa8\x9f\xb8>\x12b\xd7m-\x1fQ@\xe3\x85>\xe6\x91)l\x00/m>Da\xc6\xf2\xaatS>\x05\xd7]\xee\xe3\xf09>' # noqa + def _check_moment_func(self, static_comp, name, has_min_periods=True, + has_center=True, has_time_rule=True, + fill_value=None, zero_min_periods_equal=True, + **kwargs): - arr = np.frombuffer(data, dtype='= 0).all()) - - with catch_warnings(record=True): - result = mom.rolling_mean(arr, 2) - self.assertTrue((result[1:] >= 0).all()) - - with catch_warnings(record=True): - result = mom.rolling_var(arr, 2) - self.assertTrue((result[1:] >= 0).all()) - - # #2527, ugh - arr = np.array([0.00012456, 0.0003, 0]) - with catch_warnings(record=True): - result = mom.rolling_mean(arr, 1) - self.assertTrue(result[-1] >= 0) - - with catch_warnings(record=True): - result = mom.rolling_mean(-arr, 1) - self.assertTrue(result[-1] <= 0) - - def _check_moment_func(self, f, static_comp, name=None, window=50, - has_min_periods=True, has_center=True, - has_time_rule=True, preserve_nan=True, - fill_value=None, test_stable=False, **kwargs): - - with warnings.catch_warnings(record=True): - self._check_ndarray(f, static_comp, window=window, - has_min_periods=has_min_periods, - preserve_nan=preserve_nan, - has_center=has_center, fill_value=fill_value, - test_stable=test_stable, **kwargs) - - with warnings.catch_warnings(record=True): - self._check_structures(f, static_comp, - has_min_periods=has_min_periods, - has_time_rule=has_time_rule, - fill_value=fill_value, - has_center=has_center, **kwargs) - - # new API - if name is not None: - self._check_structures(f, static_comp, name=name, - has_min_periods=has_min_periods, - has_time_rule=has_time_rule, - fill_value=fill_value, - has_center=has_center, **kwargs) - - def _check_ndarray(self, f, static_comp, window=50, has_min_periods=True, - preserve_nan=True, has_center=True, fill_value=None, - test_stable=False, test_window=True, **kwargs): - def get_result(arr, window, min_periods=None, center=False): - return f(arr, window, min_periods=min_periods, center=center, ** - kwargs) - - result = get_result(self.arr, window) - tm.assert_almost_equal(result[-1], static_comp(self.arr[-50:])) - - if preserve_nan: - assert (np.isnan(result[self._nan_locs]).all()) - - # excluding NaNs correctly - arr = randn(50) - arr[:10] = np.NaN - arr[-10:] = np.NaN - - if has_min_periods: - result = get_result(arr, 50, min_periods=30) - tm.assert_almost_equal(result[-1], static_comp(arr[10:-10])) - - # min_periods is working correctly - result = get_result(arr, 20, min_periods=15) - self.assertTrue(np.isnan(result[23])) - self.assertFalse(np.isnan(result[24])) - - self.assertFalse(np.isnan(result[-6])) - self.assertTrue(np.isnan(result[-5])) - - arr2 = randn(20) - result = get_result(arr2, 10, min_periods=5) - self.assertTrue(isnull(result[3])) - self.assertTrue(notnull(result[4])) - - # min_periods=0 - result0 = get_result(arr, 20, min_periods=0) - result1 = get_result(arr, 20, min_periods=1) - tm.assert_almost_equal(result0, result1) - else: - result = get_result(arr, 50) - tm.assert_almost_equal(result[-1], static_comp(arr[10:-10])) - - # GH 7925 - if has_center: - if has_min_periods: - result = get_result(arr, 20, min_periods=15, center=True) - expected = get_result( - np.concatenate((arr, np.array([np.NaN] * 9))), 20, - min_periods=15)[9:] - else: - result = get_result(arr, 20, center=True) - expected = get_result( - np.concatenate((arr, np.array([np.NaN] * 9))), 20)[9:] - - self.assert_numpy_array_equal(result, expected) - - if test_stable: - result = get_result(self.arr + 1e9, window) - tm.assert_almost_equal(result[-1], - static_comp(self.arr[-50:] + 1e9)) - - # Test window larger than array, #7297 - if test_window: - if has_min_periods: - for minp in (0, len(self.arr) - 1, len(self.arr)): - result = get_result(self.arr, len(self.arr) + 1, - min_periods=minp) - expected = get_result(self.arr, len(self.arr), - min_periods=minp) - nan_mask = np.isnan(result) - self.assertTrue(np.array_equal(nan_mask, np.isnan( - expected))) - nan_mask = ~nan_mask - tm.assert_almost_equal(result[nan_mask], - expected[nan_mask]) - else: - result = get_result(self.arr, len(self.arr) + 1) - expected = get_result(self.arr, len(self.arr)) - nan_mask = np.isnan(result) - self.assertTrue(np.array_equal(nan_mask, np.isnan(expected))) - nan_mask = ~nan_mask - tm.assert_almost_equal(result[nan_mask], expected[nan_mask]) - - def _check_structures(self, f, static_comp, name=None, - has_min_periods=True, has_time_rule=True, - has_center=True, fill_value=None, **kwargs): - def get_result(obj, window, min_periods=None, freq=None, center=False): - - # check via the API calls if name is provided - if name is not None: - - # catch a freq deprecation warning if freq is provided and not - # None - with catch_warnings(record=True): - r = obj.rolling(window=window, min_periods=min_periods, - freq=freq, center=center) - return getattr(r, name)(**kwargs) - - # check via the moments API - with catch_warnings(record=True): - return f(obj, window=window, min_periods=min_periods, - freq=freq, center=center, **kwargs) + def get_result(obj, window, min_periods=None, center=False): + r = obj.rolling(window=window, min_periods=min_periods, + center=center) + return getattr(r, name)(**kwargs) series_result = get_result(self.series, window=50) - frame_result = get_result(self.frame, window=50) + assert isinstance(series_result, Series) + tm.assert_almost_equal(series_result.iloc[-1], + static_comp(self.series[-50:])) - tm.assertIsInstance(series_result, Series) - self.assertEqual(type(frame_result), DataFrame) + frame_result = get_result(self.frame, window=50) + assert isinstance(frame_result, DataFrame) + tm.assert_series_equal( + frame_result.iloc[-1, :], + self.frame.iloc[-50:, :].apply(static_comp, axis=0, raw=raw), + check_names=False) # check time_rule works if has_time_rule: win = 25 minp = 10 + series = self.series[::2].resample('B').mean() + frame = self.frame[::2].resample('B').mean() if has_min_periods: - series_result = get_result(self.series[::2], window=win, - min_periods=minp, freq='B') - frame_result = get_result(self.frame[::2], window=win, - min_periods=minp, freq='B') + series_result = get_result(series, window=win, + min_periods=minp) + frame_result = get_result(frame, window=win, + min_periods=minp) else: - series_result = get_result(self.series[::2], window=win, - freq='B') - frame_result = get_result(self.frame[::2], window=win, - freq='B') + series_result = get_result(series, window=win) + frame_result = get_result(frame, window=win) last_date = series_result.index[-1] prev_date = last_date - 24 * offsets.BDay() @@ -1395,15 +1561,79 @@ def get_result(obj, window, min_periods=None, freq=None, center=False): trunc_series = self.series[::2].truncate(prev_date, last_date) trunc_frame = self.frame[::2].truncate(prev_date, last_date) - self.assertAlmostEqual(series_result[-1], + tm.assert_almost_equal(series_result[-1], static_comp(trunc_series)) tm.assert_series_equal(frame_result.xs(last_date), - trunc_frame.apply(static_comp), + trunc_frame.apply(static_comp, raw=raw), check_names=False) - # GH 7925 + # excluding NaNs correctly + obj = Series(randn(50)) + obj[:10] = np.NaN + obj[-10:] = np.NaN + if has_min_periods: + result = get_result(obj, 50, min_periods=30) + tm.assert_almost_equal(result.iloc[-1], static_comp(obj[10:-10])) + + # min_periods is working correctly + result = get_result(obj, 20, min_periods=15) + assert isna(result.iloc[23]) + assert not isna(result.iloc[24]) + + assert not isna(result.iloc[-6]) + assert isna(result.iloc[-5]) + + obj2 = Series(randn(20)) + result = get_result(obj2, 10, min_periods=5) + assert isna(result.iloc[3]) + assert notna(result.iloc[4]) + + if zero_min_periods_equal: + # min_periods=0 may be equivalent to min_periods=1 + result0 = get_result(obj, 20, min_periods=0) + result1 = get_result(obj, 20, min_periods=1) + tm.assert_almost_equal(result0, result1) + else: + result = get_result(obj, 50) + tm.assert_almost_equal(result.iloc[-1], static_comp(obj[10:-10])) + + # window larger than series length (#7297) + if has_min_periods: + for minp in (0, len(self.series) - 1, len(self.series)): + result = get_result(self.series, len(self.series) + 1, + min_periods=minp) + expected = get_result(self.series, len(self.series), + min_periods=minp) + nan_mask = isna(result) + tm.assert_series_equal(nan_mask, isna(expected)) + + nan_mask = ~nan_mask + tm.assert_almost_equal(result[nan_mask], + expected[nan_mask]) + else: + result = get_result(self.series, len(self.series) + 1) + expected = get_result(self.series, len(self.series)) + nan_mask = isna(result) + tm.assert_series_equal(nan_mask, isna(expected)) + + nan_mask = ~nan_mask + tm.assert_almost_equal(result[nan_mask], expected[nan_mask]) + + # check center=True if has_center: + if has_min_periods: + result = get_result(obj, 20, min_periods=15, center=True) + expected = get_result( + pd.concat([obj, Series([np.NaN] * 9)]), 20, + min_periods=15)[9:].reset_index(drop=True) + else: + result = get_result(obj, 20, center=True) + expected = get_result( + pd.concat([obj, Series([np.NaN] * 9)]), + 20)[9:].reset_index(drop=True) + + tm.assert_series_equal(result, expected) # shifter index s = ['x%d' % x for x in range(12)] @@ -1443,34 +1673,27 @@ def get_result(obj, window, min_periods=None, freq=None, center=False): tm.assert_frame_equal(frame_xp, frame_rs) def test_ewma(self): - self._check_ew(mom.ewma, name='mean') + self._check_ew(name='mean') - arr = np.zeros(1000) - arr[5] = 1 - with catch_warnings(record=True): - result = mom.ewma(arr, span=100, adjust=False).sum() - self.assertTrue(np.abs(result - 1) < 1e-2) + vals = pd.Series(np.zeros(1000)) + vals[5] = 1 + result = vals.ewm(span=100, adjust=False).mean().sum() + assert np.abs(result - 1) < 1e-2 + + @pytest.mark.parametrize('adjust', [True, False]) + @pytest.mark.parametrize('ignore_na', [True, False]) + def test_ewma_cases(self, adjust, ignore_na): + # try adjust/ignore_na args matrix s = Series([1.0, 2.0, 4.0, 8.0]) - expected = Series([1.0, 1.6, 2.736842, 4.923077]) - for f in [lambda s: s.ewm(com=2.0, adjust=True).mean(), - lambda s: s.ewm(com=2.0, adjust=True, - ignore_na=False).mean(), - lambda s: s.ewm(com=2.0, adjust=True, ignore_na=True).mean(), - ]: - result = f(s) - tm.assert_series_equal(result, expected) + if adjust: + expected = Series([1.0, 1.6, 2.736842, 4.923077]) + else: + expected = Series([1.0, 1.333333, 2.222222, 4.148148]) - expected = Series([1.0, 1.333333, 2.222222, 4.148148]) - for f in [lambda s: s.ewm(com=2.0, adjust=False).mean(), - lambda s: s.ewm(com=2.0, adjust=False, - ignore_na=False).mean(), - lambda s: s.ewm(com=2.0, adjust=False, - ignore_na=True).mean(), - ]: - result = f(s) - tm.assert_series_equal(result, expected) + result = s.ewm(com=2.0, adjust=adjust, ignore_na=ignore_na).mean() + tm.assert_series_equal(result, expected) def test_ewma_nan_handling(self): s = Series([1.] + [np.nan] * 5 + [1.]) @@ -1528,55 +1751,34 @@ def simple_wma(s, w): tm.assert_series_equal(result, expected) def test_ewmvar(self): - self._check_ew(mom.ewmvar, name='var') + self._check_ew(name='var') def test_ewmvol(self): - self._check_ew(mom.ewmvol, name='vol') + self._check_ew(name='vol') def test_ewma_span_com_args(self): - with catch_warnings(record=True): - A = mom.ewma(self.arr, com=9.5) - B = mom.ewma(self.arr, span=20) - tm.assert_almost_equal(A, B) + A = self.series.ewm(com=9.5).mean() + B = self.series.ewm(span=20).mean() + tm.assert_almost_equal(A, B) - self.assertRaises(ValueError, mom.ewma, self.arr, com=9.5, span=20) - self.assertRaises(ValueError, mom.ewma, self.arr) + with pytest.raises(ValueError): + self.series.ewm(com=9.5, span=20) + with pytest.raises(ValueError): + self.series.ewm().mean() def test_ewma_halflife_arg(self): - with catch_warnings(record=True): - A = mom.ewma(self.arr, com=13.932726172912965) - B = mom.ewma(self.arr, halflife=10.0) - tm.assert_almost_equal(A, B) - - self.assertRaises(ValueError, mom.ewma, self.arr, span=20, - halflife=50) - self.assertRaises(ValueError, mom.ewma, self.arr, com=9.5, - halflife=50) - self.assertRaises(ValueError, mom.ewma, self.arr, com=9.5, span=20, - halflife=50) - self.assertRaises(ValueError, mom.ewma, self.arr) - - def test_ewma_alpha_old_api(self): - # GH 10789 - with catch_warnings(record=True): - a = mom.ewma(self.arr, alpha=0.61722699889169674) - b = mom.ewma(self.arr, com=0.62014947789973052) - c = mom.ewma(self.arr, span=2.240298955799461) - d = mom.ewma(self.arr, halflife=0.721792864318) - tm.assert_numpy_array_equal(a, b) - tm.assert_numpy_array_equal(a, c) - tm.assert_numpy_array_equal(a, d) - - def test_ewma_alpha_arg_old_api(self): - # GH 10789 - with catch_warnings(record=True): - self.assertRaises(ValueError, mom.ewma, self.arr) - self.assertRaises(ValueError, mom.ewma, self.arr, - com=10.0, alpha=0.5) - self.assertRaises(ValueError, mom.ewma, self.arr, - span=10.0, alpha=0.5) - self.assertRaises(ValueError, mom.ewma, self.arr, - halflife=10.0, alpha=0.5) + A = self.series.ewm(com=13.932726172912965).mean() + B = self.series.ewm(halflife=10.0).mean() + tm.assert_almost_equal(A, B) + + with pytest.raises(ValueError): + self.series.ewm(span=20, halflife=50) + with pytest.raises(ValueError): + self.series.ewm(com=9.5, halflife=50) + with pytest.raises(ValueError): + self.series.ewm(com=9.5, span=20, halflife=50) + with pytest.raises(ValueError): + self.series.ewm() def test_ewm_alpha(self): # GH 10789 @@ -1591,54 +1793,70 @@ def test_ewm_alpha(self): def test_ewm_alpha_arg(self): # GH 10789 - s = Series(self.arr) - self.assertRaises(ValueError, s.ewm) - self.assertRaises(ValueError, s.ewm, com=10.0, alpha=0.5) - self.assertRaises(ValueError, s.ewm, span=10.0, alpha=0.5) - self.assertRaises(ValueError, s.ewm, halflife=10.0, alpha=0.5) + s = self.series + with pytest.raises(ValueError): + s.ewm() + with pytest.raises(ValueError): + s.ewm(com=10.0, alpha=0.5) + with pytest.raises(ValueError): + s.ewm(span=10.0, alpha=0.5) + with pytest.raises(ValueError): + s.ewm(halflife=10.0, alpha=0.5) def test_ewm_domain_checks(self): # GH 12492 s = Series(self.arr) - # com must satisfy: com >= 0 - self.assertRaises(ValueError, s.ewm, com=-0.1) + msg = "comass must satisfy: comass >= 0" + with pytest.raises(ValueError, match=msg): + s.ewm(com=-0.1) s.ewm(com=0.0) s.ewm(com=0.1) - # span must satisfy: span >= 1 - self.assertRaises(ValueError, s.ewm, span=-0.1) - self.assertRaises(ValueError, s.ewm, span=0.0) - self.assertRaises(ValueError, s.ewm, span=0.9) + + msg = "span must satisfy: span >= 1" + with pytest.raises(ValueError, match=msg): + s.ewm(span=-0.1) + with pytest.raises(ValueError, match=msg): + s.ewm(span=0.0) + with pytest.raises(ValueError, match=msg): + s.ewm(span=0.9) s.ewm(span=1.0) s.ewm(span=1.1) - # halflife must satisfy: halflife > 0 - self.assertRaises(ValueError, s.ewm, halflife=-0.1) - self.assertRaises(ValueError, s.ewm, halflife=0.0) + + msg = "halflife must satisfy: halflife > 0" + with pytest.raises(ValueError, match=msg): + s.ewm(halflife=-0.1) + with pytest.raises(ValueError, match=msg): + s.ewm(halflife=0.0) s.ewm(halflife=0.1) - # alpha must satisfy: 0 < alpha <= 1 - self.assertRaises(ValueError, s.ewm, alpha=-0.1) - self.assertRaises(ValueError, s.ewm, alpha=0.0) + + msg = "alpha must satisfy: 0 < alpha <= 1" + with pytest.raises(ValueError, match=msg): + s.ewm(alpha=-0.1) + with pytest.raises(ValueError, match=msg): + s.ewm(alpha=0.0) s.ewm(alpha=0.1) s.ewm(alpha=1.0) - self.assertRaises(ValueError, s.ewm, alpha=1.1) + with pytest.raises(ValueError, match=msg): + s.ewm(alpha=1.1) - def test_ew_empty_arrays(self): - arr = np.array([], dtype=np.float64) + @pytest.mark.parametrize('method', ['mean', 'vol', 'var']) + def test_ew_empty_series(self, method): + vals = pd.Series([], dtype=np.float64) - funcs = [mom.ewma, mom.ewmvol, mom.ewmvar] - for f in funcs: - with catch_warnings(record=True): - result = f(arr, 3) - tm.assert_almost_equal(result, arr) + ewm = vals.ewm(3) + result = getattr(ewm, method)() + tm.assert_almost_equal(result, vals) - def _check_ew(self, func, name=None): - with catch_warnings(record=True): - self._check_ew_ndarray(func, name=name) - self._check_ew_structures(func, name=name) + def _check_ew(self, name=None, preserve_nan=False): + series_result = getattr(self.series.ewm(com=10), name)() + assert isinstance(series_result, Series) + + frame_result = getattr(self.frame.ewm(com=10), name)() + assert type(frame_result) == DataFrame - def _check_ew_ndarray(self, func, preserve_nan=False, name=None): - result = func(self.arr, com=10) + result = getattr(self.series.ewm(com=10), name)() if preserve_nan: - assert (np.isnan(result[self._nan_locs]).all()) + assert result[self._nan_locs].isna().all() # excluding NaNs correctly arr = randn(50) @@ -1648,44 +1866,197 @@ def _check_ew_ndarray(self, func, preserve_nan=False, name=None): # check min_periods # GH 7898 - result = func(s, 50, min_periods=2) - self.assertTrue(np.isnan(result.values[:11]).all()) - self.assertFalse(np.isnan(result.values[11:]).any()) + result = getattr(s.ewm(com=50, min_periods=2), name)() + assert result[:11].isna().all() + assert not result[11:].isna().any() for min_periods in (0, 1): - result = func(s, 50, min_periods=min_periods) - if func == mom.ewma: - self.assertTrue(np.isnan(result.values[:10]).all()) - self.assertFalse(np.isnan(result.values[10:]).any()) + result = getattr(s.ewm(com=50, min_periods=min_periods), name)() + if name == 'mean': + assert result[:10].isna().all() + assert not result[10:].isna().any() else: - # ewmstd, ewmvol, ewmvar (with bias=False) require at least two - # values - self.assertTrue(np.isnan(result.values[:11]).all()) - self.assertFalse(np.isnan(result.values[11:]).any()) + # ewm.std, ewm.vol, ewm.var (with bias=False) require at least + # two values + assert result[:11].isna().all() + assert not result[11:].isna().any() # check series of length 0 - result = func(Series([]), 50, min_periods=min_periods) - tm.assert_series_equal(result, Series([])) + result = getattr(Series().ewm(com=50, min_periods=min_periods), + name)() + tm.assert_series_equal(result, Series()) # check series of length 1 - result = func(Series([1.]), 50, min_periods=min_periods) - if func == mom.ewma: + result = getattr(Series([1.]).ewm(50, min_periods=min_periods), + name)() + if name == 'mean': tm.assert_series_equal(result, Series([1.])) else: - # ewmstd, ewmvol, ewmvar with bias=False require at least two - # values + # ewm.std, ewm.vol, ewm.var with bias=False require at least + # two values tm.assert_series_equal(result, Series([np.NaN])) # pass in ints - result2 = func(np.arange(50), span=10) - self.assertEqual(result2.dtype, np.float_) - - def _check_ew_structures(self, func, name): - series_result = getattr(self.series.ewm(com=10), name)() - tm.assertIsInstance(series_result, Series) - - frame_result = getattr(self.frame.ewm(com=10), name)() - self.assertEqual(type(frame_result), DataFrame) + result2 = getattr(Series(np.arange(50)).ewm(span=10), name)() + assert result2.dtype == np.float_ + + +class TestPairwise(object): + + # GH 7738 + df1s = [DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[0, 1]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1, 0]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1, 1]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], + columns=['C', 'C']), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1., 0]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[0., 1]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=['C', 1]), + DataFrame([[2., 4.], [1., 2.], [5., 2.], [8., 1.]], + columns=[1, 0.]), + DataFrame([[2, 4.], [1, 2.], [5, 2.], [8, 1.]], + columns=[0, 1.]), + DataFrame([[2, 4], [1, 2], [5, 2], [8, 1.]], + columns=[1., 'X']), ] + df2 = DataFrame([[None, 1, 1], [None, 1, 2], + [None, 3, 2], [None, 8, 1]], columns=['Y', 'Z', 'X']) + s = Series([1, 1, 3, 8]) + + def compare(self, result, expected): + + # since we have sorted the results + # we can only compare non-nans + result = result.dropna().values + expected = expected.dropna().values + + tm.assert_numpy_array_equal(result, expected, check_dtype=False) + + @pytest.mark.parametrize('f', [lambda x: x.cov(), lambda x: x.corr()]) + def test_no_flex(self, f): + + # DataFrame methods (which do not call _flex_binary_moment()) + + results = [f(df) for df in self.df1s] + for (df, result) in zip(self.df1s, results): + tm.assert_index_equal(result.index, df.columns) + tm.assert_index_equal(result.columns, df.columns) + for i, result in enumerate(results): + if i > 0: + self.compare(result, results[0]) + + @pytest.mark.parametrize( + 'f', [lambda x: x.expanding().cov(pairwise=True), + lambda x: x.expanding().corr(pairwise=True), + lambda x: x.rolling(window=3).cov(pairwise=True), + lambda x: x.rolling(window=3).corr(pairwise=True), + lambda x: x.ewm(com=3).cov(pairwise=True), + lambda x: x.ewm(com=3).corr(pairwise=True)]) + def test_pairwise_with_self(self, f): + + # DataFrame with itself, pairwise=True + # note that we may construct the 1st level of the MI + # in a non-motononic way, so compare accordingly + results = [] + for i, df in enumerate(self.df1s): + result = f(df) + tm.assert_index_equal(result.index.levels[0], + df.index, + check_names=False) + tm.assert_numpy_array_equal(safe_sort(result.index.levels[1]), + safe_sort(df.columns.unique())) + tm.assert_index_equal(result.columns, df.columns) + results.append(df) + + for i, result in enumerate(results): + if i > 0: + self.compare(result, results[0]) + + @pytest.mark.parametrize( + 'f', [lambda x: x.expanding().cov(pairwise=False), + lambda x: x.expanding().corr(pairwise=False), + lambda x: x.rolling(window=3).cov(pairwise=False), + lambda x: x.rolling(window=3).corr(pairwise=False), + lambda x: x.ewm(com=3).cov(pairwise=False), + lambda x: x.ewm(com=3).corr(pairwise=False), ]) + def test_no_pairwise_with_self(self, f): + + # DataFrame with itself, pairwise=False + results = [f(df) for df in self.df1s] + for (df, result) in zip(self.df1s, results): + tm.assert_index_equal(result.index, df.index) + tm.assert_index_equal(result.columns, df.columns) + for i, result in enumerate(results): + if i > 0: + self.compare(result, results[0]) + + @pytest.mark.parametrize( + 'f', [lambda x, y: x.expanding().cov(y, pairwise=True), + lambda x, y: x.expanding().corr(y, pairwise=True), + lambda x, y: x.rolling(window=3).cov(y, pairwise=True), + lambda x, y: x.rolling(window=3).corr(y, pairwise=True), + lambda x, y: x.ewm(com=3).cov(y, pairwise=True), + lambda x, y: x.ewm(com=3).corr(y, pairwise=True), ]) + def test_pairwise_with_other(self, f): + + # DataFrame with another DataFrame, pairwise=True + results = [f(df, self.df2) for df in self.df1s] + for (df, result) in zip(self.df1s, results): + tm.assert_index_equal(result.index.levels[0], + df.index, + check_names=False) + tm.assert_numpy_array_equal(safe_sort(result.index.levels[1]), + safe_sort(self.df2.columns.unique())) + for i, result in enumerate(results): + if i > 0: + self.compare(result, results[0]) + + @pytest.mark.parametrize( + 'f', [lambda x, y: x.expanding().cov(y, pairwise=False), + lambda x, y: x.expanding().corr(y, pairwise=False), + lambda x, y: x.rolling(window=3).cov(y, pairwise=False), + lambda x, y: x.rolling(window=3).corr(y, pairwise=False), + lambda x, y: x.ewm(com=3).cov(y, pairwise=False), + lambda x, y: x.ewm(com=3).corr(y, pairwise=False), ]) + def test_no_pairwise_with_other(self, f): + + # DataFrame with another DataFrame, pairwise=False + results = [f(df, self.df2) if df.columns.is_unique else None + for df in self.df1s] + for (df, result) in zip(self.df1s, results): + if result is not None: + with catch_warnings(record=True): + warnings.simplefilter("ignore", RuntimeWarning) + # we can have int and str columns + expected_index = df.index.union(self.df2.index) + expected_columns = df.columns.union(self.df2.columns) + tm.assert_index_equal(result.index, expected_index) + tm.assert_index_equal(result.columns, expected_columns) + else: + with pytest.raises(ValueError, + match="'arg1' columns are not unique"): + f(df, self.df2) + with pytest.raises(ValueError, + match="'arg2' columns are not unique"): + f(self.df2, df) + + @pytest.mark.parametrize( + 'f', [lambda x, y: x.expanding().cov(y), + lambda x, y: x.expanding().corr(y), + lambda x, y: x.rolling(window=3).cov(y), + lambda x, y: x.rolling(window=3).corr(y), + lambda x, y: x.ewm(com=3).cov(y), + lambda x, y: x.ewm(com=3).corr(y), ]) + def test_pairwise_with_series(self, f): + + # DataFrame with a Series + results = ([f(df, self.s) for df in self.df1s] + + [f(self.s, df) for df in self.df1s]) + for (df, result) in zip(self.df1s, results): + tm.assert_index_equal(result.index, df.index) + tm.assert_index_equal(result.columns, df.columns) + for i, result in enumerate(results): + if i > 0: + self.compare(result, results[0]) # create the data only once as we are not setting it @@ -1730,10 +2101,10 @@ def create_dataframes(): def is_constant(x): values = x.values.ravel() - return len(set(values[notnull(values)])) == 1 + return len(set(values[notna(values)])) == 1 def no_nans(x): - return x.notnull().all().all() + return x.notna().all().all() # data is a tuple(object, is_contant, no_nans) data = create_series() + create_dataframes() @@ -1744,6 +2115,15 @@ def no_nans(x): _consistency_data = _create_consistency_data() +def _rolling_consistency_cases(): + for window in [1, 2, 3, 10, 20]: + for min_periods in {0, 1, 2, 3, 4, window}: + if min_periods and (min_periods > window): + continue + for center in [False, True]: + yield window, min_periods, center + + class TestMomentsConsistency(Base): base_functions = [ (lambda v: Series(v).count(), None, 'count'), @@ -1760,9 +2140,6 @@ class TestMomentsConsistency(Base): # lambda v: Series(v).skew(), 3, 'skew'), # (lambda v: Series(v).kurt(), 4, 'kurt'), - # (lambda x, min_periods: mom.expanding_quantile(x, 0.3, - # min_periods=min_periods, 'quantile'), - # restore once GH 8084 is fixed # lambda v: Series(v).quantile(0.3), None, 'quantile'), @@ -1770,15 +2147,11 @@ class TestMomentsConsistency(Base): (np.nanmax, 1, 'max'), (np.nanmin, 1, 'min'), (np.nansum, 1, 'sum'), + (np.nanmean, 1, 'mean'), + (lambda v: np.nanstd(v, ddof=1), 1, 'std'), + (lambda v: np.nanvar(v, ddof=1), 1, 'var'), + (np.nanmedian, 1, 'median'), ] - if np.__version__ >= LooseVersion('1.8.0'): - base_functions += [ - (np.nanmean, 1, 'mean'), - (lambda v: np.nanstd(v, ddof=1), 1, 'std'), - (lambda v: np.nanvar(v, ddof=1), 1, 'var'), - ] - if np.__version__ >= LooseVersion('1.9.0'): - base_functions += [(np.nanmedian, 1, 'median'), ] no_nan_functions = [ (np.max, None, 'max'), (np.min, None, 'min'), @@ -1793,7 +2166,7 @@ def _create_data(self): super(TestMomentsConsistency, self)._create_data() self.data = _consistency_data - def setUp(self): + def setup_method(self, method): self._create_data() def _test_moments_consistency(self, min_periods, count, mean, mock_mean, @@ -1803,7 +2176,7 @@ def _test_moments_consistency(self, min_periods, count, mean, mock_mean, var_debiasing_factors=None): def _non_null_values(x): values = x.values.ravel() - return set(values[notnull(values)].tolist()) + return set(values[notna(values)].tolist()) for (x, is_constant, no_nans) in self.data: count_x = count(x) @@ -1816,7 +2189,8 @@ def _non_null_values(x): # check that correlation of a series with itself is either 1 or NaN corr_x_x = corr(x, x) - # self.assertTrue(_non_null_values(corr_x_x).issubset(set([1.]))) # + + # assert _non_null_values(corr_x_x).issubset(set([1.])) # restore once rolling_cov(x, x) is identically equal to var(x) if is_constant: @@ -1846,11 +2220,11 @@ def _non_null_values(x): # check that var(x), std(x), and cov(x) are all >= 0 var_x = var(x) std_x = std(x) - self.assertFalse((var_x < 0).any().any()) - self.assertFalse((std_x < 0).any().any()) + assert not (var_x < 0).any().any() + assert not (std_x < 0).any().any() if cov: cov_x_x = cov(x, x) - self.assertFalse((cov_x_x < 0).any().any()) + assert not (cov_x_x < 0).any().any() # check that var(x) == cov(x, x) assert_equal(var_x, cov_x_x) @@ -1865,7 +2239,7 @@ def _non_null_values(x): if is_constant: # check that variance of constant series is identically 0 - self.assertFalse((var_x > 0).any().any()) + assert not (var_x > 0).any().any() expected = x * np.nan expected[count_x >= max(min_periods, 1)] = 0. if var is var_unbiased: @@ -1874,7 +2248,7 @@ def _non_null_values(x): if isinstance(x, Series): for (y, is_constant, no_nans) in self.data: - if not x.isnull().equals(y.isnull()): + if not x.isna().equals(y.isna()): # can only easily test two Series with similar # structure continue @@ -1910,8 +2284,11 @@ def _non_null_values(x): assert_equal(cov_x_y, mean_x_times_y - (mean_x * mean_y)) - @tm.slow - def test_ewm_consistency(self): + @pytest.mark.slow + @pytest.mark.parametrize('min_periods', [0, 1, 2, 3, 4]) + @pytest.mark.parametrize('adjust', [True, False]) + @pytest.mark.parametrize('ignore_na', [True, False]) + def test_ewm_consistency(self, min_periods, adjust, ignore_na): def _weights(s, com, adjust, ignore_na): if isinstance(s, DataFrame): if not len(s.columns): @@ -1927,8 +2304,8 @@ def _weights(s, com, adjust, ignore_na): w = Series(np.nan, index=s.index) alpha = 1. / (1. + com) if ignore_na: - w[s.notnull()] = _weights(s[s.notnull()], com=com, - adjust=adjust, ignore_na=False) + w[s.notna()] = _weights(s[s.notna()], com=com, + adjust=adjust, ignore_na=False) elif adjust: for i in range(len(s)): if s.iat[i] == s.iat[i]: @@ -1965,52 +2342,51 @@ def _ewma(s, com, min_periods, adjust, ignore_na): return result com = 3. - for min_periods, adjust, ignore_na in product([0, 1, 2, 3, 4], - [True, False], - [False, True]): - # test consistency between different ewm* moments - self._test_moments_consistency( - min_periods=min_periods, - count=lambda x: x.expanding().count(), - mean=lambda x: x.ewm(com=com, min_periods=min_periods, - adjust=adjust, - ignore_na=ignore_na).mean(), - mock_mean=lambda x: _ewma(x, com=com, - min_periods=min_periods, - adjust=adjust, - ignore_na=ignore_na), - corr=lambda x, y: x.ewm(com=com, min_periods=min_periods, - adjust=adjust, - ignore_na=ignore_na).corr(y), - var_unbiased=lambda x: ( - x.ewm(com=com, min_periods=min_periods, - adjust=adjust, - ignore_na=ignore_na).var(bias=False)), - std_unbiased=lambda x: ( - x.ewm(com=com, min_periods=min_periods, - adjust=adjust, ignore_na=ignore_na) - .std(bias=False)), - cov_unbiased=lambda x, y: ( - x.ewm(com=com, min_periods=min_periods, - adjust=adjust, ignore_na=ignore_na) - .cov(y, bias=False)), - var_biased=lambda x: ( - x.ewm(com=com, min_periods=min_periods, - adjust=adjust, ignore_na=ignore_na) - .var(bias=True)), - std_biased=lambda x: x.ewm(com=com, min_periods=min_periods, - adjust=adjust, - ignore_na=ignore_na).std(bias=True), - cov_biased=lambda x, y: ( - x.ewm(com=com, min_periods=min_periods, - adjust=adjust, ignore_na=ignore_na) - .cov(y, bias=True)), - var_debiasing_factors=lambda x: ( - _variance_debiasing_factors(x, com=com, adjust=adjust, - ignore_na=ignore_na))) - - @tm.slow - def test_expanding_consistency(self): + # test consistency between different ewm* moments + self._test_moments_consistency( + min_periods=min_periods, + count=lambda x: x.expanding().count(), + mean=lambda x: x.ewm(com=com, min_periods=min_periods, + adjust=adjust, + ignore_na=ignore_na).mean(), + mock_mean=lambda x: _ewma(x, com=com, + min_periods=min_periods, + adjust=adjust, + ignore_na=ignore_na), + corr=lambda x, y: x.ewm(com=com, min_periods=min_periods, + adjust=adjust, + ignore_na=ignore_na).corr(y), + var_unbiased=lambda x: ( + x.ewm(com=com, min_periods=min_periods, + adjust=adjust, + ignore_na=ignore_na).var(bias=False)), + std_unbiased=lambda x: ( + x.ewm(com=com, min_periods=min_periods, + adjust=adjust, ignore_na=ignore_na) + .std(bias=False)), + cov_unbiased=lambda x, y: ( + x.ewm(com=com, min_periods=min_periods, + adjust=adjust, ignore_na=ignore_na) + .cov(y, bias=False)), + var_biased=lambda x: ( + x.ewm(com=com, min_periods=min_periods, + adjust=adjust, ignore_na=ignore_na) + .var(bias=True)), + std_biased=lambda x: x.ewm(com=com, min_periods=min_periods, + adjust=adjust, + ignore_na=ignore_na).std(bias=True), + cov_biased=lambda x, y: ( + x.ewm(com=com, min_periods=min_periods, + adjust=adjust, ignore_na=ignore_na) + .cov(y, bias=True)), + var_debiasing_factors=lambda x: ( + _variance_debiasing_factors(x, com=com, adjust=adjust, + ignore_na=ignore_na))) + + @pytest.mark.slow + @pytest.mark.parametrize( + 'min_periods', [0, 1, 2, 3, 4]) + def test_expanding_consistency(self, min_periods): # suppress warnings about empty slices, as we are deliberately testing # with empty/0-length Series/DataFrames @@ -2019,87 +2395,73 @@ def test_expanding_consistency(self): message=".*(empty slice|0 for slice).*", category=RuntimeWarning) - for min_periods in [0, 1, 2, 3, 4]: - - # test consistency between different expanding_* moments - self._test_moments_consistency( - min_periods=min_periods, - count=lambda x: x.expanding().count(), - mean=lambda x: x.expanding( - min_periods=min_periods).mean(), - mock_mean=lambda x: x.expanding( - min_periods=min_periods).sum() / x.expanding().count(), - corr=lambda x, y: x.expanding( - min_periods=min_periods).corr(y), - var_unbiased=lambda x: x.expanding( - min_periods=min_periods).var(), - std_unbiased=lambda x: x.expanding( - min_periods=min_periods).std(), - cov_unbiased=lambda x, y: x.expanding( - min_periods=min_periods).cov(y), - var_biased=lambda x: x.expanding( - min_periods=min_periods).var(ddof=0), - std_biased=lambda x: x.expanding( - min_periods=min_periods).std(ddof=0), - cov_biased=lambda x, y: x.expanding( - min_periods=min_periods).cov(y, ddof=0), - var_debiasing_factors=lambda x: ( - x.expanding().count() / - (x.expanding().count() - 1.) - .replace(0., np.nan))) - - # test consistency between expanding_xyz() and either (a) - # expanding_apply of Series.xyz(), or (b) expanding_apply of - # np.nanxyz() - for (x, is_constant, no_nans) in self.data: - functions = self.base_functions - - # GH 8269 - if no_nans: - functions = self.base_functions + self.no_nan_functions - for (f, require_min_periods, name) in functions: - expanding_f = getattr( - x.expanding(min_periods=min_periods), name) - - if (require_min_periods and - (min_periods is not None) and - (min_periods < require_min_periods)): - continue - - if name == 'count': - expanding_f_result = expanding_f() - expanding_apply_f_result = x.expanding( - min_periods=0).apply(func=f) + # test consistency between different expanding_* moments + self._test_moments_consistency( + min_periods=min_periods, + count=lambda x: x.expanding().count(), + mean=lambda x: x.expanding( + min_periods=min_periods).mean(), + mock_mean=lambda x: x.expanding( + min_periods=min_periods).sum() / x.expanding().count(), + corr=lambda x, y: x.expanding( + min_periods=min_periods).corr(y), + var_unbiased=lambda x: x.expanding( + min_periods=min_periods).var(), + std_unbiased=lambda x: x.expanding( + min_periods=min_periods).std(), + cov_unbiased=lambda x, y: x.expanding( + min_periods=min_periods).cov(y), + var_biased=lambda x: x.expanding( + min_periods=min_periods).var(ddof=0), + std_biased=lambda x: x.expanding( + min_periods=min_periods).std(ddof=0), + cov_biased=lambda x, y: x.expanding( + min_periods=min_periods).cov(y, ddof=0), + var_debiasing_factors=lambda x: ( + x.expanding().count() / + (x.expanding().count() - 1.) + .replace(0., np.nan))) + + # test consistency between expanding_xyz() and either (a) + # expanding_apply of Series.xyz(), or (b) expanding_apply of + # np.nanxyz() + for (x, is_constant, no_nans) in self.data: + functions = self.base_functions + + # GH 8269 + if no_nans: + functions = self.base_functions + self.no_nan_functions + for (f, require_min_periods, name) in functions: + expanding_f = getattr( + x.expanding(min_periods=min_periods), name) + + if (require_min_periods and + (min_periods is not None) and + (min_periods < require_min_periods)): + continue + + if name == 'count': + expanding_f_result = expanding_f() + expanding_apply_f_result = x.expanding( + min_periods=0).apply(func=f, raw=True) + else: + if name in ['cov', 'corr']: + expanding_f_result = expanding_f( + pairwise=False) else: - if name in ['cov', 'corr']: - expanding_f_result = expanding_f( - pairwise=False) - else: - expanding_f_result = expanding_f() - expanding_apply_f_result = x.expanding( - min_periods=min_periods).apply(func=f) - - if not tm._incompat_bottleneck_version(name): - assert_equal(expanding_f_result, - expanding_apply_f_result) - - if (name in ['cov', 'corr']) and isinstance(x, - DataFrame): - # test pairwise=True - expanding_f_result = expanding_f(x, pairwise=True) - expected = Panel(items=x.index, - major_axis=x.columns, - minor_axis=x.columns) - for i, _ in enumerate(x.columns): - for j, _ in enumerate(x.columns): - expected.iloc[:, i, j] = getattr( - x.iloc[:, i].expanding( - min_periods=min_periods), - name)(x.iloc[:, j]) - tm.assert_panel_equal(expanding_f_result, expected) - - @tm.slow - def test_rolling_consistency(self): + expanding_f_result = expanding_f() + expanding_apply_f_result = x.expanding( + min_periods=min_periods).apply(func=f, raw=True) + + # GH 9422 + if name in ['sum', 'prod']: + assert_equal(expanding_f_result, + expanding_apply_f_result) + + @pytest.mark.slow + @pytest.mark.parametrize( + 'window,min_periods,center', list(_rolling_consistency_cases())) + def test_rolling_consistency(self, window, min_periods, center): # suppress warnings about empty slices, as we are deliberately testing # with empty/0-length Series/DataFrames @@ -2108,119 +2470,93 @@ def test_rolling_consistency(self): message=".*(empty slice|0 for slice).*", category=RuntimeWarning) - def cases(): - for window in [1, 2, 3, 10, 20]: - for min_periods in set([0, 1, 2, 3, 4, window]): - if min_periods and (min_periods > window): - continue - for center in [False, True]: - yield window, min_periods, center - - for window, min_periods, center in cases(): - # test consistency between different rolling_* moments - self._test_moments_consistency( - min_periods=min_periods, - count=lambda x: ( - x.rolling(window=window, center=center) - .count()), - mean=lambda x: ( - x.rolling(window=window, min_periods=min_periods, - center=center).mean()), - mock_mean=lambda x: ( - x.rolling(window=window, - min_periods=min_periods, - center=center).sum() - .divide(x.rolling(window=window, - min_periods=min_periods, - center=center).count())), - corr=lambda x, y: ( - x.rolling(window=window, min_periods=min_periods, - center=center).corr(y)), - - var_unbiased=lambda x: ( - x.rolling(window=window, min_periods=min_periods, - center=center).var()), - - std_unbiased=lambda x: ( - x.rolling(window=window, min_periods=min_periods, - center=center).std()), - - cov_unbiased=lambda x, y: ( - x.rolling(window=window, min_periods=min_periods, - center=center).cov(y)), - - var_biased=lambda x: ( - x.rolling(window=window, min_periods=min_periods, - center=center).var(ddof=0)), - - std_biased=lambda x: ( - x.rolling(window=window, min_periods=min_periods, - center=center).std(ddof=0)), - - cov_biased=lambda x, y: ( - x.rolling(window=window, min_periods=min_periods, - center=center).cov(y, ddof=0)), - var_debiasing_factors=lambda x: ( - x.rolling(window=window, center=center).count() - .divide((x.rolling(window=window, center=center) - .count() - 1.) - .replace(0., np.nan)))) - - # test consistency between rolling_xyz() and either (a) - # rolling_apply of Series.xyz(), or (b) rolling_apply of - # np.nanxyz() - for (x, is_constant, no_nans) in self.data: - functions = self.base_functions - - # GH 8269 - if no_nans: - functions = self.base_functions + self.no_nan_functions - for (f, require_min_periods, name) in functions: - rolling_f = getattr( - x.rolling(window=window, center=center, - min_periods=min_periods), name) - - if require_min_periods and ( - min_periods is not None) and ( - min_periods < require_min_periods): - continue + # test consistency between different rolling_* moments + self._test_moments_consistency( + min_periods=min_periods, + count=lambda x: ( + x.rolling(window=window, center=center) + .count()), + mean=lambda x: ( + x.rolling(window=window, min_periods=min_periods, + center=center).mean()), + mock_mean=lambda x: ( + x.rolling(window=window, + min_periods=min_periods, + center=center).sum() + .divide(x.rolling(window=window, + min_periods=min_periods, + center=center).count())), + corr=lambda x, y: ( + x.rolling(window=window, min_periods=min_periods, + center=center).corr(y)), - if name == 'count': - rolling_f_result = rolling_f() - rolling_apply_f_result = x.rolling( - window=window, min_periods=0, - center=center).apply(func=f) + var_unbiased=lambda x: ( + x.rolling(window=window, min_periods=min_periods, + center=center).var()), + + std_unbiased=lambda x: ( + x.rolling(window=window, min_periods=min_periods, + center=center).std()), + + cov_unbiased=lambda x, y: ( + x.rolling(window=window, min_periods=min_periods, + center=center).cov(y)), + + var_biased=lambda x: ( + x.rolling(window=window, min_periods=min_periods, + center=center).var(ddof=0)), + + std_biased=lambda x: ( + x.rolling(window=window, min_periods=min_periods, + center=center).std(ddof=0)), + + cov_biased=lambda x, y: ( + x.rolling(window=window, min_periods=min_periods, + center=center).cov(y, ddof=0)), + var_debiasing_factors=lambda x: ( + x.rolling(window=window, center=center).count() + .divide((x.rolling(window=window, center=center) + .count() - 1.) + .replace(0., np.nan)))) + + # test consistency between rolling_xyz() and either (a) + # rolling_apply of Series.xyz(), or (b) rolling_apply of + # np.nanxyz() + for (x, is_constant, no_nans) in self.data: + functions = self.base_functions + + # GH 8269 + if no_nans: + functions = self.base_functions + self.no_nan_functions + for (f, require_min_periods, name) in functions: + rolling_f = getattr( + x.rolling(window=window, center=center, + min_periods=min_periods), name) + + if require_min_periods and ( + min_periods is not None) and ( + min_periods < require_min_periods): + continue + + if name == 'count': + rolling_f_result = rolling_f() + rolling_apply_f_result = x.rolling( + window=window, min_periods=0, + center=center).apply(func=f, raw=True) + else: + if name in ['cov', 'corr']: + rolling_f_result = rolling_f( + pairwise=False) else: - if name in ['cov', 'corr']: - rolling_f_result = rolling_f( - pairwise=False) - else: - rolling_f_result = rolling_f() - rolling_apply_f_result = x.rolling( - window=window, min_periods=min_periods, - center=center).apply(func=f) - if not tm._incompat_bottleneck_version(name): - assert_equal(rolling_f_result, - rolling_apply_f_result) - - if (name in ['cov', 'corr']) and isinstance( - x, DataFrame): - # test pairwise=True - rolling_f_result = rolling_f(x, - pairwise=True) - expected = Panel(items=x.index, - major_axis=x.columns, - minor_axis=x.columns) - for i, _ in enumerate(x.columns): - for j, _ in enumerate(x.columns): - expected.iloc[:, i, j] = ( - getattr( - x.iloc[:, i] - .rolling(window=window, - min_periods=min_periods, - center=center), - name)(x.iloc[:, j])) - tm.assert_panel_equal(rolling_f_result, expected) + rolling_f_result = rolling_f() + rolling_apply_f_result = x.rolling( + window=window, min_periods=min_periods, + center=center).apply(func=f, raw=True) + + # GH 9422 + if name in ['sum', 'prod']: + assert_equal(rolling_f_result, + rolling_apply_f_result) # binary moments def test_rolling_cov(self): @@ -2253,20 +2589,31 @@ def test_rolling_corr_pairwise(self): self._check_pairwise_moment('rolling', 'corr', window=10, min_periods=5) + @pytest.mark.parametrize('window', range(7)) + def test_rolling_corr_with_zero_variance(self, window): + # GH 18430 + s = pd.Series(np.zeros(20)) + other = pd.Series(np.arange(20)) + + assert s.rolling(window=window).corr(other=other).isna().all() + def _check_pairwise_moment(self, dispatch, name, **kwargs): def get_result(obj, obj2=None): return getattr(getattr(obj, dispatch)(**kwargs), name)(obj2) - panel = get_result(self.frame) - actual = panel.loc[:, 1, 5] + result = get_result(self.frame) + result = result.loc[(slice(None), 1), 5] + result.index = result.index.droplevel(1) expected = get_result(self.frame[1], self.frame[5]) - tm.assert_series_equal(actual, expected, check_names=False) - self.assertEqual(actual.name, 5) + tm.assert_series_equal(result, expected, check_names=False) def test_flex_binary_moment(self): # GH3155 # don't blow the stack - self.assertRaises(TypeError, rwindow._flex_binary_moment, 5, 6, None) + msg = ("arguments to moment function must be of type" + " np.ndarray/Series/DataFrame") + with pytest.raises(TypeError, match=msg): + rwindow._flex_binary_moment(5, 6, None) def test_corr_sanity(self): # GH 3155 @@ -2276,41 +2623,36 @@ def test_corr_sanity(self): [0.84780328, 0.33394331], [0.78369152, 0.63919667]])) res = df[0].rolling(5, center=True).corr(df[1]) - self.assertTrue(all([np.abs(np.nan_to_num(x)) <= 1 for x in res])) + assert all(np.abs(np.nan_to_num(x)) <= 1 for x in res) # and some fuzzing - for i in range(10): + for _ in range(10): df = DataFrame(np.random.rand(30, 2)) res = df[0].rolling(5, center=True).corr(df[1]) try: - self.assertTrue(all([np.abs(np.nan_to_num(x)) <= 1 for x in res - ])) - except: + assert all(np.abs(np.nan_to_num(x)) <= 1 for x in res) + except AssertionError: print(res) - def test_flex_binary_frame(self): - def _check(method): - series = self.frame[1] - - res = getattr(series.rolling(window=10), method)(self.frame) - res2 = getattr(self.frame.rolling(window=10), method)(series) - exp = self.frame.apply(lambda x: getattr( - series.rolling(window=10), method)(x)) + @pytest.mark.parametrize('method', ['corr', 'cov']) + def test_flex_binary_frame(self, method): + series = self.frame[1] - tm.assert_frame_equal(res, exp) - tm.assert_frame_equal(res2, exp) + res = getattr(series.rolling(window=10), method)(self.frame) + res2 = getattr(self.frame.rolling(window=10), method)(series) + exp = self.frame.apply(lambda x: getattr( + series.rolling(window=10), method)(x)) - frame2 = self.frame.copy() - frame2.values[:] = np.random.randn(*frame2.shape) + tm.assert_frame_equal(res, exp) + tm.assert_frame_equal(res2, exp) - res3 = getattr(self.frame.rolling(window=10), method)(frame2) - exp = DataFrame(dict((k, getattr(self.frame[k].rolling( - window=10), method)(frame2[k])) for k in self.frame)) - tm.assert_frame_equal(res3, exp) + frame2 = self.frame.copy() + frame2.values[:] = np.random.randn(*frame2.shape) - methods = ['corr', 'cov'] - for meth in methods: - _check(meth) + res3 = getattr(self.frame.rolling(window=10), method)(frame2) + exp = DataFrame({k: getattr(self.frame[k].rolling( + window=10), method)(frame2[k]) for k in self.frame}) + tm.assert_frame_equal(res3, exp) def test_ewmcov(self): self._check_binary_ew('cov') @@ -2335,16 +2677,16 @@ def func(A, B, com, **kwargs): B[-10:] = np.NaN result = func(A, B, 20, min_periods=5) - self.assertTrue(np.isnan(result.values[:14]).all()) - self.assertFalse(np.isnan(result.values[14:]).any()) + assert np.isnan(result.values[:14]).all() + assert not np.isnan(result.values[14:]).any() # GH 7898 for min_periods in (0, 1, 2): result = func(A, B, 20, min_periods=min_periods) # binary functions (ewmcov, ewmcorr) with bias=False require at # least two values - self.assertTrue(np.isnan(result.values[:11]).all()) - self.assertFalse(np.isnan(result.values[11:]).any()) + assert np.isnan(result.values[:11]).all() + assert not np.isnan(result.values[11:]).any() # check series of length 0 result = func(Series([]), Series([]), 50, min_periods=min_periods) @@ -2355,37 +2697,29 @@ def func(A, B, com, **kwargs): Series([1.]), Series([1.]), 50, min_periods=min_periods) tm.assert_series_equal(result, Series([np.NaN])) - self.assertRaises(Exception, func, A, randn(50), 20, min_periods=5) - - def test_expanding_apply(self): - ser = Series([]) - tm.assert_series_equal(ser, ser.expanding().apply(lambda x: x.mean())) - - def expanding_mean(x, min_periods=1, freq=None): - return mom.expanding_apply(x, lambda x: x.mean(), - min_periods=min_periods, freq=freq) + msg = "Input arrays must be of the same type!" + # exception raised is Exception + with pytest.raises(Exception, match=msg): + func(A, randn(50), 20, min_periods=5) - self._check_expanding(expanding_mean, np.mean) + def test_expanding_apply_args_kwargs(self, raw): - # GH 8080 - s = Series([None, None, None]) - result = s.expanding(min_periods=0).apply(lambda x: len(x)) - expected = Series([1., 2., 3.]) - tm.assert_series_equal(result, expected) - - def test_expanding_apply_args_kwargs(self): def mean_w_arg(x, const): return np.mean(x) + const df = DataFrame(np.random.rand(20, 3)) - expected = df.expanding().apply(np.mean) + 20. + expected = df.expanding().apply(np.mean, raw=raw) + 20. + + result = df.expanding().apply(mean_w_arg, + raw=raw, + args=(20, )) + tm.assert_frame_equal(result, expected) - tm.assert_frame_equal(df.expanding().apply(mean_w_arg, args=(20, )), - expected) - tm.assert_frame_equal(df.expanding().apply(mean_w_arg, - kwargs={'const': 20}), - expected) + result = df.expanding().apply(mean_w_arg, + raw=raw, + kwargs={'const': 20}) + tm.assert_frame_equal(result, expected) def test_expanding_corr(self): A = self.series.dropna() @@ -2420,26 +2754,20 @@ def test_expanding_cov(self): tm.assert_almost_equal(rolling_result, result) - def test_expanding_max(self): - self._check_expanding(mom.expanding_max, np.max, preserve_nan=False) - def test_expanding_cov_pairwise(self): result = self.frame.expanding().corr() rolling_result = self.frame.rolling(window=len(self.frame), min_periods=1).corr() - for i in result.items: - tm.assert_almost_equal(result[i], rolling_result[i]) + tm.assert_frame_equal(result, rolling_result) def test_expanding_corr_pairwise(self): result = self.frame.expanding().corr() rolling_result = self.frame.rolling(window=len(self.frame), min_periods=1).corr() - - for i in result.items: - tm.assert_almost_equal(result[i], rolling_result[i]) + tm.assert_frame_equal(result, rolling_result) def test_expanding_cov_diff_index(self): # GH 7512 @@ -2501,52 +2829,66 @@ def test_rolling_corr_diff_length(self): result = s1.rolling(window=3, min_periods=2).corr(s2a) tm.assert_series_equal(result, expected) - def test_rolling_functions_window_non_shrinkage(self): + @pytest.mark.parametrize( + 'f', + [ + lambda x: (x.rolling(window=10, min_periods=5) + .cov(x, pairwise=False)), + lambda x: (x.rolling(window=10, min_periods=5) + .corr(x, pairwise=False)), + lambda x: x.rolling(window=10, min_periods=5).max(), + lambda x: x.rolling(window=10, min_periods=5).min(), + lambda x: x.rolling(window=10, min_periods=5).sum(), + lambda x: x.rolling(window=10, min_periods=5).mean(), + lambda x: x.rolling(window=10, min_periods=5).std(), + lambda x: x.rolling(window=10, min_periods=5).var(), + lambda x: x.rolling(window=10, min_periods=5).skew(), + lambda x: x.rolling(window=10, min_periods=5).kurt(), + lambda x: x.rolling( + window=10, min_periods=5).quantile(quantile=0.5), + lambda x: x.rolling(window=10, min_periods=5).median(), + lambda x: x.rolling(window=10, min_periods=5).apply( + sum, raw=False), + lambda x: x.rolling(window=10, min_periods=5).apply( + sum, raw=True), + lambda x: x.rolling(win_type='boxcar', + window=10, min_periods=5).mean()]) + def test_rolling_functions_window_non_shrinkage(self, f): # GH 7764 s = Series(range(4)) s_expected = Series(np.nan, index=s.index) df = DataFrame([[1, 5], [3, 2], [3, 9], [-1, 0]], columns=['A', 'B']) df_expected = DataFrame(np.nan, index=df.index, columns=df.columns) - df_expected_panel = Panel(items=df.index, major_axis=df.columns, - minor_axis=df.columns) - - functions = [lambda x: (x.rolling(window=10, min_periods=5) - .cov(x, pairwise=False)), - lambda x: (x.rolling(window=10, min_periods=5) - .corr(x, pairwise=False)), - lambda x: x.rolling(window=10, min_periods=5).max(), - lambda x: x.rolling(window=10, min_periods=5).min(), - lambda x: x.rolling(window=10, min_periods=5).sum(), - lambda x: x.rolling(window=10, min_periods=5).mean(), - lambda x: x.rolling(window=10, min_periods=5).std(), - lambda x: x.rolling(window=10, min_periods=5).var(), - lambda x: x.rolling(window=10, min_periods=5).skew(), - lambda x: x.rolling(window=10, min_periods=5).kurt(), - lambda x: x.rolling( - window=10, min_periods=5).quantile(quantile=0.5), - lambda x: x.rolling(window=10, min_periods=5).median(), - lambda x: x.rolling(window=10, min_periods=5).apply(sum), - lambda x: x.rolling(win_type='boxcar', - window=10, min_periods=5).mean()] - for f in functions: - try: - s_result = f(s) - tm.assert_series_equal(s_result, s_expected) - - df_result = f(df) - tm.assert_frame_equal(df_result, df_expected) - except (ImportError): - - # scipy needed for rolling_window - continue + try: + s_result = f(s) + tm.assert_series_equal(s_result, s_expected) + + df_result = f(df) + tm.assert_frame_equal(df_result, df_expected) + except (ImportError): + + # scipy needed for rolling_window + pytest.skip("scipy not available") + + def test_rolling_functions_window_non_shrinkage_binary(self): + + # corr/cov return a MI DataFrame + df = DataFrame([[1, 5], [3, 2], [3, 9], [-1, 0]], + columns=Index(['A', 'B'], name='foo'), + index=Index(range(4), name='bar')) + df_expected = DataFrame( + columns=Index(['A', 'B'], name='foo'), + index=pd.MultiIndex.from_product([df.index, df.columns], + names=['bar', 'foo']), + dtype='float64') functions = [lambda x: (x.rolling(window=10, min_periods=5) .cov(x, pairwise=True)), lambda x: (x.rolling(window=10, min_periods=5) .corr(x, pairwise=True))] for f in functions: - df_result_panel = f(df) - tm.assert_panel_equal(df_result_panel, df_expected_panel) + df_result = f(df) + tm.assert_frame_equal(df_result, df_expected) def test_moment_functions_zero_length(self): # GH 8056 @@ -2554,13 +2896,9 @@ def test_moment_functions_zero_length(self): s_expected = s df1 = DataFrame() df1_expected = df1 - df1_expected_panel = Panel(items=df1.index, major_axis=df1.columns, - minor_axis=df1.columns) df2 = DataFrame(columns=['a']) df2['a'] = df2['a'].astype('float64') df2_expected = df2 - df2_expected_panel = Panel(items=df2.index, major_axis=df2.columns, - minor_axis=df2.columns) functions = [lambda x: x.expanding().count(), lambda x: x.expanding(min_periods=5).cov( @@ -2577,7 +2915,10 @@ def test_moment_functions_zero_length(self): lambda x: x.expanding(min_periods=5).kurt(), lambda x: x.expanding(min_periods=5).quantile(0.5), lambda x: x.expanding(min_periods=5).median(), - lambda x: x.expanding(min_periods=5).apply(sum), + lambda x: x.expanding(min_periods=5).apply( + sum, raw=False), + lambda x: x.expanding(min_periods=5).apply( + sum, raw=True), lambda x: x.rolling(window=10).count(), lambda x: x.rolling(window=10, min_periods=5).cov( x, pairwise=False), @@ -2594,7 +2935,10 @@ def test_moment_functions_zero_length(self): lambda x: x.rolling( window=10, min_periods=5).quantile(0.5), lambda x: x.rolling(window=10, min_periods=5).median(), - lambda x: x.rolling(window=10, min_periods=5).apply(sum), + lambda x: x.rolling(window=10, min_periods=5).apply( + sum, raw=False), + lambda x: x.rolling(window=10, min_periods=5).apply( + sum, raw=True), lambda x: x.rolling(win_type='boxcar', window=10, min_periods=5).mean(), ] @@ -2613,6 +2957,23 @@ def test_moment_functions_zero_length(self): # scipy needed for rolling_window continue + def test_moment_functions_zero_length_pairwise(self): + + df1 = DataFrame() + df1_expected = df1 + df2 = DataFrame(columns=Index(['a'], name='foo'), + index=Index([], name='bar')) + df2['a'] = df2['a'].astype('float64') + + df1_expected = DataFrame( + index=pd.MultiIndex.from_product([df1.index, df1.columns]), + columns=Index([])) + df2_expected = DataFrame( + index=pd.MultiIndex.from_product([df2.index, df2.columns], + names=['bar', 'foo']), + columns=Index(['a'], name='foo'), + dtype='float64') + functions = [lambda x: (x.expanding(min_periods=5) .cov(x, pairwise=True)), lambda x: (x.expanding(min_periods=5) @@ -2623,24 +2984,33 @@ def test_moment_functions_zero_length(self): .corr(x, pairwise=True)), ] for f in functions: - df1_result_panel = f(df1) - tm.assert_panel_equal(df1_result_panel, df1_expected_panel) + df1_result = f(df1) + tm.assert_frame_equal(df1_result, df1_expected) - df2_result_panel = f(df2) - tm.assert_panel_equal(df2_result_panel, df2_expected_panel) + df2_result = f(df2) + tm.assert_frame_equal(df2_result, df2_expected) def test_expanding_cov_pairwise_diff_length(self): # GH 7512 - df1 = DataFrame([[1, 5], [3, 2], [3, 9]], columns=['A', 'B']) - df1a = DataFrame([[1, 5], [3, 9]], index=[0, 2], columns=['A', 'B']) - df2 = DataFrame([[5, 6], [None, None], [2, 1]], columns=['X', 'Y']) - df2a = DataFrame([[5, 6], [2, 1]], index=[0, 2], columns=['X', 'Y']) - result1 = df1.expanding().cov(df2a, pairwise=True)[2] - result2 = df1.expanding().cov(df2a, pairwise=True)[2] - result3 = df1a.expanding().cov(df2, pairwise=True)[2] - result4 = df1a.expanding().cov(df2a, pairwise=True)[2] - expected = DataFrame([[-3., -5.], [-6., -10.]], index=['A', 'B'], - columns=['X', 'Y']) + df1 = DataFrame([[1, 5], [3, 2], [3, 9]], + columns=Index(['A', 'B'], name='foo')) + df1a = DataFrame([[1, 5], [3, 9]], + index=[0, 2], + columns=Index(['A', 'B'], name='foo')) + df2 = DataFrame([[5, 6], [None, None], [2, 1]], + columns=Index(['X', 'Y'], name='foo')) + df2a = DataFrame([[5, 6], [2, 1]], + index=[0, 2], + columns=Index(['X', 'Y'], name='foo')) + # TODO: xref gh-15826 + # .loc is not preserving the names + result1 = df1.expanding().cov(df2a, pairwise=True).loc[2] + result2 = df1.expanding().cov(df2a, pairwise=True).loc[2] + result3 = df1a.expanding().cov(df2, pairwise=True).loc[2] + result4 = df1a.expanding().cov(df2a, pairwise=True).loc[2] + expected = DataFrame([[-3.0, -6.0], [-5.0, -10.0]], + columns=Index(['A', 'B'], name='foo'), + index=Index(['X', 'Y'], name='foo')) tm.assert_frame_equal(result1, expected) tm.assert_frame_equal(result2, expected) tm.assert_frame_equal(result3, expected) @@ -2648,149 +3018,30 @@ def test_expanding_cov_pairwise_diff_length(self): def test_expanding_corr_pairwise_diff_length(self): # GH 7512 - df1 = DataFrame([[1, 2], [3, 2], [3, 4]], columns=['A', 'B']) - df1a = DataFrame([[1, 2], [3, 4]], index=[0, 2], columns=['A', 'B']) - df2 = DataFrame([[5, 6], [None, None], [2, 1]], columns=['X', 'Y']) - df2a = DataFrame([[5, 6], [2, 1]], index=[0, 2], columns=['X', 'Y']) - result1 = df1.expanding().corr(df2, pairwise=True)[2] - result2 = df1.expanding().corr(df2a, pairwise=True)[2] - result3 = df1a.expanding().corr(df2, pairwise=True)[2] - result4 = df1a.expanding().corr(df2a, pairwise=True)[2] - expected = DataFrame([[-1.0, -1.0], [-1.0, -1.0]], index=['A', 'B'], - columns=['X', 'Y']) + df1 = DataFrame([[1, 2], [3, 2], [3, 4]], + columns=['A', 'B'], + index=Index(range(3), name='bar')) + df1a = DataFrame([[1, 2], [3, 4]], + index=Index([0, 2], name='bar'), + columns=['A', 'B']) + df2 = DataFrame([[5, 6], [None, None], [2, 1]], + columns=['X', 'Y'], + index=Index(range(3), name='bar')) + df2a = DataFrame([[5, 6], [2, 1]], + index=Index([0, 2], name='bar'), + columns=['X', 'Y']) + result1 = df1.expanding().corr(df2, pairwise=True).loc[2] + result2 = df1.expanding().corr(df2a, pairwise=True).loc[2] + result3 = df1a.expanding().corr(df2, pairwise=True).loc[2] + result4 = df1a.expanding().corr(df2a, pairwise=True).loc[2] + expected = DataFrame([[-1.0, -1.0], [-1.0, -1.0]], + columns=['A', 'B'], + index=Index(['X', 'Y'])) tm.assert_frame_equal(result1, expected) tm.assert_frame_equal(result2, expected) tm.assert_frame_equal(result3, expected) tm.assert_frame_equal(result4, expected) - def test_pairwise_stats_column_names_order(self): - # GH 7738 - df1s = [DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[0, 1]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1, 0]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1, 1]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], - columns=['C', 'C']), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[1., 0]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=[0., 1]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1]], columns=['C', 1]), - DataFrame([[2., 4.], [1., 2.], [5., 2.], [8., 1.]], - columns=[1, 0.]), - DataFrame([[2, 4.], [1, 2.], [5, 2.], [8, 1.]], - columns=[0, 1.]), - DataFrame([[2, 4], [1, 2], [5, 2], [8, 1.]], - columns=[1., 'X']), ] - df2 = DataFrame([[None, 1, 1], [None, 1, 2], - [None, 3, 2], [None, 8, 1]], columns=['Y', 'Z', 'X']) - s = Series([1, 1, 3, 8]) - - # suppress warnings about incomparable objects, as we are deliberately - # testing with such column labels - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", - message=".*incomparable objects.*", - category=RuntimeWarning) - - # DataFrame methods (which do not call _flex_binary_moment()) - for f in [lambda x: x.cov(), lambda x: x.corr(), ]: - results = [f(df) for df in df1s] - for (df, result) in zip(df1s, results): - tm.assert_index_equal(result.index, df.columns) - tm.assert_index_equal(result.columns, df.columns) - for i, result in enumerate(results): - if i > 0: - # compare internal values, as columns can be different - self.assert_numpy_array_equal(result.values, - results[0].values) - - # DataFrame with itself, pairwise=True - for f in [lambda x: x.expanding().cov(pairwise=True), - lambda x: x.expanding().corr(pairwise=True), - lambda x: x.rolling(window=3).cov(pairwise=True), - lambda x: x.rolling(window=3).corr(pairwise=True), - lambda x: x.ewm(com=3).cov(pairwise=True), - lambda x: x.ewm(com=3).corr(pairwise=True), ]: - results = [f(df) for df in df1s] - for (df, result) in zip(df1s, results): - tm.assert_index_equal(result.items, df.index) - tm.assert_index_equal(result.major_axis, df.columns) - tm.assert_index_equal(result.minor_axis, df.columns) - for i, result in enumerate(results): - if i > 0: - self.assert_numpy_array_equal(result.values, - results[0].values) - - # DataFrame with itself, pairwise=False - for f in [lambda x: x.expanding().cov(pairwise=False), - lambda x: x.expanding().corr(pairwise=False), - lambda x: x.rolling(window=3).cov(pairwise=False), - lambda x: x.rolling(window=3).corr(pairwise=False), - lambda x: x.ewm(com=3).cov(pairwise=False), - lambda x: x.ewm(com=3).corr(pairwise=False), ]: - results = [f(df) for df in df1s] - for (df, result) in zip(df1s, results): - tm.assert_index_equal(result.index, df.index) - tm.assert_index_equal(result.columns, df.columns) - for i, result in enumerate(results): - if i > 0: - self.assert_numpy_array_equal(result.values, - results[0].values) - - # DataFrame with another DataFrame, pairwise=True - for f in [lambda x, y: x.expanding().cov(y, pairwise=True), - lambda x, y: x.expanding().corr(y, pairwise=True), - lambda x, y: x.rolling(window=3).cov(y, pairwise=True), - lambda x, y: x.rolling(window=3).corr(y, pairwise=True), - lambda x, y: x.ewm(com=3).cov(y, pairwise=True), - lambda x, y: x.ewm(com=3).corr(y, pairwise=True), ]: - results = [f(df, df2) for df in df1s] - for (df, result) in zip(df1s, results): - tm.assert_index_equal(result.items, df.index) - tm.assert_index_equal(result.major_axis, df.columns) - tm.assert_index_equal(result.minor_axis, df2.columns) - for i, result in enumerate(results): - if i > 0: - self.assert_numpy_array_equal(result.values, - results[0].values) - - # DataFrame with another DataFrame, pairwise=False - for f in [lambda x, y: x.expanding().cov(y, pairwise=False), - lambda x, y: x.expanding().corr(y, pairwise=False), - lambda x, y: x.rolling(window=3).cov(y, pairwise=False), - lambda x, y: x.rolling(window=3).corr(y, pairwise=False), - lambda x, y: x.ewm(com=3).cov(y, pairwise=False), - lambda x, y: x.ewm(com=3).corr(y, pairwise=False), ]: - results = [f(df, df2) if df.columns.is_unique else None - for df in df1s] - for (df, result) in zip(df1s, results): - if result is not None: - expected_index = df.index.union(df2.index) - expected_columns = df.columns.union(df2.columns) - tm.assert_index_equal(result.index, expected_index) - tm.assert_index_equal(result.columns, expected_columns) - else: - tm.assertRaisesRegexp( - ValueError, "'arg1' columns are not unique", f, df, - df2) - tm.assertRaisesRegexp( - ValueError, "'arg2' columns are not unique", f, - df2, df) - - # DataFrame with a Series - for f in [lambda x, y: x.expanding().cov(y), - lambda x, y: x.expanding().corr(y), - lambda x, y: x.rolling(window=3).cov(y), - lambda x, y: x.rolling(window=3).corr(y), - lambda x, y: x.ewm(com=3).cov(y), - lambda x, y: x.ewm(com=3).corr(y), ]: - results = [f(df, s) for df in df1s] + [f(s, df) for df in df1s] - for (df, result) in zip(df1s, results): - tm.assert_index_equal(result.index, df.index) - tm.assert_index_equal(result.columns, df.columns) - for i, result in enumerate(results): - if i > 0: - self.assert_numpy_array_equal(result.values, - results[0].values) - def test_rolling_skew_edge_cases(self): all_nan = Series([np.NaN] * 5) @@ -2833,55 +3084,88 @@ def test_rolling_kurt_edge_cases(self): x = d.rolling(window=4).kurt() tm.assert_series_equal(expected, x) - def _check_expanding_ndarray(self, func, static_comp, has_min_periods=True, - has_time_rule=True, preserve_nan=True): - result = func(self.arr) + def test_rolling_skew_eq_value_fperr(self): + # #18804 all rolling skew for all equal values should return Nan + a = Series([1.1] * 15).rolling(window=10).skew() + assert np.isnan(a).all() + + def test_rolling_kurt_eq_value_fperr(self): + # #18804 all rolling kurt for all equal values should return Nan + a = Series([1.1] * 15).rolling(window=10).kurt() + assert np.isnan(a).all() + + @pytest.mark.parametrize('func,static_comp', [('sum', np.sum), + ('mean', np.mean), + ('max', np.max), + ('min', np.min)], + ids=['sum', 'mean', 'max', 'min']) + def test_expanding_func(self, func, static_comp): + def expanding_func(x, min_periods=1, center=False, axis=0): + exp = x.expanding(min_periods=min_periods, + center=center, axis=axis) + return getattr(exp, func)() + self._check_expanding(expanding_func, static_comp, preserve_nan=False) + + def test_expanding_apply(self, raw): + + def expanding_mean(x, min_periods=1): + + exp = x.expanding(min_periods=min_periods) + result = exp.apply(lambda x: x.mean(), raw=raw) + return result + + # TODO(jreback), needed to add preserve_nan=False + # here to make this pass + self._check_expanding(expanding_mean, np.mean, preserve_nan=False) + + ser = Series([]) + tm.assert_series_equal(ser, ser.expanding().apply( + lambda x: x.mean(), raw=raw)) + + # GH 8080 + s = Series([None, None, None]) + result = s.expanding(min_periods=0).apply(lambda x: len(x), raw=raw) + expected = Series([1., 2., 3.]) + tm.assert_series_equal(result, expected) - tm.assert_almost_equal(result[10], static_comp(self.arr[:11])) + def _check_expanding(self, func, static_comp, has_min_periods=True, + has_time_rule=True, preserve_nan=True): + + series_result = func(self.series) + assert isinstance(series_result, Series) + frame_result = func(self.frame) + assert isinstance(frame_result, DataFrame) + + result = func(self.series) + tm.assert_almost_equal(result[10], static_comp(self.series[:11])) if preserve_nan: - assert (np.isnan(result[self._nan_locs]).all()) + assert result.iloc[self._nan_locs].isna().all() - arr = randn(50) + ser = Series(randn(50)) if has_min_periods: - result = func(arr, min_periods=30) - assert (np.isnan(result[:29]).all()) - tm.assert_almost_equal(result[-1], static_comp(arr[:50])) + result = func(ser, min_periods=30) + assert result[:29].isna().all() + tm.assert_almost_equal(result.iloc[-1], static_comp(ser[:50])) # min_periods is working correctly - result = func(arr, min_periods=15) - self.assertTrue(np.isnan(result[13])) - self.assertFalse(np.isnan(result[14])) + result = func(ser, min_periods=15) + assert isna(result.iloc[13]) + assert notna(result.iloc[14]) - arr2 = randn(20) - result = func(arr2, min_periods=5) - self.assertTrue(isnull(result[3])) - self.assertTrue(notnull(result[4])) + ser2 = Series(randn(20)) + result = func(ser2, min_periods=5) + assert isna(result[3]) + assert notna(result[4]) # min_periods=0 - result0 = func(arr, min_periods=0) - result1 = func(arr, min_periods=1) + result0 = func(ser, min_periods=0) + result1 = func(ser, min_periods=1) tm.assert_almost_equal(result0, result1) else: - result = func(arr) - tm.assert_almost_equal(result[-1], static_comp(arr[:50])) - - def _check_expanding_structures(self, func): - series_result = func(self.series) - tm.assertIsInstance(series_result, Series) - frame_result = func(self.frame) - self.assertEqual(type(frame_result), DataFrame) - - def _check_expanding(self, func, static_comp, has_min_periods=True, - has_time_rule=True, preserve_nan=True): - with warnings.catch_warnings(record=True): - self._check_expanding_ndarray(func, static_comp, - has_min_periods=has_min_periods, - has_time_rule=has_time_rule, - preserve_nan=preserve_nan) - with warnings.catch_warnings(record=True): - self._check_expanding_structures(func) + result = func(ser) + tm.assert_almost_equal(result.iloc[-1], static_comp(ser[:50])) def test_rolling_max_gh6297(self): """Replicate result expected in GH #6297""" @@ -2897,11 +3181,10 @@ def test_rolling_max_gh6297(self): expected = Series([1.0, 2.0, 6.0, 4.0, 5.0], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - x = series.rolling(window=1, freq='D').max() + x = series.resample('D').max().rolling(window=1).max() tm.assert_series_equal(expected, x) - def test_rolling_max_how_resample(self): + def test_rolling_max_resample(self): indices = [datetime(1975, 1, i) for i in range(1, 6)] # So that we can have 3 datapoints on last day (4, 10, and 20) @@ -2916,26 +3199,23 @@ def test_rolling_max_how_resample(self): # Default how should be max expected = Series([0.0, 1.0, 2.0, 3.0, 20.0], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - x = series.rolling(window=1, freq='D').max() + x = series.resample('D').max().rolling(window=1).max() tm.assert_series_equal(expected, x) # Now specify median (10.0) expected = Series([0.0, 1.0, 2.0, 3.0, 10.0], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - x = series.rolling(window=1, freq='D').max(how='median') + x = series.resample('D').median().rolling(window=1).max() tm.assert_series_equal(expected, x) # Now specify mean (4+10+20)/3 v = (4.0 + 10.0 + 20.0) / 3.0 expected = Series([0.0, 1.0, 2.0, 3.0, v], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - x = series.rolling(window=1, freq='D').max(how='mean') - tm.assert_series_equal(expected, x) + x = series.resample('D').mean().rolling(window=1).max() + tm.assert_series_equal(expected, x) - def test_rolling_min_how_resample(self): + def test_rolling_min_resample(self): indices = [datetime(1975, 1, i) for i in range(1, 6)] # So that we can have 3 datapoints on last day (4, 10, and 20) @@ -2950,11 +3230,10 @@ def test_rolling_min_how_resample(self): # Default how should be min expected = Series([0.0, 1.0, 2.0, 3.0, 4.0], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - r = series.rolling(window=1, freq='D') - tm.assert_series_equal(expected, r.min()) + r = series.resample('D').min().rolling(window=1) + tm.assert_series_equal(expected, r.min()) - def test_rolling_median_how_resample(self): + def test_rolling_median_resample(self): indices = [datetime(1975, 1, i) for i in range(1, 6)] # So that we can have 3 datapoints on last day (4, 10, and 20) @@ -2969,9 +3248,8 @@ def test_rolling_median_how_resample(self): # Default how should be median expected = Series([0.0, 1.0, 2.0, 3.0, 10], index=[datetime(1975, 1, i, 0) for i in range(1, 6)]) - with catch_warnings(record=True): - x = series.rolling(window=1, freq='D').median() - tm.assert_series_equal(expected, x) + x = series.resample('D').median().rolling(window=1).median() + tm.assert_series_equal(expected, x) def test_rolling_median_memory_error(self): # GH11722 @@ -2991,29 +3269,29 @@ def test_rolling_min_max_numeric_types(self): # correctness result = (DataFrame(np.arange(20, dtype=data_type)) .rolling(window=5).max()) - self.assertEqual(result.dtypes[0], np.dtype("f8")) + assert result.dtypes[0] == np.dtype("f8") result = (DataFrame(np.arange(20, dtype=data_type)) .rolling(window=5).min()) - self.assertEqual(result.dtypes[0], np.dtype("f8")) + assert result.dtypes[0] == np.dtype("f8") -class TestGrouperGrouping(tm.TestCase): +class TestGrouperGrouping(object): - def setUp(self): + def setup_method(self, method): self.series = Series(np.arange(10)) self.frame = DataFrame({'A': [1] * 20 + [2] * 12 + [3] * 8, 'B': np.arange(40)}) def test_mutated(self): - def f(): + msg = r"group\(\) got an unexpected keyword argument 'foo'" + with pytest.raises(TypeError, match=msg): self.frame.groupby('A', foo=1) - self.assertRaises(TypeError, f) g = self.frame.groupby('A') - self.assertFalse(g.mutated) + assert not g.mutated g = self.frame.groupby('A', mutated=True) - self.assertTrue(g.mutated) + assert g.mutated def test_getitem(self): g = self.frame.groupby('A') @@ -3085,13 +3363,36 @@ def func(x): expected = g.apply(func) tm.assert_series_equal(result, expected) - def test_rolling_apply(self): + def test_rolling_apply(self, raw): g = self.frame.groupby('A') r = g.rolling(window=4) # reduction - result = r.apply(lambda x: x.sum()) - expected = g.apply(lambda x: x.rolling(4).apply(lambda y: y.sum())) + result = r.apply(lambda x: x.sum(), raw=raw) + expected = g.apply( + lambda x: x.rolling(4).apply(lambda y: y.sum(), raw=raw)) + tm.assert_frame_equal(result, expected) + + def test_rolling_apply_mutability(self): + # GH 14013 + df = pd.DataFrame({'A': ['foo'] * 3 + ['bar'] * 3, 'B': [1] * 6}) + g = df.groupby('A') + + mi = pd.MultiIndex.from_tuples([('bar', 3), ('bar', 4), ('bar', 5), + ('foo', 0), ('foo', 1), ('foo', 2)]) + + mi.names = ['A', None] + # Grouped column should not be a part of the output + expected = pd.DataFrame([np.nan, 2., 2.] * 2, columns=['B'], index=mi) + + result = g.rolling(window=2).sum() + tm.assert_frame_equal(result, expected) + + # Call an arbitrary function on the groupby + g.sum() + + # Make sure nothing has been mutated + result = g.rolling(window=2).sum() tm.assert_frame_equal(result, expected) def test_expanding(self): @@ -3132,22 +3433,23 @@ def func(x): expected = g.apply(func) tm.assert_series_equal(result, expected) - def test_expanding_apply(self): + def test_expanding_apply(self, raw): g = self.frame.groupby('A') r = g.expanding() # reduction - result = r.apply(lambda x: x.sum()) - expected = g.apply(lambda x: x.expanding().apply(lambda y: y.sum())) + result = r.apply(lambda x: x.sum(), raw=raw) + expected = g.apply( + lambda x: x.expanding().apply(lambda y: y.sum(), raw=raw)) tm.assert_frame_equal(result, expected) -class TestRollingTS(tm.TestCase): +class TestRollingTS(object): # rolling time-series friendly # xref GH13327 - def setUp(self): + def setup_method(self, method): self.regular = DataFrame({'A': pd.date_range('20130101', periods=5, @@ -3177,16 +3479,16 @@ def test_valid(self): df = self.regular # not a valid freq - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling(window='foobar') # not a datetimelike index - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.reset_index().rolling(window='foobar') # non-fixed freqs for freq in ['2MS', pd.offsets.MonthBegin(2)]: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling(window=freq) for freq in ['1D', pd.offsets.Day(2), '2ms']: @@ -3194,11 +3496,11 @@ def test_valid(self): # non-integer min_periods for minp in [1.0, 'foo', np.array([1, 2, 3])]: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling(window='1D', min_periods=minp) # center is not implemented - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): df.rolling(window='1D', center=True) def test_on(self): @@ -3206,7 +3508,7 @@ def test_on(self): df = self.regular # not a valid column - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling(window='2s', on='foobar') # column is valid @@ -3215,7 +3517,7 @@ def test_on(self): df.rolling(window='2d', on='C').sum() # invalid columns - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling(window='2d', on='B') # ok even though on non-selected @@ -3229,22 +3531,22 @@ def test_monotonic_on(self): freq='s'), 'B': range(5)}) - self.assertTrue(df.A.is_monotonic) + assert df.A.is_monotonic df.rolling('2s', on='A').sum() df = df.set_index('A') - self.assertTrue(df.index.is_monotonic) + assert df.index.is_monotonic df.rolling('2s').sum() # non-monotonic df.index = reversed(df.index.tolist()) - self.assertFalse(df.index.is_monotonic) + assert not df.index.is_monotonic - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling('2s').sum() df = df.reset_index() - with self.assertRaises(ValueError): + with pytest.raises(ValueError): df.rolling('2s', on='A').sum() def test_frame_on(self): @@ -3276,7 +3578,7 @@ def test_frame_on(self): # test as a frame # we should be ignoring the 'on' as an aggregation column - # note that the expected is setting, computing, and reseting + # note that the expected is setting, computing, and resetting # so the columns need to be switched compared # to the actual result where they are ordered as in the # original @@ -3296,11 +3598,11 @@ def test_frame_on2(self): # using multiple aggregation columns df = DataFrame({'A': [0, 1, 2, 3, 4], 'B': [0, 1, 2, np.nan, 4], - 'C': pd.Index([pd.Timestamp('20130101 09:00:00'), - pd.Timestamp('20130101 09:00:02'), - pd.Timestamp('20130101 09:00:03'), - pd.Timestamp('20130101 09:00:05'), - pd.Timestamp('20130101 09:00:06')])}, + 'C': Index([Timestamp('20130101 09:00:00'), + Timestamp('20130101 09:00:02'), + Timestamp('20130101 09:00:03'), + Timestamp('20130101 09:00:05'), + Timestamp('20130101 09:00:06')])}, columns=['A', 'C', 'B']) expected1 = DataFrame({'A': [0., 1, 3, 3, 7], @@ -3356,6 +3658,45 @@ def test_min_periods(self): result = df.rolling('2s', min_periods=1).sum() tm.assert_frame_equal(result, expected) + def test_closed(self): + + # xref GH13965 + + df = DataFrame({'A': [1] * 5}, + index=[Timestamp('20130101 09:00:01'), + Timestamp('20130101 09:00:02'), + Timestamp('20130101 09:00:03'), + Timestamp('20130101 09:00:04'), + Timestamp('20130101 09:00:06')]) + + # closed must be 'right', 'left', 'both', 'neither' + with pytest.raises(ValueError): + self.regular.rolling(window='2s', closed="blabla") + + expected = df.copy() + expected["A"] = [1.0, 2, 2, 2, 1] + result = df.rolling('2s', closed='right').sum() + tm.assert_frame_equal(result, expected) + + # default should be 'right' + result = df.rolling('2s').sum() + tm.assert_frame_equal(result, expected) + + expected = df.copy() + expected["A"] = [1.0, 2, 3, 3, 2] + result = df.rolling('2s', closed='both').sum() + tm.assert_frame_equal(result, expected) + + expected = df.copy() + expected["A"] = [np.nan, 1.0, 2, 2, 1] + result = df.rolling('2s', closed='left').sum() + tm.assert_frame_equal(result, expected) + + expected = df.copy() + expected["A"] = [np.nan, 1.0, 1, 1, np.nan] + result = df.rolling('2s', closed='neither').sum() + tm.assert_frame_equal(result, expected) + def test_ragged_sum(self): df = self.ragged @@ -3435,7 +3776,7 @@ def test_ragged_quantile(self): result = df.rolling(window='2s', min_periods=1).quantile(0.5) expected = df.copy() - expected['B'] = [0.0, 1, 1.0, 3.0, 3.0] + expected['B'] = [0.0, 1, 1.5, 3.0, 3.5] tm.assert_frame_equal(result, expected) def test_ragged_std(self): @@ -3588,11 +3929,11 @@ def test_perf_min(self): freq='s')) expected = dfp.rolling(2, min_periods=1).min() result = dfp.rolling('2s').min() - self.assertTrue(((result - expected) < 0.01).all().bool()) + assert ((result - expected) < 0.01).all().bool() expected = dfp.rolling(200, min_periods=1).min() result = dfp.rolling('200s').min() - self.assertTrue(((result - expected) < 0.01).all().bool()) + assert ((result - expected) < 0.01).all().bool() def test_ragged_max(self): @@ -3613,29 +3954,29 @@ def test_ragged_max(self): expected['B'] = [0.0, 1, 2, 3, 4] tm.assert_frame_equal(result, expected) - def test_ragged_apply(self): + def test_ragged_apply(self, raw): df = self.ragged f = lambda x: 1 - result = df.rolling(window='1s', min_periods=1).apply(f) + result = df.rolling(window='1s', min_periods=1).apply(f, raw=raw) expected = df.copy() expected['B'] = 1. tm.assert_frame_equal(result, expected) - result = df.rolling(window='2s', min_periods=1).apply(f) + result = df.rolling(window='2s', min_periods=1).apply(f, raw=raw) expected = df.copy() expected['B'] = 1. tm.assert_frame_equal(result, expected) - result = df.rolling(window='5s', min_periods=1).apply(f) + result = df.rolling(window='5s', min_periods=1).apply(f, raw=raw) expected = df.copy() expected['B'] = 1. tm.assert_frame_equal(result, expected) def test_all(self): - # simple comparision of integer vs time-based windowing + # simple comparison of integer vs time-based windowing df = self.regular * 2 er = df.rolling(window=1) r = df.rolling(window='1s') @@ -3651,13 +3992,19 @@ def test_all(self): expected = er.quantile(0.5) tm.assert_frame_equal(result, expected) - result = r.apply(lambda x: 1) - expected = er.apply(lambda x: 1) + def test_all_apply(self, raw): + + df = self.regular * 2 + er = df.rolling(window=1) + r = df.rolling(window='1s') + + result = r.apply(lambda x: 1, raw=raw) + expected = er.apply(lambda x: 1, raw=raw) tm.assert_frame_equal(result, expected) def test_all2(self): - # more sophisticated comparision of integer vs. + # more sophisticated comparison of integer vs. # time-based windowing df = DataFrame({'B': np.arange(50)}, index=pd.date_range('20130101', @@ -3697,10 +4044,48 @@ def test_groupby_monotonic(self): ['Ryan', '3/31/2016', 50], ['Joe', '7/1/2015', 100], ['Joe', '9/9/2015', 500], ['Joe', '10/15/2015', 50]] - df = pd.DataFrame(data=data, columns=['name', 'date', 'amount']) + df = DataFrame(data=data, columns=['name', 'date', 'amount']) df['date'] = pd.to_datetime(df['date']) expected = df.set_index('date').groupby('name').apply( lambda x: x.rolling('180D')['amount'].sum()) result = df.groupby('name').rolling('180D', on='date')['amount'].sum() tm.assert_series_equal(result, expected) + + def test_non_monotonic(self): + # GH 13966 (similar to #15130, closed by #15175) + + dates = pd.date_range(start='2016-01-01 09:30:00', + periods=20, freq='s') + df = DataFrame({'A': [1] * 20 + [2] * 12 + [3] * 8, + 'B': np.concatenate((dates, dates)), + 'C': np.arange(40)}) + + result = df.groupby('A').rolling('4s', on='B').C.mean() + expected = df.set_index('B').groupby('A').apply( + lambda x: x.rolling('4s')['C'].mean()) + tm.assert_series_equal(result, expected) + + df2 = df.sort_values('B') + result = df2.groupby('A').rolling('4s', on='B').C.mean() + tm.assert_series_equal(result, expected) + + def test_rolling_cov_offset(self): + # GH16058 + + idx = pd.date_range('2017-01-01', periods=24, freq='1h') + ss = Series(np.arange(len(idx)), index=idx) + + result = ss.rolling('2h').cov() + expected = Series([np.nan] + [0.5] * (len(idx) - 1), index=idx) + tm.assert_series_equal(result, expected) + + expected2 = ss.rolling(2, min_periods=1).cov() + tm.assert_series_equal(result, expected2) + + result = ss.rolling('3h').cov() + expected = Series([np.nan, 0.5] + [1.0] * (len(idx) - 2), index=idx) + tm.assert_series_equal(result, expected) + + expected2 = ss.rolling(3, min_periods=1).cov() + tm.assert_series_equal(result, expected2) diff --git a/pandas/tests/tools/test_hashing.py b/pandas/tests/tools/test_hashing.py deleted file mode 100644 index 9bed0d428bc41..0000000000000 --- a/pandas/tests/tools/test_hashing.py +++ /dev/null @@ -1,234 +0,0 @@ -import numpy as np -import pandas as pd - -from pandas import DataFrame, Series, Index, MultiIndex -from pandas.tools.hashing import hash_array, hash_tuples, hash_pandas_object -import pandas.util.testing as tm - - -class TestHashing(tm.TestCase): - - def setUp(self): - self.df = DataFrame( - {'i32': np.array([1, 2, 3] * 3, dtype='int32'), - 'f32': np.array([None, 2.5, 3.5] * 3, dtype='float32'), - 'cat': Series(['a', 'b', 'c'] * 3).astype('category'), - 'obj': Series(['d', 'e', 'f'] * 3), - 'bool': np.array([True, False, True] * 3), - 'dt': Series(pd.date_range('20130101', periods=9)), - 'dt_tz': Series(pd.date_range('20130101', periods=9, - tz='US/Eastern')), - 'td': Series(pd.timedelta_range('2000', periods=9))}) - - def test_consistency(self): - # check that our hash doesn't change because of a mistake - # in the actual code; this is the ground truth - result = hash_pandas_object(Index(['foo', 'bar', 'baz'])) - expected = Series(np.array([3600424527151052760, 1374399572096150070, - 477881037637427054], dtype='uint64'), - index=['foo', 'bar', 'baz']) - tm.assert_series_equal(result, expected) - - def test_hash_array(self): - for name, s in self.df.iteritems(): - a = s.values - tm.assert_numpy_array_equal(hash_array(a), hash_array(a)) - - def test_hash_array_mixed(self): - result1 = hash_array(np.array([3, 4, 'All'])) - result2 = hash_array(np.array(['3', '4', 'All'])) - result3 = hash_array(np.array([3, 4, 'All'], dtype=object)) - tm.assert_numpy_array_equal(result1, result2) - tm.assert_numpy_array_equal(result1, result3) - - def test_hash_array_errors(self): - - for val in [5, 'foo', pd.Timestamp('20130101')]: - self.assertRaises(TypeError, hash_array, val) - - def check_equal(self, obj, **kwargs): - a = hash_pandas_object(obj, **kwargs) - b = hash_pandas_object(obj, **kwargs) - tm.assert_series_equal(a, b) - - kwargs.pop('index', None) - a = hash_pandas_object(obj, **kwargs) - b = hash_pandas_object(obj, **kwargs) - tm.assert_series_equal(a, b) - - def check_not_equal_with_index(self, obj): - - # check that we are not hashing the same if - # we include the index - if not isinstance(obj, Index): - a = hash_pandas_object(obj, index=True) - b = hash_pandas_object(obj, index=False) - if len(obj): - self.assertFalse((a == b).all()) - - def test_hash_tuples(self): - tups = [(1, 'one'), (1, 'two'), (2, 'one')] - result = hash_tuples(tups) - expected = hash_pandas_object(MultiIndex.from_tuples(tups)).values - self.assert_numpy_array_equal(result, expected) - - result = hash_tuples(tups[0]) - self.assertEqual(result, expected[0]) - - def test_hash_tuples_err(self): - - for val in [5, 'foo', pd.Timestamp('20130101')]: - self.assertRaises(TypeError, hash_tuples, val) - - def test_multiindex_unique(self): - mi = MultiIndex.from_tuples([(118, 472), (236, 118), - (51, 204), (102, 51)]) - self.assertTrue(mi.is_unique) - result = hash_pandas_object(mi) - self.assertTrue(result.is_unique) - - def test_hash_pandas_object(self): - - for obj in [Series([1, 2, 3]), - Series([1.0, 1.5, 3.2]), - Series([1.0, 1.5, np.nan]), - Series([1.0, 1.5, 3.2], index=[1.5, 1.1, 3.3]), - Series(['a', 'b', 'c']), - Series(['a', np.nan, 'c']), - Series(['a', None, 'c']), - Series([True, False, True]), - Series(), - Index([1, 2, 3]), - Index([True, False, True]), - DataFrame({'x': ['a', 'b', 'c'], 'y': [1, 2, 3]}), - DataFrame(), - tm.makeMissingDataframe(), - tm.makeMixedDataFrame(), - tm.makeTimeDataFrame(), - tm.makeTimeSeries(), - tm.makeTimedeltaIndex(), - tm.makePeriodIndex(), - Series(tm.makePeriodIndex()), - Series(pd.date_range('20130101', - periods=3, tz='US/Eastern')), - MultiIndex.from_product( - [range(5), - ['foo', 'bar', 'baz'], - pd.date_range('20130101', periods=2)]), - MultiIndex.from_product( - [pd.CategoricalIndex(list('aabc')), - range(3)])]: - self.check_equal(obj) - self.check_not_equal_with_index(obj) - - def test_hash_pandas_object2(self): - for name, s in self.df.iteritems(): - self.check_equal(s) - self.check_not_equal_with_index(s) - - def test_hash_pandas_empty_object(self): - for obj in [Series([], dtype='float64'), - Series([], dtype='object'), - Index([])]: - self.check_equal(obj) - - # these are by-definition the same with - # or w/o the index as the data is empty - - def test_categorical_consistency(self): - # GH15143 - # Check that categoricals hash consistent with their values, not codes - # This should work for categoricals of any dtype - for s1 in [Series(['a', 'b', 'c', 'd']), - Series([1000, 2000, 3000, 4000]), - Series(pd.date_range(0, periods=4))]: - s2 = s1.astype('category').cat.set_categories(s1) - s3 = s2.cat.set_categories(list(reversed(s1))) - for categorize in [True, False]: - # These should all hash identically - h1 = hash_pandas_object(s1, categorize=categorize) - h2 = hash_pandas_object(s2, categorize=categorize) - h3 = hash_pandas_object(s3, categorize=categorize) - tm.assert_series_equal(h1, h2) - tm.assert_series_equal(h1, h3) - - def test_categorical_with_nan_consistency(self): - c = pd.Categorical.from_codes( - [-1, 0, 1, 2, 3, 4], - categories=pd.date_range('2012-01-01', periods=5, name='B')) - expected = hash_array(c, categorize=False) - c = pd.Categorical.from_codes( - [-1, 0], - categories=[pd.Timestamp('2012-01-01')]) - result = hash_array(c, categorize=False) - assert result[0] in expected - assert result[1] in expected - - def test_pandas_errors(self): - - for obj in [pd.Timestamp('20130101'), tm.makePanel()]: - def f(): - hash_pandas_object(f) - - self.assertRaises(TypeError, f) - - def test_hash_keys(self): - # using different hash keys, should have different hashes - # for the same data - - # this only matters for object dtypes - obj = Series(list('abc')) - a = hash_pandas_object(obj, hash_key='9876543210123456') - b = hash_pandas_object(obj, hash_key='9876543210123465') - self.assertTrue((a != b).all()) - - def test_invalid_key(self): - # this only matters for object dtypes - def f(): - hash_pandas_object(Series(list('abc')), hash_key='foo') - self.assertRaises(ValueError, f) - - def test_alread_encoded(self): - # if already encoded then ok - - obj = Series(list('abc')).str.encode('utf8') - self.check_equal(obj) - - def test_alternate_encoding(self): - - obj = Series(list('abc')) - self.check_equal(obj, encoding='ascii') - - def test_same_len_hash_collisions(self): - - for l in range(8): - length = 2**(l + 8) + 1 - s = tm.rands_array(length, 2) - result = hash_array(s, 'utf8') - self.assertFalse(result[0] == result[1]) - - for l in range(8): - length = 2**(l + 8) - s = tm.rands_array(length, 2) - result = hash_array(s, 'utf8') - self.assertFalse(result[0] == result[1]) - - def test_hash_collisions(self): - - # hash collisions are bad - # https://github.com/pandas-dev/pandas/issues/14711#issuecomment-264885726 - L = ['Ingrid-9Z9fKIZmkO7i7Cn51Li34pJm44fgX6DYGBNj3VPlOH50m7HnBlPxfIwFMrcNJNMP6PSgLmwWnInciMWrCSAlLEvt7JkJl4IxiMrVbXSa8ZQoVaq5xoQPjltuJEfwdNlO6jo8qRRHvD8sBEBMQASrRa6TsdaPTPCBo3nwIBpE7YzzmyH0vMBhjQZLx1aCT7faSEx7PgFxQhHdKFWROcysamgy9iVj8DO2Fmwg1NNl93rIAqC3mdqfrCxrzfvIY8aJdzin2cHVzy3QUJxZgHvtUtOLxoqnUHsYbNTeq0xcLXpTZEZCxD4PGubIuCNf32c33M7HFsnjWSEjE2yVdWKhmSVodyF8hFYVmhYnMCztQnJrt3O8ZvVRXd5IKwlLexiSp4h888w7SzAIcKgc3g5XQJf6MlSMftDXm9lIsE1mJNiJEv6uY6pgvC3fUPhatlR5JPpVAHNSbSEE73MBzJrhCAbOLXQumyOXigZuPoME7QgJcBalliQol7YZ9', # noqa - 'Tim-b9MddTxOWW2AT1Py6vtVbZwGAmYCjbp89p8mxsiFoVX4FyDOF3wFiAkyQTUgwg9sVqVYOZo09Dh1AzhFHbgij52ylF0SEwgzjzHH8TGY8Lypart4p4onnDoDvVMBa0kdthVGKl6K0BDVGzyOXPXKpmnMF1H6rJzqHJ0HywfwS4XYpVwlAkoeNsiicHkJUFdUAhG229INzvIAiJuAHeJDUoyO4DCBqtoZ5TDend6TK7Y914yHlfH3g1WZu5LksKv68VQHJriWFYusW5e6ZZ6dKaMjTwEGuRgdT66iU5nqWTHRH8WSzpXoCFwGcTOwyuqPSe0fTe21DVtJn1FKj9F9nEnR9xOvJUO7E0piCIF4Ad9yAIDY4DBimpsTfKXCu1vdHpKYerzbndfuFe5AhfMduLYZJi5iAw8qKSwR5h86ttXV0Mc0QmXz8dsRvDgxjXSmupPxBggdlqUlC828hXiTPD7am0yETBV0F3bEtvPiNJfremszcV8NcqAoARMe'] # noqa - - # these should be different! - result1 = hash_array(np.asarray(L[0:1], dtype=object), 'utf8') - expected1 = np.array([14963968704024874985], dtype=np.uint64) - self.assert_numpy_array_equal(result1, expected1) - - result2 = hash_array(np.asarray(L[1:2], dtype=object), 'utf8') - expected2 = np.array([16428432627716348016], dtype=np.uint64) - self.assert_numpy_array_equal(result2, expected2) - - result = hash_array(np.asarray(L, dtype=object), 'utf8') - self.assert_numpy_array_equal( - result, np.concatenate([expected1, expected2], axis=0)) diff --git a/pandas/tests/tools/test_merge.py b/pandas/tests/tools/test_merge.py deleted file mode 100644 index 8011bc4a1cfc2..0000000000000 --- a/pandas/tests/tools/test_merge.py +++ /dev/null @@ -1,1405 +0,0 @@ -# pylint: disable=E1103 - -import pytest -from datetime import datetime -from numpy.random import randn -from numpy import nan -import numpy as np -import random - -import pandas as pd -from pandas.compat import lrange, lzip -from pandas.tools.concat import concat -from pandas.tools.merge import merge, MergeError -from pandas.util.testing import assert_frame_equal, assert_series_equal -from pandas.types.dtypes import CategoricalDtype -from pandas.types.common import is_categorical_dtype, is_object_dtype -from pandas import DataFrame, Index, MultiIndex, Series, Categorical -import pandas.util.testing as tm - - -N = 50 -NGROUPS = 8 - - -def get_test_data(ngroups=NGROUPS, n=N): - unique_groups = lrange(ngroups) - arr = np.asarray(np.tile(unique_groups, n // ngroups)) - - if len(arr) < n: - arr = np.asarray(list(arr) + unique_groups[:n - len(arr)]) - - random.shuffle(arr) - return arr - - -class TestMerge(tm.TestCase): - - def setUp(self): - # aggregate multiple columns - self.df = DataFrame({'key1': get_test_data(), - 'key2': get_test_data(), - 'data1': np.random.randn(N), - 'data2': np.random.randn(N)}) - - # exclude a couple keys for fun - self.df = self.df[self.df['key2'] > 1] - - self.df2 = DataFrame({'key1': get_test_data(n=N // 5), - 'key2': get_test_data(ngroups=NGROUPS // 2, - n=N // 5), - 'value': np.random.randn(N // 5)}) - - self.left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], - 'v1': np.random.randn(7)}) - self.right = DataFrame({'v2': np.random.randn(4)}, - index=['d', 'b', 'c', 'a']) - - def test_merge_inner_join_empty(self): - # GH 15328 - df_empty = pd.DataFrame() - df_a = pd.DataFrame({'a': [1, 2]}, index=[0, 1], dtype='int64') - result = pd.merge(df_empty, df_a, left_index=True, right_index=True) - expected = pd.DataFrame({'a': []}, index=[], dtype='int64') - assert_frame_equal(result, expected) - - def test_merge_common(self): - joined = merge(self.df, self.df2) - exp = merge(self.df, self.df2, on=['key1', 'key2']) - tm.assert_frame_equal(joined, exp) - - def test_merge_index_singlekey_right_vs_left(self): - left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], - 'v1': np.random.randn(7)}) - right = DataFrame({'v2': np.random.randn(4)}, - index=['d', 'b', 'c', 'a']) - - merged1 = merge(left, right, left_on='key', - right_index=True, how='left', sort=False) - merged2 = merge(right, left, right_on='key', - left_index=True, how='right', sort=False) - assert_frame_equal(merged1, merged2.loc[:, merged1.columns]) - - merged1 = merge(left, right, left_on='key', - right_index=True, how='left', sort=True) - merged2 = merge(right, left, right_on='key', - left_index=True, how='right', sort=True) - assert_frame_equal(merged1, merged2.loc[:, merged1.columns]) - - def test_merge_index_singlekey_inner(self): - left = DataFrame({'key': ['a', 'b', 'c', 'd', 'e', 'e', 'a'], - 'v1': np.random.randn(7)}) - right = DataFrame({'v2': np.random.randn(4)}, - index=['d', 'b', 'c', 'a']) - - # inner join - result = merge(left, right, left_on='key', right_index=True, - how='inner') - expected = left.join(right, on='key').loc[result.index] - assert_frame_equal(result, expected) - - result = merge(right, left, right_on='key', left_index=True, - how='inner') - expected = left.join(right, on='key').loc[result.index] - assert_frame_equal(result, expected.loc[:, result.columns]) - - def test_merge_misspecified(self): - self.assertRaises(ValueError, merge, self.left, self.right, - left_index=True) - self.assertRaises(ValueError, merge, self.left, self.right, - right_index=True) - - self.assertRaises(ValueError, merge, self.left, self.left, - left_on='key', on='key') - - self.assertRaises(ValueError, merge, self.df, self.df2, - left_on=['key1'], right_on=['key1', 'key2']) - - def test_index_and_on_parameters_confusion(self): - self.assertRaises(ValueError, merge, self.df, self.df2, how='left', - left_index=False, right_index=['key1', 'key2']) - self.assertRaises(ValueError, merge, self.df, self.df2, how='left', - left_index=['key1', 'key2'], right_index=False) - self.assertRaises(ValueError, merge, self.df, self.df2, how='left', - left_index=['key1', 'key2'], - right_index=['key1', 'key2']) - - def test_merge_overlap(self): - merged = merge(self.left, self.left, on='key') - exp_len = (self.left['key'].value_counts() ** 2).sum() - self.assertEqual(len(merged), exp_len) - self.assertIn('v1_x', merged) - self.assertIn('v1_y', merged) - - def test_merge_different_column_key_names(self): - left = DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'], - 'value': [1, 2, 3, 4]}) - right = DataFrame({'rkey': ['foo', 'bar', 'qux', 'foo'], - 'value': [5, 6, 7, 8]}) - - merged = left.merge(right, left_on='lkey', right_on='rkey', - how='outer', sort=True) - - exp = pd.Series(['bar', 'baz', 'foo', 'foo', 'foo', 'foo', np.nan], - name='lkey') - tm.assert_series_equal(merged['lkey'], exp) - - exp = pd.Series(['bar', np.nan, 'foo', 'foo', 'foo', 'foo', 'qux'], - name='rkey') - tm.assert_series_equal(merged['rkey'], exp) - - exp = pd.Series([2, 3, 1, 1, 4, 4, np.nan], name='value_x') - tm.assert_series_equal(merged['value_x'], exp) - - exp = pd.Series([6, np.nan, 5, 8, 5, 8, 7], name='value_y') - tm.assert_series_equal(merged['value_y'], exp) - - def test_merge_copy(self): - left = DataFrame({'a': 0, 'b': 1}, index=lrange(10)) - right = DataFrame({'c': 'foo', 'd': 'bar'}, index=lrange(10)) - - merged = merge(left, right, left_index=True, - right_index=True, copy=True) - - merged['a'] = 6 - self.assertTrue((left['a'] == 0).all()) - - merged['d'] = 'peekaboo' - self.assertTrue((right['d'] == 'bar').all()) - - def test_merge_nocopy(self): - left = DataFrame({'a': 0, 'b': 1}, index=lrange(10)) - right = DataFrame({'c': 'foo', 'd': 'bar'}, index=lrange(10)) - - merged = merge(left, right, left_index=True, - right_index=True, copy=False) - - merged['a'] = 6 - self.assertTrue((left['a'] == 6).all()) - - merged['d'] = 'peekaboo' - self.assertTrue((right['d'] == 'peekaboo').all()) - - def test_intelligently_handle_join_key(self): - # #733, be a bit more 1337 about not returning unconsolidated DataFrame - - left = DataFrame({'key': [1, 1, 2, 2, 3], - 'value': lrange(5)}, columns=['value', 'key']) - right = DataFrame({'key': [1, 1, 2, 3, 4, 5], - 'rvalue': lrange(6)}) - - joined = merge(left, right, on='key', how='outer') - expected = DataFrame({'key': [1, 1, 1, 1, 2, 2, 3, 4, 5], - 'value': np.array([0, 0, 1, 1, 2, 3, 4, - np.nan, np.nan]), - 'rvalue': [0, 1, 0, 1, 2, 2, 3, 4, 5]}, - columns=['value', 'key', 'rvalue']) - assert_frame_equal(joined, expected) - - def test_merge_join_key_dtype_cast(self): - # #8596 - - df1 = DataFrame({'key': [1], 'v1': [10]}) - df2 = DataFrame({'key': [2], 'v1': [20]}) - df = merge(df1, df2, how='outer') - self.assertEqual(df['key'].dtype, 'int64') - - df1 = DataFrame({'key': [True], 'v1': [1]}) - df2 = DataFrame({'key': [False], 'v1': [0]}) - df = merge(df1, df2, how='outer') - - # GH13169 - # this really should be bool - self.assertEqual(df['key'].dtype, 'object') - - df1 = DataFrame({'val': [1]}) - df2 = DataFrame({'val': [2]}) - lkey = np.array([1]) - rkey = np.array([2]) - df = merge(df1, df2, left_on=lkey, right_on=rkey, how='outer') - self.assertEqual(df['key_0'].dtype, 'int64') - - def test_handle_join_key_pass_array(self): - left = DataFrame({'key': [1, 1, 2, 2, 3], - 'value': lrange(5)}, columns=['value', 'key']) - right = DataFrame({'rvalue': lrange(6)}) - key = np.array([1, 1, 2, 3, 4, 5]) - - merged = merge(left, right, left_on='key', right_on=key, how='outer') - merged2 = merge(right, left, left_on=key, right_on='key', how='outer') - - assert_series_equal(merged['key'], merged2['key']) - self.assertTrue(merged['key'].notnull().all()) - self.assertTrue(merged2['key'].notnull().all()) - - left = DataFrame({'value': lrange(5)}, columns=['value']) - right = DataFrame({'rvalue': lrange(6)}) - lkey = np.array([1, 1, 2, 2, 3]) - rkey = np.array([1, 1, 2, 3, 4, 5]) - - merged = merge(left, right, left_on=lkey, right_on=rkey, how='outer') - self.assert_series_equal(merged['key_0'], - Series([1, 1, 1, 1, 2, 2, 3, 4, 5], - name='key_0')) - - left = DataFrame({'value': lrange(3)}) - right = DataFrame({'rvalue': lrange(6)}) - - key = np.array([0, 1, 1, 2, 2, 3], dtype=np.int64) - merged = merge(left, right, left_index=True, right_on=key, how='outer') - self.assert_series_equal(merged['key_0'], Series(key, name='key_0')) - - def test_no_overlap_more_informative_error(self): - dt = datetime.now() - df1 = DataFrame({'x': ['a']}, index=[dt]) - - df2 = DataFrame({'y': ['b', 'c']}, index=[dt, dt]) - self.assertRaises(MergeError, merge, df1, df2) - - def test_merge_non_unique_indexes(self): - - dt = datetime(2012, 5, 1) - dt2 = datetime(2012, 5, 2) - dt3 = datetime(2012, 5, 3) - dt4 = datetime(2012, 5, 4) - - df1 = DataFrame({'x': ['a']}, index=[dt]) - df2 = DataFrame({'y': ['b', 'c']}, index=[dt, dt]) - _check_merge(df1, df2) - - # Not monotonic - df1 = DataFrame({'x': ['a', 'b', 'q']}, index=[dt2, dt, dt4]) - df2 = DataFrame({'y': ['c', 'd', 'e', 'f', 'g', 'h']}, - index=[dt3, dt3, dt2, dt2, dt, dt]) - _check_merge(df1, df2) - - df1 = DataFrame({'x': ['a', 'b']}, index=[dt, dt]) - df2 = DataFrame({'y': ['c', 'd']}, index=[dt, dt]) - _check_merge(df1, df2) - - def test_merge_non_unique_index_many_to_many(self): - dt = datetime(2012, 5, 1) - dt2 = datetime(2012, 5, 2) - dt3 = datetime(2012, 5, 3) - df1 = DataFrame({'x': ['a', 'b', 'c', 'd']}, - index=[dt2, dt2, dt, dt]) - df2 = DataFrame({'y': ['e', 'f', 'g', ' h', 'i']}, - index=[dt2, dt2, dt3, dt, dt]) - _check_merge(df1, df2) - - def test_left_merge_empty_dataframe(self): - left = DataFrame({'key': [1], 'value': [2]}) - right = DataFrame({'key': []}) - - result = merge(left, right, on='key', how='left') - assert_frame_equal(result, left) - - result = merge(right, left, on='key', how='right') - assert_frame_equal(result, left) - - def test_merge_left_empty_right_empty(self): - # GH 10824 - left = pd.DataFrame([], columns=['a', 'b', 'c']) - right = pd.DataFrame([], columns=['x', 'y', 'z']) - - exp_in = pd.DataFrame([], columns=['a', 'b', 'c', 'x', 'y', 'z'], - index=pd.Index([], dtype=object), - dtype=object) - - for kwarg in [dict(left_index=True, right_index=True), - dict(left_index=True, right_on='x'), - dict(left_on='a', right_index=True), - dict(left_on='a', right_on='x')]: - - result = pd.merge(left, right, how='inner', **kwarg) - tm.assert_frame_equal(result, exp_in) - result = pd.merge(left, right, how='left', **kwarg) - tm.assert_frame_equal(result, exp_in) - result = pd.merge(left, right, how='right', **kwarg) - tm.assert_frame_equal(result, exp_in) - result = pd.merge(left, right, how='outer', **kwarg) - tm.assert_frame_equal(result, exp_in) - - def test_merge_left_empty_right_notempty(self): - # GH 10824 - left = pd.DataFrame([], columns=['a', 'b', 'c']) - right = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], - columns=['x', 'y', 'z']) - - exp_out = pd.DataFrame({'a': np.array([np.nan] * 3, dtype=object), - 'b': np.array([np.nan] * 3, dtype=object), - 'c': np.array([np.nan] * 3, dtype=object), - 'x': [1, 4, 7], - 'y': [2, 5, 8], - 'z': [3, 6, 9]}, - columns=['a', 'b', 'c', 'x', 'y', 'z']) - exp_in = exp_out[0:0] # make empty DataFrame keeping dtype - # result will have object dtype - exp_in.index = exp_in.index.astype(object) - - def check1(exp, kwarg): - result = pd.merge(left, right, how='inner', **kwarg) - tm.assert_frame_equal(result, exp) - result = pd.merge(left, right, how='left', **kwarg) - tm.assert_frame_equal(result, exp) - - def check2(exp, kwarg): - result = pd.merge(left, right, how='right', **kwarg) - tm.assert_frame_equal(result, exp) - result = pd.merge(left, right, how='outer', **kwarg) - tm.assert_frame_equal(result, exp) - - for kwarg in [dict(left_index=True, right_index=True), - dict(left_index=True, right_on='x')]: - check1(exp_in, kwarg) - check2(exp_out, kwarg) - - kwarg = dict(left_on='a', right_index=True) - check1(exp_in, kwarg) - exp_out['a'] = [0, 1, 2] - check2(exp_out, kwarg) - - kwarg = dict(left_on='a', right_on='x') - check1(exp_in, kwarg) - exp_out['a'] = np.array([np.nan] * 3, dtype=object) - check2(exp_out, kwarg) - - def test_merge_left_notempty_right_empty(self): - # GH 10824 - left = pd.DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]], - columns=['a', 'b', 'c']) - right = pd.DataFrame([], columns=['x', 'y', 'z']) - - exp_out = pd.DataFrame({'a': [1, 4, 7], - 'b': [2, 5, 8], - 'c': [3, 6, 9], - 'x': np.array([np.nan] * 3, dtype=object), - 'y': np.array([np.nan] * 3, dtype=object), - 'z': np.array([np.nan] * 3, dtype=object)}, - columns=['a', 'b', 'c', 'x', 'y', 'z']) - exp_in = exp_out[0:0] # make empty DataFrame keeping dtype - # result will have object dtype - exp_in.index = exp_in.index.astype(object) - - def check1(exp, kwarg): - result = pd.merge(left, right, how='inner', **kwarg) - tm.assert_frame_equal(result, exp) - result = pd.merge(left, right, how='right', **kwarg) - tm.assert_frame_equal(result, exp) - - def check2(exp, kwarg): - result = pd.merge(left, right, how='left', **kwarg) - tm.assert_frame_equal(result, exp) - result = pd.merge(left, right, how='outer', **kwarg) - tm.assert_frame_equal(result, exp) - - for kwarg in [dict(left_index=True, right_index=True), - dict(left_index=True, right_on='x'), - dict(left_on='a', right_index=True), - dict(left_on='a', right_on='x')]: - check1(exp_in, kwarg) - check2(exp_out, kwarg) - - def test_merge_nosort(self): - # #2098, anything to do? - - from datetime import datetime - - d = {"var1": np.random.randint(0, 10, size=10), - "var2": np.random.randint(0, 10, size=10), - "var3": [datetime(2012, 1, 12), datetime(2011, 2, 4), - datetime( - 2010, 2, 3), datetime(2012, 1, 12), - datetime( - 2011, 2, 4), datetime(2012, 4, 3), - datetime( - 2012, 3, 4), datetime(2008, 5, 1), - datetime(2010, 2, 3), datetime(2012, 2, 3)]} - df = DataFrame.from_dict(d) - var3 = df.var3.unique() - var3.sort() - new = DataFrame.from_dict({"var3": var3, - "var8": np.random.random(7)}) - - result = df.merge(new, on="var3", sort=False) - exp = merge(df, new, on='var3', sort=False) - assert_frame_equal(result, exp) - - self.assertTrue((df.var3.unique() == result.var3.unique()).all()) - - def test_merge_nan_right(self): - df1 = DataFrame({"i1": [0, 1], "i2": [0, 1]}) - df2 = DataFrame({"i1": [0], "i3": [0]}) - result = df1.join(df2, on="i1", rsuffix="_") - expected = (DataFrame({'i1': {0: 0.0, 1: 1}, 'i2': {0: 0, 1: 1}, - 'i1_': {0: 0, 1: np.nan}, - 'i3': {0: 0.0, 1: np.nan}, - None: {0: 0, 1: 0}}) - .set_index(None) - .reset_index()[['i1', 'i2', 'i1_', 'i3']]) - assert_frame_equal(result, expected, check_dtype=False) - - df1 = DataFrame({"i1": [0, 1], "i2": [0.5, 1.5]}) - df2 = DataFrame({"i1": [0], "i3": [0.7]}) - result = df1.join(df2, rsuffix="_", on='i1') - expected = (DataFrame({'i1': {0: 0, 1: 1}, 'i1_': {0: 0.0, 1: nan}, - 'i2': {0: 0.5, 1: 1.5}, - 'i3': {0: 0.69999999999999996, - 1: nan}}) - [['i1', 'i2', 'i1_', 'i3']]) - assert_frame_equal(result, expected) - - def test_merge_type(self): - class NotADataFrame(DataFrame): - - @property - def _constructor(self): - return NotADataFrame - - nad = NotADataFrame(self.df) - result = nad.merge(self.df2, on='key1') - - tm.assertIsInstance(result, NotADataFrame) - - def test_join_append_timedeltas(self): - - import datetime as dt - from pandas import NaT - - # timedelta64 issues with join/merge - # GH 5695 - - d = {'d': dt.datetime(2013, 11, 5, 5, 56), 't': dt.timedelta(0, 22500)} - df = DataFrame(columns=list('dt')) - df = df.append(d, ignore_index=True) - result = df.append(d, ignore_index=True) - expected = DataFrame({'d': [dt.datetime(2013, 11, 5, 5, 56), - dt.datetime(2013, 11, 5, 5, 56)], - 't': [dt.timedelta(0, 22500), - dt.timedelta(0, 22500)]}) - assert_frame_equal(result, expected) - - td = np.timedelta64(300000000) - lhs = DataFrame(Series([td, td], index=["A", "B"])) - rhs = DataFrame(Series([td], index=["A"])) - - result = lhs.join(rhs, rsuffix='r', how="left") - expected = DataFrame({'0': Series([td, td], index=list('AB')), - '0r': Series([td, NaT], index=list('AB'))}) - assert_frame_equal(result, expected) - - def test_other_datetime_unit(self): - # GH 13389 - df1 = pd.DataFrame({'entity_id': [101, 102]}) - s = pd.Series([None, None], index=[101, 102], name='days') - - for dtype in ['datetime64[D]', 'datetime64[h]', 'datetime64[m]', - 'datetime64[s]', 'datetime64[ms]', 'datetime64[us]', - 'datetime64[ns]']: - - df2 = s.astype(dtype).to_frame('days') - # coerces to datetime64[ns], thus sholuld not be affected - self.assertEqual(df2['days'].dtype, 'datetime64[ns]') - - result = df1.merge(df2, left_on='entity_id', right_index=True) - - exp = pd.DataFrame({'entity_id': [101, 102], - 'days': np.array(['nat', 'nat'], - dtype='datetime64[ns]')}, - columns=['entity_id', 'days']) - tm.assert_frame_equal(result, exp) - - def test_other_timedelta_unit(self): - # GH 13389 - df1 = pd.DataFrame({'entity_id': [101, 102]}) - s = pd.Series([None, None], index=[101, 102], name='days') - - for dtype in ['timedelta64[D]', 'timedelta64[h]', 'timedelta64[m]', - 'timedelta64[s]', 'timedelta64[ms]', 'timedelta64[us]', - 'timedelta64[ns]']: - - df2 = s.astype(dtype).to_frame('days') - self.assertEqual(df2['days'].dtype, dtype) - - result = df1.merge(df2, left_on='entity_id', right_index=True) - - exp = pd.DataFrame({'entity_id': [101, 102], - 'days': np.array(['nat', 'nat'], - dtype=dtype)}, - columns=['entity_id', 'days']) - tm.assert_frame_equal(result, exp) - - def test_overlapping_columns_error_message(self): - df = DataFrame({'key': [1, 2, 3], - 'v1': [4, 5, 6], - 'v2': [7, 8, 9]}) - df2 = DataFrame({'key': [1, 2, 3], - 'v1': [4, 5, 6], - 'v2': [7, 8, 9]}) - - df.columns = ['key', 'foo', 'foo'] - df2.columns = ['key', 'bar', 'bar'] - expected = DataFrame({'key': [1, 2, 3], - 'v1': [4, 5, 6], - 'v2': [7, 8, 9], - 'v3': [4, 5, 6], - 'v4': [7, 8, 9]}) - expected.columns = ['key', 'foo', 'foo', 'bar', 'bar'] - assert_frame_equal(merge(df, df2), expected) - - # #2649, #10639 - df2.columns = ['key1', 'foo', 'foo'] - self.assertRaises(ValueError, merge, df, df2) - - def test_merge_on_datetime64tz(self): - - # GH11405 - left = pd.DataFrame({'key': pd.date_range('20151010', periods=2, - tz='US/Eastern'), - 'value': [1, 2]}) - right = pd.DataFrame({'key': pd.date_range('20151011', periods=3, - tz='US/Eastern'), - 'value': [1, 2, 3]}) - - expected = DataFrame({'key': pd.date_range('20151010', periods=4, - tz='US/Eastern'), - 'value_x': [1, 2, np.nan, np.nan], - 'value_y': [np.nan, 1, 2, 3]}) - result = pd.merge(left, right, on='key', how='outer') - assert_frame_equal(result, expected) - - left = pd.DataFrame({'value': pd.date_range('20151010', periods=2, - tz='US/Eastern'), - 'key': [1, 2]}) - right = pd.DataFrame({'value': pd.date_range('20151011', periods=2, - tz='US/Eastern'), - 'key': [2, 3]}) - expected = DataFrame({ - 'value_x': list(pd.date_range('20151010', periods=2, - tz='US/Eastern')) + [pd.NaT], - 'value_y': [pd.NaT] + list(pd.date_range('20151011', periods=2, - tz='US/Eastern')), - 'key': [1, 2, 3]}) - result = pd.merge(left, right, on='key', how='outer') - assert_frame_equal(result, expected) - self.assertEqual(result['value_x'].dtype, 'datetime64[ns, US/Eastern]') - self.assertEqual(result['value_y'].dtype, 'datetime64[ns, US/Eastern]') - - def test_merge_on_periods(self): - left = pd.DataFrame({'key': pd.period_range('20151010', periods=2, - freq='D'), - 'value': [1, 2]}) - right = pd.DataFrame({'key': pd.period_range('20151011', periods=3, - freq='D'), - 'value': [1, 2, 3]}) - - expected = DataFrame({'key': pd.period_range('20151010', periods=4, - freq='D'), - 'value_x': [1, 2, np.nan, np.nan], - 'value_y': [np.nan, 1, 2, 3]}) - result = pd.merge(left, right, on='key', how='outer') - assert_frame_equal(result, expected) - - left = pd.DataFrame({'value': pd.period_range('20151010', periods=2, - freq='D'), - 'key': [1, 2]}) - right = pd.DataFrame({'value': pd.period_range('20151011', periods=2, - freq='D'), - 'key': [2, 3]}) - - exp_x = pd.period_range('20151010', periods=2, freq='D') - exp_y = pd.period_range('20151011', periods=2, freq='D') - expected = DataFrame({'value_x': list(exp_x) + [pd.NaT], - 'value_y': [pd.NaT] + list(exp_y), - 'key': [1, 2, 3]}) - result = pd.merge(left, right, on='key', how='outer') - assert_frame_equal(result, expected) - self.assertEqual(result['value_x'].dtype, 'object') - self.assertEqual(result['value_y'].dtype, 'object') - - def test_indicator(self): - # PR #10054. xref #7412 and closes #8790. - df1 = DataFrame({'col1': [0, 1], 'col_left': [ - 'a', 'b'], 'col_conflict': [1, 2]}) - df1_copy = df1.copy() - - df2 = DataFrame({'col1': [1, 2, 3, 4, 5], 'col_right': [2, 2, 2, 2, 2], - 'col_conflict': [1, 2, 3, 4, 5]}) - df2_copy = df2.copy() - - df_result = DataFrame({ - 'col1': [0, 1, 2, 3, 4, 5], - 'col_conflict_x': [1, 2, np.nan, np.nan, np.nan, np.nan], - 'col_left': ['a', 'b', np.nan, np.nan, np.nan, np.nan], - 'col_conflict_y': [np.nan, 1, 2, 3, 4, 5], - 'col_right': [np.nan, 2, 2, 2, 2, 2]}) - df_result['_merge'] = Categorical( - ['left_only', 'both', 'right_only', - 'right_only', 'right_only', 'right_only'], - categories=['left_only', 'right_only', 'both']) - - df_result = df_result[['col1', 'col_conflict_x', 'col_left', - 'col_conflict_y', 'col_right', '_merge']] - - test = merge(df1, df2, on='col1', how='outer', indicator=True) - assert_frame_equal(test, df_result) - test = df1.merge(df2, on='col1', how='outer', indicator=True) - assert_frame_equal(test, df_result) - - # No side effects - assert_frame_equal(df1, df1_copy) - assert_frame_equal(df2, df2_copy) - - # Check with custom name - df_result_custom_name = df_result - df_result_custom_name = df_result_custom_name.rename( - columns={'_merge': 'custom_name'}) - - test_custom_name = merge( - df1, df2, on='col1', how='outer', indicator='custom_name') - assert_frame_equal(test_custom_name, df_result_custom_name) - test_custom_name = df1.merge( - df2, on='col1', how='outer', indicator='custom_name') - assert_frame_equal(test_custom_name, df_result_custom_name) - - # Check only accepts strings and booleans - with tm.assertRaises(ValueError): - merge(df1, df2, on='col1', how='outer', indicator=5) - with tm.assertRaises(ValueError): - df1.merge(df2, on='col1', how='outer', indicator=5) - - # Check result integrity - - test2 = merge(df1, df2, on='col1', how='left', indicator=True) - self.assertTrue((test2._merge != 'right_only').all()) - test2 = df1.merge(df2, on='col1', how='left', indicator=True) - self.assertTrue((test2._merge != 'right_only').all()) - - test3 = merge(df1, df2, on='col1', how='right', indicator=True) - self.assertTrue((test3._merge != 'left_only').all()) - test3 = df1.merge(df2, on='col1', how='right', indicator=True) - self.assertTrue((test3._merge != 'left_only').all()) - - test4 = merge(df1, df2, on='col1', how='inner', indicator=True) - self.assertTrue((test4._merge == 'both').all()) - test4 = df1.merge(df2, on='col1', how='inner', indicator=True) - self.assertTrue((test4._merge == 'both').all()) - - # Check if working name in df - for i in ['_right_indicator', '_left_indicator', '_merge']: - df_badcolumn = DataFrame({'col1': [1, 2], i: [2, 2]}) - - with tm.assertRaises(ValueError): - merge(df1, df_badcolumn, on='col1', - how='outer', indicator=True) - with tm.assertRaises(ValueError): - df1.merge(df_badcolumn, on='col1', how='outer', indicator=True) - - # Check for name conflict with custom name - df_badcolumn = DataFrame( - {'col1': [1, 2], 'custom_column_name': [2, 2]}) - - with tm.assertRaises(ValueError): - merge(df1, df_badcolumn, on='col1', how='outer', - indicator='custom_column_name') - with tm.assertRaises(ValueError): - df1.merge(df_badcolumn, on='col1', how='outer', - indicator='custom_column_name') - - # Merge on multiple columns - df3 = DataFrame({'col1': [0, 1], 'col2': ['a', 'b']}) - - df4 = DataFrame({'col1': [1, 1, 3], 'col2': ['b', 'x', 'y']}) - - hand_coded_result = DataFrame({'col1': [0, 1, 1, 3], - 'col2': ['a', 'b', 'x', 'y']}) - hand_coded_result['_merge'] = Categorical( - ['left_only', 'both', 'right_only', 'right_only'], - categories=['left_only', 'right_only', 'both']) - - test5 = merge(df3, df4, on=['col1', 'col2'], - how='outer', indicator=True) - assert_frame_equal(test5, hand_coded_result) - test5 = df3.merge(df4, on=['col1', 'col2'], - how='outer', indicator=True) - assert_frame_equal(test5, hand_coded_result) - - -def _check_merge(x, y): - for how in ['inner', 'left', 'outer']: - result = x.join(y, how=how) - - expected = merge(x.reset_index(), y.reset_index(), how=how, - sort=True) - expected = expected.set_index('index') - - # TODO check_names on merge? - assert_frame_equal(result, expected, check_names=False) - - -class TestMergeMulti(tm.TestCase): - - def setUp(self): - self.index = MultiIndex(levels=[['foo', 'bar', 'baz', 'qux'], - ['one', 'two', 'three']], - labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3, 3], - [0, 1, 2, 0, 1, 1, 2, 0, 1, 2]], - names=['first', 'second']) - self.to_join = DataFrame(np.random.randn(10, 3), index=self.index, - columns=['j_one', 'j_two', 'j_three']) - - # a little relevant example with NAs - key1 = ['bar', 'bar', 'bar', 'foo', 'foo', 'baz', 'baz', 'qux', - 'qux', 'snap'] - key2 = ['two', 'one', 'three', 'one', 'two', 'one', 'two', 'two', - 'three', 'one'] - - data = np.random.randn(len(key1)) - self.data = DataFrame({'key1': key1, 'key2': key2, - 'data': data}) - - def test_merge_on_multikey(self): - joined = self.data.join(self.to_join, on=['key1', 'key2']) - - join_key = Index(lzip(self.data['key1'], self.data['key2'])) - indexer = self.to_join.index.get_indexer(join_key) - ex_values = self.to_join.values.take(indexer, axis=0) - ex_values[indexer == -1] = np.nan - expected = self.data.join(DataFrame(ex_values, - columns=self.to_join.columns)) - - # TODO: columns aren't in the same order yet - assert_frame_equal(joined, expected.loc[:, joined.columns]) - - left = self.data.join(self.to_join, on=['key1', 'key2'], sort=True) - right = expected.loc[:, joined.columns].sort_values(['key1', 'key2'], - kind='mergesort') - assert_frame_equal(left, right) - - def test_left_join_multi_index(self): - icols = ['1st', '2nd', '3rd'] - - def bind_cols(df): - iord = lambda a: 0 if a != a else ord(a) - f = lambda ts: ts.map(iord) - ord('a') - return (f(df['1st']) + f(df['3rd']) * 1e2 + - df['2nd'].fillna(0) * 1e4) - - def run_asserts(left, right): - for sort in [False, True]: - res = left.join(right, on=icols, how='left', sort=sort) - - self.assertTrue(len(left) < len(res) + 1) - self.assertFalse(res['4th'].isnull().any()) - self.assertFalse(res['5th'].isnull().any()) - - tm.assert_series_equal( - res['4th'], - res['5th'], check_names=False) - result = bind_cols(res.iloc[:, :-2]) - tm.assert_series_equal(res['4th'], result, check_names=False) - self.assertTrue(result.name is None) - - if sort: - tm.assert_frame_equal( - res, res.sort_values(icols, kind='mergesort')) - - out = merge(left, right.reset_index(), on=icols, - sort=sort, how='left') - - res.index = np.arange(len(res)) - tm.assert_frame_equal(out, res) - - lc = list(map(chr, np.arange(ord('a'), ord('z') + 1))) - left = DataFrame(np.random.choice(lc, (5000, 2)), - columns=['1st', '3rd']) - left.insert(1, '2nd', np.random.randint(0, 1000, len(left))) - - i = np.random.permutation(len(left)) - right = left.iloc[i].copy() - - left['4th'] = bind_cols(left) - right['5th'] = - bind_cols(right) - right.set_index(icols, inplace=True) - - run_asserts(left, right) - - # inject some nulls - left.loc[1::23, '1st'] = np.nan - left.loc[2::37, '2nd'] = np.nan - left.loc[3::43, '3rd'] = np.nan - left['4th'] = bind_cols(left) - - i = np.random.permutation(len(left)) - right = left.iloc[i, :-1] - right['5th'] = - bind_cols(right) - right.set_index(icols, inplace=True) - - run_asserts(left, right) - - def test_merge_right_vs_left(self): - # compare left vs right merge with multikey - for sort in [False, True]: - merged1 = self.data.merge(self.to_join, left_on=['key1', 'key2'], - right_index=True, how='left', sort=sort) - - merged2 = self.to_join.merge(self.data, right_on=['key1', 'key2'], - left_index=True, how='right', - sort=sort) - - merged2 = merged2.loc[:, merged1.columns] - assert_frame_equal(merged1, merged2) - - def test_compress_group_combinations(self): - - # ~ 40000000 possible unique groups - key1 = tm.rands_array(10, 10000) - key1 = np.tile(key1, 2) - key2 = key1[::-1] - - df = DataFrame({'key1': key1, 'key2': key2, - 'value1': np.random.randn(20000)}) - - df2 = DataFrame({'key1': key1[::2], 'key2': key2[::2], - 'value2': np.random.randn(10000)}) - - # just to hit the label compression code path - merge(df, df2, how='outer') - - def test_left_join_index_preserve_order(self): - - left = DataFrame({'k1': [0, 1, 2] * 8, - 'k2': ['foo', 'bar'] * 12, - 'v': np.array(np.arange(24), dtype=np.int64)}) - - index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) - right = DataFrame({'v2': [5, 7]}, index=index) - - result = left.join(right, on=['k1', 'k2']) - - expected = left.copy() - expected['v2'] = np.nan - expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 - expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 - - tm.assert_frame_equal(result, expected) - tm.assert_frame_equal( - result.sort_values(['k1', 'k2'], kind='mergesort'), - left.join(right, on=['k1', 'k2'], sort=True)) - - # test join with multi dtypes blocks - left = DataFrame({'k1': [0, 1, 2] * 8, - 'k2': ['foo', 'bar'] * 12, - 'k3': np.array([0, 1, 2] * 8, dtype=np.float32), - 'v': np.array(np.arange(24), dtype=np.int32)}) - - index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) - right = DataFrame({'v2': [5, 7]}, index=index) - - result = left.join(right, on=['k1', 'k2']) - - expected = left.copy() - expected['v2'] = np.nan - expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 - expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 - - tm.assert_frame_equal(result, expected) - tm.assert_frame_equal( - result.sort_values(['k1', 'k2'], kind='mergesort'), - left.join(right, on=['k1', 'k2'], sort=True)) - - # do a right join for an extra test - joined = merge(right, left, left_index=True, - right_on=['k1', 'k2'], how='right') - tm.assert_frame_equal(joined.loc[:, expected.columns], expected) - - def test_left_join_index_multi_match_multiindex(self): - left = DataFrame([ - ['X', 'Y', 'C', 'a'], - ['W', 'Y', 'C', 'e'], - ['V', 'Q', 'A', 'h'], - ['V', 'R', 'D', 'i'], - ['X', 'Y', 'D', 'b'], - ['X', 'Y', 'A', 'c'], - ['W', 'Q', 'B', 'f'], - ['W', 'R', 'C', 'g'], - ['V', 'Y', 'C', 'j'], - ['X', 'Y', 'B', 'd']], - columns=['cola', 'colb', 'colc', 'tag'], - index=[3, 2, 0, 1, 7, 6, 4, 5, 9, 8]) - - right = DataFrame([ - ['W', 'R', 'C', 0], - ['W', 'Q', 'B', 3], - ['W', 'Q', 'B', 8], - ['X', 'Y', 'A', 1], - ['X', 'Y', 'A', 4], - ['X', 'Y', 'B', 5], - ['X', 'Y', 'C', 6], - ['X', 'Y', 'C', 9], - ['X', 'Q', 'C', -6], - ['X', 'R', 'C', -9], - ['V', 'Y', 'C', 7], - ['V', 'R', 'D', 2], - ['V', 'R', 'D', -1], - ['V', 'Q', 'A', -3]], - columns=['col1', 'col2', 'col3', 'val']) - - right.set_index(['col1', 'col2', 'col3'], inplace=True) - result = left.join(right, on=['cola', 'colb', 'colc'], how='left') - - expected = DataFrame([ - ['X', 'Y', 'C', 'a', 6], - ['X', 'Y', 'C', 'a', 9], - ['W', 'Y', 'C', 'e', nan], - ['V', 'Q', 'A', 'h', -3], - ['V', 'R', 'D', 'i', 2], - ['V', 'R', 'D', 'i', -1], - ['X', 'Y', 'D', 'b', nan], - ['X', 'Y', 'A', 'c', 1], - ['X', 'Y', 'A', 'c', 4], - ['W', 'Q', 'B', 'f', 3], - ['W', 'Q', 'B', 'f', 8], - ['W', 'R', 'C', 'g', 0], - ['V', 'Y', 'C', 'j', 7], - ['X', 'Y', 'B', 'd', 5]], - columns=['cola', 'colb', 'colc', 'tag', 'val'], - index=[3, 3, 2, 0, 1, 1, 7, 6, 6, 4, 4, 5, 9, 8]) - - tm.assert_frame_equal(result, expected) - - result = left.join(right, on=['cola', 'colb', 'colc'], - how='left', sort=True) - - tm.assert_frame_equal( - result, - expected.sort_values(['cola', 'colb', 'colc'], kind='mergesort')) - - # GH7331 - maintain left frame order in left merge - right.reset_index(inplace=True) - right.columns = left.columns[:3].tolist() + right.columns[-1:].tolist() - result = merge(left, right, how='left', on=left.columns[:-1].tolist()) - expected.index = np.arange(len(expected)) - tm.assert_frame_equal(result, expected) - - def test_left_join_index_multi_match(self): - left = DataFrame([ - ['c', 0], - ['b', 1], - ['a', 2], - ['b', 3]], - columns=['tag', 'val'], - index=[2, 0, 1, 3]) - - right = DataFrame([ - ['a', 'v'], - ['c', 'w'], - ['c', 'x'], - ['d', 'y'], - ['a', 'z'], - ['c', 'r'], - ['e', 'q'], - ['c', 's']], - columns=['tag', 'char']) - - right.set_index('tag', inplace=True) - result = left.join(right, on='tag', how='left') - - expected = DataFrame([ - ['c', 0, 'w'], - ['c', 0, 'x'], - ['c', 0, 'r'], - ['c', 0, 's'], - ['b', 1, nan], - ['a', 2, 'v'], - ['a', 2, 'z'], - ['b', 3, nan]], - columns=['tag', 'val', 'char'], - index=[2, 2, 2, 2, 0, 1, 1, 3]) - - tm.assert_frame_equal(result, expected) - - result = left.join(right, on='tag', how='left', sort=True) - tm.assert_frame_equal( - result, expected.sort_values('tag', kind='mergesort')) - - # GH7331 - maintain left frame order in left merge - result = merge(left, right.reset_index(), how='left', on='tag') - expected.index = np.arange(len(expected)) - tm.assert_frame_equal(result, expected) - - def test_left_merge_na_buglet(self): - left = DataFrame({'id': list('abcde'), 'v1': randn(5), - 'v2': randn(5), 'dummy': list('abcde'), - 'v3': randn(5)}, - columns=['id', 'v1', 'v2', 'dummy', 'v3']) - right = DataFrame({'id': ['a', 'b', np.nan, np.nan, np.nan], - 'sv3': [1.234, 5.678, np.nan, np.nan, np.nan]}) - - merged = merge(left, right, on='id', how='left') - - rdf = right.drop(['id'], axis=1) - expected = left.join(rdf) - tm.assert_frame_equal(merged, expected) - - def test_merge_na_keys(self): - data = [[1950, "A", 1.5], - [1950, "B", 1.5], - [1955, "B", 1.5], - [1960, "B", np.nan], - [1970, "B", 4.], - [1950, "C", 4.], - [1960, "C", np.nan], - [1965, "C", 3.], - [1970, "C", 4.]] - - frame = DataFrame(data, columns=["year", "panel", "data"]) - - other_data = [[1960, 'A', np.nan], - [1970, 'A', np.nan], - [1955, 'A', np.nan], - [1965, 'A', np.nan], - [1965, 'B', np.nan], - [1955, 'C', np.nan]] - other = DataFrame(other_data, columns=['year', 'panel', 'data']) - - result = frame.merge(other, how='outer') - - expected = frame.fillna(-999).merge(other.fillna(-999), how='outer') - expected = expected.replace(-999, np.nan) - - tm.assert_frame_equal(result, expected) - - def test_join_multi_levels(self): - - # GH 3662 - # merge multi-levels - household = ( - DataFrame( - dict(household_id=[1, 2, 3], - male=[0, 1, 0], - wealth=[196087.3, 316478.7, 294750]), - columns=['household_id', 'male', 'wealth']) - .set_index('household_id')) - portfolio = ( - DataFrame( - dict(household_id=[1, 2, 2, 3, 3, 3, 4], - asset_id=["nl0000301109", "nl0000289783", "gb00b03mlx29", - "gb00b03mlx29", "lu0197800237", "nl0000289965", - np.nan], - name=["ABN Amro", "Robeco", "Royal Dutch Shell", - "Royal Dutch Shell", - "AAB Eastern Europe Equity Fund", - "Postbank BioTech Fonds", np.nan], - share=[1.0, 0.4, 0.6, 0.15, 0.6, 0.25, 1.0]), - columns=['household_id', 'asset_id', 'name', 'share']) - .set_index(['household_id', 'asset_id'])) - result = household.join(portfolio, how='inner') - expected = ( - DataFrame( - dict(male=[0, 1, 1, 0, 0, 0], - wealth=[196087.3, 316478.7, 316478.7, - 294750.0, 294750.0, 294750.0], - name=['ABN Amro', 'Robeco', 'Royal Dutch Shell', - 'Royal Dutch Shell', - 'AAB Eastern Europe Equity Fund', - 'Postbank BioTech Fonds'], - share=[1.00, 0.40, 0.60, 0.15, 0.60, 0.25], - household_id=[1, 2, 2, 3, 3, 3], - asset_id=['nl0000301109', 'nl0000289783', 'gb00b03mlx29', - 'gb00b03mlx29', 'lu0197800237', - 'nl0000289965'])) - .set_index(['household_id', 'asset_id']) - .reindex(columns=['male', 'wealth', 'name', 'share'])) - assert_frame_equal(result, expected) - - assert_frame_equal(result, expected) - - # equivalency - result2 = (merge(household.reset_index(), portfolio.reset_index(), - on=['household_id'], how='inner') - .set_index(['household_id', 'asset_id'])) - assert_frame_equal(result2, expected) - - result = household.join(portfolio, how='outer') - expected = (concat([ - expected, - (DataFrame( - dict(share=[1.00]), - index=MultiIndex.from_tuples( - [(4, np.nan)], - names=['household_id', 'asset_id']))) - ], axis=0).reindex(columns=expected.columns)) - assert_frame_equal(result, expected) - - # invalid cases - household.index.name = 'foo' - - def f(): - household.join(portfolio, how='inner') - self.assertRaises(ValueError, f) - - portfolio2 = portfolio.copy() - portfolio2.index.set_names(['household_id', 'foo']) - - def f(): - portfolio2.join(portfolio, how='inner') - self.assertRaises(ValueError, f) - - def test_join_multi_levels2(self): - - # some more advanced merges - # GH6360 - household = ( - DataFrame( - dict(household_id=[1, 2, 2, 3, 3, 3, 4], - asset_id=["nl0000301109", "nl0000301109", "gb00b03mlx29", - "gb00b03mlx29", "lu0197800237", "nl0000289965", - np.nan], - share=[1.0, 0.4, 0.6, 0.15, 0.6, 0.25, 1.0]), - columns=['household_id', 'asset_id', 'share']) - .set_index(['household_id', 'asset_id'])) - - log_return = DataFrame(dict( - asset_id=["gb00b03mlx29", "gb00b03mlx29", - "gb00b03mlx29", "lu0197800237", "lu0197800237"], - t=[233, 234, 235, 180, 181], - log_return=[.09604978, -.06524096, .03532373, .03025441, .036997] - )).set_index(["asset_id", "t"]) - - expected = ( - DataFrame(dict( - household_id=[2, 2, 2, 3, 3, 3, 3, 3], - asset_id=["gb00b03mlx29", "gb00b03mlx29", - "gb00b03mlx29", "gb00b03mlx29", - "gb00b03mlx29", "gb00b03mlx29", - "lu0197800237", "lu0197800237"], - t=[233, 234, 235, 233, 234, 235, 180, 181], - share=[0.6, 0.6, 0.6, 0.15, 0.15, 0.15, 0.6, 0.6], - log_return=[.09604978, -.06524096, .03532373, - .09604978, -.06524096, .03532373, - .03025441, .036997] - )) - .set_index(["household_id", "asset_id", "t"]) - .reindex(columns=['share', 'log_return'])) - - def f(): - household.join(log_return, how='inner') - self.assertRaises(NotImplementedError, f) - - # this is the equivalency - result = (merge(household.reset_index(), log_return.reset_index(), - on=['asset_id'], how='inner') - .set_index(['household_id', 'asset_id', 't'])) - assert_frame_equal(result, expected) - - expected = ( - DataFrame(dict( - household_id=[1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4], - asset_id=["nl0000301109", "nl0000289783", "gb00b03mlx29", - "gb00b03mlx29", "gb00b03mlx29", - "gb00b03mlx29", "gb00b03mlx29", "gb00b03mlx29", - "lu0197800237", "lu0197800237", - "nl0000289965", None], - t=[None, None, 233, 234, 235, 233, 234, - 235, 180, 181, None, None], - share=[1.0, 0.4, 0.6, 0.6, 0.6, 0.15, - 0.15, 0.15, 0.6, 0.6, 0.25, 1.0], - log_return=[None, None, .09604978, -.06524096, .03532373, - .09604978, -.06524096, .03532373, - .03025441, .036997, None, None] - )) - .set_index(["household_id", "asset_id", "t"])) - - def f(): - household.join(log_return, how='outer') - self.assertRaises(NotImplementedError, f) - - -@pytest.fixture -def df(): - return DataFrame( - {'A': ['foo', 'bar'], - 'B': Series(['foo', 'bar']).astype('category'), - 'C': [1, 2], - 'D': [1.0, 2.0], - 'E': Series([1, 2], dtype='uint64'), - 'F': Series([1, 2], dtype='int32')}) - - -class TestMergeDtypes(object): - - def test_different(self, df): - - # we expect differences by kind - # to be ok, while other differences should return object - - left = df - for col in df.columns: - right = DataFrame({'A': df[col]}) - result = pd.merge(left, right, on='A') - assert is_object_dtype(result.A.dtype) - - @pytest.mark.parametrize('d1', [np.int64, np.int32, - np.int16, np.int8, np.uint8]) - @pytest.mark.parametrize('d2', [np.int64, np.float64, - np.float32, np.float16]) - def test_join_multi_dtypes(self, d1, d2): - - dtype1 = np.dtype(d1) - dtype2 = np.dtype(d2) - - left = DataFrame({'k1': np.array([0, 1, 2] * 8, dtype=dtype1), - 'k2': ['foo', 'bar'] * 12, - 'v': np.array(np.arange(24), dtype=np.int64)}) - - index = MultiIndex.from_tuples([(2, 'bar'), (1, 'foo')]) - right = DataFrame({'v2': np.array([5, 7], dtype=dtype2)}, index=index) - - result = left.join(right, on=['k1', 'k2']) - - expected = left.copy() - - if dtype2.kind == 'i': - dtype2 = np.dtype('float64') - expected['v2'] = np.array(np.nan, dtype=dtype2) - expected.loc[(expected.k1 == 2) & (expected.k2 == 'bar'), 'v2'] = 5 - expected.loc[(expected.k1 == 1) & (expected.k2 == 'foo'), 'v2'] = 7 - - tm.assert_frame_equal(result, expected) - - result = left.join(right, on=['k1', 'k2'], sort=True) - expected.sort_values(['k1', 'k2'], kind='mergesort', inplace=True) - tm.assert_frame_equal(result, expected) - - -@pytest.fixture -def left(): - np.random.seed(1234) - return DataFrame( - {'X': Series(np.random.choice( - ['foo', 'bar'], - size=(10,))).astype('category', categories=['foo', 'bar']), - 'Y': np.random.choice(['one', 'two', 'three'], size=(10,))}) - - -@pytest.fixture -def right(): - np.random.seed(1234) - return DataFrame( - {'X': Series(['foo', 'bar']).astype('category', - categories=['foo', 'bar']), - 'Z': [1, 2]}) - - -class TestMergeCategorical(object): - - def test_identical(self, left): - # merging on the same, should preserve dtypes - merged = pd.merge(left, left, on='X') - result = merged.dtypes.sort_index() - expected = Series([CategoricalDtype(), - np.dtype('O'), - np.dtype('O')], - index=['X', 'Y_x', 'Y_y']) - assert_series_equal(result, expected) - - def test_basic(self, left, right): - # we have matching Categorical dtypes in X - # so should preserve the merged column - merged = pd.merge(left, right, on='X') - result = merged.dtypes.sort_index() - expected = Series([CategoricalDtype(), - np.dtype('O'), - np.dtype('int64')], - index=['X', 'Y', 'Z']) - assert_series_equal(result, expected) - - def test_other_columns(self, left, right): - # non-merge columns should preserve if possible - right = right.assign(Z=right.Z.astype('category')) - - merged = pd.merge(left, right, on='X') - result = merged.dtypes.sort_index() - expected = Series([CategoricalDtype(), - np.dtype('O'), - CategoricalDtype()], - index=['X', 'Y', 'Z']) - assert_series_equal(result, expected) - - # categories are preserved - assert left.X.values.is_dtype_equal(merged.X.values) - assert right.Z.values.is_dtype_equal(merged.Z.values) - - @pytest.mark.parametrize( - 'change', [lambda x: x, - lambda x: x.astype('category', - categories=['bar', 'foo']), - lambda x: x.astype('category', - categories=['foo', 'bar', 'bah']), - lambda x: x.astype('category', ordered=True)]) - @pytest.mark.parametrize('how', ['inner', 'outer', 'left', 'right']) - def test_dtype_on_merged_different(self, change, how, left, right): - # our merging columns, X now has 2 different dtypes - # so we must be object as a result - - X = change(right.X.astype('object')) - right = right.assign(X=X) - assert is_categorical_dtype(left.X.values) - assert not left.X.values.is_dtype_equal(right.X.values) - - merged = pd.merge(left, right, on='X', how=how) - - result = merged.dtypes.sort_index() - expected = Series([np.dtype('O'), - np.dtype('O'), - np.dtype('int64')], - index=['X', 'Y', 'Z']) - assert_series_equal(result, expected) - - -@pytest.fixture -def left_df(): - return DataFrame({'a': [20, 10, 0]}, index=[2, 1, 0]) - - -@pytest.fixture -def right_df(): - return DataFrame({'b': [300, 100, 200]}, index=[3, 1, 2]) - - -class TestMergeOnIndexes(object): - - @pytest.mark.parametrize( - "how, sort, expected", - [('inner', False, DataFrame({'a': [20, 10], - 'b': [200, 100]}, - index=[2, 1])), - ('inner', True, DataFrame({'a': [10, 20], - 'b': [100, 200]}, - index=[1, 2])), - ('left', False, DataFrame({'a': [20, 10, 0], - 'b': [200, 100, np.nan]}, - index=[2, 1, 0])), - ('left', True, DataFrame({'a': [0, 10, 20], - 'b': [np.nan, 100, 200]}, - index=[0, 1, 2])), - ('right', False, DataFrame({'a': [np.nan, 10, 20], - 'b': [300, 100, 200]}, - index=[3, 1, 2])), - ('right', True, DataFrame({'a': [10, 20, np.nan], - 'b': [100, 200, 300]}, - index=[1, 2, 3])), - ('outer', False, DataFrame({'a': [0, 10, 20, np.nan], - 'b': [np.nan, 100, 200, 300]}, - index=[0, 1, 2, 3])), - ('outer', True, DataFrame({'a': [0, 10, 20, np.nan], - 'b': [np.nan, 100, 200, 300]}, - index=[0, 1, 2, 3]))]) - def test_merge_on_indexes(self, left_df, right_df, how, sort, expected): - - result = pd.merge(left_df, right_df, - left_index=True, - right_index=True, - how=how, - sort=sort) - tm.assert_frame_equal(result, expected) diff --git a/pandas/tests/tools/test_numeric.py b/pandas/tests/tools/test_numeric.py new file mode 100644 index 0000000000000..97e1dc2f6aefc --- /dev/null +++ b/pandas/tests/tools/test_numeric.py @@ -0,0 +1,583 @@ +import decimal + +import numpy as np +from numpy import iinfo +import pytest + +import pandas.compat as compat + +import pandas as pd +from pandas import DataFrame, Index, Series, to_numeric +from pandas.util import testing as tm + + +@pytest.fixture(params=[None, "ignore", "raise", "coerce"]) +def errors(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def signed(request): + return request.param + + +@pytest.fixture(params=[lambda x: x, str], ids=["identity", "str"]) +def transform(request): + return request.param + + +@pytest.fixture(params=[ + 47393996303418497800, + 100000000000000000000 +]) +def large_val(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def multiple_elts(request): + return request.param + + +@pytest.fixture(params=[ + (lambda x: Index(x, name="idx"), tm.assert_index_equal), + (lambda x: Series(x, name="ser"), tm.assert_series_equal), + (lambda x: np.array(Index(x).values), tm.assert_numpy_array_equal) +]) +def transform_assert_equal(request): + return request.param + + +@pytest.mark.parametrize("input_kwargs,result_kwargs", [ + (dict(), dict(dtype=np.int64)), + (dict(errors="coerce", downcast="integer"), dict(dtype=np.int8)) +]) +def test_empty(input_kwargs, result_kwargs): + # see gh-16302 + ser = Series([], dtype=object) + result = to_numeric(ser, **input_kwargs) + + expected = Series([], **result_kwargs) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("last_val", ["7", 7]) +def test_series(last_val): + ser = Series(["1", "-3.14", last_val]) + result = to_numeric(ser) + + expected = Series([1, -3.14, 7]) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("data", [ + [1, 3, 4, 5], + [1., 3., 4., 5.], + + # Bool is regarded as numeric. + [True, False, True, True] +]) +def test_series_numeric(data): + ser = Series(data, index=list("ABCD"), name="EFG") + + result = to_numeric(ser) + tm.assert_series_equal(result, ser) + + +@pytest.mark.parametrize("data,msg", [ + ([1, -3.14, "apple"], + 'Unable to parse string "apple" at position 2'), + (["orange", 1, -3.14, "apple"], + 'Unable to parse string "orange" at position 0') +]) +def test_error(data, msg): + ser = Series(data) + + with pytest.raises(ValueError, match=msg): + to_numeric(ser, errors="raise") + + +@pytest.mark.parametrize("errors,exp_data", [ + ("ignore", [1, -3.14, "apple"]), + ("coerce", [1, -3.14, np.nan]) +]) +def test_ignore_error(errors, exp_data): + ser = Series([1, -3.14, "apple"]) + result = to_numeric(ser, errors=errors) + + expected = Series(exp_data) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("errors,exp", [ + ("raise", 'Unable to parse string "apple" at position 2'), + ("ignore", [True, False, "apple"]), + + # Coerces to float. + ("coerce", [1., 0., np.nan]) +]) +def test_bool_handling(errors, exp): + ser = Series([True, False, "apple"]) + + if isinstance(exp, str): + with pytest.raises(ValueError, match=exp): + to_numeric(ser, errors=errors) + else: + result = to_numeric(ser, errors=errors) + expected = Series(exp) + + tm.assert_series_equal(result, expected) + + +def test_list(): + ser = ["1", "-3.14", "7"] + res = to_numeric(ser) + + expected = np.array([1, -3.14, 7]) + tm.assert_numpy_array_equal(res, expected) + + +@pytest.mark.parametrize("data,arr_kwargs", [ + ([1, 3, 4, 5], dict(dtype=np.int64)), + ([1., 3., 4., 5.], dict()), + + # Boolean is regarded as numeric. + ([True, False, True, True], dict()) +]) +def test_list_numeric(data, arr_kwargs): + result = to_numeric(data) + expected = np.array(data, **arr_kwargs) + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize("kwargs", [ + dict(dtype="O"), dict() +]) +def test_numeric(kwargs): + data = [1, -3.14, 7] + + ser = Series(data, **kwargs) + result = to_numeric(ser) + + expected = Series(data) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("columns", [ + # One column. + "a", + + # Multiple columns. + ["a", "b"] +]) +def test_numeric_df_columns(columns): + # see gh-14827 + df = DataFrame(dict( + a=[1.2, decimal.Decimal(3.14), decimal.Decimal("infinity"), "0.1"], + b=[1.0, 2.0, 3.0, 4.0], + )) + + expected = DataFrame(dict( + a=[1.2, 3.14, np.inf, 0.1], + b=[1.0, 2.0, 3.0, 4.0], + )) + + df_copy = df.copy() + df_copy[columns] = df_copy[columns].apply(to_numeric) + + tm.assert_frame_equal(df_copy, expected) + + +@pytest.mark.parametrize("data,exp_data", [ + ([[decimal.Decimal(3.14), 1.0], decimal.Decimal(1.6), 0.1], + [[3.14, 1.0], 1.6, 0.1]), + ([np.array([decimal.Decimal(3.14), 1.0]), 0.1], + [[3.14, 1.0], 0.1]) +]) +def test_numeric_embedded_arr_likes(data, exp_data): + # Test to_numeric with embedded lists and arrays + df = DataFrame(dict(a=data)) + df["a"] = df["a"].apply(to_numeric) + + expected = DataFrame(dict(a=exp_data)) + tm.assert_frame_equal(df, expected) + + +def test_all_nan(): + ser = Series(["a", "b", "c"]) + result = to_numeric(ser, errors="coerce") + + expected = Series([np.nan, np.nan, np.nan]) + tm.assert_series_equal(result, expected) + + +def test_type_check(errors): + # see gh-11776 + df = DataFrame({"a": [1, -3.14, 7], "b": ["4", "5", "6"]}) + kwargs = dict(errors=errors) if errors is not None else dict() + error_ctx = pytest.raises(TypeError, match="1-d array") + + with error_ctx: + to_numeric(df, **kwargs) + + +@pytest.mark.parametrize("val", [1, 1.1, 20001]) +def test_scalar(val, signed, transform): + val = -val if signed else val + assert to_numeric(transform(val)) == float(val) + + +def test_really_large_scalar(large_val, signed, transform, errors): + # see gh-24910 + kwargs = dict(errors=errors) if errors is not None else dict() + val = -large_val if signed else large_val + + val = transform(val) + val_is_string = isinstance(val, str) + + if val_is_string and errors in (None, "raise"): + msg = "Integer out of range. at position 0" + with pytest.raises(ValueError, match=msg): + to_numeric(val, **kwargs) + else: + expected = float(val) if (errors == "coerce" and + val_is_string) else val + assert tm.assert_almost_equal(to_numeric(val, **kwargs), expected) + + +def test_really_large_in_arr(large_val, signed, transform, + multiple_elts, errors): + # see gh-24910 + kwargs = dict(errors=errors) if errors is not None else dict() + val = -large_val if signed else large_val + val = transform(val) + + extra_elt = "string" + arr = [val] + multiple_elts * [extra_elt] + + val_is_string = isinstance(val, str) + coercing = errors == "coerce" + + if errors in (None, "raise") and (val_is_string or multiple_elts): + if val_is_string: + msg = "Integer out of range. at position 0" + else: + msg = 'Unable to parse string "string" at position 1' + + with pytest.raises(ValueError, match=msg): + to_numeric(arr, **kwargs) + else: + result = to_numeric(arr, **kwargs) + + exp_val = float(val) if (coercing and val_is_string) else val + expected = [exp_val] + + if multiple_elts: + if coercing: + expected.append(np.nan) + exp_dtype = float + else: + expected.append(extra_elt) + exp_dtype = object + else: + exp_dtype = float if isinstance(exp_val, ( + int, compat.long, float)) else object + + tm.assert_almost_equal(result, np.array(expected, dtype=exp_dtype)) + + +def test_really_large_in_arr_consistent(large_val, signed, + multiple_elts, errors): + # see gh-24910 + # + # Even if we discover that we have to hold float, does not mean + # we should be lenient on subsequent elements that fail to be integer. + kwargs = dict(errors=errors) if errors is not None else dict() + arr = [str(-large_val if signed else large_val)] + + if multiple_elts: + arr.insert(0, large_val) + + if errors in (None, "raise"): + index = int(multiple_elts) + msg = "Integer out of range. at position {index}".format(index=index) + + with pytest.raises(ValueError, match=msg): + to_numeric(arr, **kwargs) + else: + result = to_numeric(arr, **kwargs) + + if errors == "coerce": + expected = [float(i) for i in arr] + exp_dtype = float + else: + expected = arr + exp_dtype = object + + tm.assert_almost_equal(result, np.array(expected, dtype=exp_dtype)) + + +@pytest.mark.parametrize("errors,checker", [ + ("raise", 'Unable to parse string "fail" at position 0'), + ("ignore", lambda x: x == "fail"), + ("coerce", lambda x: np.isnan(x)) +]) +def test_scalar_fail(errors, checker): + scalar = "fail" + + if isinstance(checker, str): + with pytest.raises(ValueError, match=checker): + to_numeric(scalar, errors=errors) + else: + assert checker(to_numeric(scalar, errors=errors)) + + +@pytest.mark.parametrize("data", [ + [1, 2, 3], + [1., np.nan, 3, np.nan] +]) +def test_numeric_dtypes(data, transform_assert_equal): + transform, assert_equal = transform_assert_equal + data = transform(data) + + result = to_numeric(data) + assert_equal(result, data) + + +@pytest.mark.parametrize("data,exp", [ + (["1", "2", "3"], np.array([1, 2, 3], dtype="int64")), + (["1.5", "2.7", "3.4"], np.array([1.5, 2.7, 3.4])) +]) +def test_str(data, exp, transform_assert_equal): + transform, assert_equal = transform_assert_equal + result = to_numeric(transform(data)) + + expected = transform(exp) + assert_equal(result, expected) + + +def test_datetime_like(tz_naive_fixture, transform_assert_equal): + transform, assert_equal = transform_assert_equal + idx = pd.date_range("20130101", periods=3, tz=tz_naive_fixture) + + result = to_numeric(transform(idx)) + expected = transform(idx.asi8) + assert_equal(result, expected) + + +def test_timedelta(transform_assert_equal): + transform, assert_equal = transform_assert_equal + idx = pd.timedelta_range("1 days", periods=3, freq="D") + + result = to_numeric(transform(idx)) + expected = transform(idx.asi8) + assert_equal(result, expected) + + +def test_period(transform_assert_equal): + transform, assert_equal = transform_assert_equal + + idx = pd.period_range("2011-01", periods=3, freq="M", name="") + inp = transform(idx) + + if isinstance(inp, Index): + result = to_numeric(inp) + expected = transform(idx.asi8) + assert_equal(result, expected) + else: + # TODO: PeriodDtype, so support it in to_numeric. + pytest.skip("Missing PeriodDtype support in to_numeric") + + +@pytest.mark.parametrize("errors,expected", [ + ("raise", "Invalid object type at position 0"), + ("ignore", Series([[10.0, 2], 1.0, "apple"])), + ("coerce", Series([np.nan, 1.0, np.nan])) +]) +def test_non_hashable(errors, expected): + # see gh-13324 + ser = Series([[10.0, 2], 1.0, "apple"]) + + if isinstance(expected, str): + with pytest.raises(TypeError, match=expected): + to_numeric(ser, errors=errors) + else: + result = to_numeric(ser, errors=errors) + tm.assert_series_equal(result, expected) + + +def test_downcast_invalid_cast(): + # see gh-13352 + data = ["1", 2, 3] + invalid_downcast = "unsigned-integer" + msg = "invalid downcasting method provided" + + with pytest.raises(ValueError, match=msg): + to_numeric(data, downcast=invalid_downcast) + + +@pytest.mark.parametrize("data", [ + ["1", 2, 3], + [1, 2, 3], + np.array(["1970-01-02", "1970-01-03", + "1970-01-04"], dtype="datetime64[D]") +]) +@pytest.mark.parametrize("kwargs,exp_dtype", [ + # Basic function tests. + (dict(), np.int64), + (dict(downcast=None), np.int64), + + # Support below np.float32 is rare and far between. + (dict(downcast="float"), np.dtype(np.float32).char), + + # Basic dtype support. + (dict(downcast="unsigned"), np.dtype(np.typecodes["UnsignedInteger"][0])) +]) +def test_downcast_basic(data, kwargs, exp_dtype): + # see gh-13352 + result = to_numeric(data, **kwargs) + expected = np.array([1, 2, 3], dtype=exp_dtype) + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize("signed_downcast", ["integer", "signed"]) +@pytest.mark.parametrize("data", [ + ["1", 2, 3], + [1, 2, 3], + np.array(["1970-01-02", "1970-01-03", + "1970-01-04"], dtype="datetime64[D]") +]) +def test_signed_downcast(data, signed_downcast): + # see gh-13352 + smallest_int_dtype = np.dtype(np.typecodes["Integer"][0]) + expected = np.array([1, 2, 3], dtype=smallest_int_dtype) + + res = to_numeric(data, downcast=signed_downcast) + tm.assert_numpy_array_equal(res, expected) + + +def test_ignore_downcast_invalid_data(): + # If we can't successfully cast the given + # data to a numeric dtype, do not bother + # with the downcast parameter. + data = ["foo", 2, 3] + expected = np.array(data, dtype=object) + + res = to_numeric(data, errors="ignore", + downcast="unsigned") + tm.assert_numpy_array_equal(res, expected) + + +def test_ignore_downcast_neg_to_unsigned(): + # Cannot cast to an unsigned integer + # because we have a negative number. + data = ["-1", 2, 3] + expected = np.array([-1, 2, 3], dtype=np.int64) + + res = to_numeric(data, downcast="unsigned") + tm.assert_numpy_array_equal(res, expected) + + +@pytest.mark.parametrize("downcast", ["integer", "signed", "unsigned"]) +@pytest.mark.parametrize("data,expected", [ + (["1.1", 2, 3], + np.array([1.1, 2, 3], dtype=np.float64)), + ([10000.0, 20000, 3000, 40000.36, 50000, 50000.00], + np.array([10000.0, 20000, 3000, + 40000.36, 50000, 50000.00], dtype=np.float64)) +]) +def test_ignore_downcast_cannot_convert_float(data, expected, downcast): + # Cannot cast to an integer (signed or unsigned) + # because we have a float number. + res = to_numeric(data, downcast=downcast) + tm.assert_numpy_array_equal(res, expected) + + +@pytest.mark.parametrize("downcast,expected_dtype", [ + ("integer", np.int16), + ("signed", np.int16), + ("unsigned", np.uint16) +]) +def test_downcast_not8bit(downcast, expected_dtype): + # the smallest integer dtype need not be np.(u)int8 + data = ["256", 257, 258] + + expected = np.array([256, 257, 258], dtype=expected_dtype) + res = to_numeric(data, downcast=downcast) + tm.assert_numpy_array_equal(res, expected) + + +@pytest.mark.parametrize("dtype,downcast,min_max", [ + ("int8", "integer", [iinfo(np.int8).min, + iinfo(np.int8).max]), + ("int16", "integer", [iinfo(np.int16).min, + iinfo(np.int16).max]), + ("int32", "integer", [iinfo(np.int32).min, + iinfo(np.int32).max]), + ("int64", "integer", [iinfo(np.int64).min, + iinfo(np.int64).max]), + ("uint8", "unsigned", [iinfo(np.uint8).min, + iinfo(np.uint8).max]), + ("uint16", "unsigned", [iinfo(np.uint16).min, + iinfo(np.uint16).max]), + ("uint32", "unsigned", [iinfo(np.uint32).min, + iinfo(np.uint32).max]), + ("uint64", "unsigned", [iinfo(np.uint64).min, + iinfo(np.uint64).max]), + ("int16", "integer", [iinfo(np.int8).min, + iinfo(np.int8).max + 1]), + ("int32", "integer", [iinfo(np.int16).min, + iinfo(np.int16).max + 1]), + ("int64", "integer", [iinfo(np.int32).min, + iinfo(np.int32).max + 1]), + ("int16", "integer", [iinfo(np.int8).min - 1, + iinfo(np.int16).max]), + ("int32", "integer", [iinfo(np.int16).min - 1, + iinfo(np.int32).max]), + ("int64", "integer", [iinfo(np.int32).min - 1, + iinfo(np.int64).max]), + ("uint16", "unsigned", [iinfo(np.uint8).min, + iinfo(np.uint8).max + 1]), + ("uint32", "unsigned", [iinfo(np.uint16).min, + iinfo(np.uint16).max + 1]), + ("uint64", "unsigned", [iinfo(np.uint32).min, + iinfo(np.uint32).max + 1]) +]) +def test_downcast_limits(dtype, downcast, min_max): + # see gh-14404: test the limits of each downcast. + series = to_numeric(Series(min_max), downcast=downcast) + assert series.dtype == dtype + + +@pytest.mark.parametrize("data,exp_data", [ + ([200, 300, "", "NaN", 30000000000000000000], + [200, 300, np.nan, np.nan, 30000000000000000000]), + (["12345678901234567890", "1234567890", "ITEM"], + [12345678901234567890, 1234567890, np.nan]) +]) +def test_coerce_uint64_conflict(data, exp_data): + # see gh-17007 and gh-17125 + # + # Still returns float despite the uint64-nan conflict, + # which would normally force the casting to object. + result = to_numeric(Series(data), errors="coerce") + expected = Series(exp_data, dtype=float) + tm.assert_series_equal(result, expected) + + +@pytest.mark.parametrize("errors,exp", [ + ("ignore", Series(["12345678901234567890", "1234567890", "ITEM"])), + ("raise", "Unable to parse string") +]) +def test_non_coerce_uint64_conflict(errors, exp): + # see gh-17007 and gh-17125 + # + # For completeness. + ser = Series(["12345678901234567890", "1234567890", "ITEM"]) + + if isinstance(exp, str): + with pytest.raises(ValueError, match=exp): + to_numeric(ser, errors=errors) + else: + result = to_numeric(ser, errors=errors) + tm.assert_series_equal(result, ser) diff --git a/pandas/tests/tools/test_tile.py b/pandas/tests/tools/test_tile.py deleted file mode 100644 index cc80c1ff5db29..0000000000000 --- a/pandas/tests/tools/test_tile.py +++ /dev/null @@ -1,427 +0,0 @@ -import os - -import numpy as np -from pandas.compat import zip - -from pandas import Series, Index, Categorical -import pandas.util.testing as tm -from pandas.util.testing import assertRaisesRegexp -import pandas.core.common as com - -from pandas.core.algorithms import quantile -from pandas.tools.tile import cut, qcut -import pandas.tools.tile as tmod -from pandas import to_datetime, DatetimeIndex, Timestamp - - -class TestCut(tm.TestCase): - - def test_simple(self): - data = np.ones(5) - result = cut(data, 4, labels=False) - expected = np.array([1, 1, 1, 1, 1]) - tm.assert_numpy_array_equal(result, expected, - check_dtype=False) - - def test_bins(self): - data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1]) - result, bins = cut(data, 3, retbins=True) - - exp_codes = np.array([0, 0, 0, 1, 2, 0], dtype=np.int8) - tm.assert_numpy_array_equal(result.codes, exp_codes) - exp = np.array([0.1905, 3.36666667, 6.53333333, 9.7]) - tm.assert_almost_equal(bins, exp) - - def test_right(self): - data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) - result, bins = cut(data, 4, right=True, retbins=True) - exp_codes = np.array([0, 0, 0, 2, 3, 0, 0], dtype=np.int8) - tm.assert_numpy_array_equal(result.codes, exp_codes) - exp = np.array([0.1905, 2.575, 4.95, 7.325, 9.7]) - tm.assert_numpy_array_equal(bins, exp) - - def test_noright(self): - data = np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1, 2.575]) - result, bins = cut(data, 4, right=False, retbins=True) - exp_codes = np.array([0, 0, 0, 2, 3, 0, 1], dtype=np.int8) - tm.assert_numpy_array_equal(result.codes, exp_codes) - exp = np.array([0.2, 2.575, 4.95, 7.325, 9.7095]) - tm.assert_almost_equal(bins, exp) - - def test_arraylike(self): - data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] - result, bins = cut(data, 3, retbins=True) - exp_codes = np.array([0, 0, 0, 1, 2, 0], dtype=np.int8) - tm.assert_numpy_array_equal(result.codes, exp_codes) - exp = np.array([0.1905, 3.36666667, 6.53333333, 9.7]) - tm.assert_almost_equal(bins, exp) - - def test_bins_not_monotonic(self): - data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] - self.assertRaises(ValueError, cut, data, [0.1, 1.5, 1, 10]) - - def test_wrong_num_labels(self): - data = [.2, 1.4, 2.5, 6.2, 9.7, 2.1] - self.assertRaises(ValueError, cut, data, [0, 1, 10], - labels=['foo', 'bar', 'baz']) - - def test_cut_corner(self): - # h3h - self.assertRaises(ValueError, cut, [], 2) - - self.assertRaises(ValueError, cut, [1, 2, 3], 0.5) - - def test_cut_out_of_range_more(self): - # #1511 - s = Series([0, -1, 0, 1, -3], name='x') - ind = cut(s, [0, 1], labels=False) - exp = Series([np.nan, np.nan, np.nan, 0, np.nan], name='x') - tm.assert_series_equal(ind, exp) - - def test_labels(self): - arr = np.tile(np.arange(0, 1.01, 0.1), 4) - - result, bins = cut(arr, 4, retbins=True) - ex_levels = Index(['(-0.001, 0.25]', '(0.25, 0.5]', '(0.5, 0.75]', - '(0.75, 1]']) - self.assert_index_equal(result.categories, ex_levels) - - result, bins = cut(arr, 4, retbins=True, right=False) - ex_levels = Index(['[0, 0.25)', '[0.25, 0.5)', '[0.5, 0.75)', - '[0.75, 1.001)']) - self.assert_index_equal(result.categories, ex_levels) - - def test_cut_pass_series_name_to_factor(self): - s = Series(np.random.randn(100), name='foo') - - factor = cut(s, 4) - self.assertEqual(factor.name, 'foo') - - def test_label_precision(self): - arr = np.arange(0, 0.73, 0.01) - - result = cut(arr, 4, precision=2) - ex_levels = Index(['(-0.00072, 0.18]', '(0.18, 0.36]', - '(0.36, 0.54]', '(0.54, 0.72]']) - self.assert_index_equal(result.categories, ex_levels) - - def test_na_handling(self): - arr = np.arange(0, 0.75, 0.01) - arr[::3] = np.nan - - result = cut(arr, 4) - - result_arr = np.asarray(result) - - ex_arr = np.where(com.isnull(arr), np.nan, result_arr) - - tm.assert_almost_equal(result_arr, ex_arr) - - result = cut(arr, 4, labels=False) - ex_result = np.where(com.isnull(arr), np.nan, result) - tm.assert_almost_equal(result, ex_result) - - def test_inf_handling(self): - data = np.arange(6) - data_ser = Series(data, dtype='int64') - - result = cut(data, [-np.inf, 2, 4, np.inf]) - result_ser = cut(data_ser, [-np.inf, 2, 4, np.inf]) - - ex_categories = Index(['(-inf, 2]', '(2, 4]', '(4, inf]']) - - tm.assert_index_equal(result.categories, ex_categories) - tm.assert_index_equal(result_ser.cat.categories, ex_categories) - self.assertEqual(result[5], '(4, inf]') - self.assertEqual(result[0], '(-inf, 2]') - self.assertEqual(result_ser[5], '(4, inf]') - self.assertEqual(result_ser[0], '(-inf, 2]') - - def test_qcut(self): - arr = np.random.randn(1000) - - labels, bins = qcut(arr, 4, retbins=True) - ex_bins = quantile(arr, [0, .25, .5, .75, 1.]) - tm.assert_almost_equal(bins, ex_bins) - - ex_levels = cut(arr, ex_bins, include_lowest=True) - self.assert_categorical_equal(labels, ex_levels) - - def test_qcut_bounds(self): - arr = np.random.randn(1000) - - factor = qcut(arr, 10, labels=False) - self.assertEqual(len(np.unique(factor)), 10) - - def test_qcut_specify_quantiles(self): - arr = np.random.randn(100) - - factor = qcut(arr, [0, .25, .5, .75, 1.]) - expected = qcut(arr, 4) - tm.assert_categorical_equal(factor, expected) - - def test_qcut_all_bins_same(self): - assertRaisesRegexp(ValueError, "edges.*unique", qcut, - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 3) - - def test_cut_out_of_bounds(self): - arr = np.random.randn(100) - - result = cut(arr, [-1, 0, 1]) - - mask = result.codes == -1 - ex_mask = (arr < -1) | (arr > 1) - self.assert_numpy_array_equal(mask, ex_mask) - - def test_cut_pass_labels(self): - arr = [50, 5, 10, 15, 20, 30, 70] - bins = [0, 25, 50, 100] - labels = ['Small', 'Medium', 'Large'] - - result = cut(arr, bins, labels=labels) - - exp = cut(arr, bins) - exp.categories = labels - - tm.assert_categorical_equal(result, exp) - - def test_qcut_include_lowest(self): - values = np.arange(10) - - cats = qcut(values, 4) - - ex_levels = ['[0, 2.25]', '(2.25, 4.5]', '(4.5, 6.75]', '(6.75, 9]'] - self.assertTrue((cats.categories == ex_levels).all()) - - def test_qcut_nas(self): - arr = np.random.randn(100) - arr[:20] = np.nan - - result = qcut(arr, 4) - self.assertTrue(com.isnull(result[:20]).all()) - - def test_label_formatting(self): - self.assertEqual(tmod._trim_zeros('1.000'), '1') - - # it works - result = cut(np.arange(11.), 2) - - result = cut(np.arange(11.) / 1e10, 2) - - # #1979, negative numbers - - result = tmod._format_label(-117.9998, precision=3) - self.assertEqual(result, '-118') - result = tmod._format_label(117.9998, precision=3) - self.assertEqual(result, '118') - - def test_qcut_binning_issues(self): - # #1978, 1979 - path = os.path.join(tm.get_data_path(), 'cut_data.csv') - arr = np.loadtxt(path) - - result = qcut(arr, 20) - - starts = [] - ends = [] - for lev in result.categories: - s, e = lev[1:-1].split(',') - - self.assertTrue(s != e) - - starts.append(float(s)) - ends.append(float(e)) - - for (sp, sn), (ep, en) in zip(zip(starts[:-1], starts[1:]), - zip(ends[:-1], ends[1:])): - self.assertTrue(sp < sn) - self.assertTrue(ep < en) - self.assertTrue(ep <= sn) - - def test_cut_return_categorical(self): - s = Series([0, 1, 2, 3, 4, 5, 6, 7, 8]) - res = cut(s, 3) - exp = Series(Categorical.from_codes([0, 0, 0, 1, 1, 1, 2, 2, 2], - ["(-0.008, 2.667]", - "(2.667, 5.333]", "(5.333, 8]"], - ordered=True)) - tm.assert_series_equal(res, exp) - - def test_qcut_return_categorical(self): - s = Series([0, 1, 2, 3, 4, 5, 6, 7, 8]) - res = qcut(s, [0, 0.333, 0.666, 1]) - exp = Series(Categorical.from_codes([0, 0, 0, 1, 1, 1, 2, 2, 2], - ["[0, 2.664]", - "(2.664, 5.328]", "(5.328, 8]"], - ordered=True)) - tm.assert_series_equal(res, exp) - - def test_series_retbins(self): - # GH 8589 - s = Series(np.arange(4)) - result, bins = cut(s, 2, retbins=True) - tm.assert_numpy_array_equal(result.cat.codes.values, - np.array([0, 0, 1, 1], dtype=np.int8)) - tm.assert_numpy_array_equal(bins, np.array([-0.003, 1.5, 3])) - - result, bins = qcut(s, 2, retbins=True) - tm.assert_numpy_array_equal(result.cat.codes.values, - np.array([0, 0, 1, 1], dtype=np.int8)) - tm.assert_numpy_array_equal(bins, np.array([0, 1.5, 3])) - - def test_qcut_duplicates_bin(self): - # GH 7751 - values = [0, 0, 0, 0, 1, 2, 3] - result_levels = ['[0, 1]', '(1, 3]'] - - cats = qcut(values, 3, duplicates='drop') - self.assertTrue((cats.categories == result_levels).all()) - - self.assertRaises(ValueError, qcut, values, 3) - self.assertRaises(ValueError, qcut, values, 3, duplicates='raise') - - # invalid - self.assertRaises(ValueError, qcut, values, 3, duplicates='foo') - - def test_single_quantile(self): - # issue 15431 - expected = Series([0, 0]) - - s = Series([9., 9.]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0, 0], ["[9, 9]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - s = Series([-9., -9.]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0, 0], ["[-9, -9]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - s = Series([0., 0.]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0, 0], ["[0, 0]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - expected = Series([0]) - - s = Series([9]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0], ["[9, 9]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - s = Series([-9]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0], ["[-9, -9]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - s = Series([0]) - result = qcut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - result = qcut(s, 1) - exp_lab = Series(Categorical.from_codes([0], ["[0, 0]"], - ordered=True)) - tm.assert_series_equal(result, exp_lab) - - def test_single_bin(self): - # issue 14652 - expected = Series([0, 0]) - - s = Series([9., 9.]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - s = Series([-9., -9.]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - expected = Series([0]) - - s = Series([9]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - s = Series([-9]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - # issue 15428 - expected = Series([0, 0]) - - s = Series([0., 0.]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - expected = Series([0]) - - s = Series([0]) - result = cut(s, 1, labels=False) - tm.assert_series_equal(result, expected) - - def test_datetime_cut(self): - # GH 14714 - # testing for time data to be present as series - data = to_datetime(Series(['2013-01-01', '2013-01-02', '2013-01-03'])) - result, bins = cut(data, 3, retbins=True) - expected = Series(['(2012-12-31 23:57:07.200000, 2013-01-01 16:00:00]', - '(2013-01-01 16:00:00, 2013-01-02 08:00:00]', - '(2013-01-02 08:00:00, 2013-01-03 00:00:00]'], - ).astype("category", ordered=True) - tm.assert_series_equal(result, expected) - - # testing for time data to be present as list - data = [np.datetime64('2013-01-01'), np.datetime64('2013-01-02'), - np.datetime64('2013-01-03')] - result, bins = cut(data, 3, retbins=True) - tm.assert_series_equal(Series(result), expected) - - # testing for time data to be present as ndarray - data = np.array([np.datetime64('2013-01-01'), - np.datetime64('2013-01-02'), - np.datetime64('2013-01-03')]) - result, bins = cut(data, 3, retbins=True) - tm.assert_series_equal(Series(result), expected) - - # testing for time data to be present as datetime index - data = DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03']) - result, bins = cut(data, 3, retbins=True) - tm.assert_series_equal(Series(result), expected) - - def test_datetime_bin(self): - data = [np.datetime64('2012-12-13'), np.datetime64('2012-12-15')] - bin_data = ['2012-12-12', '2012-12-14', '2012-12-16'] - expected = Series(['(2012-12-12 00:00:00, 2012-12-14 00:00:00]', - '(2012-12-14 00:00:00, 2012-12-16 00:00:00]'], - ).astype("category", ordered=True) - - for conv in [Timestamp, Timestamp, np.datetime64]: - bins = [conv(v) for v in bin_data] - result = cut(data, bins=bins) - tm.assert_series_equal(Series(result), expected) - - bin_pydatetime = [Timestamp(v).to_pydatetime() for v in bin_data] - result = cut(data, bins=bin_pydatetime) - tm.assert_series_equal(Series(result), expected) - - bins = to_datetime(bin_data) - result = cut(data, bins=bin_pydatetime) - tm.assert_series_equal(Series(result), expected) - - -def curpath(): - pth, _ = os.path.split(os.path.abspath(__file__)) - return pth diff --git a/pandas/tests/tools/test_util.py b/pandas/tests/tools/test_util.py deleted file mode 100644 index ed64e8f42d84b..0000000000000 --- a/pandas/tests/tools/test_util.py +++ /dev/null @@ -1,485 +0,0 @@ -import os -import locale -import codecs -import pytest -import decimal - -import numpy as np -from numpy import iinfo - -import pandas as pd -from pandas import (date_range, Index, _np_version_under1p9) -import pandas.util.testing as tm -from pandas.tools.util import cartesian_product, to_numeric - -CURRENT_LOCALE = locale.getlocale() -LOCALE_OVERRIDE = os.environ.get('LOCALE_OVERRIDE', None) - - -class TestCartesianProduct(tm.TestCase): - - def test_simple(self): - x, y = list('ABC'), [1, 22] - result1, result2 = cartesian_product([x, y]) - expected1 = np.array(['A', 'A', 'B', 'B', 'C', 'C']) - expected2 = np.array([1, 22, 1, 22, 1, 22]) - tm.assert_numpy_array_equal(result1, expected1) - tm.assert_numpy_array_equal(result2, expected2) - - def test_datetimeindex(self): - # regression test for GitHub issue #6439 - # make sure that the ordering on datetimeindex is consistent - x = date_range('2000-01-01', periods=2) - result1, result2 = [Index(y).day for y in cartesian_product([x, x])] - expected1 = Index([1, 1, 2, 2]) - expected2 = Index([1, 2, 1, 2]) - tm.assert_index_equal(result1, expected1) - tm.assert_index_equal(result2, expected2) - - def test_empty(self): - # product of empty factors - X = [[], [0, 1], []] - Y = [[], [], ['a', 'b', 'c']] - for x, y in zip(X, Y): - expected1 = np.array([], dtype=np.asarray(x).dtype) - expected2 = np.array([], dtype=np.asarray(y).dtype) - result1, result2 = cartesian_product([x, y]) - tm.assert_numpy_array_equal(result1, expected1) - tm.assert_numpy_array_equal(result2, expected2) - - # empty product (empty input): - result = cartesian_product([]) - expected = [] - tm.assert_equal(result, expected) - - def test_invalid_input(self): - invalid_inputs = [1, [1], [1, 2], [[1], 2], - 'a', ['a'], ['a', 'b'], [['a'], 'b']] - msg = "Input must be a list-like of list-likes" - for X in invalid_inputs: - tm.assertRaisesRegexp(TypeError, msg, cartesian_product, X=X) - - -class TestLocaleUtils(tm.TestCase): - - @classmethod - def setUpClass(cls): - super(TestLocaleUtils, cls).setUpClass() - cls.locales = tm.get_locales() - - if not cls.locales: - pytest.skip("No locales found") - - tm._skip_if_windows() - - @classmethod - def tearDownClass(cls): - super(TestLocaleUtils, cls).tearDownClass() - del cls.locales - - def test_get_locales(self): - # all systems should have at least a single locale - assert len(tm.get_locales()) > 0 - - def test_get_locales_prefix(self): - if len(self.locales) == 1: - pytest.skip("Only a single locale found, no point in " - "trying to test filtering locale prefixes") - first_locale = self.locales[0] - assert len(tm.get_locales(prefix=first_locale[:2])) > 0 - - def test_set_locale(self): - if len(self.locales) == 1: - pytest.skip("Only a single locale found, no point in " - "trying to test setting another locale") - - if all(x is None for x in CURRENT_LOCALE): - # Not sure why, but on some travis runs with pytest, - # getlocale() returned (None, None). - pytest.skip("CURRENT_LOCALE is not set.") - - if LOCALE_OVERRIDE is None: - lang, enc = 'it_CH', 'UTF-8' - elif LOCALE_OVERRIDE == 'C': - lang, enc = 'en_US', 'ascii' - else: - lang, enc = LOCALE_OVERRIDE.split('.') - - enc = codecs.lookup(enc).name - new_locale = lang, enc - - if not tm._can_set_locale(new_locale): - with tm.assertRaises(locale.Error): - with tm.set_locale(new_locale): - pass - else: - with tm.set_locale(new_locale) as normalized_locale: - new_lang, new_enc = normalized_locale.split('.') - new_enc = codecs.lookup(enc).name - normalized_locale = new_lang, new_enc - self.assertEqual(normalized_locale, new_locale) - - current_locale = locale.getlocale() - self.assertEqual(current_locale, CURRENT_LOCALE) - - -class TestToNumeric(tm.TestCase): - - def test_series(self): - s = pd.Series(['1', '-3.14', '7']) - res = to_numeric(s) - expected = pd.Series([1, -3.14, 7]) - tm.assert_series_equal(res, expected) - - s = pd.Series(['1', '-3.14', 7]) - res = to_numeric(s) - tm.assert_series_equal(res, expected) - - def test_series_numeric(self): - s = pd.Series([1, 3, 4, 5], index=list('ABCD'), name='XXX') - res = to_numeric(s) - tm.assert_series_equal(res, s) - - s = pd.Series([1., 3., 4., 5.], index=list('ABCD'), name='XXX') - res = to_numeric(s) - tm.assert_series_equal(res, s) - - # bool is regarded as numeric - s = pd.Series([True, False, True, True], - index=list('ABCD'), name='XXX') - res = to_numeric(s) - tm.assert_series_equal(res, s) - - def test_error(self): - s = pd.Series([1, -3.14, 'apple']) - msg = 'Unable to parse string "apple" at position 2' - with tm.assertRaisesRegexp(ValueError, msg): - to_numeric(s, errors='raise') - - res = to_numeric(s, errors='ignore') - expected = pd.Series([1, -3.14, 'apple']) - tm.assert_series_equal(res, expected) - - res = to_numeric(s, errors='coerce') - expected = pd.Series([1, -3.14, np.nan]) - tm.assert_series_equal(res, expected) - - s = pd.Series(['orange', 1, -3.14, 'apple']) - msg = 'Unable to parse string "orange" at position 0' - with tm.assertRaisesRegexp(ValueError, msg): - to_numeric(s, errors='raise') - - def test_error_seen_bool(self): - s = pd.Series([True, False, 'apple']) - msg = 'Unable to parse string "apple" at position 2' - with tm.assertRaisesRegexp(ValueError, msg): - to_numeric(s, errors='raise') - - res = to_numeric(s, errors='ignore') - expected = pd.Series([True, False, 'apple']) - tm.assert_series_equal(res, expected) - - # coerces to float - res = to_numeric(s, errors='coerce') - expected = pd.Series([1., 0., np.nan]) - tm.assert_series_equal(res, expected) - - def test_list(self): - s = ['1', '-3.14', '7'] - res = to_numeric(s) - expected = np.array([1, -3.14, 7]) - tm.assert_numpy_array_equal(res, expected) - - def test_list_numeric(self): - s = [1, 3, 4, 5] - res = to_numeric(s) - tm.assert_numpy_array_equal(res, np.array(s, dtype=np.int64)) - - s = [1., 3., 4., 5.] - res = to_numeric(s) - tm.assert_numpy_array_equal(res, np.array(s)) - - # bool is regarded as numeric - s = [True, False, True, True] - res = to_numeric(s) - tm.assert_numpy_array_equal(res, np.array(s)) - - def test_numeric(self): - s = pd.Series([1, -3.14, 7], dtype='O') - res = to_numeric(s) - expected = pd.Series([1, -3.14, 7]) - tm.assert_series_equal(res, expected) - - s = pd.Series([1, -3.14, 7]) - res = to_numeric(s) - tm.assert_series_equal(res, expected) - - # GH 14827 - df = pd.DataFrame(dict( - a=[1.2, decimal.Decimal(3.14), decimal.Decimal("infinity"), '0.1'], - b=[1.0, 2.0, 3.0, 4.0], - )) - expected = pd.DataFrame(dict( - a=[1.2, 3.14, np.inf, 0.1], - b=[1.0, 2.0, 3.0, 4.0], - )) - - # Test to_numeric over one column - df_copy = df.copy() - df_copy['a'] = df_copy['a'].apply(to_numeric) - tm.assert_frame_equal(df_copy, expected) - - # Test to_numeric over multiple columns - df_copy = df.copy() - df_copy[['a', 'b']] = df_copy[['a', 'b']].apply(to_numeric) - tm.assert_frame_equal(df_copy, expected) - - def test_numeric_lists_and_arrays(self): - # Test to_numeric with embedded lists and arrays - df = pd.DataFrame(dict( - a=[[decimal.Decimal(3.14), 1.0], decimal.Decimal(1.6), 0.1] - )) - df['a'] = df['a'].apply(to_numeric) - expected = pd.DataFrame(dict( - a=[[3.14, 1.0], 1.6, 0.1], - )) - tm.assert_frame_equal(df, expected) - - df = pd.DataFrame(dict( - a=[np.array([decimal.Decimal(3.14), 1.0]), 0.1] - )) - df['a'] = df['a'].apply(to_numeric) - expected = pd.DataFrame(dict( - a=[[3.14, 1.0], 0.1], - )) - tm.assert_frame_equal(df, expected) - - def test_all_nan(self): - s = pd.Series(['a', 'b', 'c']) - res = to_numeric(s, errors='coerce') - expected = pd.Series([np.nan, np.nan, np.nan]) - tm.assert_series_equal(res, expected) - - def test_type_check(self): - # GH 11776 - df = pd.DataFrame({'a': [1, -3.14, 7], 'b': ['4', '5', '6']}) - with tm.assertRaisesRegexp(TypeError, "1-d array"): - to_numeric(df) - for errors in ['ignore', 'raise', 'coerce']: - with tm.assertRaisesRegexp(TypeError, "1-d array"): - to_numeric(df, errors=errors) - - def test_scalar(self): - self.assertEqual(pd.to_numeric(1), 1) - self.assertEqual(pd.to_numeric(1.1), 1.1) - - self.assertEqual(pd.to_numeric('1'), 1) - self.assertEqual(pd.to_numeric('1.1'), 1.1) - - with tm.assertRaises(ValueError): - to_numeric('XX', errors='raise') - - self.assertEqual(to_numeric('XX', errors='ignore'), 'XX') - self.assertTrue(np.isnan(to_numeric('XX', errors='coerce'))) - - def test_numeric_dtypes(self): - idx = pd.Index([1, 2, 3], name='xxx') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, idx) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(idx, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, idx.values) - - idx = pd.Index([1., np.nan, 3., np.nan], name='xxx') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, idx) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(idx, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, idx.values) - - def test_str(self): - idx = pd.Index(['1', '2', '3'], name='xxx') - exp = np.array([1, 2, 3], dtype='int64') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, pd.Index(exp, name='xxx')) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(exp, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, exp) - - idx = pd.Index(['1.5', '2.7', '3.4'], name='xxx') - exp = np.array([1.5, 2.7, 3.4]) - res = pd.to_numeric(idx) - tm.assert_index_equal(res, pd.Index(exp, name='xxx')) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(exp, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, exp) - - def test_datetimelike(self): - for tz in [None, 'US/Eastern', 'Asia/Tokyo']: - idx = pd.date_range('20130101', periods=3, tz=tz, name='xxx') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, pd.Index(idx.asi8, name='xxx')) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(idx.asi8, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, idx.asi8) - - def test_timedelta(self): - idx = pd.timedelta_range('1 days', periods=3, freq='D', name='xxx') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, pd.Index(idx.asi8, name='xxx')) - - res = pd.to_numeric(pd.Series(idx, name='xxx')) - tm.assert_series_equal(res, pd.Series(idx.asi8, name='xxx')) - - res = pd.to_numeric(idx.values) - tm.assert_numpy_array_equal(res, idx.asi8) - - def test_period(self): - idx = pd.period_range('2011-01', periods=3, freq='M', name='xxx') - res = pd.to_numeric(idx) - tm.assert_index_equal(res, pd.Index(idx.asi8, name='xxx')) - - # ToDo: enable when we can support native PeriodDtype - # res = pd.to_numeric(pd.Series(idx, name='xxx')) - # tm.assert_series_equal(res, pd.Series(idx.asi8, name='xxx')) - - def test_non_hashable(self): - # Test for Bug #13324 - s = pd.Series([[10.0, 2], 1.0, 'apple']) - res = pd.to_numeric(s, errors='coerce') - tm.assert_series_equal(res, pd.Series([np.nan, 1.0, np.nan])) - - res = pd.to_numeric(s, errors='ignore') - tm.assert_series_equal(res, pd.Series([[10.0, 2], 1.0, 'apple'])) - - with self.assertRaisesRegexp(TypeError, "Invalid object type"): - pd.to_numeric(s) - - def test_downcast(self): - # see gh-13352 - mixed_data = ['1', 2, 3] - int_data = [1, 2, 3] - date_data = np.array(['1970-01-02', '1970-01-03', - '1970-01-04'], dtype='datetime64[D]') - - invalid_downcast = 'unsigned-integer' - msg = 'invalid downcasting method provided' - - smallest_int_dtype = np.dtype(np.typecodes['Integer'][0]) - smallest_uint_dtype = np.dtype(np.typecodes['UnsignedInteger'][0]) - - # support below np.float32 is rare and far between - float_32_char = np.dtype(np.float32).char - smallest_float_dtype = float_32_char - - for data in (mixed_data, int_data, date_data): - with self.assertRaisesRegexp(ValueError, msg): - pd.to_numeric(data, downcast=invalid_downcast) - - expected = np.array([1, 2, 3], dtype=np.int64) - - res = pd.to_numeric(data) - tm.assert_numpy_array_equal(res, expected) - - res = pd.to_numeric(data, downcast=None) - tm.assert_numpy_array_equal(res, expected) - - expected = np.array([1, 2, 3], dtype=smallest_int_dtype) - - for signed_downcast in ('integer', 'signed'): - res = pd.to_numeric(data, downcast=signed_downcast) - tm.assert_numpy_array_equal(res, expected) - - expected = np.array([1, 2, 3], dtype=smallest_uint_dtype) - res = pd.to_numeric(data, downcast='unsigned') - tm.assert_numpy_array_equal(res, expected) - - expected = np.array([1, 2, 3], dtype=smallest_float_dtype) - res = pd.to_numeric(data, downcast='float') - tm.assert_numpy_array_equal(res, expected) - - # if we can't successfully cast the given - # data to a numeric dtype, do not bother - # with the downcast parameter - data = ['foo', 2, 3] - expected = np.array(data, dtype=object) - res = pd.to_numeric(data, errors='ignore', - downcast='unsigned') - tm.assert_numpy_array_equal(res, expected) - - # cannot cast to an unsigned integer because - # we have a negative number - data = ['-1', 2, 3] - expected = np.array([-1, 2, 3], dtype=np.int64) - res = pd.to_numeric(data, downcast='unsigned') - tm.assert_numpy_array_equal(res, expected) - - # cannot cast to an integer (signed or unsigned) - # because we have a float number - data = (['1.1', 2, 3], - [10000.0, 20000, 3000, 40000.36, 50000, 50000.00]) - expected = (np.array([1.1, 2, 3], dtype=np.float64), - np.array([10000.0, 20000, 3000, - 40000.36, 50000, 50000.00], dtype=np.float64)) - - for _data, _expected in zip(data, expected): - for downcast in ('integer', 'signed', 'unsigned'): - res = pd.to_numeric(_data, downcast=downcast) - tm.assert_numpy_array_equal(res, _expected) - - # the smallest integer dtype need not be np.(u)int8 - data = ['256', 257, 258] - - for downcast, expected_dtype in zip( - ['integer', 'signed', 'unsigned'], - [np.int16, np.int16, np.uint16]): - expected = np.array([256, 257, 258], dtype=expected_dtype) - res = pd.to_numeric(data, downcast=downcast) - tm.assert_numpy_array_equal(res, expected) - - def test_downcast_limits(self): - # Test the limits of each downcast. Bug: #14401. - # Check to make sure numpy is new enough to run this test. - if _np_version_under1p9: - pytest.skip("Numpy version is under 1.9") - - i = 'integer' - u = 'unsigned' - dtype_downcast_min_max = [ - ('int8', i, [iinfo(np.int8).min, iinfo(np.int8).max]), - ('int16', i, [iinfo(np.int16).min, iinfo(np.int16).max]), - ('int32', i, [iinfo(np.int32).min, iinfo(np.int32).max]), - ('int64', i, [iinfo(np.int64).min, iinfo(np.int64).max]), - ('uint8', u, [iinfo(np.uint8).min, iinfo(np.uint8).max]), - ('uint16', u, [iinfo(np.uint16).min, iinfo(np.uint16).max]), - ('uint32', u, [iinfo(np.uint32).min, iinfo(np.uint32).max]), - ('uint64', u, [iinfo(np.uint64).min, iinfo(np.uint64).max]), - ('int16', i, [iinfo(np.int8).min, iinfo(np.int8).max + 1]), - ('int32', i, [iinfo(np.int16).min, iinfo(np.int16).max + 1]), - ('int64', i, [iinfo(np.int32).min, iinfo(np.int32).max + 1]), - ('int16', i, [iinfo(np.int8).min - 1, iinfo(np.int16).max]), - ('int32', i, [iinfo(np.int16).min - 1, iinfo(np.int32).max]), - ('int64', i, [iinfo(np.int32).min - 1, iinfo(np.int64).max]), - ('uint16', u, [iinfo(np.uint8).min, iinfo(np.uint8).max + 1]), - ('uint32', u, [iinfo(np.uint16).min, iinfo(np.uint16).max + 1]), - ('uint64', u, [iinfo(np.uint32).min, iinfo(np.uint32).max + 1]) - ] - - for dtype, downcast, min_max in dtype_downcast_min_max: - series = pd.to_numeric(pd.Series(min_max), downcast=downcast) - tm.assert_equal(series.dtype, dtype) diff --git a/pandas/tests/tseries/frequencies/__init__.py b/pandas/tests/tseries/frequencies/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/tseries/frequencies/test_freq_code.py b/pandas/tests/tseries/frequencies/test_freq_code.py new file mode 100644 index 0000000000000..0aa29e451b1ba --- /dev/null +++ b/pandas/tests/tseries/frequencies/test_freq_code.py @@ -0,0 +1,149 @@ +import pytest + +from pandas._libs.tslibs import frequencies as libfrequencies, resolution +from pandas._libs.tslibs.frequencies import ( + FreqGroup, _period_code_map, get_freq, get_freq_code) +import pandas.compat as compat + +import pandas.tseries.offsets as offsets + + +@pytest.fixture(params=list(compat.iteritems(_period_code_map))) +def period_code_item(request): + return request.param + + +@pytest.mark.parametrize("freqstr,expected", [ + ("A", 1000), ("3A", 1000), ("-1A", 1000), + ("Y", 1000), ("3Y", 1000), ("-1Y", 1000), + ("W", 4000), ("W-MON", 4001), ("W-FRI", 4005) +]) +def test_freq_code(freqstr, expected): + assert get_freq(freqstr) == expected + + +def test_freq_code_match(period_code_item): + freqstr, code = period_code_item + assert get_freq(freqstr) == code + + +@pytest.mark.parametrize("freqstr,expected", [ + ("A", 1000), ("3A", 1000), ("-1A", 1000), ("A-JAN", 1000), + ("A-MAY", 1000), ("Y", 1000), ("3Y", 1000), ("-1Y", 1000), + ("Y-JAN", 1000), ("Y-MAY", 1000), (offsets.YearEnd(), 1000), + (offsets.YearEnd(month=1), 1000), (offsets.YearEnd(month=5), 1000), + ("W", 4000), ("W-MON", 4000), ("W-FRI", 4000), (offsets.Week(), 4000), + (offsets.Week(weekday=1), 4000), (offsets.Week(weekday=5), 4000), + ("T", FreqGroup.FR_MIN), +]) +def test_freq_group(freqstr, expected): + assert resolution.get_freq_group(freqstr) == expected + + +def test_freq_group_match(period_code_item): + freqstr, code = period_code_item + + str_group = resolution.get_freq_group(freqstr) + code_group = resolution.get_freq_group(code) + + assert str_group == code_group == code // 1000 * 1000 + + +@pytest.mark.parametrize("freqstr,exp_freqstr", [ + ("D", "D"), ("W", "D"), ("M", "D"), + ("S", "S"), ("T", "S"), ("H", "S") +]) +def test_get_to_timestamp_base(freqstr, exp_freqstr): + tsb = libfrequencies.get_to_timestamp_base + + assert tsb(get_freq_code(freqstr)[0]) == get_freq_code(exp_freqstr)[0] + + +_reso = resolution.Resolution + + +@pytest.mark.parametrize("freqstr,expected", [ + ("A", "year"), ("Q", "quarter"), ("M", "month"), + ("D", "day"), ("H", "hour"), ("T", "minute"), + ("S", "second"), ("L", "millisecond"), + ("U", "microsecond"), ("N", "nanosecond") +]) +def test_get_str_from_freq(freqstr, expected): + assert _reso.get_str_from_freq(freqstr) == expected + + +@pytest.mark.parametrize("freq", ["A", "Q", "M", "D", "H", + "T", "S", "L", "U", "N"]) +def test_get_freq_roundtrip(freq): + result = _reso.get_freq(_reso.get_str_from_freq(freq)) + assert freq == result + + +@pytest.mark.parametrize("freq", ["D", "H", "T", "S", "L", "U"]) +def test_get_freq_roundtrip2(freq): + result = _reso.get_freq(_reso.get_str(_reso.get_reso_from_freq(freq))) + assert freq == result + + +@pytest.mark.parametrize("args,expected", [ + ((1.5, "T"), (90, "S")), ((62.4, "T"), (3744, "S")), + ((1.04, "H"), (3744, "S")), ((1, "D"), (1, "D")), + ((0.342931, "H"), (1234551600, "U")), ((1.2345, "D"), (106660800, "L")) +]) +def test_resolution_bumping(args, expected): + # see gh-14378 + assert _reso.get_stride_from_decimal(*args) == expected + + +@pytest.mark.parametrize("args", [ + (0.5, "N"), + + # Too much precision in the input can prevent. + (0.3429324798798269273987982, "H") +]) +def test_cat(args): + msg = "Could not convert to integer offset at any resolution" + + with pytest.raises(ValueError, match=msg): + _reso.get_stride_from_decimal(*args) + + +@pytest.mark.parametrize("freq_input,expected", [ + # Frequency string. + ("A", (get_freq("A"), 1)), + ("3D", (get_freq("D"), 3)), + ("-2M", (get_freq("M"), -2)), + + # Tuple. + (("D", 1), (get_freq("D"), 1)), + (("A", 3), (get_freq("A"), 3)), + (("M", -2), (get_freq("M"), -2)), + ((5, "T"), (FreqGroup.FR_MIN, 5)), + + # Numeric Tuple. + ((1000, 1), (1000, 1)), + + # Offsets. + (offsets.Day(), (get_freq("D"), 1)), + (offsets.Day(3), (get_freq("D"), 3)), + (offsets.Day(-2), (get_freq("D"), -2)), + (offsets.MonthEnd(), (get_freq("M"), 1)), + (offsets.MonthEnd(3), (get_freq("M"), 3)), + (offsets.MonthEnd(-2), (get_freq("M"), -2)), + (offsets.Week(), (get_freq("W"), 1)), + (offsets.Week(3), (get_freq("W"), 3)), + (offsets.Week(-2), (get_freq("W"), -2)), + (offsets.Hour(), (FreqGroup.FR_HR, 1)), + + # Monday is weekday=0. + (offsets.Week(weekday=1), (get_freq("W-TUE"), 1)), + (offsets.Week(3, weekday=0), (get_freq("W-MON"), 3)), + (offsets.Week(-2, weekday=4), (get_freq("W-FRI"), -2)), +]) +def test_get_freq_code(freq_input, expected): + assert get_freq_code(freq_input) == expected + + +def test_get_code_invalid(): + with pytest.raises(ValueError, match="Invalid frequency"): + get_freq_code((5, "baz")) diff --git a/pandas/tests/tseries/frequencies/test_inference.py b/pandas/tests/tseries/frequencies/test_inference.py new file mode 100644 index 0000000000000..9e7ddbc45bba8 --- /dev/null +++ b/pandas/tests/tseries/frequencies/test_inference.py @@ -0,0 +1,406 @@ +from datetime import datetime, timedelta + +import numpy as np +import pytest + +from pandas._libs.tslibs.ccalendar import DAYS, MONTHS +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG +import pandas.compat as compat +from pandas.compat import is_platform_windows, range + +from pandas import ( + DatetimeIndex, Index, Series, Timestamp, date_range, period_range) +from pandas.core.tools.datetimes import to_datetime +import pandas.util.testing as tm + +import pandas.tseries.frequencies as frequencies +import pandas.tseries.offsets as offsets + + +def _check_generated_range(start, periods, freq): + """ + Check the range generated from a given start, frequency, and period count. + + Parameters + ---------- + start : str + The start date. + periods : int + The number of periods. + freq : str + The frequency of the range. + """ + freq = freq.upper() + + gen = date_range(start, periods=periods, freq=freq) + index = DatetimeIndex(gen.values) + + if not freq.startswith("Q-"): + assert frequencies.infer_freq(index) == gen.freqstr + else: + inf_freq = frequencies.infer_freq(index) + is_dec_range = inf_freq == "Q-DEC" and gen.freqstr in ( + "Q", "Q-DEC", "Q-SEP", "Q-JUN", "Q-MAR") + is_nov_range = inf_freq == "Q-NOV" and gen.freqstr in ( + "Q-NOV", "Q-AUG", "Q-MAY", "Q-FEB") + is_oct_range = inf_freq == "Q-OCT" and gen.freqstr in ( + "Q-OCT", "Q-JUL", "Q-APR", "Q-JAN") + assert is_dec_range or is_nov_range or is_oct_range + + +@pytest.fixture(params=[(timedelta(1), "D"), + (timedelta(hours=1), "H"), + (timedelta(minutes=1), "T"), + (timedelta(seconds=1), "S"), + (np.timedelta64(1, "ns"), "N"), + (timedelta(microseconds=1), "U"), + (timedelta(microseconds=1000), "L")]) +def base_delta_code_pair(request): + return request.param + + +@pytest.fixture(params=[1, 2, 3, 4]) +def count(request): + return request.param + + +@pytest.fixture(params=DAYS) +def day(request): + return request.param + + +@pytest.fixture(params=MONTHS) +def month(request): + return request.param + + +@pytest.fixture(params=[5, 7]) +def periods(request): + return request.param + + +def test_raise_if_period_index(): + index = period_range(start="1/1/1990", periods=20, freq="M") + msg = "Check the `freq` attribute instead of using infer_freq" + + with pytest.raises(TypeError, match=msg): + frequencies.infer_freq(index) + + +def test_raise_if_too_few(): + index = DatetimeIndex(["12/31/1998", "1/3/1999"]) + msg = "Need at least 3 dates to infer frequency" + + with pytest.raises(ValueError, match=msg): + frequencies.infer_freq(index) + + +def test_business_daily(): + index = DatetimeIndex(["01/01/1999", "1/4/1999", "1/5/1999"]) + assert frequencies.infer_freq(index) == "B" + + +def test_business_daily_look_alike(): + # see gh-16624 + # + # Do not infer "B when "weekend" (2-day gap) in wrong place. + index = DatetimeIndex(["12/31/1998", "1/3/1999", "1/4/1999"]) + assert frequencies.infer_freq(index) is None + + +def test_day_corner(): + index = DatetimeIndex(["1/1/2000", "1/2/2000", "1/3/2000"]) + assert frequencies.infer_freq(index) == "D" + + +def test_non_datetime_index(): + dates = to_datetime(["1/1/2000", "1/2/2000", "1/3/2000"]) + assert frequencies.infer_freq(dates) == "D" + + +def test_fifth_week_of_month_infer(): + # see gh-9425 + # + # Only attempt to infer up to WOM-4. + index = DatetimeIndex(["2014-03-31", "2014-06-30", "2015-03-30"]) + assert frequencies.infer_freq(index) is None + + +def test_week_of_month_fake(): + # All of these dates are on same day + # of week and are 4 or 5 weeks apart. + index = DatetimeIndex(["2013-08-27", "2013-10-01", + "2013-10-29", "2013-11-26"]) + assert frequencies.infer_freq(index) != "WOM-4TUE" + + +def test_fifth_week_of_month(): + # see gh-9425 + # + # Only supports freq up to WOM-4. + msg = ("Of the four parameters: start, end, periods, " + "and freq, exactly three must be specified") + + with pytest.raises(ValueError, match=msg): + date_range("2014-01-01", freq="WOM-5MON") + + +def test_monthly_ambiguous(): + rng = DatetimeIndex(["1/31/2000", "2/29/2000", "3/31/2000"]) + assert rng.inferred_freq == "M" + + +def test_annual_ambiguous(): + rng = DatetimeIndex(["1/31/2000", "1/31/2001", "1/31/2002"]) + assert rng.inferred_freq == "A-JAN" + + +def test_infer_freq_delta(base_delta_code_pair, count): + b = Timestamp(datetime.now()) + base_delta, code = base_delta_code_pair + + inc = base_delta * count + index = DatetimeIndex([b + inc * j for j in range(3)]) + + exp_freq = "%d%s" % (count, code) if count > 1 else code + assert frequencies.infer_freq(index) == exp_freq + + +@pytest.mark.parametrize("constructor", [ + lambda now, delta: DatetimeIndex([now + delta * 7] + + [now + delta * j for j in range(3)]), + lambda now, delta: DatetimeIndex([now + delta * j for j in range(3)] + + [now + delta * 7]) +]) +def test_infer_freq_custom(base_delta_code_pair, constructor): + b = Timestamp(datetime.now()) + base_delta, _ = base_delta_code_pair + + index = constructor(b, base_delta) + assert frequencies.infer_freq(index) is None + + +def test_weekly_infer(periods, day): + _check_generated_range("1/1/2000", periods, "W-{day}".format(day=day)) + + +def test_week_of_month_infer(periods, day, count): + _check_generated_range("1/1/2000", periods, + "WOM-{count}{day}".format(count=count, day=day)) + + +@pytest.mark.parametrize("freq", ["M", "BM", "BMS"]) +def test_monthly_infer(periods, freq): + _check_generated_range("1/1/2000", periods, "M") + + +def test_quarterly_infer(month, periods): + _check_generated_range("1/1/2000", periods, + "Q-{month}".format(month=month)) + + +@pytest.mark.parametrize("annual", ["A", "BA"]) +def test_annually_infer(month, periods, annual): + _check_generated_range("1/1/2000", periods, + "{annual}-{month}".format(annual=annual, + month=month)) + + +@pytest.mark.parametrize("freq,expected", [ + ("Q", "Q-DEC"), ("Q-NOV", "Q-NOV"), ("Q-OCT", "Q-OCT") +]) +def test_infer_freq_index(freq, expected): + rng = period_range("1959Q2", "2009Q3", freq=freq) + rng = Index(rng.to_timestamp("D", how="e").astype(object)) + + assert rng.inferred_freq == expected + + +@pytest.mark.parametrize( + "expected,dates", + list(compat.iteritems( + {"AS-JAN": ["2009-01-01", "2010-01-01", "2011-01-01", "2012-01-01"], + "Q-OCT": ["2009-01-31", "2009-04-30", "2009-07-31", "2009-10-31"], + "M": ["2010-11-30", "2010-12-31", "2011-01-31", "2011-02-28"], + "W-SAT": ["2010-12-25", "2011-01-01", "2011-01-08", "2011-01-15"], + "D": ["2011-01-01", "2011-01-02", "2011-01-03", "2011-01-04"], + "H": ["2011-12-31 22:00", "2011-12-31 23:00", + "2012-01-01 00:00", "2012-01-01 01:00"]})) +) +def test_infer_freq_tz(tz_naive_fixture, expected, dates): + # see gh-7310 + tz = tz_naive_fixture + idx = DatetimeIndex(dates, tz=tz) + assert idx.inferred_freq == expected + + +@pytest.mark.parametrize("date_pair", [ + ["2013-11-02", "2013-11-5"], # Fall DST + ["2014-03-08", "2014-03-11"], # Spring DST + ["2014-01-01", "2014-01-03"] # Regular Time +]) +@pytest.mark.parametrize("freq", [ + "3H", "10T", "3601S", "3600001L", "3600000001U", "3600000000001N" +]) +def test_infer_freq_tz_transition(tz_naive_fixture, date_pair, freq): + # see gh-8772 + tz = tz_naive_fixture + idx = date_range(date_pair[0], date_pair[1], freq=freq, tz=tz) + assert idx.inferred_freq == freq + + +def test_infer_freq_tz_transition_custom(): + index = date_range("2013-11-03", periods=5, + freq="3H").tz_localize("America/Chicago") + assert index.inferred_freq is None + + +@pytest.mark.parametrize("data,expected", [ + # Hourly freq in a day must result in "H" + (["2014-07-01 09:00", "2014-07-01 10:00", "2014-07-01 11:00", + "2014-07-01 12:00", "2014-07-01 13:00", "2014-07-01 14:00"], "H"), + + (["2014-07-01 09:00", "2014-07-01 10:00", "2014-07-01 11:00", + "2014-07-01 12:00", "2014-07-01 13:00", "2014-07-01 14:00", + "2014-07-01 15:00", "2014-07-01 16:00", "2014-07-02 09:00", + "2014-07-02 10:00", "2014-07-02 11:00"], "BH"), + (["2014-07-04 09:00", "2014-07-04 10:00", "2014-07-04 11:00", + "2014-07-04 12:00", "2014-07-04 13:00", "2014-07-04 14:00", + "2014-07-04 15:00", "2014-07-04 16:00", "2014-07-07 09:00", + "2014-07-07 10:00", "2014-07-07 11:00"], "BH"), + (["2014-07-04 09:00", "2014-07-04 10:00", "2014-07-04 11:00", + "2014-07-04 12:00", "2014-07-04 13:00", "2014-07-04 14:00", + "2014-07-04 15:00", "2014-07-04 16:00", "2014-07-07 09:00", + "2014-07-07 10:00", "2014-07-07 11:00", "2014-07-07 12:00", + "2014-07-07 13:00", "2014-07-07 14:00", "2014-07-07 15:00", + "2014-07-07 16:00", "2014-07-08 09:00", "2014-07-08 10:00", + "2014-07-08 11:00", "2014-07-08 12:00", "2014-07-08 13:00", + "2014-07-08 14:00", "2014-07-08 15:00", "2014-07-08 16:00"], "BH"), +]) +def test_infer_freq_business_hour(data, expected): + # see gh-7905 + idx = DatetimeIndex(data) + assert idx.inferred_freq == expected + + +def test_not_monotonic(): + rng = DatetimeIndex(["1/31/2000", "1/31/2001", "1/31/2002"]) + rng = rng[::-1] + + assert rng.inferred_freq == "-1A-JAN" + + +def test_non_datetime_index2(): + rng = DatetimeIndex(["1/31/2000", "1/31/2001", "1/31/2002"]) + vals = rng.to_pydatetime() + + result = frequencies.infer_freq(vals) + assert result == rng.inferred_freq + + +@pytest.mark.parametrize("idx", [ + tm.makeIntIndex(10), tm.makeFloatIndex(10), tm.makePeriodIndex(10) +]) +def test_invalid_index_types(idx): + msg = ("(cannot infer freq from a non-convertible)|" + "(Check the `freq` attribute instead of using infer_freq)") + + with pytest.raises(TypeError, match=msg): + frequencies.infer_freq(idx) + + +@pytest.mark.skipif(is_platform_windows(), + reason="see gh-10822: Windows issue") +@pytest.mark.parametrize("idx", [tm.makeStringIndex(10), + tm.makeUnicodeIndex(10)]) +def test_invalid_index_types_unicode(idx): + # see gh-10822 + # + # Odd error message on conversions to datetime for unicode. + msg = "Unknown string format" + + with pytest.raises(ValueError, match=msg): + frequencies.infer_freq(idx) + + +def test_string_datetime_like_compat(): + # see gh-6463 + data = ["2004-01", "2004-02", "2004-03", "2004-04"] + + expected = frequencies.infer_freq(data) + result = frequencies.infer_freq(Index(data)) + + assert result == expected + + +def test_series(): + # see gh-6407 + s = Series(date_range("20130101", "20130110")) + inferred = frequencies.infer_freq(s) + assert inferred == "D" + + +@pytest.mark.parametrize("end", [10, 10.]) +def test_series_invalid_type(end): + # see gh-6407 + msg = "cannot infer freq from a non-convertible dtype on a Series" + s = Series(np.arange(end)) + + with pytest.raises(TypeError, match=msg): + frequencies.infer_freq(s) + + +def test_series_inconvertible_string(): + # see gh-6407 + msg = "Unknown string format" + + with pytest.raises(ValueError, match=msg): + frequencies.infer_freq(Series(["foo", "bar"])) + + +@pytest.mark.parametrize("freq", [None, "L"]) +def test_series_period_index(freq): + # see gh-6407 + # + # Cannot infer on PeriodIndex + msg = "cannot infer freq from a non-convertible dtype on a Series" + s = Series(period_range("2013", periods=10, freq=freq)) + + with pytest.raises(TypeError, match=msg): + frequencies.infer_freq(s) + + +@pytest.mark.parametrize("freq", ["M", "L", "S"]) +def test_series_datetime_index(freq): + s = Series(date_range("20130101", periods=10, freq=freq)) + inferred = frequencies.infer_freq(s) + assert inferred == freq + + +@pytest.mark.parametrize("offset_func", [ + frequencies.get_offset, + lambda freq: date_range("2011-01-01", periods=5, freq=freq) +]) +@pytest.mark.parametrize("freq", [ + "WEEKDAY", "EOM", "W@MON", "W@TUE", "W@WED", "W@THU", + "W@FRI", "W@SAT", "W@SUN", "Q@JAN", "Q@FEB", "Q@MAR", + "A@JAN", "A@FEB", "A@MAR", "A@APR", "A@MAY", "A@JUN", + "A@JUL", "A@AUG", "A@SEP", "A@OCT", "A@NOV", "A@DEC", + "Y@JAN", "WOM@1MON", "WOM@2MON", "WOM@3MON", + "WOM@4MON", "WOM@1TUE", "WOM@2TUE", "WOM@3TUE", + "WOM@4TUE", "WOM@1WED", "WOM@2WED", "WOM@3WED", + "WOM@4WED", "WOM@1THU", "WOM@2THU", "WOM@3THU", + "WOM@4THU", "WOM@1FRI", "WOM@2FRI", "WOM@3FRI", + "WOM@4FRI" +]) +def test_legacy_offset_warnings(offset_func, freq): + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + offset_func(freq) + + +def test_ms_vs_capital_ms(): + left = frequencies.get_offset("ms") + right = frequencies.get_offset("MS") + + assert left == offsets.Milli() + assert right == offsets.MonthBegin() diff --git a/pandas/tests/tseries/frequencies/test_to_offset.py b/pandas/tests/tseries/frequencies/test_to_offset.py new file mode 100644 index 0000000000000..c9c35b47f3475 --- /dev/null +++ b/pandas/tests/tseries/frequencies/test_to_offset.py @@ -0,0 +1,146 @@ +import re + +import pytest + +from pandas import Timedelta + +import pandas.tseries.frequencies as frequencies +import pandas.tseries.offsets as offsets + + +@pytest.mark.parametrize("freq_input,expected", [ + (frequencies.to_offset("10us"), offsets.Micro(10)), + (offsets.Hour(), offsets.Hour()), + ((5, "T"), offsets.Minute(5)), + ("2h30min", offsets.Minute(150)), + ("2h 30min", offsets.Minute(150)), + ("2h30min15s", offsets.Second(150 * 60 + 15)), + ("2h 60min", offsets.Hour(3)), + ("2h 20.5min", offsets.Second(8430)), + ("1.5min", offsets.Second(90)), + ("0.5S", offsets.Milli(500)), + ("15l500u", offsets.Micro(15500)), + ("10s75L", offsets.Milli(10075)), + ("1s0.25ms", offsets.Micro(1000250)), + ("1s0.25L", offsets.Micro(1000250)), + ("2800N", offsets.Nano(2800)), + ("2SM", offsets.SemiMonthEnd(2)), + ("2SM-16", offsets.SemiMonthEnd(2, day_of_month=16)), + ("2SMS-14", offsets.SemiMonthBegin(2, day_of_month=14)), + ("2SMS-15", offsets.SemiMonthBegin(2)), +]) +def test_to_offset(freq_input, expected): + result = frequencies.to_offset(freq_input) + assert result == expected + + +@pytest.mark.parametrize("freqstr,expected", [ + ("-1S", -1), + ("-2SM", -2), + ("-1SMS", -1), + ("-5min10s", -310), +]) +def test_to_offset_negative(freqstr, expected): + result = frequencies.to_offset(freqstr) + assert result.n == expected + + +@pytest.mark.parametrize("freqstr", [ + "2h20m", "U1", "-U", "3U1", "-2-3U", "-2D:3H", + "1.5.0S", "2SMS-15-15", "2SMS-15D", "100foo", + + # Invalid leading +/- signs. + "+-1d", "-+1h", "+1", "-7", "+d", "-m", + + # Invalid shortcut anchors. + "SM-0", "SM-28", "SM-29", "SM-FOO", "BSM", "SM--1", "SMS-1", + "SMS-28", "SMS-30", "SMS-BAR", "SMS-BYR", "BSMS", "SMS--2" +]) +def test_to_offset_invalid(freqstr): + # see gh-13930 + + # We escape string because some of our + # inputs contain regex special characters. + msg = re.escape("Invalid frequency: {freqstr}".format(freqstr=freqstr)) + with pytest.raises(ValueError, match=msg): + frequencies.to_offset(freqstr) + + +def test_to_offset_no_evaluate(): + with pytest.raises(ValueError, match="Could not evaluate"): + frequencies.to_offset(("", "")) + + +@pytest.mark.parametrize("freqstr,expected", [ + ("2D 3H", offsets.Hour(51)), + ("2 D3 H", offsets.Hour(51)), + ("2 D 3 H", offsets.Hour(51)), + (" 2 D 3 H ", offsets.Hour(51)), + (" H ", offsets.Hour()), + (" 3 H ", offsets.Hour(3)), +]) +def test_to_offset_whitespace(freqstr, expected): + result = frequencies.to_offset(freqstr) + assert result == expected + + +@pytest.mark.parametrize("freqstr,expected", [ + ("00H 00T 01S", 1), + ("-00H 03T 14S", -194), +]) +def test_to_offset_leading_zero(freqstr, expected): + result = frequencies.to_offset(freqstr) + assert result.n == expected + + +@pytest.mark.parametrize("freqstr,expected", [ + ("+1d", 1), + ("+2h30min", 150), +]) +def test_to_offset_leading_plus(freqstr, expected): + result = frequencies.to_offset(freqstr) + assert result.n == expected + + +@pytest.mark.parametrize("kwargs,expected", [ + (dict(days=1, seconds=1), offsets.Second(86401)), + (dict(days=-1, seconds=1), offsets.Second(-86399)), + (dict(hours=1, minutes=10), offsets.Minute(70)), + (dict(hours=1, minutes=-10), offsets.Minute(50)), + (dict(weeks=1), offsets.Day(7)), + (dict(hours=1), offsets.Hour(1)), + (dict(hours=1), frequencies.to_offset("60min")), + (dict(microseconds=1), offsets.Micro(1)) +]) +def test_to_offset_pd_timedelta(kwargs, expected): + # see gh-9064 + td = Timedelta(**kwargs) + result = frequencies.to_offset(td) + assert result == expected + + +def test_to_offset_pd_timedelta_invalid(): + # see gh-9064 + msg = "Invalid frequency: 0 days 00:00:00" + td = Timedelta(microseconds=0) + + with pytest.raises(ValueError, match=msg): + frequencies.to_offset(td) + + +@pytest.mark.parametrize("shortcut,expected", [ + ("W", offsets.Week(weekday=6)), + ("W-SUN", offsets.Week(weekday=6)), + ("Q", offsets.QuarterEnd(startingMonth=12)), + ("Q-DEC", offsets.QuarterEnd(startingMonth=12)), + ("Q-MAY", offsets.QuarterEnd(startingMonth=5)), + ("SM", offsets.SemiMonthEnd(day_of_month=15)), + ("SM-15", offsets.SemiMonthEnd(day_of_month=15)), + ("SM-1", offsets.SemiMonthEnd(day_of_month=1)), + ("SM-27", offsets.SemiMonthEnd(day_of_month=27)), + ("SMS-2", offsets.SemiMonthBegin(day_of_month=2)), + ("SMS-27", offsets.SemiMonthBegin(day_of_month=27)), +]) +def test_anchored_shortcuts(shortcut, expected): + result = frequencies.to_offset(shortcut) + assert result == expected diff --git a/pandas/tests/tseries/holiday/__init__.py b/pandas/tests/tseries/holiday/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/tseries/holiday/test_calendar.py b/pandas/tests/tseries/holiday/test_calendar.py new file mode 100644 index 0000000000000..a5cc4095ce583 --- /dev/null +++ b/pandas/tests/tseries/holiday/test_calendar.py @@ -0,0 +1,77 @@ +from datetime import datetime + +import pytest + +from pandas import DatetimeIndex +import pandas.util.testing as tm + +from pandas.tseries.holiday import ( + AbstractHolidayCalendar, Holiday, Timestamp, USFederalHolidayCalendar, + USThanksgivingDay, get_calendar) + + +@pytest.mark.parametrize("transform", [ + lambda x: x, + lambda x: x.strftime("%Y-%m-%d"), + lambda x: Timestamp(x) +]) +def test_calendar(transform): + start_date = datetime(2012, 1, 1) + end_date = datetime(2012, 12, 31) + + calendar = USFederalHolidayCalendar() + holidays = calendar.holidays(transform(start_date), transform(end_date)) + + expected = [ + datetime(2012, 1, 2), + datetime(2012, 1, 16), + datetime(2012, 2, 20), + datetime(2012, 5, 28), + datetime(2012, 7, 4), + datetime(2012, 9, 3), + datetime(2012, 10, 8), + datetime(2012, 11, 12), + datetime(2012, 11, 22), + datetime(2012, 12, 25) + ] + + assert list(holidays.to_pydatetime()) == expected + + +def test_calendar_caching(): + # see gh-9552. + + class TestCalendar(AbstractHolidayCalendar): + def __init__(self, name=None, rules=None): + super(TestCalendar, self).__init__(name=name, rules=rules) + + jan1 = TestCalendar(rules=[Holiday("jan1", year=2015, month=1, day=1)]) + jan2 = TestCalendar(rules=[Holiday("jan2", year=2015, month=1, day=2)]) + + # Getting holidays for Jan 1 should not alter results for Jan 2. + tm.assert_index_equal(jan1.holidays(), DatetimeIndex(["01-Jan-2015"])) + tm.assert_index_equal(jan2.holidays(), DatetimeIndex(["02-Jan-2015"])) + + +def test_calendar_observance_dates(): + # see gh-11477 + us_fed_cal = get_calendar("USFederalHolidayCalendar") + holidays0 = us_fed_cal.holidays(datetime(2015, 7, 3), datetime( + 2015, 7, 3)) # <-- same start and end dates + holidays1 = us_fed_cal.holidays(datetime(2015, 7, 3), datetime( + 2015, 7, 6)) # <-- different start and end dates + holidays2 = us_fed_cal.holidays(datetime(2015, 7, 3), datetime( + 2015, 7, 3)) # <-- same start and end dates + + # These should all produce the same result. + # + # In addition, calling with different start and end + # dates should not alter the output if we call the + # function again with the same start and end date. + tm.assert_index_equal(holidays0, holidays1) + tm.assert_index_equal(holidays0, holidays2) + + +def test_rule_from_name(): + us_fed_cal = get_calendar("USFederalHolidayCalendar") + assert us_fed_cal.rule_from_name("Thanksgiving") == USThanksgivingDay diff --git a/pandas/tests/tseries/holiday/test_federal.py b/pandas/tests/tseries/holiday/test_federal.py new file mode 100644 index 0000000000000..62b5ab2b849ae --- /dev/null +++ b/pandas/tests/tseries/holiday/test_federal.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from pandas.tseries.holiday import ( + AbstractHolidayCalendar, USMartinLutherKingJr, USMemorialDay) + + +def test_no_mlk_before_1986(): + # see gh-10278 + class MLKCalendar(AbstractHolidayCalendar): + rules = [USMartinLutherKingJr] + + holidays = MLKCalendar().holidays(start="1984", + end="1988").to_pydatetime().tolist() + + # Testing to make sure holiday is not incorrectly observed before 1986. + assert holidays == [datetime(1986, 1, 20, 0, 0), + datetime(1987, 1, 19, 0, 0)] + + +def test_memorial_day(): + class MemorialDay(AbstractHolidayCalendar): + rules = [USMemorialDay] + + holidays = MemorialDay().holidays(start="1971", + end="1980").to_pydatetime().tolist() + + # Fixes 5/31 error and checked manually against Wikipedia. + assert holidays == [datetime(1971, 5, 31, 0, 0), + datetime(1972, 5, 29, 0, 0), + datetime(1973, 5, 28, 0, 0), + datetime(1974, 5, 27, 0, 0), + datetime(1975, 5, 26, 0, 0), + datetime(1976, 5, 31, 0, 0), + datetime(1977, 5, 30, 0, 0), + datetime(1978, 5, 29, 0, 0), + datetime(1979, 5, 28, 0, 0)] diff --git a/pandas/tests/tseries/holiday/test_holiday.py b/pandas/tests/tseries/holiday/test_holiday.py new file mode 100644 index 0000000000000..27bba1cc89dee --- /dev/null +++ b/pandas/tests/tseries/holiday/test_holiday.py @@ -0,0 +1,193 @@ +from datetime import datetime + +import pytest +from pytz import utc + +import pandas.util.testing as tm + +from pandas.tseries.holiday import ( + MO, SA, AbstractHolidayCalendar, DateOffset, EasterMonday, GoodFriday, + Holiday, HolidayCalendarFactory, Timestamp, USColumbusDay, USLaborDay, + USMartinLutherKingJr, USMemorialDay, USPresidentsDay, USThanksgivingDay, + get_calendar, next_monday) + + +def _check_holiday_results(holiday, start, end, expected): + """ + Check that the dates for a given holiday match in date and timezone. + + Parameters + ---------- + holiday : Holiday + The holiday to check. + start : datetime-like + The start date of range in which to collect dates for a given holiday. + end : datetime-like + The end date of range in which to collect dates for a given holiday. + expected : list + The list of dates we expect to get. + """ + assert list(holiday.dates(start, end)) == expected + + # Verify that timezone info is preserved. + assert (list(holiday.dates(utc.localize(Timestamp(start)), + utc.localize(Timestamp(end)))) == + [utc.localize(dt) for dt in expected]) + + +@pytest.mark.parametrize("holiday,start_date,end_date,expected", [ + (USMemorialDay, datetime(2011, 1, 1), datetime(2020, 12, 31), + [datetime(2011, 5, 30), datetime(2012, 5, 28), datetime(2013, 5, 27), + datetime(2014, 5, 26), datetime(2015, 5, 25), datetime(2016, 5, 30), + datetime(2017, 5, 29), datetime(2018, 5, 28), datetime(2019, 5, 27), + datetime(2020, 5, 25)]), + + (Holiday("July 4th Eve", month=7, day=3), "2001-01-01", "2003-03-03", + [Timestamp("2001-07-03 00:00:00"), Timestamp("2002-07-03 00:00:00")]), + (Holiday("July 4th Eve", month=7, day=3, days_of_week=(0, 1, 2, 3)), + "2001-01-01", "2008-03-03", [ + Timestamp("2001-07-03 00:00:00"), Timestamp("2002-07-03 00:00:00"), + Timestamp("2003-07-03 00:00:00"), Timestamp("2006-07-03 00:00:00"), + Timestamp("2007-07-03 00:00:00")]), + + (EasterMonday, datetime(2011, 1, 1), datetime(2020, 12, 31), + [Timestamp("2011-04-25 00:00:00"), Timestamp("2012-04-09 00:00:00"), + Timestamp("2013-04-01 00:00:00"), Timestamp("2014-04-21 00:00:00"), + Timestamp("2015-04-06 00:00:00"), Timestamp("2016-03-28 00:00:00"), + Timestamp("2017-04-17 00:00:00"), Timestamp("2018-04-02 00:00:00"), + Timestamp("2019-04-22 00:00:00"), Timestamp("2020-04-13 00:00:00")]), + (GoodFriday, datetime(2011, 1, 1), datetime(2020, 12, 31), + [Timestamp("2011-04-22 00:00:00"), Timestamp("2012-04-06 00:00:00"), + Timestamp("2013-03-29 00:00:00"), Timestamp("2014-04-18 00:00:00"), + Timestamp("2015-04-03 00:00:00"), Timestamp("2016-03-25 00:00:00"), + Timestamp("2017-04-14 00:00:00"), Timestamp("2018-03-30 00:00:00"), + Timestamp("2019-04-19 00:00:00"), Timestamp("2020-04-10 00:00:00")]), + + (USThanksgivingDay, datetime(2011, 1, 1), datetime(2020, 12, 31), + [datetime(2011, 11, 24), datetime(2012, 11, 22), datetime(2013, 11, 28), + datetime(2014, 11, 27), datetime(2015, 11, 26), datetime(2016, 11, 24), + datetime(2017, 11, 23), datetime(2018, 11, 22), datetime(2019, 11, 28), + datetime(2020, 11, 26)]) +]) +def test_holiday_dates(holiday, start_date, end_date, expected): + _check_holiday_results(holiday, start_date, end_date, expected) + + +@pytest.mark.parametrize("holiday,start,expected", [ + (USMemorialDay, datetime(2015, 7, 1), []), + (USMemorialDay, "2015-05-25", "2015-05-25"), + + (USLaborDay, datetime(2015, 7, 1), []), + (USLaborDay, "2015-09-07", "2015-09-07"), + + (USColumbusDay, datetime(2015, 7, 1), []), + (USColumbusDay, "2015-10-12", "2015-10-12"), + + (USThanksgivingDay, datetime(2015, 7, 1), []), + (USThanksgivingDay, "2015-11-26", "2015-11-26"), + + (USMartinLutherKingJr, datetime(2015, 7, 1), []), + (USMartinLutherKingJr, "2015-01-19", "2015-01-19"), + + (USPresidentsDay, datetime(2015, 7, 1), []), + (USPresidentsDay, "2015-02-16", "2015-02-16"), + + (GoodFriday, datetime(2015, 7, 1), []), + (GoodFriday, "2015-04-03", "2015-04-03"), + + (EasterMonday, "2015-04-06", "2015-04-06"), + (EasterMonday, datetime(2015, 7, 1), []), + (EasterMonday, "2015-04-05", []), + + ("New Years Day", "2015-01-01", "2015-01-01"), + ("New Years Day", "2010-12-31", "2010-12-31"), + ("New Years Day", datetime(2015, 7, 1), []), + ("New Years Day", "2011-01-01", []), + + ("July 4th", "2015-07-03", "2015-07-03"), + ("July 4th", datetime(2015, 7, 1), []), + ("July 4th", "2015-07-04", []), + + ("Veterans Day", "2012-11-12", "2012-11-12"), + ("Veterans Day", datetime(2015, 7, 1), []), + ("Veterans Day", "2012-11-11", []), + + ("Christmas", "2011-12-26", "2011-12-26"), + ("Christmas", datetime(2015, 7, 1), []), + ("Christmas", "2011-12-25", []), +]) +def test_holidays_within_dates(holiday, start, expected): + # see gh-11477 + # + # Fix holiday behavior where holiday.dates returned dates outside + # start/end date, or observed rules could not be applied because the + # holiday was not in the original date range (e.g., 7/4/2015 -> 7/3/2015). + if isinstance(holiday, str): + calendar = get_calendar("USFederalHolidayCalendar") + holiday = calendar.rule_from_name(holiday) + + if isinstance(expected, str): + expected = [Timestamp(expected)] + + _check_holiday_results(holiday, start, start, expected) + + +@pytest.mark.parametrize("transform", [ + lambda x: x.strftime("%Y-%m-%d"), + lambda x: Timestamp(x) +]) +def test_argument_types(transform): + start_date = datetime(2011, 1, 1) + end_date = datetime(2020, 12, 31) + + holidays = USThanksgivingDay.dates(start_date, end_date) + holidays2 = USThanksgivingDay.dates( + transform(start_date), transform(end_date)) + tm.assert_index_equal(holidays, holidays2) + + +@pytest.mark.parametrize("name,kwargs", [ + ("One-Time", dict(year=2012, month=5, day=28)), + ("Range", dict(month=5, day=28, start_date=datetime(2012, 1, 1), + end_date=datetime(2012, 12, 31), + offset=DateOffset(weekday=MO(1)))) +]) +def test_special_holidays(name, kwargs): + base_date = [datetime(2012, 5, 28)] + holiday = Holiday(name, **kwargs) + + start_date = datetime(2011, 1, 1) + end_date = datetime(2020, 12, 31) + + assert base_date == holiday.dates(start_date, end_date) + + +def test_get_calendar(): + class TestCalendar(AbstractHolidayCalendar): + rules = [] + + calendar = get_calendar("TestCalendar") + assert TestCalendar == calendar.__class__ + + +def test_factory(): + class_1 = HolidayCalendarFactory("MemorialDay", + AbstractHolidayCalendar, + USMemorialDay) + class_2 = HolidayCalendarFactory("Thanksgiving", + AbstractHolidayCalendar, + USThanksgivingDay) + class_3 = HolidayCalendarFactory("Combined", class_1, class_2) + + assert len(class_1.rules) == 1 + assert len(class_2.rules) == 1 + assert len(class_3.rules) == 2 + + +def test_both_offset_observance_raises(): + # see gh-10217 + msg = "Cannot use both offset and observance" + with pytest.raises(NotImplementedError, match=msg): + Holiday("Cyber Monday", month=11, day=1, + offset=[DateOffset(weekday=SA(4))], + observance=next_monday) diff --git a/pandas/tests/tseries/holiday/test_observance.py b/pandas/tests/tseries/holiday/test_observance.py new file mode 100644 index 0000000000000..1c22918b2efd8 --- /dev/null +++ b/pandas/tests/tseries/holiday/test_observance.py @@ -0,0 +1,93 @@ +from datetime import datetime + +import pytest + +from pandas.tseries.holiday import ( + after_nearest_workday, before_nearest_workday, nearest_workday, + next_monday, next_monday_or_tuesday, next_workday, previous_friday, + previous_workday, sunday_to_monday, weekend_to_monday) + +_WEDNESDAY = datetime(2014, 4, 9) +_THURSDAY = datetime(2014, 4, 10) +_FRIDAY = datetime(2014, 4, 11) +_SATURDAY = datetime(2014, 4, 12) +_SUNDAY = datetime(2014, 4, 13) +_MONDAY = datetime(2014, 4, 14) +_TUESDAY = datetime(2014, 4, 15) + + +@pytest.mark.parametrize("day", [_SATURDAY, _SUNDAY]) +def test_next_monday(day): + assert next_monday(day) == _MONDAY + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _MONDAY), + (_SUNDAY, _TUESDAY), + (_MONDAY, _TUESDAY) +]) +def test_next_monday_or_tuesday(day, expected): + assert next_monday_or_tuesday(day) == expected + + +@pytest.mark.parametrize("day", [_SATURDAY, _SUNDAY]) +def test_previous_friday(day): + assert previous_friday(day) == _FRIDAY + + +def test_sunday_to_monday(): + assert sunday_to_monday(_SUNDAY) == _MONDAY + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _FRIDAY), + (_SUNDAY, _MONDAY), + (_MONDAY, _MONDAY) +]) +def test_nearest_workday(day, expected): + assert nearest_workday(day) == expected + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _MONDAY), + (_SUNDAY, _MONDAY), + (_MONDAY, _MONDAY) +]) +def test_weekend_to_monday(day, expected): + assert weekend_to_monday(day) == expected + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _MONDAY), + (_SUNDAY, _MONDAY), + (_MONDAY, _TUESDAY) +]) +def test_next_workday(day, expected): + assert next_workday(day) == expected + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _FRIDAY), + (_SUNDAY, _FRIDAY), + (_TUESDAY, _MONDAY) +]) +def test_previous_workday(day, expected): + assert previous_workday(day) == expected + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _THURSDAY), + (_SUNDAY, _FRIDAY), + (_TUESDAY, _MONDAY) +]) +def test_before_nearest_workday(day, expected): + assert before_nearest_workday(day) == expected + + +@pytest.mark.parametrize("day,expected", [ + (_SATURDAY, _MONDAY), + (_SUNDAY, _TUESDAY), + (_FRIDAY, _MONDAY) +]) +def test_after_nearest_workday(day, expected): + assert after_nearest_workday(day) == expected diff --git a/pandas/tests/tseries/offsets/__init__.py b/pandas/tests/tseries/offsets/__init__.py new file mode 100644 index 0000000000000..40a96afc6ff09 --- /dev/null +++ b/pandas/tests/tseries/offsets/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/pandas/tests/tseries/offsets/common.py b/pandas/tests/tseries/offsets/common.py new file mode 100644 index 0000000000000..2e8eb224bca7f --- /dev/null +++ b/pandas/tests/tseries/offsets/common.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" +Assertion helpers for offsets tests +""" + + +def assert_offset_equal(offset, base, expected): + actual = offset + base + actual_swapped = base + offset + actual_apply = offset.apply(base) + try: + assert actual == expected + assert actual_swapped == expected + assert actual_apply == expected + except AssertionError: + raise AssertionError("\nExpected: %s\nActual: %s\nFor Offset: %s)" + "\nAt Date: %s" % + (expected, actual, offset, base)) + + +def assert_onOffset(offset, date, expected): + actual = offset.onOffset(date) + assert actual == expected, ("\nExpected: %s\nActual: %s\nFor Offset: %s)" + "\nAt Date: %s" % + (expected, actual, offset, date)) diff --git a/pandas/tests/tseries/offsets/conftest.py b/pandas/tests/tseries/offsets/conftest.py new file mode 100644 index 0000000000000..c192a56b205ca --- /dev/null +++ b/pandas/tests/tseries/offsets/conftest.py @@ -0,0 +1,21 @@ +import pytest + +import pandas.tseries.offsets as offsets + + +@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__]) +def offset_types(request): + """ + Fixture for all the datetime offsets available for a time series. + """ + return request.param + + +@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__ if + issubclass(getattr(offsets, o), offsets.MonthOffset) + and o != 'MonthOffset']) +def month_classes(request): + """ + Fixture for month based datetime offsets available for a time series. + """ + return request.param diff --git a/pandas/tests/tseries/data/cday-0.14.1.pickle b/pandas/tests/tseries/offsets/data/cday-0.14.1.pickle similarity index 100% rename from pandas/tests/tseries/data/cday-0.14.1.pickle rename to pandas/tests/tseries/offsets/data/cday-0.14.1.pickle diff --git a/pandas/tests/tseries/data/dateoffset_0_15_2.pickle b/pandas/tests/tseries/offsets/data/dateoffset_0_15_2.pickle similarity index 100% rename from pandas/tests/tseries/data/dateoffset_0_15_2.pickle rename to pandas/tests/tseries/offsets/data/dateoffset_0_15_2.pickle diff --git a/pandas/tests/tseries/offsets/test_fiscal.py b/pandas/tests/tseries/offsets/test_fiscal.py new file mode 100644 index 0000000000000..a5d7460921fb4 --- /dev/null +++ b/pandas/tests/tseries/offsets/test_fiscal.py @@ -0,0 +1,657 @@ +# -*- coding: utf-8 -*- +""" +Tests for Fiscal Year and Fiscal Quarter offset classes +""" +from datetime import datetime + +from dateutil.relativedelta import relativedelta +import pytest + +from pandas._libs.tslibs.frequencies import INVALID_FREQ_ERR_MSG + +from pandas import Timestamp + +from pandas.tseries.frequencies import get_offset +from pandas.tseries.offsets import FY5253, FY5253Quarter + +from .common import assert_offset_equal, assert_onOffset +from .test_offsets import Base, WeekDay + + +def makeFY5253LastOfMonthQuarter(*args, **kwds): + return FY5253Quarter(*args, variation="last", **kwds) + + +def makeFY5253NearestEndMonthQuarter(*args, **kwds): + return FY5253Quarter(*args, variation="nearest", **kwds) + + +def makeFY5253NearestEndMonth(*args, **kwds): + return FY5253(*args, variation="nearest", **kwds) + + +def makeFY5253LastOfMonth(*args, **kwds): + return FY5253(*args, variation="last", **kwds) + + +def test_get_offset_name(): + assert (makeFY5253LastOfMonthQuarter( + weekday=1, startingMonth=3, + qtr_with_extra_week=4).freqstr == "REQ-L-MAR-TUE-4") + assert (makeFY5253NearestEndMonthQuarter( + weekday=1, startingMonth=3, + qtr_with_extra_week=3).freqstr == "REQ-N-MAR-TUE-3") + + +def test_get_offset(): + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + get_offset('gibberish') + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + get_offset('QS-JAN-B') + + pairs = [ + ("RE-N-DEC-MON", + makeFY5253NearestEndMonth(weekday=0, startingMonth=12)), + ("RE-L-DEC-TUE", + makeFY5253LastOfMonth(weekday=1, startingMonth=12)), + ("REQ-L-MAR-TUE-4", + makeFY5253LastOfMonthQuarter(weekday=1, + startingMonth=3, + qtr_with_extra_week=4)), + ("REQ-L-DEC-MON-3", + makeFY5253LastOfMonthQuarter(weekday=0, + startingMonth=12, + qtr_with_extra_week=3)), + ("REQ-N-DEC-MON-3", + makeFY5253NearestEndMonthQuarter(weekday=0, + startingMonth=12, + qtr_with_extra_week=3))] + + for name, expected in pairs: + offset = get_offset(name) + assert offset == expected, ("Expected %r to yield %r (actual: %r)" % + (name, expected, offset)) + + +class TestFY5253LastOfMonth(Base): + offset_lom_sat_aug = makeFY5253LastOfMonth(1, startingMonth=8, + weekday=WeekDay.SAT) + offset_lom_sat_sep = makeFY5253LastOfMonth(1, startingMonth=9, + weekday=WeekDay.SAT) + + on_offset_cases = [ + # From Wikipedia (see: + # http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar#Last_Saturday_of_the_month_at_fiscal_year_end) + (offset_lom_sat_aug, datetime(2006, 8, 26), True), + (offset_lom_sat_aug, datetime(2007, 8, 25), True), + (offset_lom_sat_aug, datetime(2008, 8, 30), True), + (offset_lom_sat_aug, datetime(2009, 8, 29), True), + (offset_lom_sat_aug, datetime(2010, 8, 28), True), + (offset_lom_sat_aug, datetime(2011, 8, 27), True), + (offset_lom_sat_aug, datetime(2012, 8, 25), True), + (offset_lom_sat_aug, datetime(2013, 8, 31), True), + (offset_lom_sat_aug, datetime(2014, 8, 30), True), + (offset_lom_sat_aug, datetime(2015, 8, 29), True), + (offset_lom_sat_aug, datetime(2016, 8, 27), True), + (offset_lom_sat_aug, datetime(2017, 8, 26), True), + (offset_lom_sat_aug, datetime(2018, 8, 25), True), + (offset_lom_sat_aug, datetime(2019, 8, 31), True), + + (offset_lom_sat_aug, datetime(2006, 8, 27), False), + (offset_lom_sat_aug, datetime(2007, 8, 28), False), + (offset_lom_sat_aug, datetime(2008, 8, 31), False), + (offset_lom_sat_aug, datetime(2009, 8, 30), False), + (offset_lom_sat_aug, datetime(2010, 8, 29), False), + (offset_lom_sat_aug, datetime(2011, 8, 28), False), + + (offset_lom_sat_aug, datetime(2006, 8, 25), False), + (offset_lom_sat_aug, datetime(2007, 8, 24), False), + (offset_lom_sat_aug, datetime(2008, 8, 29), False), + (offset_lom_sat_aug, datetime(2009, 8, 28), False), + (offset_lom_sat_aug, datetime(2010, 8, 27), False), + (offset_lom_sat_aug, datetime(2011, 8, 26), False), + (offset_lom_sat_aug, datetime(2019, 8, 30), False), + + # From GMCR (see for example: + # http://yahoo.brand.edgar-online.com/Default.aspx? + # companyid=3184&formtypeID=7) + (offset_lom_sat_sep, datetime(2010, 9, 25), True), + (offset_lom_sat_sep, datetime(2011, 9, 24), True), + (offset_lom_sat_sep, datetime(2012, 9, 29), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + def test_apply(self): + offset_lom_aug_sat = makeFY5253LastOfMonth(startingMonth=8, + weekday=WeekDay.SAT) + offset_lom_aug_sat_1 = makeFY5253LastOfMonth(n=1, startingMonth=8, + weekday=WeekDay.SAT) + + date_seq_lom_aug_sat = [datetime(2006, 8, 26), datetime(2007, 8, 25), + datetime(2008, 8, 30), datetime(2009, 8, 29), + datetime(2010, 8, 28), datetime(2011, 8, 27), + datetime(2012, 8, 25), datetime(2013, 8, 31), + datetime(2014, 8, 30), datetime(2015, 8, 29), + datetime(2016, 8, 27)] + + tests = [ + (offset_lom_aug_sat, date_seq_lom_aug_sat), + (offset_lom_aug_sat_1, date_seq_lom_aug_sat), + (offset_lom_aug_sat, [ + datetime(2006, 8, 25)] + date_seq_lom_aug_sat), + (offset_lom_aug_sat_1, [ + datetime(2006, 8, 27)] + date_seq_lom_aug_sat[1:]), + (makeFY5253LastOfMonth(n=-1, startingMonth=8, + weekday=WeekDay.SAT), + list(reversed(date_seq_lom_aug_sat))), + ] + for test in tests: + offset, data = test + current = data[0] + for datum in data[1:]: + current = current + offset + assert current == datum + + +class TestFY5253NearestEndMonth(Base): + + def test_get_year_end(self): + assert (makeFY5253NearestEndMonth( + startingMonth=8, weekday=WeekDay.SAT).get_year_end( + datetime(2013, 1, 1)) == datetime(2013, 8, 31)) + assert (makeFY5253NearestEndMonth( + startingMonth=8, weekday=WeekDay.SUN).get_year_end( + datetime(2013, 1, 1)) == datetime(2013, 9, 1)) + assert (makeFY5253NearestEndMonth( + startingMonth=8, weekday=WeekDay.FRI).get_year_end( + datetime(2013, 1, 1)) == datetime(2013, 8, 30)) + + offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, + variation="nearest") + assert (offset_n.get_year_end(datetime(2012, 1, 1)) == + datetime(2013, 1, 1)) + assert (offset_n.get_year_end(datetime(2012, 1, 10)) == + datetime(2013, 1, 1)) + + assert (offset_n.get_year_end(datetime(2013, 1, 1)) == + datetime(2013, 12, 31)) + assert (offset_n.get_year_end(datetime(2013, 1, 2)) == + datetime(2013, 12, 31)) + assert (offset_n.get_year_end(datetime(2013, 1, 3)) == + datetime(2013, 12, 31)) + assert (offset_n.get_year_end(datetime(2013, 1, 10)) == + datetime(2013, 12, 31)) + + JNJ = FY5253(n=1, startingMonth=12, weekday=6, variation="nearest") + assert (JNJ.get_year_end(datetime(2006, 1, 1)) == + datetime(2006, 12, 31)) + + offset_lom_aug_sat = makeFY5253NearestEndMonth(1, startingMonth=8, + weekday=WeekDay.SAT) + offset_lom_aug_thu = makeFY5253NearestEndMonth(1, startingMonth=8, + weekday=WeekDay.THU) + offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, + variation="nearest") + + on_offset_cases = [ + # From Wikipedia (see: + # http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar + # #Saturday_nearest_the_end_of_month) + # 2006-09-02 2006 September 2 + # 2007-09-01 2007 September 1 + # 2008-08-30 2008 August 30 (leap year) + # 2009-08-29 2009 August 29 + # 2010-08-28 2010 August 28 + # 2011-09-03 2011 September 3 + # 2012-09-01 2012 September 1 (leap year) + # 2013-08-31 2013 August 31 + # 2014-08-30 2014 August 30 + # 2015-08-29 2015 August 29 + # 2016-09-03 2016 September 3 (leap year) + # 2017-09-02 2017 September 2 + # 2018-09-01 2018 September 1 + # 2019-08-31 2019 August 31 + (offset_lom_aug_sat, datetime(2006, 9, 2), True), + (offset_lom_aug_sat, datetime(2007, 9, 1), True), + (offset_lom_aug_sat, datetime(2008, 8, 30), True), + (offset_lom_aug_sat, datetime(2009, 8, 29), True), + (offset_lom_aug_sat, datetime(2010, 8, 28), True), + (offset_lom_aug_sat, datetime(2011, 9, 3), True), + + (offset_lom_aug_sat, datetime(2016, 9, 3), True), + (offset_lom_aug_sat, datetime(2017, 9, 2), True), + (offset_lom_aug_sat, datetime(2018, 9, 1), True), + (offset_lom_aug_sat, datetime(2019, 8, 31), True), + + (offset_lom_aug_sat, datetime(2006, 8, 27), False), + (offset_lom_aug_sat, datetime(2007, 8, 28), False), + (offset_lom_aug_sat, datetime(2008, 8, 31), False), + (offset_lom_aug_sat, datetime(2009, 8, 30), False), + (offset_lom_aug_sat, datetime(2010, 8, 29), False), + (offset_lom_aug_sat, datetime(2011, 8, 28), False), + + (offset_lom_aug_sat, datetime(2006, 8, 25), False), + (offset_lom_aug_sat, datetime(2007, 8, 24), False), + (offset_lom_aug_sat, datetime(2008, 8, 29), False), + (offset_lom_aug_sat, datetime(2009, 8, 28), False), + (offset_lom_aug_sat, datetime(2010, 8, 27), False), + (offset_lom_aug_sat, datetime(2011, 8, 26), False), + (offset_lom_aug_sat, datetime(2019, 8, 30), False), + + # From Micron, see: + # http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 + (offset_lom_aug_thu, datetime(2012, 8, 30), True), + (offset_lom_aug_thu, datetime(2011, 9, 1), True), + + (offset_n, datetime(2012, 12, 31), False), + (offset_n, datetime(2013, 1, 1), True), + (offset_n, datetime(2013, 1, 2), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + def test_apply(self): + date_seq_nem_8_sat = [datetime(2006, 9, 2), datetime(2007, 9, 1), + datetime(2008, 8, 30), datetime(2009, 8, 29), + datetime(2010, 8, 28), datetime(2011, 9, 3)] + + JNJ = [datetime(2005, 1, 2), datetime(2006, 1, 1), + datetime(2006, 12, 31), datetime(2007, 12, 30), + datetime(2008, 12, 28), datetime(2010, 1, 3), + datetime(2011, 1, 2), datetime(2012, 1, 1), + datetime(2012, 12, 30)] + + DEC_SAT = FY5253(n=-1, startingMonth=12, weekday=5, + variation="nearest") + + tests = [ + (makeFY5253NearestEndMonth(startingMonth=8, + weekday=WeekDay.SAT), + date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(n=1, startingMonth=8, + weekday=WeekDay.SAT), + date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT), + [datetime(2006, 9, 1)] + date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(n=1, startingMonth=8, + weekday=WeekDay.SAT), + [datetime(2006, 9, 3)] + date_seq_nem_8_sat[1:]), + (makeFY5253NearestEndMonth(n=-1, startingMonth=8, + weekday=WeekDay.SAT), + list(reversed(date_seq_nem_8_sat))), + (makeFY5253NearestEndMonth(n=1, startingMonth=12, + weekday=WeekDay.SUN), JNJ), + (makeFY5253NearestEndMonth(n=-1, startingMonth=12, + weekday=WeekDay.SUN), + list(reversed(JNJ))), + (makeFY5253NearestEndMonth(n=1, startingMonth=12, + weekday=WeekDay.SUN), + [datetime(2005, 1, 2), datetime(2006, 1, 1)]), + (makeFY5253NearestEndMonth(n=1, startingMonth=12, + weekday=WeekDay.SUN), + [datetime(2006, 1, 2), datetime(2006, 12, 31)]), + (DEC_SAT, [datetime(2013, 1, 15), datetime(2012, 12, 29)]) + ] + for test in tests: + offset, data = test + current = data[0] + for datum in data[1:]: + current = current + offset + assert current == datum + + +class TestFY5253LastOfMonthQuarter(Base): + + def test_isAnchored(self): + assert makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SAT, + qtr_with_extra_week=4).isAnchored() + assert makeFY5253LastOfMonthQuarter( + weekday=WeekDay.SAT, startingMonth=3, + qtr_with_extra_week=4).isAnchored() + assert not makeFY5253LastOfMonthQuarter( + 2, startingMonth=1, weekday=WeekDay.SAT, + qtr_with_extra_week=4).isAnchored() + + def test_equality(self): + assert (makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SAT, + qtr_with_extra_week=4) == makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4)) + assert (makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SAT, + qtr_with_extra_week=4) != makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SUN, qtr_with_extra_week=4)) + assert (makeFY5253LastOfMonthQuarter( + startingMonth=1, weekday=WeekDay.SAT, + qtr_with_extra_week=4) != makeFY5253LastOfMonthQuarter( + startingMonth=2, weekday=WeekDay.SAT, qtr_with_extra_week=4)) + + def test_offset(self): + offset = makeFY5253LastOfMonthQuarter(1, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + offset2 = makeFY5253LastOfMonthQuarter(2, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + offset4 = makeFY5253LastOfMonthQuarter(4, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + + offset_neg1 = makeFY5253LastOfMonthQuarter(-1, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + offset_neg2 = makeFY5253LastOfMonthQuarter(-2, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + + GMCR = [datetime(2010, 3, 27), datetime(2010, 6, 26), + datetime(2010, 9, 25), datetime(2010, 12, 25), + datetime(2011, 3, 26), datetime(2011, 6, 25), + datetime(2011, 9, 24), datetime(2011, 12, 24), + datetime(2012, 3, 24), datetime(2012, 6, 23), + datetime(2012, 9, 29), datetime(2012, 12, 29), + datetime(2013, 3, 30), datetime(2013, 6, 29)] + + assert_offset_equal(offset, base=GMCR[0], expected=GMCR[1]) + assert_offset_equal(offset, base=GMCR[0] + relativedelta(days=-1), + expected=GMCR[0]) + assert_offset_equal(offset, base=GMCR[1], expected=GMCR[2]) + + assert_offset_equal(offset2, base=GMCR[0], expected=GMCR[2]) + assert_offset_equal(offset4, base=GMCR[0], expected=GMCR[4]) + + assert_offset_equal(offset_neg1, base=GMCR[-1], expected=GMCR[-2]) + assert_offset_equal(offset_neg1, + base=GMCR[-1] + relativedelta(days=+1), + expected=GMCR[-1]) + assert_offset_equal(offset_neg2, base=GMCR[-1], expected=GMCR[-3]) + + date = GMCR[0] + relativedelta(days=-1) + for expected in GMCR: + assert_offset_equal(offset, date, expected) + date = date + offset + + date = GMCR[-1] + relativedelta(days=+1) + for expected in reversed(GMCR): + assert_offset_equal(offset_neg1, date, expected) + date = date + offset_neg1 + + lomq_aug_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=8, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + lomq_sep_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=9, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + + on_offset_cases = [ + # From Wikipedia + (lomq_aug_sat_4, datetime(2006, 8, 26), True), + (lomq_aug_sat_4, datetime(2007, 8, 25), True), + (lomq_aug_sat_4, datetime(2008, 8, 30), True), + (lomq_aug_sat_4, datetime(2009, 8, 29), True), + (lomq_aug_sat_4, datetime(2010, 8, 28), True), + (lomq_aug_sat_4, datetime(2011, 8, 27), True), + (lomq_aug_sat_4, datetime(2019, 8, 31), True), + + (lomq_aug_sat_4, datetime(2006, 8, 27), False), + (lomq_aug_sat_4, datetime(2007, 8, 28), False), + (lomq_aug_sat_4, datetime(2008, 8, 31), False), + (lomq_aug_sat_4, datetime(2009, 8, 30), False), + (lomq_aug_sat_4, datetime(2010, 8, 29), False), + (lomq_aug_sat_4, datetime(2011, 8, 28), False), + + (lomq_aug_sat_4, datetime(2006, 8, 25), False), + (lomq_aug_sat_4, datetime(2007, 8, 24), False), + (lomq_aug_sat_4, datetime(2008, 8, 29), False), + (lomq_aug_sat_4, datetime(2009, 8, 28), False), + (lomq_aug_sat_4, datetime(2010, 8, 27), False), + (lomq_aug_sat_4, datetime(2011, 8, 26), False), + (lomq_aug_sat_4, datetime(2019, 8, 30), False), + + # From GMCR + (lomq_sep_sat_4, datetime(2010, 9, 25), True), + (lomq_sep_sat_4, datetime(2011, 9, 24), True), + (lomq_sep_sat_4, datetime(2012, 9, 29), True), + + (lomq_sep_sat_4, datetime(2013, 6, 29), True), + (lomq_sep_sat_4, datetime(2012, 6, 23), True), + (lomq_sep_sat_4, datetime(2012, 6, 30), False), + + (lomq_sep_sat_4, datetime(2013, 3, 30), True), + (lomq_sep_sat_4, datetime(2012, 3, 24), True), + + (lomq_sep_sat_4, datetime(2012, 12, 29), True), + (lomq_sep_sat_4, datetime(2011, 12, 24), True), + + # INTC (extra week in Q1) + # See: http://www.intc.com/releasedetail.cfm?ReleaseID=542844 + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=1), + datetime(2011, 4, 2), True), + + # see: http://google.brand.edgar-online.com/?sym=INTC&formtypeID=7 + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=1), + datetime(2012, 12, 29), True), + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=1), + datetime(2011, 12, 31), True), + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=1), + datetime(2010, 12, 25), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + def test_year_has_extra_week(self): + # End of long Q1 + assert makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(2011, 4, 2)) + + # Start of long Q1 + assert makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(2010, 12, 26)) + + # End of year before year with long Q1 + assert not makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(2010, 12, 25)) + + for year in [x + for x in range(1994, 2011 + 1) + if x not in [2011, 2005, 2000, 1994]]: + assert not makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week( + datetime(year, 4, 2)) + + # Other long years + assert makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(2005, 4, 2)) + + assert makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(2000, 4, 2)) + + assert makeFY5253LastOfMonthQuarter( + 1, startingMonth=12, weekday=WeekDay.SAT, + qtr_with_extra_week=1).year_has_extra_week(datetime(1994, 4, 2)) + + def test_get_weeks(self): + sat_dec_1 = makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=1) + sat_dec_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=12, + weekday=WeekDay.SAT, + qtr_with_extra_week=4) + + assert sat_dec_1.get_weeks(datetime(2011, 4, 2)) == [14, 13, 13, 13] + assert sat_dec_4.get_weeks(datetime(2011, 4, 2)) == [13, 13, 13, 14] + assert sat_dec_1.get_weeks(datetime(2010, 12, 25)) == [13, 13, 13, 13] + + +class TestFY5253NearestEndMonthQuarter(Base): + + offset_nem_sat_aug_4 = makeFY5253NearestEndMonthQuarter( + 1, startingMonth=8, weekday=WeekDay.SAT, + qtr_with_extra_week=4) + offset_nem_thu_aug_4 = makeFY5253NearestEndMonthQuarter( + 1, startingMonth=8, weekday=WeekDay.THU, + qtr_with_extra_week=4) + offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, + variation="nearest") + + on_offset_cases = [ + # From Wikipedia + (offset_nem_sat_aug_4, datetime(2006, 9, 2), True), + (offset_nem_sat_aug_4, datetime(2007, 9, 1), True), + (offset_nem_sat_aug_4, datetime(2008, 8, 30), True), + (offset_nem_sat_aug_4, datetime(2009, 8, 29), True), + (offset_nem_sat_aug_4, datetime(2010, 8, 28), True), + (offset_nem_sat_aug_4, datetime(2011, 9, 3), True), + + (offset_nem_sat_aug_4, datetime(2016, 9, 3), True), + (offset_nem_sat_aug_4, datetime(2017, 9, 2), True), + (offset_nem_sat_aug_4, datetime(2018, 9, 1), True), + (offset_nem_sat_aug_4, datetime(2019, 8, 31), True), + + (offset_nem_sat_aug_4, datetime(2006, 8, 27), False), + (offset_nem_sat_aug_4, datetime(2007, 8, 28), False), + (offset_nem_sat_aug_4, datetime(2008, 8, 31), False), + (offset_nem_sat_aug_4, datetime(2009, 8, 30), False), + (offset_nem_sat_aug_4, datetime(2010, 8, 29), False), + (offset_nem_sat_aug_4, datetime(2011, 8, 28), False), + + (offset_nem_sat_aug_4, datetime(2006, 8, 25), False), + (offset_nem_sat_aug_4, datetime(2007, 8, 24), False), + (offset_nem_sat_aug_4, datetime(2008, 8, 29), False), + (offset_nem_sat_aug_4, datetime(2009, 8, 28), False), + (offset_nem_sat_aug_4, datetime(2010, 8, 27), False), + (offset_nem_sat_aug_4, datetime(2011, 8, 26), False), + (offset_nem_sat_aug_4, datetime(2019, 8, 30), False), + + # From Micron, see: + # http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 + (offset_nem_thu_aug_4, datetime(2012, 8, 30), True), + (offset_nem_thu_aug_4, datetime(2011, 9, 1), True), + + # See: http://google.brand.edgar-online.com/?sym=MU&formtypeID=13 + (offset_nem_thu_aug_4, datetime(2013, 5, 30), True), + (offset_nem_thu_aug_4, datetime(2013, 2, 28), True), + (offset_nem_thu_aug_4, datetime(2012, 11, 29), True), + (offset_nem_thu_aug_4, datetime(2012, 5, 31), True), + (offset_nem_thu_aug_4, datetime(2007, 3, 1), True), + (offset_nem_thu_aug_4, datetime(1994, 3, 3), True), + + (offset_n, datetime(2012, 12, 31), False), + (offset_n, datetime(2013, 1, 1), True), + (offset_n, datetime(2013, 1, 2), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + def test_offset(self): + offset = makeFY5253NearestEndMonthQuarter(1, startingMonth=8, + weekday=WeekDay.THU, + qtr_with_extra_week=4) + + MU = [datetime(2012, 5, 31), + datetime(2012, 8, 30), datetime(2012, 11, 29), + datetime(2013, 2, 28), datetime(2013, 5, 30)] + + date = MU[0] + relativedelta(days=-1) + for expected in MU: + assert_offset_equal(offset, date, expected) + date = date + offset + + assert_offset_equal(offset, + datetime(2012, 5, 31), + datetime(2012, 8, 30)) + assert_offset_equal(offset, + datetime(2012, 5, 30), + datetime(2012, 5, 31)) + + offset2 = FY5253Quarter(weekday=5, startingMonth=12, variation="last", + qtr_with_extra_week=4) + + assert_offset_equal(offset2, + datetime(2013, 1, 15), + datetime(2013, 3, 30)) + + +def test_bunched_yearends(): + # GH#14774 cases with two fiscal year-ends in the same calendar-year + fy = FY5253(n=1, weekday=5, startingMonth=12, variation='nearest') + dt = Timestamp('2004-01-01') + assert fy.rollback(dt) == Timestamp('2002-12-28') + assert (-fy).apply(dt) == Timestamp('2002-12-28') + assert dt - fy == Timestamp('2002-12-28') + + assert fy.rollforward(dt) == Timestamp('2004-01-03') + assert fy.apply(dt) == Timestamp('2004-01-03') + assert fy + dt == Timestamp('2004-01-03') + assert dt + fy == Timestamp('2004-01-03') + + # Same thing, but starting from a Timestamp in the previous year. + dt = Timestamp('2003-12-31') + assert fy.rollback(dt) == Timestamp('2002-12-28') + assert (-fy).apply(dt) == Timestamp('2002-12-28') + assert dt - fy == Timestamp('2002-12-28') + + +def test_fy5253_last_onoffset(): + # GH#18877 dates on the year-end but not normalized to midnight + offset = FY5253(n=-5, startingMonth=5, variation="last", weekday=0) + ts = Timestamp('1984-05-28 06:29:43.955911354+0200', + tz='Europe/San_Marino') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + +def test_fy5253_nearest_onoffset(): + # GH#18877 dates on the year-end but not normalized to midnight + offset = FY5253(n=3, startingMonth=7, variation="nearest", weekday=2) + ts = Timestamp('2032-07-28 00:12:59.035729419+0000', tz='Africa/Dakar') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + +def test_fy5253qtr_onoffset_nearest(): + # GH#19036 + ts = Timestamp('1985-09-02 23:57:46.232550356-0300', + tz='Atlantic/Bermuda') + offset = FY5253Quarter(n=3, qtr_with_extra_week=1, startingMonth=2, + variation="nearest", weekday=0) + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + +def test_fy5253qtr_onoffset_last(): + # GH#19036 + offset = FY5253Quarter(n=-2, qtr_with_extra_week=1, + startingMonth=7, variation="last", weekday=2) + ts = Timestamp('2011-01-26 19:03:40.331096129+0200', + tz='Africa/Windhoek') + slow = (ts + offset) - offset == ts + fast = offset.onOffset(ts) + assert fast == slow diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py new file mode 100644 index 0000000000000..e6f21a7b47c3b --- /dev/null +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -0,0 +1,3172 @@ +from datetime import date, datetime, timedelta +from distutils.version import LooseVersion + +import numpy as np +import pytest + +from pandas._libs.tslibs import ( + NaT, OutOfBoundsDatetime, Timestamp, conversion, timezones) +from pandas._libs.tslibs.frequencies import ( + INVALID_FREQ_ERR_MSG, get_freq_code, get_freq_str) +import pandas._libs.tslibs.offsets as liboffsets +from pandas._libs.tslibs.offsets import ApplyTypeError +import pandas.compat as compat +from pandas.compat import range +from pandas.compat.numpy import np_datetime64_compat + +from pandas.core.indexes.datetimes import DatetimeIndex, _to_M8, date_range +from pandas.core.series import Series +import pandas.util.testing as tm + +from pandas.io.pickle import read_pickle +from pandas.tseries.frequencies import _offset_map, get_offset +from pandas.tseries.holiday import USFederalHolidayCalendar +import pandas.tseries.offsets as offsets +from pandas.tseries.offsets import ( + FY5253, BDay, BMonthBegin, BMonthEnd, BQuarterBegin, BQuarterEnd, + BusinessHour, BYearBegin, BYearEnd, CBMonthBegin, CBMonthEnd, CDay, + CustomBusinessHour, DateOffset, Day, Easter, FY5253Quarter, + LastWeekOfMonth, MonthBegin, MonthEnd, Nano, QuarterBegin, QuarterEnd, + SemiMonthBegin, SemiMonthEnd, Tick, Week, WeekOfMonth, YearBegin, YearEnd) + +from .common import assert_offset_equal, assert_onOffset + + +class WeekDay(object): + # TODO: Remove: This is not used outside of tests + MON = 0 + TUE = 1 + WED = 2 + THU = 3 + FRI = 4 + SAT = 5 + SUN = 6 + + +#### +# Misc function tests +#### + + +def test_to_M8(): + valb = datetime(2007, 10, 1) + valu = _to_M8(valb) + assert isinstance(valu, np.datetime64) + + +##### +# DateOffset Tests +##### + + +class Base(object): + _offset = None + d = Timestamp(datetime(2008, 1, 2)) + + timezones = [None, 'UTC', 'Asia/Tokyo', 'US/Eastern', + 'dateutil/Asia/Tokyo', 'dateutil/US/Pacific'] + + def _get_offset(self, klass, value=1, normalize=False): + # create instance from offset class + if klass is FY5253: + klass = klass(n=value, startingMonth=1, weekday=1, + variation='last', normalize=normalize) + elif klass is FY5253Quarter: + klass = klass(n=value, startingMonth=1, weekday=1, + qtr_with_extra_week=1, variation='last', + normalize=normalize) + elif klass is LastWeekOfMonth: + klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is WeekOfMonth: + klass = klass(n=value, week=1, weekday=5, normalize=normalize) + elif klass is Week: + klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is DateOffset: + klass = klass(days=value, normalize=normalize) + else: + try: + klass = klass(value, normalize=normalize) + except Exception: + klass = klass(normalize=normalize) + return klass + + def test_apply_out_of_range(self, tz_naive_fixture): + tz = tz_naive_fixture + if self._offset is None: + return + + # try to create an out-of-bounds result timestamp; if we can't create + # the offset skip + try: + if self._offset in (BusinessHour, CustomBusinessHour): + # Using 10000 in BusinessHour fails in tz check because of DST + # difference + offset = self._get_offset(self._offset, value=100000) + else: + offset = self._get_offset(self._offset, value=10000) + + result = Timestamp('20080101') + offset + assert isinstance(result, datetime) + assert result.tzinfo is None + + # Check tz is preserved + t = Timestamp('20080101', tz=tz) + result = t + offset + assert isinstance(result, datetime) + assert t.tzinfo == result.tzinfo + + except OutOfBoundsDatetime: + raise + except (ValueError, KeyError): + # we are creating an invalid offset + # so ignore + pass + + def test_offsets_compare_equal(self): + # root cause of GH#456: __ne__ was not implemented + if self._offset is None: + return + offset1 = self._offset() + offset2 = self._offset() + assert not offset1 != offset2 + assert offset1 == offset2 + + def test_rsub(self): + if self._offset is None or not hasattr(self, "offset2"): + # i.e. skip for TestCommon and YQM subclasses that do not have + # offset2 attr + return + assert self.d - self.offset2 == (-self.offset2).apply(self.d) + + def test_radd(self): + if self._offset is None or not hasattr(self, "offset2"): + # i.e. skip for TestCommon and YQM subclasses that do not have + # offset2 attr + return + assert self.d + self.offset2 == self.offset2 + self.d + + def test_sub(self): + if self._offset is None or not hasattr(self, "offset2"): + # i.e. skip for TestCommon and YQM subclasses that do not have + # offset2 attr + return + off = self.offset2 + msg = "Cannot subtract datetime from offset" + with pytest.raises(TypeError, match=msg): + off - self.d + + assert 2 * off - off == off + assert self.d - self.offset2 == self.d + self._offset(-2) + assert self.d - self.offset2 == self.d - (2 * off - off) + + def testMult1(self): + if self._offset is None or not hasattr(self, "offset1"): + # i.e. skip for TestCommon and YQM subclasses that do not have + # offset1 attr + return + assert self.d + 10 * self.offset1 == self.d + self._offset(10) + assert self.d + 5 * self.offset1 == self.d + self._offset(5) + + def testMult2(self): + if self._offset is None: + return + assert self.d + (-5 * self._offset(-10)) == self.d + self._offset(50) + assert self.d + (-3 * self._offset(-2)) == self.d + self._offset(6) + + def test_compare_str(self): + # GH#23524 + # comparing to strings that cannot be cast to DateOffsets should + # not raise for __eq__ or __ne__ + if self._offset is None: + return + off = self._get_offset(self._offset) + + assert not off == "infer" + assert off != "foo" + # Note: inequalities are only implemented for Tick subclasses; + # tests for this are in test_ticks + + +class TestCommon(Base): + # exected value created by Base._get_offset + # are applied to 2011/01/01 09:00 (Saturday) + # used for .apply and .rollforward + expecteds = {'Day': Timestamp('2011-01-02 09:00:00'), + 'DateOffset': Timestamp('2011-01-02 09:00:00'), + 'BusinessDay': Timestamp('2011-01-03 09:00:00'), + 'CustomBusinessDay': Timestamp('2011-01-03 09:00:00'), + 'CustomBusinessMonthEnd': Timestamp('2011-01-31 09:00:00'), + 'CustomBusinessMonthBegin': Timestamp('2011-01-03 09:00:00'), + 'MonthBegin': Timestamp('2011-02-01 09:00:00'), + 'BusinessMonthBegin': Timestamp('2011-01-03 09:00:00'), + 'MonthEnd': Timestamp('2011-01-31 09:00:00'), + 'SemiMonthEnd': Timestamp('2011-01-15 09:00:00'), + 'SemiMonthBegin': Timestamp('2011-01-15 09:00:00'), + 'BusinessMonthEnd': Timestamp('2011-01-31 09:00:00'), + 'YearBegin': Timestamp('2012-01-01 09:00:00'), + 'BYearBegin': Timestamp('2011-01-03 09:00:00'), + 'YearEnd': Timestamp('2011-12-31 09:00:00'), + 'BYearEnd': Timestamp('2011-12-30 09:00:00'), + 'QuarterBegin': Timestamp('2011-03-01 09:00:00'), + 'BQuarterBegin': Timestamp('2011-03-01 09:00:00'), + 'QuarterEnd': Timestamp('2011-03-31 09:00:00'), + 'BQuarterEnd': Timestamp('2011-03-31 09:00:00'), + 'BusinessHour': Timestamp('2011-01-03 10:00:00'), + 'CustomBusinessHour': Timestamp('2011-01-03 10:00:00'), + 'WeekOfMonth': Timestamp('2011-01-08 09:00:00'), + 'LastWeekOfMonth': Timestamp('2011-01-29 09:00:00'), + 'FY5253Quarter': Timestamp('2011-01-25 09:00:00'), + 'FY5253': Timestamp('2011-01-25 09:00:00'), + 'Week': Timestamp('2011-01-08 09:00:00'), + 'Easter': Timestamp('2011-04-24 09:00:00'), + 'Hour': Timestamp('2011-01-01 10:00:00'), + 'Minute': Timestamp('2011-01-01 09:01:00'), + 'Second': Timestamp('2011-01-01 09:00:01'), + 'Milli': Timestamp('2011-01-01 09:00:00.001000'), + 'Micro': Timestamp('2011-01-01 09:00:00.000001'), + 'Nano': Timestamp(np_datetime64_compat( + '2011-01-01T09:00:00.000000001Z'))} + + def test_immutable(self, offset_types): + # GH#21341 check that __setattr__ raises + offset = self._get_offset(offset_types) + with pytest.raises(AttributeError): + offset.normalize = True + with pytest.raises(AttributeError): + offset.n = 91 + + def test_return_type(self, offset_types): + offset = self._get_offset(offset_types) + + # make sure that we are returning a Timestamp + result = Timestamp('20080101') + offset + assert isinstance(result, Timestamp) + + # make sure that we are returning NaT + assert NaT + offset is NaT + assert offset + NaT is NaT + + assert NaT - offset is NaT + assert (-offset).apply(NaT) is NaT + + def test_offset_n(self, offset_types): + offset = self._get_offset(offset_types) + assert offset.n == 1 + + neg_offset = offset * -1 + assert neg_offset.n == -1 + + mul_offset = offset * 3 + assert mul_offset.n == 3 + + def test_offset_timedelta64_arg(self, offset_types): + # check that offset._validate_n raises TypeError on a timedelt64 + # object + off = self._get_offset(offset_types) + + td64 = np.timedelta64(4567, 's') + with pytest.raises(TypeError, match="argument must be an integer"): + type(off)(n=td64, **off.kwds) + + def test_offset_mul_ndarray(self, offset_types): + off = self._get_offset(offset_types) + + expected = np.array([[off, off * 2], [off * 3, off * 4]]) + + result = np.array([[1, 2], [3, 4]]) * off + tm.assert_numpy_array_equal(result, expected) + + result = off * np.array([[1, 2], [3, 4]]) + tm.assert_numpy_array_equal(result, expected) + + def test_offset_freqstr(self, offset_types): + offset = self._get_offset(offset_types) + + freqstr = offset.freqstr + if freqstr not in ('', + "", + 'LWOM-SAT', ): + code = get_offset(freqstr) + assert offset.rule_code == code + + def _check_offsetfunc_works(self, offset, funcname, dt, expected, + normalize=False): + + if normalize and issubclass(offset, Tick): + # normalize=True disallowed for Tick subclasses GH#21427 + return + + offset_s = self._get_offset(offset, normalize=normalize) + func = getattr(offset_s, funcname) + + result = func(dt) + assert isinstance(result, Timestamp) + assert result == expected + + result = func(Timestamp(dt)) + assert isinstance(result, Timestamp) + assert result == expected + + # see gh-14101 + exp_warning = None + ts = Timestamp(dt) + Nano(5) + + if (offset_s.__class__.__name__ == 'DateOffset' and + (funcname == 'apply' or normalize) and + ts.nanosecond > 0): + exp_warning = UserWarning + + # test nanosecond is preserved + with tm.assert_produces_warning(exp_warning, + check_stacklevel=False): + result = func(ts) + assert isinstance(result, Timestamp) + if normalize is False: + assert result == expected + Nano(5) + else: + assert result == expected + + if isinstance(dt, np.datetime64): + # test tz when input is datetime or Timestamp + return + + for tz in self.timezones: + expected_localize = expected.tz_localize(tz) + tz_obj = timezones.maybe_get_tz(tz) + dt_tz = conversion.localize_pydatetime(dt, tz_obj) + + result = func(dt_tz) + assert isinstance(result, Timestamp) + assert result == expected_localize + + result = func(Timestamp(dt, tz=tz)) + assert isinstance(result, Timestamp) + assert result == expected_localize + + # see gh-14101 + exp_warning = None + ts = Timestamp(dt, tz=tz) + Nano(5) + + if (offset_s.__class__.__name__ == 'DateOffset' and + (funcname == 'apply' or normalize) and + ts.nanosecond > 0): + exp_warning = UserWarning + + # test nanosecond is preserved + with tm.assert_produces_warning(exp_warning, + check_stacklevel=False): + result = func(ts) + assert isinstance(result, Timestamp) + if normalize is False: + assert result == expected_localize + Nano(5) + else: + assert result == expected_localize + + def test_apply(self, offset_types): + sdt = datetime(2011, 1, 1, 9, 0) + ndt = np_datetime64_compat('2011-01-01 09:00Z') + + for dt in [sdt, ndt]: + expected = self.expecteds[offset_types.__name__] + self._check_offsetfunc_works(offset_types, 'apply', dt, expected) + + expected = Timestamp(expected.date()) + self._check_offsetfunc_works(offset_types, 'apply', dt, expected, + normalize=True) + + def test_rollforward(self, offset_types): + expecteds = self.expecteds.copy() + + # result will not be changed if the target is on the offset + no_changes = ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', + 'Week', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', + 'Nano', 'DateOffset'] + for n in no_changes: + expecteds[n] = Timestamp('2011/01/01 09:00') + + expecteds['BusinessHour'] = Timestamp('2011-01-03 09:00:00') + expecteds['CustomBusinessHour'] = Timestamp('2011-01-03 09:00:00') + + # but be changed when normalize=True + norm_expected = expecteds.copy() + for k in norm_expected: + norm_expected[k] = Timestamp(norm_expected[k].date()) + + normalized = {'Day': Timestamp('2011-01-02 00:00:00'), + 'DateOffset': Timestamp('2011-01-02 00:00:00'), + 'MonthBegin': Timestamp('2011-02-01 00:00:00'), + 'SemiMonthBegin': Timestamp('2011-01-15 00:00:00'), + 'YearBegin': Timestamp('2012-01-01 00:00:00'), + 'Week': Timestamp('2011-01-08 00:00:00'), + 'Hour': Timestamp('2011-01-01 00:00:00'), + 'Minute': Timestamp('2011-01-01 00:00:00'), + 'Second': Timestamp('2011-01-01 00:00:00'), + 'Milli': Timestamp('2011-01-01 00:00:00'), + 'Micro': Timestamp('2011-01-01 00:00:00')} + norm_expected.update(normalized) + + sdt = datetime(2011, 1, 1, 9, 0) + ndt = np_datetime64_compat('2011-01-01 09:00Z') + + for dt in [sdt, ndt]: + expected = expecteds[offset_types.__name__] + self._check_offsetfunc_works(offset_types, 'rollforward', dt, + expected) + expected = norm_expected[offset_types.__name__] + self._check_offsetfunc_works(offset_types, 'rollforward', dt, + expected, normalize=True) + + def test_rollback(self, offset_types): + expecteds = {'BusinessDay': Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessDay': Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessMonthEnd': + Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessMonthBegin': + Timestamp('2010-12-01 09:00:00'), + 'BusinessMonthBegin': Timestamp('2010-12-01 09:00:00'), + 'MonthEnd': Timestamp('2010-12-31 09:00:00'), + 'SemiMonthEnd': Timestamp('2010-12-31 09:00:00'), + 'BusinessMonthEnd': Timestamp('2010-12-31 09:00:00'), + 'BYearBegin': Timestamp('2010-01-01 09:00:00'), + 'YearEnd': Timestamp('2010-12-31 09:00:00'), + 'BYearEnd': Timestamp('2010-12-31 09:00:00'), + 'QuarterBegin': Timestamp('2010-12-01 09:00:00'), + 'BQuarterBegin': Timestamp('2010-12-01 09:00:00'), + 'QuarterEnd': Timestamp('2010-12-31 09:00:00'), + 'BQuarterEnd': Timestamp('2010-12-31 09:00:00'), + 'BusinessHour': Timestamp('2010-12-31 17:00:00'), + 'CustomBusinessHour': Timestamp('2010-12-31 17:00:00'), + 'WeekOfMonth': Timestamp('2010-12-11 09:00:00'), + 'LastWeekOfMonth': Timestamp('2010-12-25 09:00:00'), + 'FY5253Quarter': Timestamp('2010-10-26 09:00:00'), + 'FY5253': Timestamp('2010-01-26 09:00:00'), + 'Easter': Timestamp('2010-04-04 09:00:00')} + + # result will not be changed if the target is on the offset + for n in ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', 'Week', + 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano', + 'DateOffset']: + expecteds[n] = Timestamp('2011/01/01 09:00') + + # but be changed when normalize=True + norm_expected = expecteds.copy() + for k in norm_expected: + norm_expected[k] = Timestamp(norm_expected[k].date()) + + normalized = {'Day': Timestamp('2010-12-31 00:00:00'), + 'DateOffset': Timestamp('2010-12-31 00:00:00'), + 'MonthBegin': Timestamp('2010-12-01 00:00:00'), + 'SemiMonthBegin': Timestamp('2010-12-15 00:00:00'), + 'YearBegin': Timestamp('2010-01-01 00:00:00'), + 'Week': Timestamp('2010-12-25 00:00:00'), + 'Hour': Timestamp('2011-01-01 00:00:00'), + 'Minute': Timestamp('2011-01-01 00:00:00'), + 'Second': Timestamp('2011-01-01 00:00:00'), + 'Milli': Timestamp('2011-01-01 00:00:00'), + 'Micro': Timestamp('2011-01-01 00:00:00')} + norm_expected.update(normalized) + + sdt = datetime(2011, 1, 1, 9, 0) + ndt = np_datetime64_compat('2011-01-01 09:00Z') + + for dt in [sdt, ndt]: + expected = expecteds[offset_types.__name__] + self._check_offsetfunc_works(offset_types, 'rollback', dt, + expected) + + expected = norm_expected[offset_types.__name__] + self._check_offsetfunc_works(offset_types, 'rollback', dt, + expected, normalize=True) + + def test_onOffset(self, offset_types): + dt = self.expecteds[offset_types.__name__] + offset_s = self._get_offset(offset_types) + assert offset_s.onOffset(dt) + + # when normalize=True, onOffset checks time is 00:00:00 + if issubclass(offset_types, Tick): + # normalize=True disallowed for Tick subclasses GH#21427 + return + offset_n = self._get_offset(offset_types, normalize=True) + assert not offset_n.onOffset(dt) + + if offset_types in (BusinessHour, CustomBusinessHour): + # In default BusinessHour (9:00-17:00), normalized time + # cannot be in business hour range + return + date = datetime(dt.year, dt.month, dt.day) + assert offset_n.onOffset(date) + + def test_add(self, offset_types, tz_naive_fixture): + tz = tz_naive_fixture + dt = datetime(2011, 1, 1, 9, 0) + + offset_s = self._get_offset(offset_types) + expected = self.expecteds[offset_types.__name__] + + result_dt = dt + offset_s + result_ts = Timestamp(dt) + offset_s + for result in [result_dt, result_ts]: + assert isinstance(result, Timestamp) + assert result == expected + + expected_localize = expected.tz_localize(tz) + result = Timestamp(dt, tz=tz) + offset_s + assert isinstance(result, Timestamp) + assert result == expected_localize + + # normalize=True, disallowed for Tick subclasses GH#21427 + if issubclass(offset_types, Tick): + return + offset_s = self._get_offset(offset_types, normalize=True) + expected = Timestamp(expected.date()) + + result_dt = dt + offset_s + result_ts = Timestamp(dt) + offset_s + for result in [result_dt, result_ts]: + assert isinstance(result, Timestamp) + assert result == expected + + expected_localize = expected.tz_localize(tz) + result = Timestamp(dt, tz=tz) + offset_s + assert isinstance(result, Timestamp) + assert result == expected_localize + + def test_pickle_v0_15_2(self, datapath): + offsets = {'DateOffset': DateOffset(years=1), + 'MonthBegin': MonthBegin(1), + 'Day': Day(1), + 'YearBegin': YearBegin(1), + 'Week': Week(1)} + + pickle_path = datapath('tseries', 'offsets', 'data', + 'dateoffset_0_15_2.pickle') + # This code was executed once on v0.15.2 to generate the pickle: + # with open(pickle_path, 'wb') as f: pickle.dump(offsets, f) + # + tm.assert_dict_equal(offsets, read_pickle(pickle_path)) + + +class TestDateOffset(Base): + + def setup_method(self, method): + self.d = Timestamp(datetime(2008, 1, 2)) + _offset_map.clear() + + def test_repr(self): + repr(DateOffset()) + repr(DateOffset(2)) + repr(2 * DateOffset()) + repr(2 * DateOffset(months=2)) + + def test_mul(self): + assert DateOffset(2) == 2 * DateOffset(1) + assert DateOffset(2) == DateOffset(1) * 2 + + def test_constructor(self): + + assert ((self.d + DateOffset(months=2)) == datetime(2008, 3, 2)) + assert ((self.d - DateOffset(months=2)) == datetime(2007, 11, 2)) + + assert ((self.d + DateOffset(2)) == datetime(2008, 1, 4)) + + assert not DateOffset(2).isAnchored() + assert DateOffset(1).isAnchored() + + d = datetime(2008, 1, 31) + assert ((d + DateOffset(months=1)) == datetime(2008, 2, 29)) + + def test_copy(self): + assert (DateOffset(months=2).copy() == DateOffset(months=2)) + + def test_eq(self): + offset1 = DateOffset(days=1) + offset2 = DateOffset(days=365) + + assert offset1 != offset2 + + +class TestBusinessDay(Base): + _offset = BDay + + def setup_method(self, method): + self.d = datetime(2008, 1, 1) + + self.offset = BDay() + self.offset1 = self.offset + self.offset2 = BDay(2) + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset) == '' + assert repr(self.offset2) == '<2 * BusinessDays>' + + if compat.PY37: + expected = '' + else: + expected = '' + assert repr(self.offset + timedelta(1)) == expected + + def test_with_offset(self): + offset = self.offset + timedelta(hours=2) + + assert (self.d + offset) == datetime(2008, 1, 2, 2) + + def test_eq(self): + assert self.offset2 == self.offset2 + + def test_mul(self): + pass + + def test_hash(self): + assert hash(self.offset2) == hash(self.offset2) + + def test_call(self): + assert self.offset2(self.d) == datetime(2008, 1, 3) + + def testRollback1(self): + assert BDay(10).rollback(self.d) == self.d + + def testRollback2(self): + assert (BDay(10).rollback(datetime(2008, 1, 5)) == + datetime(2008, 1, 4)) + + def testRollforward1(self): + assert BDay(10).rollforward(self.d) == self.d + + def testRollforward2(self): + assert (BDay(10).rollforward(datetime(2008, 1, 5)) == + datetime(2008, 1, 7)) + + def test_roll_date_object(self): + offset = BDay() + + dt = date(2012, 9, 15) + + result = offset.rollback(dt) + assert result == datetime(2012, 9, 14) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 17) + + offset = offsets.Day() + result = offset.rollback(dt) + assert result == datetime(2012, 9, 15) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 15) + + def test_onOffset(self): + tests = [(BDay(), datetime(2008, 1, 1), True), + (BDay(), datetime(2008, 1, 5), False)] + + for offset, d, expected in tests: + assert_onOffset(offset, d, expected) + + apply_cases = [] + apply_cases.append((BDay(), { + datetime(2008, 1, 1): datetime(2008, 1, 2), + datetime(2008, 1, 4): datetime(2008, 1, 7), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 8)})) + + apply_cases.append((2 * BDay(), { + datetime(2008, 1, 1): datetime(2008, 1, 3), + datetime(2008, 1, 4): datetime(2008, 1, 8), + datetime(2008, 1, 5): datetime(2008, 1, 8), + datetime(2008, 1, 6): datetime(2008, 1, 8), + datetime(2008, 1, 7): datetime(2008, 1, 9)})) + + apply_cases.append((-BDay(), { + datetime(2008, 1, 1): datetime(2007, 12, 31), + datetime(2008, 1, 4): datetime(2008, 1, 3), + datetime(2008, 1, 5): datetime(2008, 1, 4), + datetime(2008, 1, 6): datetime(2008, 1, 4), + datetime(2008, 1, 7): datetime(2008, 1, 4), + datetime(2008, 1, 8): datetime(2008, 1, 7)})) + + apply_cases.append((-2 * BDay(), { + datetime(2008, 1, 1): datetime(2007, 12, 28), + datetime(2008, 1, 4): datetime(2008, 1, 2), + datetime(2008, 1, 5): datetime(2008, 1, 3), + datetime(2008, 1, 6): datetime(2008, 1, 3), + datetime(2008, 1, 7): datetime(2008, 1, 3), + datetime(2008, 1, 8): datetime(2008, 1, 4), + datetime(2008, 1, 9): datetime(2008, 1, 7)})) + + apply_cases.append((BDay(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 1, 4): datetime(2008, 1, 4), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 7)})) + + @pytest.mark.parametrize('case', apply_cases) + def test_apply(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_apply_large_n(self): + dt = datetime(2012, 10, 23) + + result = dt + BDay(10) + assert result == datetime(2012, 11, 6) + + result = dt + BDay(100) - BDay(100) + assert result == dt + + off = BDay() * 6 + rs = datetime(2012, 1, 1) - off + xp = datetime(2011, 12, 23) + assert rs == xp + + st = datetime(2011, 12, 18) + rs = st + off + xp = datetime(2011, 12, 26) + assert rs == xp + + off = BDay() * 10 + rs = datetime(2014, 1, 5) + off # see #5890 + xp = datetime(2014, 1, 17) + assert rs == xp + + def test_apply_corner(self): + msg = ("Only know how to combine business day with datetime or" + " timedelta") + with pytest.raises(ApplyTypeError, match=msg): + BDay().apply(BMonthEnd()) + + +class TestBusinessHour(Base): + _offset = BusinessHour + + def setup_method(self, method): + self.d = datetime(2014, 7, 1, 10, 00) + + self.offset1 = BusinessHour() + self.offset2 = BusinessHour(n=3) + + self.offset3 = BusinessHour(n=-1) + self.offset4 = BusinessHour(n=-4) + + from datetime import time as dt_time + self.offset5 = BusinessHour(start=dt_time(11, 0), end=dt_time(14, 30)) + self.offset6 = BusinessHour(start='20:00', end='05:00') + self.offset7 = BusinessHour(n=-2, start=dt_time(21, 30), + end=dt_time(6, 30)) + + def test_constructor_errors(self): + from datetime import time as dt_time + with pytest.raises(ValueError): + BusinessHour(start=dt_time(11, 0, 5)) + with pytest.raises(ValueError): + BusinessHour(start='AAA') + with pytest.raises(ValueError): + BusinessHour(start='14:00:05') + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset1) == '' + assert repr(self.offset2) == '<3 * BusinessHours: BH=09:00-17:00>' + assert repr(self.offset3) == '<-1 * BusinessHour: BH=09:00-17:00>' + assert repr(self.offset4) == '<-4 * BusinessHours: BH=09:00-17:00>' + + assert repr(self.offset5) == '' + assert repr(self.offset6) == '' + assert repr(self.offset7) == '<-2 * BusinessHours: BH=21:30-06:30>' + + def test_with_offset(self): + expected = Timestamp('2014-07-01 13:00') + + assert self.d + BusinessHour() * 3 == expected + assert self.d + BusinessHour(n=3) == expected + + def test_eq(self): + for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: + assert offset == offset + + assert BusinessHour() != BusinessHour(-1) + assert BusinessHour(start='09:00') == BusinessHour() + assert BusinessHour(start='09:00') != BusinessHour(start='09:01') + assert (BusinessHour(start='09:00', end='17:00') != + BusinessHour(start='17:00', end='09:01')) + + def test_hash(self): + for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: + assert hash(offset) == hash(offset) + + def test_call(self): + assert self.offset1(self.d) == datetime(2014, 7, 1, 11) + assert self.offset2(self.d) == datetime(2014, 7, 1, 13) + assert self.offset3(self.d) == datetime(2014, 6, 30, 17) + assert self.offset4(self.d) == datetime(2014, 6, 30, 14) + + def test_sub(self): + # we have to override test_sub here becasue self.offset2 is not + # defined as self._offset(2) + off = self.offset2 + msg = "Cannot subtract datetime from offset" + with pytest.raises(TypeError, match=msg): + off - self.d + assert 2 * off - off == off + + assert self.d - self.offset2 == self.d + self._offset(-3) + + def testRollback1(self): + assert self.offset1.rollback(self.d) == self.d + assert self.offset2.rollback(self.d) == self.d + assert self.offset3.rollback(self.d) == self.d + assert self.offset4.rollback(self.d) == self.d + assert self.offset5.rollback(self.d) == datetime(2014, 6, 30, 14, 30) + assert self.offset6.rollback(self.d) == datetime(2014, 7, 1, 5, 0) + assert self.offset7.rollback(self.d) == datetime(2014, 7, 1, 6, 30) + + d = datetime(2014, 7, 1, 0) + assert self.offset1.rollback(d) == datetime(2014, 6, 30, 17) + assert self.offset2.rollback(d) == datetime(2014, 6, 30, 17) + assert self.offset3.rollback(d) == datetime(2014, 6, 30, 17) + assert self.offset4.rollback(d) == datetime(2014, 6, 30, 17) + assert self.offset5.rollback(d) == datetime(2014, 6, 30, 14, 30) + assert self.offset6.rollback(d) == d + assert self.offset7.rollback(d) == d + + assert self._offset(5).rollback(self.d) == self.d + + def testRollback2(self): + assert (self._offset(-3).rollback(datetime(2014, 7, 5, 15, 0)) == + datetime(2014, 7, 4, 17, 0)) + + def testRollforward1(self): + assert self.offset1.rollforward(self.d) == self.d + assert self.offset2.rollforward(self.d) == self.d + assert self.offset3.rollforward(self.d) == self.d + assert self.offset4.rollforward(self.d) == self.d + assert (self.offset5.rollforward(self.d) == + datetime(2014, 7, 1, 11, 0)) + assert (self.offset6.rollforward(self.d) == + datetime(2014, 7, 1, 20, 0)) + assert (self.offset7.rollforward(self.d) == + datetime(2014, 7, 1, 21, 30)) + + d = datetime(2014, 7, 1, 0) + assert self.offset1.rollforward(d) == datetime(2014, 7, 1, 9) + assert self.offset2.rollforward(d) == datetime(2014, 7, 1, 9) + assert self.offset3.rollforward(d) == datetime(2014, 7, 1, 9) + assert self.offset4.rollforward(d) == datetime(2014, 7, 1, 9) + assert self.offset5.rollforward(d) == datetime(2014, 7, 1, 11) + assert self.offset6.rollforward(d) == d + assert self.offset7.rollforward(d) == d + + assert self._offset(5).rollforward(self.d) == self.d + + def testRollforward2(self): + assert (self._offset(-3).rollforward(datetime(2014, 7, 5, 16, 0)) == + datetime(2014, 7, 7, 9)) + + def test_roll_date_object(self): + offset = BusinessHour() + + dt = datetime(2014, 7, 6, 15, 0) + + result = offset.rollback(dt) + assert result == datetime(2014, 7, 4, 17) + + result = offset.rollforward(dt) + assert result == datetime(2014, 7, 7, 9) + + normalize_cases = [] + normalize_cases.append((BusinessHour(normalize=True), { + datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2), + datetime(2014, 7, 1, 23): datetime(2014, 7, 2), + datetime(2014, 7, 1, 0): datetime(2014, 7, 1), + datetime(2014, 7, 4, 15): datetime(2014, 7, 4), + datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 7), + datetime(2014, 7, 6, 10): datetime(2014, 7, 7)})) + + normalize_cases.append((BusinessHour(-1, normalize=True), { + datetime(2014, 7, 1, 8): datetime(2014, 6, 30), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1), + datetime(2014, 7, 1, 10): datetime(2014, 6, 30), + datetime(2014, 7, 1, 0): datetime(2014, 6, 30), + datetime(2014, 7, 7, 10): datetime(2014, 7, 4), + datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 4), + datetime(2014, 7, 6, 10): datetime(2014, 7, 4)})) + + normalize_cases.append((BusinessHour(1, normalize=True, start='17:00', + end='04:00'), { + datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 23): datetime(2014, 7, 2), + datetime(2014, 7, 2, 2): datetime(2014, 7, 2), + datetime(2014, 7, 2, 3): datetime(2014, 7, 2), + datetime(2014, 7, 4, 23): datetime(2014, 7, 5), + datetime(2014, 7, 5, 2): datetime(2014, 7, 5), + datetime(2014, 7, 7, 2): datetime(2014, 7, 7), + datetime(2014, 7, 7, 17): datetime(2014, 7, 7)})) + + @pytest.mark.parametrize('case', normalize_cases) + def test_normalize(self, case): + offset, cases = case + for dt, expected in compat.iteritems(cases): + assert offset.apply(dt) == expected + + on_offset_cases = [] + on_offset_cases.append((BusinessHour(), { + datetime(2014, 7, 1, 9): True, + datetime(2014, 7, 1, 8, 59): False, + datetime(2014, 7, 1, 8): False, + datetime(2014, 7, 1, 17): True, + datetime(2014, 7, 1, 17, 1): False, + datetime(2014, 7, 1, 18): False, + datetime(2014, 7, 5, 9): False, + datetime(2014, 7, 6, 12): False})) + + on_offset_cases.append((BusinessHour(start='10:00', end='15:00'), { + datetime(2014, 7, 1, 9): False, + datetime(2014, 7, 1, 10): True, + datetime(2014, 7, 1, 15): True, + datetime(2014, 7, 1, 15, 1): False, + datetime(2014, 7, 5, 12): False, + datetime(2014, 7, 6, 12): False})) + + on_offset_cases.append((BusinessHour(start='19:00', end='05:00'), { + datetime(2014, 7, 1, 9, 0): False, + datetime(2014, 7, 1, 10, 0): False, + datetime(2014, 7, 1, 15): False, + datetime(2014, 7, 1, 15, 1): False, + datetime(2014, 7, 5, 12, 0): False, + datetime(2014, 7, 6, 12, 0): False, + datetime(2014, 7, 1, 19, 0): True, + datetime(2014, 7, 2, 0, 0): True, + datetime(2014, 7, 4, 23): True, + datetime(2014, 7, 5, 1): True, + datetime(2014, 7, 5, 5, 0): True, + datetime(2014, 7, 6, 23, 0): False, + datetime(2014, 7, 7, 3, 0): False})) + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, cases = case + for dt, expected in compat.iteritems(cases): + assert offset.onOffset(dt) == expected + + opening_time_cases = [] + # opening time should be affected by sign of n, not by n's value and + # end + opening_time_cases.append(([BusinessHour(), BusinessHour(n=2), + BusinessHour(n=4), BusinessHour(end='10:00'), + BusinessHour(n=2, end='4:00'), + BusinessHour(n=4, end='15:00')], { + datetime(2014, 7, 1, 11): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 9)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 9)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 9)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 9)), + # if timestamp is on opening time, next opening time is + # as it is + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 3, 9), + datetime(2014, 7, 2, 9)), + # 2014-07-05 is saturday + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 4, 9)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 4, 9)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 4, 9)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 4, 9)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 4, 9)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 8, 9), + datetime(2014, 7, 7, 9))})) + + opening_time_cases.append(([BusinessHour(start='11:15'), + BusinessHour(n=2, start='11:15'), + BusinessHour(n=3, start='11:15'), + BusinessHour(start='11:15', end='10:00'), + BusinessHour(n=2, start='11:15', end='4:00'), + BusinessHour(n=3, start='11:15', + end='15:00')], { + datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 11, 15), + datetime(2014, 6, 30, 11, 15)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 1, 11, 15)), + datetime(2014, 7, 2, 11, 15): (datetime(2014, 7, 2, 11, 15), + datetime(2014, 7, 2, 11, 15)), + datetime(2014, 7, 2, 11, 15, 1): (datetime(2014, 7, 3, 11, 15), + datetime(2014, 7, 2, 11, 15)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 11, 15), + datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 11, 15), + datetime(2014, 7, 3, 11, 15)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 11, 15), + datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 11, 15), + datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 11, 15), + datetime(2014, 7, 4, 11, 15)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 7, 11, 15), + datetime(2014, 7, 4, 11, 15))})) + + opening_time_cases.append(([BusinessHour(-1), BusinessHour(n=-2), + BusinessHour(n=-4), + BusinessHour(n=-1, end='10:00'), + BusinessHour(n=-2, end='4:00'), + BusinessHour(n=-4, end='15:00')], { + datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 2, 9)), + datetime(2014, 7, 2, 10): (datetime(2014, 7, 2, 9), + datetime(2014, 7, 3, 9)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 9): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 7, 9)), + datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 7, 9), + datetime(2014, 7, 8, 9))})) + + opening_time_cases.append(([BusinessHour(start='17:00', end='05:00'), + BusinessHour(n=3, start='17:00', + end='03:00')], { + datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 17), + datetime(2014, 6, 30, 17)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 17), + datetime(2014, 7, 1, 17)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 17), + datetime(2014, 7, 1, 17)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 17), + datetime(2014, 7, 1, 17)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 17), + datetime(2014, 7, 1, 17)), + datetime(2014, 7, 4, 17): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 3, 17)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 7, 17, 1): (datetime(2014, 7, 8, 17), + datetime(2014, 7, 7, 17)), })) + + opening_time_cases.append(([BusinessHour(-1, start='17:00', end='05:00'), + BusinessHour(n=-2, start='17:00', + end='03:00')], { + datetime(2014, 7, 1, 11): (datetime(2014, 6, 30, 17), + datetime(2014, 7, 1, 17)), + datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 17), + datetime(2014, 7, 2, 17)), + datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 17), + datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 17), + datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 9): (datetime(2014, 7, 1, 17), + datetime(2014, 7, 2, 17)), + datetime(2014, 7, 2, 16, 59): (datetime(2014, 7, 1, 17), + datetime(2014, 7, 2, 17)), + datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 7, 17)), + datetime(2014, 7, 4, 10): (datetime(2014, 7, 3, 17), + datetime(2014, 7, 4, 17)), + datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 7, 17)), + datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 7, 17)), + datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 17), + datetime(2014, 7, 7, 17)), + datetime(2014, 7, 7, 18): (datetime(2014, 7, 7, 17), + datetime(2014, 7, 8, 17))})) + + @pytest.mark.parametrize('case', opening_time_cases) + def test_opening_time(self, case): + _offsets, cases = case + for offset in _offsets: + for dt, (exp_next, exp_prev) in compat.iteritems(cases): + assert offset._next_opening_time(dt) == exp_next + assert offset._prev_opening_time(dt) == exp_prev + + apply_cases = [] + apply_cases.append((BusinessHour(), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16), + datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 10), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 2, 9, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 10), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 12), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 10), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10), + # saturday + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, 30)})) + + apply_cases.append((BusinessHour(4), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 13): datetime(2014, 7, 2, 9), + datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, 30)})) + + apply_cases.append((BusinessHour(-1), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 10), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 10): datetime(2014, 6, 30, 17), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 1, 15, 30, 15), + datetime(2014, 7, 1, 9, 30, 15): datetime(2014, 6, 30, 16, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 16), + datetime(2014, 7, 1, 5): datetime(2014, 6, 30, 16), + datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 10), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 16), + datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 16), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 16), + # saturday + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 16), + datetime(2014, 7, 7, 9): datetime(2014, 7, 4, 16), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 16, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 16, 30, 30)})) + + apply_cases.append((BusinessHour(-4), { + datetime(2014, 7, 1, 11): datetime(2014, 6, 30, 15), + datetime(2014, 7, 1, 13): datetime(2014, 6, 30, 17), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 13), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 18): datetime(2014, 7, 4, 13), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 13, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 13, 30, 30)})) + + apply_cases.append((BusinessHour(start='13:00', end='16:00'), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 13), + datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 14), + datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 14), + datetime(2014, 7, 1, 15, 30, 15): datetime(2014, 7, 2, 13, 30, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 14), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 14)})) + + apply_cases.append((BusinessHour(n=2, start='13:00', end='16:00'), { + datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 14): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 15), + datetime(2014, 7, 2, 14, 30): datetime(2014, 7, 3, 13, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 15), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 15), + datetime(2014, 7, 4, 14, 30): datetime(2014, 7, 7, 13, 30), + datetime(2014, 7, 4, 14, 30, 30): datetime(2014, 7, 7, 13, 30, 30)})) + + apply_cases.append((BusinessHour(n=-1, start='13:00', end='16:00'), { + datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 15), + datetime(2014, 7, 2, 14): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 15): datetime(2014, 7, 2, 14), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 16): datetime(2014, 7, 2, 15), + datetime(2014, 7, 2, 13, 30, 15): datetime(2014, 7, 1, 15, 30, 15), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 15), + datetime(2014, 7, 7, 11): datetime(2014, 7, 4, 15)})) + + apply_cases.append((BusinessHour(n=-3, start='10:00', end='16:00'), { + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 11), + datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), + datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 16), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), + datetime(2014, 7, 2, 11, 30): datetime(2014, 7, 1, 14, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), + datetime(2014, 7, 4, 10): datetime(2014, 7, 3, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 16): datetime(2014, 7, 4, 13), + datetime(2014, 7, 4, 12, 30): datetime(2014, 7, 3, 15, 30), + datetime(2014, 7, 4, 12, 30, 30): datetime(2014, 7, 3, 15, 30, 30)})) + + apply_cases.append((BusinessHour(start='19:00', end='05:00'), { + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 20), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 20), + datetime(2014, 7, 2, 4, 30): datetime(2014, 7, 2, 19, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 1), + datetime(2014, 7, 4, 10): datetime(2014, 7, 4, 20), + datetime(2014, 7, 4, 23): datetime(2014, 7, 5, 0), + datetime(2014, 7, 5, 0): datetime(2014, 7, 5, 1), + datetime(2014, 7, 5, 4): datetime(2014, 7, 7, 19), + datetime(2014, 7, 5, 4, 30): datetime(2014, 7, 7, 19, 30), + datetime(2014, 7, 5, 4, 30, 30): datetime(2014, 7, 7, 19, 30, 30)})) + + apply_cases.append((BusinessHour(n=-1, start='19:00', end='05:00'), { + datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 4), + datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 20): datetime(2014, 7, 2, 5), + datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 4), + datetime(2014, 7, 2, 19, 30): datetime(2014, 7, 2, 4, 30), + datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 23), + datetime(2014, 7, 3, 6): datetime(2014, 7, 3, 4), + datetime(2014, 7, 4, 23): datetime(2014, 7, 4, 22), + datetime(2014, 7, 5, 0): datetime(2014, 7, 4, 23), + datetime(2014, 7, 5, 4): datetime(2014, 7, 5, 3), + datetime(2014, 7, 7, 19, 30): datetime(2014, 7, 5, 4, 30), + datetime(2014, 7, 7, 19, 30, 30): datetime(2014, 7, 5, 4, 30, 30)})) + + @pytest.mark.parametrize('case', apply_cases) + def test_apply(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + apply_large_n_cases = [] + # A week later + apply_large_n_cases.append((BusinessHour(40), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 8, 11), + datetime(2014, 7, 1, 13): datetime(2014, 7, 8, 13), + datetime(2014, 7, 1, 15): datetime(2014, 7, 8, 15), + datetime(2014, 7, 1, 16): datetime(2014, 7, 8, 16), + datetime(2014, 7, 1, 17): datetime(2014, 7, 9, 9), + datetime(2014, 7, 2, 11): datetime(2014, 7, 9, 11), + datetime(2014, 7, 2, 8): datetime(2014, 7, 9, 9), + datetime(2014, 7, 2, 19): datetime(2014, 7, 10, 9), + datetime(2014, 7, 2, 23): datetime(2014, 7, 10, 9), + datetime(2014, 7, 3, 0): datetime(2014, 7, 10, 9), + datetime(2014, 7, 5, 15): datetime(2014, 7, 14, 9), + datetime(2014, 7, 4, 18): datetime(2014, 7, 14, 9), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 14, 9, 30), + datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 14, 9, 30, 30)})) + + # 3 days and 1 hour before + apply_large_n_cases.append((BusinessHour(-25), { + datetime(2014, 7, 1, 11): datetime(2014, 6, 26, 10), + datetime(2014, 7, 1, 13): datetime(2014, 6, 26, 12), + datetime(2014, 7, 1, 9): datetime(2014, 6, 25, 16), + datetime(2014, 7, 1, 10): datetime(2014, 6, 25, 17), + datetime(2014, 7, 3, 11): datetime(2014, 6, 30, 10), + datetime(2014, 7, 3, 8): datetime(2014, 6, 27, 16), + datetime(2014, 7, 3, 19): datetime(2014, 6, 30, 16), + datetime(2014, 7, 3, 23): datetime(2014, 6, 30, 16), + datetime(2014, 7, 4, 9): datetime(2014, 6, 30, 16), + datetime(2014, 7, 5, 15): datetime(2014, 7, 1, 16), + datetime(2014, 7, 6, 18): datetime(2014, 7, 1, 16), + datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 1, 16, 30), + datetime(2014, 7, 7, 10, 30, 30): datetime(2014, 7, 2, 9, 30, 30)})) + + # 5 days and 3 hours later + apply_large_n_cases.append((BusinessHour(28, start='21:00', end='02:00'), { + datetime(2014, 7, 1, 11): datetime(2014, 7, 9, 0), + datetime(2014, 7, 1, 22): datetime(2014, 7, 9, 1), + datetime(2014, 7, 1, 23): datetime(2014, 7, 9, 21), + datetime(2014, 7, 2, 2): datetime(2014, 7, 10, 0), + datetime(2014, 7, 3, 21): datetime(2014, 7, 11, 0), + datetime(2014, 7, 4, 1): datetime(2014, 7, 11, 23), + datetime(2014, 7, 4, 2): datetime(2014, 7, 12, 0), + datetime(2014, 7, 4, 3): datetime(2014, 7, 12, 0), + datetime(2014, 7, 5, 1): datetime(2014, 7, 14, 23), + datetime(2014, 7, 5, 15): datetime(2014, 7, 15, 0), + datetime(2014, 7, 6, 18): datetime(2014, 7, 15, 0), + datetime(2014, 7, 7, 1): datetime(2014, 7, 15, 0), + datetime(2014, 7, 7, 23, 30): datetime(2014, 7, 15, 21, 30)})) + + @pytest.mark.parametrize('case', apply_large_n_cases) + def test_apply_large_n(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_apply_nanoseconds(self): + tests = [] + + tests.append((BusinessHour(), + {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp( + '2014-07-04 16:00') + Nano(5), + Timestamp('2014-07-04 16:00') + Nano(5): Timestamp( + '2014-07-07 09:00') + Nano(5), + Timestamp('2014-07-04 16:00') - Nano(5): Timestamp( + '2014-07-04 17:00') - Nano(5)})) + + tests.append((BusinessHour(-1), + {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp( + '2014-07-04 14:00') + Nano(5), + Timestamp('2014-07-04 10:00') + Nano(5): Timestamp( + '2014-07-04 09:00') + Nano(5), + Timestamp('2014-07-04 10:00') - Nano(5): Timestamp( + '2014-07-03 17:00') - Nano(5), })) + + for offset, cases in tests: + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_datetimeindex(self): + idx1 = date_range(start='2014-07-04 15:00', end='2014-07-08 10:00', + freq='BH') + idx2 = date_range(start='2014-07-04 15:00', periods=12, freq='BH') + idx3 = date_range(end='2014-07-08 10:00', periods=12, freq='BH') + expected = DatetimeIndex(['2014-07-04 15:00', '2014-07-04 16:00', + '2014-07-07 09:00', + '2014-07-07 10:00', '2014-07-07 11:00', + '2014-07-07 12:00', + '2014-07-07 13:00', '2014-07-07 14:00', + '2014-07-07 15:00', + '2014-07-07 16:00', '2014-07-08 09:00', + '2014-07-08 10:00'], + freq='BH') + for idx in [idx1, idx2, idx3]: + tm.assert_index_equal(idx, expected) + + idx1 = date_range(start='2014-07-04 15:45', end='2014-07-08 10:45', + freq='BH') + idx2 = date_range(start='2014-07-04 15:45', periods=12, freq='BH') + idx3 = date_range(end='2014-07-08 10:45', periods=12, freq='BH') + + expected = DatetimeIndex(['2014-07-04 15:45', '2014-07-04 16:45', + '2014-07-07 09:45', + '2014-07-07 10:45', '2014-07-07 11:45', + '2014-07-07 12:45', + '2014-07-07 13:45', '2014-07-07 14:45', + '2014-07-07 15:45', + '2014-07-07 16:45', '2014-07-08 09:45', + '2014-07-08 10:45'], + freq='BH') + expected = idx1 + for idx in [idx1, idx2, idx3]: + tm.assert_index_equal(idx, expected) + + +class TestCustomBusinessHour(Base): + _offset = CustomBusinessHour + holidays = ['2014-06-27', datetime(2014, 6, 30), + np.datetime64('2014-07-02')] + + def setup_method(self, method): + # 2014 Calendar to check custom holidays + # Sun Mon Tue Wed Thu Fri Sat + # 6/22 23 24 25 26 27 28 + # 29 30 7/1 2 3 4 5 + # 6 7 8 9 10 11 12 + self.d = datetime(2014, 7, 1, 10, 00) + self.offset1 = CustomBusinessHour(weekmask='Tue Wed Thu Fri') + + self.offset2 = CustomBusinessHour(holidays=self.holidays) + + def test_constructor_errors(self): + from datetime import time as dt_time + with pytest.raises(ValueError): + CustomBusinessHour(start=dt_time(11, 0, 5)) + with pytest.raises(ValueError): + CustomBusinessHour(start='AAA') + with pytest.raises(ValueError): + CustomBusinessHour(start='14:00:05') + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset1) == '' + assert repr(self.offset2) == '' + + def test_with_offset(self): + expected = Timestamp('2014-07-01 13:00') + + assert self.d + CustomBusinessHour() * 3 == expected + assert self.d + CustomBusinessHour(n=3) == expected + + def test_eq(self): + for offset in [self.offset1, self.offset2]: + assert offset == offset + + assert CustomBusinessHour() != CustomBusinessHour(-1) + assert (CustomBusinessHour(start='09:00') == + CustomBusinessHour()) + assert (CustomBusinessHour(start='09:00') != + CustomBusinessHour(start='09:01')) + assert (CustomBusinessHour(start='09:00', end='17:00') != + CustomBusinessHour(start='17:00', end='09:01')) + + assert (CustomBusinessHour(weekmask='Tue Wed Thu Fri') != + CustomBusinessHour(weekmask='Mon Tue Wed Thu Fri')) + assert (CustomBusinessHour(holidays=['2014-06-27']) != + CustomBusinessHour(holidays=['2014-06-28'])) + + def test_sub(self): + # override the Base.test_sub implementation because self.offset2 is + # defined differently in this class than the test expects + pass + + def test_hash(self): + assert hash(self.offset1) == hash(self.offset1) + assert hash(self.offset2) == hash(self.offset2) + + def test_call(self): + assert self.offset1(self.d) == datetime(2014, 7, 1, 11) + assert self.offset2(self.d) == datetime(2014, 7, 1, 11) + + def testRollback1(self): + assert self.offset1.rollback(self.d) == self.d + assert self.offset2.rollback(self.d) == self.d + + d = datetime(2014, 7, 1, 0) + + # 2014/07/01 is Tuesday, 06/30 is Monday(holiday) + assert self.offset1.rollback(d) == datetime(2014, 6, 27, 17) + + # 2014/6/30 and 2014/6/27 are holidays + assert self.offset2.rollback(d) == datetime(2014, 6, 26, 17) + + def testRollback2(self): + assert (self._offset(-3).rollback(datetime(2014, 7, 5, 15, 0)) == + datetime(2014, 7, 4, 17, 0)) + + def testRollforward1(self): + assert self.offset1.rollforward(self.d) == self.d + assert self.offset2.rollforward(self.d) == self.d + + d = datetime(2014, 7, 1, 0) + assert self.offset1.rollforward(d) == datetime(2014, 7, 1, 9) + assert self.offset2.rollforward(d) == datetime(2014, 7, 1, 9) + + def testRollforward2(self): + assert (self._offset(-3).rollforward(datetime(2014, 7, 5, 16, 0)) == + datetime(2014, 7, 7, 9)) + + def test_roll_date_object(self): + offset = BusinessHour() + + dt = datetime(2014, 7, 6, 15, 0) + + result = offset.rollback(dt) + assert result == datetime(2014, 7, 4, 17) + + result = offset.rollforward(dt) + assert result == datetime(2014, 7, 7, 9) + + normalize_cases = [] + normalize_cases.append(( + CustomBusinessHour(normalize=True, holidays=holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3), + datetime(2014, 7, 1, 23): datetime(2014, 7, 3), + datetime(2014, 7, 1, 0): datetime(2014, 7, 1), + datetime(2014, 7, 4, 15): datetime(2014, 7, 4), + datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 7), + datetime(2014, 7, 6, 10): datetime(2014, 7, 7)})) + + normalize_cases.append(( + CustomBusinessHour(-1, normalize=True, holidays=holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 6, 26), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 16): datetime(2014, 7, 1), + datetime(2014, 7, 1, 10): datetime(2014, 6, 26), + datetime(2014, 7, 1, 0): datetime(2014, 6, 26), + datetime(2014, 7, 7, 10): datetime(2014, 7, 4), + datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7), + datetime(2014, 7, 5, 23): datetime(2014, 7, 4), + datetime(2014, 7, 6, 10): datetime(2014, 7, 4)})) + + normalize_cases.append(( + CustomBusinessHour(1, normalize=True, + start='17:00', end='04:00', + holidays=holidays), + {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), + datetime(2014, 7, 1, 17): datetime(2014, 7, 1), + datetime(2014, 7, 1, 23): datetime(2014, 7, 2), + datetime(2014, 7, 2, 2): datetime(2014, 7, 2), + datetime(2014, 7, 2, 3): datetime(2014, 7, 3), + datetime(2014, 7, 4, 23): datetime(2014, 7, 5), + datetime(2014, 7, 5, 2): datetime(2014, 7, 5), + datetime(2014, 7, 7, 2): datetime(2014, 7, 7), + datetime(2014, 7, 7, 17): datetime(2014, 7, 7)})) + + @pytest.mark.parametrize('norm_cases', normalize_cases) + def test_normalize(self, norm_cases): + offset, cases = norm_cases + for dt, expected in compat.iteritems(cases): + assert offset.apply(dt) == expected + + def test_onOffset(self): + tests = [] + + tests.append((CustomBusinessHour(start='10:00', end='15:00', + holidays=self.holidays), + {datetime(2014, 7, 1, 9): False, + datetime(2014, 7, 1, 10): True, + datetime(2014, 7, 1, 15): True, + datetime(2014, 7, 1, 15, 1): False, + datetime(2014, 7, 5, 12): False, + datetime(2014, 7, 6, 12): False})) + + for offset, cases in tests: + for dt, expected in compat.iteritems(cases): + assert offset.onOffset(dt) == expected + + apply_cases = [] + apply_cases.append(( + CustomBusinessHour(holidays=holidays), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12), + datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), + datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16), + datetime(2014, 7, 1, 19): datetime(2014, 7, 3, 10), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 9), + datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 3, 9, 30, 15), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 10), + # out of business hours + datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10), + # saturday + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, 30)})) + + apply_cases.append(( + CustomBusinessHour(4, holidays=holidays), + {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), + datetime(2014, 7, 1, 13): datetime(2014, 7, 3, 9), + datetime(2014, 7, 1, 15): datetime(2014, 7, 3, 11), + datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 12), + datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13), + datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13), + datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13), + datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13), + datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30), + datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, 30)})) + + @pytest.mark.parametrize('apply_case', apply_cases) + def test_apply(self, apply_case): + offset, cases = apply_case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + nano_cases = [] + nano_cases.append( + (CustomBusinessHour(holidays=holidays), + {Timestamp('2014-07-01 15:00') + Nano(5): + Timestamp('2014-07-01 16:00') + Nano(5), + Timestamp('2014-07-01 16:00') + Nano(5): + Timestamp('2014-07-03 09:00') + Nano(5), + Timestamp('2014-07-01 16:00') - Nano(5): + Timestamp('2014-07-01 17:00') - Nano(5)})) + + nano_cases.append( + (CustomBusinessHour(-1, holidays=holidays), + {Timestamp('2014-07-01 15:00') + Nano(5): + Timestamp('2014-07-01 14:00') + Nano(5), + Timestamp('2014-07-01 10:00') + Nano(5): + Timestamp('2014-07-01 09:00') + Nano(5), + Timestamp('2014-07-01 10:00') - Nano(5): + Timestamp('2014-06-26 17:00') - Nano(5)})) + + @pytest.mark.parametrize('nano_case', nano_cases) + def test_apply_nanoseconds(self, nano_case): + offset, cases = nano_case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + +class TestCustomBusinessDay(Base): + _offset = CDay + + def setup_method(self, method): + self.d = datetime(2008, 1, 1) + self.nd = np_datetime64_compat('2008-01-01 00:00:00Z') + + self.offset = CDay() + self.offset1 = self.offset + self.offset2 = CDay(2) + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset) == '' + assert repr(self.offset2) == '<2 * CustomBusinessDays>' + + if compat.PY37: + expected = '' + else: + expected = '' + assert repr(self.offset + timedelta(1)) == expected + + def test_with_offset(self): + offset = self.offset + timedelta(hours=2) + + assert (self.d + offset) == datetime(2008, 1, 2, 2) + + def test_eq(self): + assert self.offset2 == self.offset2 + + def test_mul(self): + pass + + def test_hash(self): + assert hash(self.offset2) == hash(self.offset2) + + def test_call(self): + assert self.offset2(self.d) == datetime(2008, 1, 3) + assert self.offset2(self.nd) == datetime(2008, 1, 3) + + def testRollback1(self): + assert CDay(10).rollback(self.d) == self.d + + def testRollback2(self): + assert (CDay(10).rollback(datetime(2008, 1, 5)) == + datetime(2008, 1, 4)) + + def testRollforward1(self): + assert CDay(10).rollforward(self.d) == self.d + + def testRollforward2(self): + assert (CDay(10).rollforward(datetime(2008, 1, 5)) == + datetime(2008, 1, 7)) + + def test_roll_date_object(self): + offset = CDay() + + dt = date(2012, 9, 15) + + result = offset.rollback(dt) + assert result == datetime(2012, 9, 14) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 17) + + offset = offsets.Day() + result = offset.rollback(dt) + assert result == datetime(2012, 9, 15) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 15) + + on_offset_cases = [(CDay(), datetime(2008, 1, 1), True), + (CDay(), datetime(2008, 1, 5), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, d, expected = case + assert_onOffset(offset, d, expected) + + apply_cases = [] + apply_cases.append((CDay(), { + datetime(2008, 1, 1): datetime(2008, 1, 2), + datetime(2008, 1, 4): datetime(2008, 1, 7), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 8)})) + + apply_cases.append((2 * CDay(), { + datetime(2008, 1, 1): datetime(2008, 1, 3), + datetime(2008, 1, 4): datetime(2008, 1, 8), + datetime(2008, 1, 5): datetime(2008, 1, 8), + datetime(2008, 1, 6): datetime(2008, 1, 8), + datetime(2008, 1, 7): datetime(2008, 1, 9)})) + + apply_cases.append((-CDay(), { + datetime(2008, 1, 1): datetime(2007, 12, 31), + datetime(2008, 1, 4): datetime(2008, 1, 3), + datetime(2008, 1, 5): datetime(2008, 1, 4), + datetime(2008, 1, 6): datetime(2008, 1, 4), + datetime(2008, 1, 7): datetime(2008, 1, 4), + datetime(2008, 1, 8): datetime(2008, 1, 7)})) + + apply_cases.append((-2 * CDay(), { + datetime(2008, 1, 1): datetime(2007, 12, 28), + datetime(2008, 1, 4): datetime(2008, 1, 2), + datetime(2008, 1, 5): datetime(2008, 1, 3), + datetime(2008, 1, 6): datetime(2008, 1, 3), + datetime(2008, 1, 7): datetime(2008, 1, 3), + datetime(2008, 1, 8): datetime(2008, 1, 4), + datetime(2008, 1, 9): datetime(2008, 1, 7)})) + + apply_cases.append((CDay(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 1, 4): datetime(2008, 1, 4), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 7)})) + + @pytest.mark.parametrize('case', apply_cases) + def test_apply(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_apply_large_n(self): + dt = datetime(2012, 10, 23) + + result = dt + CDay(10) + assert result == datetime(2012, 11, 6) + + result = dt + CDay(100) - CDay(100) + assert result == dt + + off = CDay() * 6 + rs = datetime(2012, 1, 1) - off + xp = datetime(2011, 12, 23) + assert rs == xp + + st = datetime(2011, 12, 18) + rs = st + off + xp = datetime(2011, 12, 26) + assert rs == xp + + def test_apply_corner(self): + msg = ("Only know how to combine trading day with datetime, datetime64" + " or timedelta") + with pytest.raises(ApplyTypeError, match=msg): + CDay().apply(BMonthEnd()) + + def test_holidays(self): + # Define a TradingDay offset + holidays = ['2012-05-01', datetime(2013, 5, 1), + np.datetime64('2014-05-01')] + tday = CDay(holidays=holidays) + for year in range(2012, 2015): + dt = datetime(year, 4, 30) + xp = datetime(year, 5, 2) + rs = dt + tday + assert rs == xp + + def test_weekmask(self): + weekmask_saudi = 'Sat Sun Mon Tue Wed' # Thu-Fri Weekend + weekmask_uae = '1111001' # Fri-Sat Weekend + weekmask_egypt = [1, 1, 1, 1, 0, 0, 1] # Fri-Sat Weekend + bday_saudi = CDay(weekmask=weekmask_saudi) + bday_uae = CDay(weekmask=weekmask_uae) + bday_egypt = CDay(weekmask=weekmask_egypt) + dt = datetime(2013, 5, 1) + xp_saudi = datetime(2013, 5, 4) + xp_uae = datetime(2013, 5, 2) + xp_egypt = datetime(2013, 5, 2) + assert xp_saudi == dt + bday_saudi + assert xp_uae == dt + bday_uae + assert xp_egypt == dt + bday_egypt + xp2 = datetime(2013, 5, 5) + assert xp2 == dt + 2 * bday_saudi + assert xp2 == dt + 2 * bday_uae + assert xp2 == dt + 2 * bday_egypt + + def test_weekmask_and_holidays(self): + weekmask_egypt = 'Sun Mon Tue Wed Thu' # Fri-Sat Weekend + holidays = ['2012-05-01', datetime(2013, 5, 1), + np.datetime64('2014-05-01')] + bday_egypt = CDay(holidays=holidays, weekmask=weekmask_egypt) + dt = datetime(2013, 4, 30) + xp_egypt = datetime(2013, 5, 5) + assert xp_egypt == dt + 2 * bday_egypt + + @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning") + def test_calendar(self): + calendar = USFederalHolidayCalendar() + dt = datetime(2014, 1, 17) + assert_offset_equal(CDay(calendar=calendar), dt, datetime(2014, 1, 21)) + + def test_roundtrip_pickle(self): + def _check_roundtrip(obj): + unpickled = tm.round_trip_pickle(obj) + assert unpickled == obj + + _check_roundtrip(self.offset) + _check_roundtrip(self.offset2) + _check_roundtrip(self.offset * 2) + + def test_pickle_compat_0_14_1(self, datapath): + hdays = [datetime(2013, 1, 1) for ele in range(4)] + pth = datapath('tseries', 'offsets', 'data', 'cday-0.14.1.pickle') + cday0_14_1 = read_pickle(pth) + cday = CDay(holidays=hdays) + assert cday == cday0_14_1 + + +class CustomBusinessMonthBase(object): + + def setup_method(self, method): + self.d = datetime(2008, 1, 1) + + self.offset = self._offset() + self.offset1 = self.offset + self.offset2 = self._offset(2) + + def test_eq(self): + assert self.offset2 == self.offset2 + + def test_mul(self): + pass + + def test_hash(self): + assert hash(self.offset2) == hash(self.offset2) + + def test_roundtrip_pickle(self): + def _check_roundtrip(obj): + unpickled = tm.round_trip_pickle(obj) + assert unpickled == obj + + _check_roundtrip(self._offset()) + _check_roundtrip(self._offset(2)) + _check_roundtrip(self._offset() * 2) + + def test_copy(self): + # GH 17452 + off = self._offset(weekmask='Mon Wed Fri') + assert off == off.copy() + + +class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base): + _offset = CBMonthEnd + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset) == '' + assert repr(self.offset2) == '<2 * CustomBusinessMonthEnds>' + + def testCall(self): + assert self.offset2(self.d) == datetime(2008, 2, 29) + + def testRollback1(self): + assert (CDay(10).rollback(datetime(2007, 12, 31)) == + datetime(2007, 12, 31)) + + def testRollback2(self): + assert CBMonthEnd(10).rollback(self.d) == datetime(2007, 12, 31) + + def testRollforward1(self): + assert CBMonthEnd(10).rollforward(self.d) == datetime(2008, 1, 31) + + def test_roll_date_object(self): + offset = CBMonthEnd() + + dt = date(2012, 9, 15) + + result = offset.rollback(dt) + assert result == datetime(2012, 8, 31) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 28) + + offset = offsets.Day() + result = offset.rollback(dt) + assert result == datetime(2012, 9, 15) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 15) + + on_offset_cases = [(CBMonthEnd(), datetime(2008, 1, 31), True), + (CBMonthEnd(), datetime(2008, 1, 1), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, d, expected = case + assert_onOffset(offset, d, expected) + + apply_cases = [] + apply_cases.append((CBMonthEnd(), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 2, 7): datetime(2008, 2, 29)})) + + apply_cases.append((2 * CBMonthEnd(), { + datetime(2008, 1, 1): datetime(2008, 2, 29), + datetime(2008, 2, 7): datetime(2008, 3, 31)})) + + apply_cases.append((-CBMonthEnd(), { + datetime(2008, 1, 1): datetime(2007, 12, 31), + datetime(2008, 2, 8): datetime(2008, 1, 31)})) + + apply_cases.append((-2 * CBMonthEnd(), { + datetime(2008, 1, 1): datetime(2007, 11, 30), + datetime(2008, 2, 9): datetime(2007, 12, 31)})) + + apply_cases.append((CBMonthEnd(0), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 2, 7): datetime(2008, 2, 29)})) + + @pytest.mark.parametrize('case', apply_cases) + def test_apply(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_apply_large_n(self): + dt = datetime(2012, 10, 23) + + result = dt + CBMonthEnd(10) + assert result == datetime(2013, 7, 31) + + result = dt + CDay(100) - CDay(100) + assert result == dt + + off = CBMonthEnd() * 6 + rs = datetime(2012, 1, 1) - off + xp = datetime(2011, 7, 29) + assert rs == xp + + st = datetime(2011, 12, 18) + rs = st + off + xp = datetime(2012, 5, 31) + assert rs == xp + + def test_holidays(self): + # Define a TradingDay offset + holidays = ['2012-01-31', datetime(2012, 2, 28), + np.datetime64('2012-02-29')] + bm_offset = CBMonthEnd(holidays=holidays) + dt = datetime(2012, 1, 1) + assert dt + bm_offset == datetime(2012, 1, 30) + assert dt + 2 * bm_offset == datetime(2012, 2, 27) + + @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning") + def test_datetimeindex(self): + from pandas.tseries.holiday import USFederalHolidayCalendar + hcal = USFederalHolidayCalendar() + freq = CBMonthEnd(calendar=hcal) + + assert (date_range(start='20120101', end='20130101', + freq=freq).tolist()[0] == datetime(2012, 1, 31)) + + +class TestCustomBusinessMonthBegin(CustomBusinessMonthBase, Base): + _offset = CBMonthBegin + + def test_different_normalize_equals(self): + # GH#21404 changed __eq__ to return False when `normalize` doesnt match + offset = self._offset() + offset2 = self._offset(normalize=True) + assert offset != offset2 + + def test_repr(self): + assert repr(self.offset) == '' + assert repr(self.offset2) == '<2 * CustomBusinessMonthBegins>' + + def testCall(self): + assert self.offset2(self.d) == datetime(2008, 3, 3) + + def testRollback1(self): + assert (CDay(10).rollback(datetime(2007, 12, 31)) == + datetime(2007, 12, 31)) + + def testRollback2(self): + assert CBMonthBegin(10).rollback(self.d) == datetime(2008, 1, 1) + + def testRollforward1(self): + assert CBMonthBegin(10).rollforward(self.d) == datetime(2008, 1, 1) + + def test_roll_date_object(self): + offset = CBMonthBegin() + + dt = date(2012, 9, 15) + + result = offset.rollback(dt) + assert result == datetime(2012, 9, 3) + + result = offset.rollforward(dt) + assert result == datetime(2012, 10, 1) + + offset = offsets.Day() + result = offset.rollback(dt) + assert result == datetime(2012, 9, 15) + + result = offset.rollforward(dt) + assert result == datetime(2012, 9, 15) + + on_offset_cases = [(CBMonthBegin(), datetime(2008, 1, 1), True), + (CBMonthBegin(), datetime(2008, 1, 31), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + apply_cases = [] + apply_cases.append((CBMonthBegin(), { + datetime(2008, 1, 1): datetime(2008, 2, 1), + datetime(2008, 2, 7): datetime(2008, 3, 3)})) + + apply_cases.append((2 * CBMonthBegin(), { + datetime(2008, 1, 1): datetime(2008, 3, 3), + datetime(2008, 2, 7): datetime(2008, 4, 1)})) + + apply_cases.append((-CBMonthBegin(), { + datetime(2008, 1, 1): datetime(2007, 12, 3), + datetime(2008, 2, 8): datetime(2008, 2, 1)})) + + apply_cases.append((-2 * CBMonthBegin(), { + datetime(2008, 1, 1): datetime(2007, 11, 1), + datetime(2008, 2, 9): datetime(2008, 1, 1)})) + + apply_cases.append((CBMonthBegin(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 1, 7): datetime(2008, 2, 1)})) + + @pytest.mark.parametrize('case', apply_cases) + def test_apply(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_apply_large_n(self): + dt = datetime(2012, 10, 23) + + result = dt + CBMonthBegin(10) + assert result == datetime(2013, 8, 1) + + result = dt + CDay(100) - CDay(100) + assert result == dt + + off = CBMonthBegin() * 6 + rs = datetime(2012, 1, 1) - off + xp = datetime(2011, 7, 1) + assert rs == xp + + st = datetime(2011, 12, 18) + rs = st + off + + xp = datetime(2012, 6, 1) + assert rs == xp + + def test_holidays(self): + # Define a TradingDay offset + holidays = ['2012-02-01', datetime(2012, 2, 2), + np.datetime64('2012-03-01')] + bm_offset = CBMonthBegin(holidays=holidays) + dt = datetime(2012, 1, 1) + + assert dt + bm_offset == datetime(2012, 1, 2) + assert dt + 2 * bm_offset == datetime(2012, 2, 3) + + @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning") + def test_datetimeindex(self): + hcal = USFederalHolidayCalendar() + cbmb = CBMonthBegin(calendar=hcal) + assert (date_range(start='20120101', end='20130101', + freq=cbmb).tolist()[0] == datetime(2012, 1, 3)) + + +class TestWeek(Base): + _offset = Week + d = Timestamp(datetime(2008, 1, 2)) + offset1 = _offset() + offset2 = _offset(2) + + def test_repr(self): + assert repr(Week(weekday=0)) == "" + assert repr(Week(n=-1, weekday=0)) == "<-1 * Week: weekday=0>" + assert repr(Week(n=-2, weekday=0)) == "<-2 * Weeks: weekday=0>" + + def test_corner(self): + with pytest.raises(ValueError): + Week(weekday=7) + + with pytest.raises(ValueError, match="Day must be"): + Week(weekday=-1) + + def test_isAnchored(self): + assert Week(weekday=0).isAnchored() + assert not Week().isAnchored() + assert not Week(2, weekday=2).isAnchored() + assert not Week(2).isAnchored() + + offset_cases = [] + # not business week + offset_cases.append((Week(), { + datetime(2008, 1, 1): datetime(2008, 1, 8), + datetime(2008, 1, 4): datetime(2008, 1, 11), + datetime(2008, 1, 5): datetime(2008, 1, 12), + datetime(2008, 1, 6): datetime(2008, 1, 13), + datetime(2008, 1, 7): datetime(2008, 1, 14)})) + + # Mon + offset_cases.append((Week(weekday=0), { + datetime(2007, 12, 31): datetime(2008, 1, 7), + datetime(2008, 1, 4): datetime(2008, 1, 7), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 14)})) + + # n=0 -> roll forward. Mon + offset_cases.append((Week(0, weekday=0), { + datetime(2007, 12, 31): datetime(2007, 12, 31), + datetime(2008, 1, 4): datetime(2008, 1, 7), + datetime(2008, 1, 5): datetime(2008, 1, 7), + datetime(2008, 1, 6): datetime(2008, 1, 7), + datetime(2008, 1, 7): datetime(2008, 1, 7)})) + + # n=0 -> roll forward. Mon + offset_cases.append((Week(-2, weekday=1), { + datetime(2010, 4, 6): datetime(2010, 3, 23), + datetime(2010, 4, 8): datetime(2010, 3, 30), + datetime(2010, 4, 5): datetime(2010, 3, 23)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + @pytest.mark.parametrize('weekday', range(7)) + def test_onOffset(self, weekday): + offset = Week(weekday=weekday) + + for day in range(1, 8): + date = datetime(2008, 1, day) + + if day % 7 == weekday: + expected = True + else: + expected = False + assert_onOffset(offset, date, expected) + + +class TestWeekOfMonth(Base): + _offset = WeekOfMonth + offset1 = _offset() + offset2 = _offset(2) + + def test_constructor(self): + with pytest.raises(ValueError, match="^Week"): + WeekOfMonth(n=1, week=4, weekday=0) + + with pytest.raises(ValueError, match="^Week"): + WeekOfMonth(n=1, week=-1, weekday=0) + + with pytest.raises(ValueError, match="^Day"): + WeekOfMonth(n=1, week=0, weekday=-1) + + with pytest.raises(ValueError, match="^Day"): + WeekOfMonth(n=1, week=0, weekday=-7) + + def test_repr(self): + assert (repr(WeekOfMonth(weekday=1, week=2)) == + "") + + def test_offset(self): + date1 = datetime(2011, 1, 4) # 1st Tuesday of Month + date2 = datetime(2011, 1, 11) # 2nd Tuesday of Month + date3 = datetime(2011, 1, 18) # 3rd Tuesday of Month + date4 = datetime(2011, 1, 25) # 4th Tuesday of Month + + # see for loop for structure + test_cases = [ + (-2, 2, 1, date1, datetime(2010, 11, 16)), + (-2, 2, 1, date2, datetime(2010, 11, 16)), + (-2, 2, 1, date3, datetime(2010, 11, 16)), + (-2, 2, 1, date4, datetime(2010, 12, 21)), + + (-1, 2, 1, date1, datetime(2010, 12, 21)), + (-1, 2, 1, date2, datetime(2010, 12, 21)), + (-1, 2, 1, date3, datetime(2010, 12, 21)), + (-1, 2, 1, date4, datetime(2011, 1, 18)), + + (0, 0, 1, date1, datetime(2011, 1, 4)), + (0, 0, 1, date2, datetime(2011, 2, 1)), + (0, 0, 1, date3, datetime(2011, 2, 1)), + (0, 0, 1, date4, datetime(2011, 2, 1)), + (0, 1, 1, date1, datetime(2011, 1, 11)), + (0, 1, 1, date2, datetime(2011, 1, 11)), + (0, 1, 1, date3, datetime(2011, 2, 8)), + (0, 1, 1, date4, datetime(2011, 2, 8)), + (0, 0, 1, date1, datetime(2011, 1, 4)), + (0, 1, 1, date2, datetime(2011, 1, 11)), + (0, 2, 1, date3, datetime(2011, 1, 18)), + (0, 3, 1, date4, datetime(2011, 1, 25)), + + (1, 0, 0, date1, datetime(2011, 2, 7)), + (1, 0, 0, date2, datetime(2011, 2, 7)), + (1, 0, 0, date3, datetime(2011, 2, 7)), + (1, 0, 0, date4, datetime(2011, 2, 7)), + (1, 0, 1, date1, datetime(2011, 2, 1)), + (1, 0, 1, date2, datetime(2011, 2, 1)), + (1, 0, 1, date3, datetime(2011, 2, 1)), + (1, 0, 1, date4, datetime(2011, 2, 1)), + (1, 0, 2, date1, datetime(2011, 1, 5)), + (1, 0, 2, date2, datetime(2011, 2, 2)), + (1, 0, 2, date3, datetime(2011, 2, 2)), + (1, 0, 2, date4, datetime(2011, 2, 2)), + + (1, 2, 1, date1, datetime(2011, 1, 18)), + (1, 2, 1, date2, datetime(2011, 1, 18)), + (1, 2, 1, date3, datetime(2011, 2, 15)), + (1, 2, 1, date4, datetime(2011, 2, 15)), + + (2, 2, 1, date1, datetime(2011, 2, 15)), + (2, 2, 1, date2, datetime(2011, 2, 15)), + (2, 2, 1, date3, datetime(2011, 3, 15)), + (2, 2, 1, date4, datetime(2011, 3, 15))] + + for n, week, weekday, dt, expected in test_cases: + offset = WeekOfMonth(n, week=week, weekday=weekday) + assert_offset_equal(offset, dt, expected) + + # try subtracting + result = datetime(2011, 2, 1) - WeekOfMonth(week=1, weekday=2) + assert result == datetime(2011, 1, 12) + + result = datetime(2011, 2, 3) - WeekOfMonth(week=0, weekday=2) + assert result == datetime(2011, 2, 2) + + on_offset_cases = [(0, 0, datetime(2011, 2, 7), True), + (0, 0, datetime(2011, 2, 6), False), + (0, 0, datetime(2011, 2, 14), False), + (1, 0, datetime(2011, 2, 14), True), + (0, 1, datetime(2011, 2, 1), True), + (0, 1, datetime(2011, 2, 8), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + week, weekday, dt, expected = case + offset = WeekOfMonth(week=week, weekday=weekday) + assert offset.onOffset(dt) == expected + + +class TestLastWeekOfMonth(Base): + _offset = LastWeekOfMonth + offset1 = _offset() + offset2 = _offset(2) + + def test_constructor(self): + with pytest.raises(ValueError, match="^N cannot be 0"): + LastWeekOfMonth(n=0, weekday=1) + + with pytest.raises(ValueError, match="^Day"): + LastWeekOfMonth(n=1, weekday=-1) + + with pytest.raises(ValueError, match="^Day"): + LastWeekOfMonth(n=1, weekday=7) + + def test_offset(self): + # Saturday + last_sat = datetime(2013, 8, 31) + next_sat = datetime(2013, 9, 28) + offset_sat = LastWeekOfMonth(n=1, weekday=5) + + one_day_before = (last_sat + timedelta(days=-1)) + assert one_day_before + offset_sat == last_sat + + one_day_after = (last_sat + timedelta(days=+1)) + assert one_day_after + offset_sat == next_sat + + # Test On that day + assert last_sat + offset_sat == next_sat + + # Thursday + + offset_thur = LastWeekOfMonth(n=1, weekday=3) + last_thurs = datetime(2013, 1, 31) + next_thurs = datetime(2013, 2, 28) + + one_day_before = last_thurs + timedelta(days=-1) + assert one_day_before + offset_thur == last_thurs + + one_day_after = last_thurs + timedelta(days=+1) + assert one_day_after + offset_thur == next_thurs + + # Test on that day + assert last_thurs + offset_thur == next_thurs + + three_before = last_thurs + timedelta(days=-3) + assert three_before + offset_thur == last_thurs + + two_after = last_thurs + timedelta(days=+2) + assert two_after + offset_thur == next_thurs + + offset_sunday = LastWeekOfMonth(n=1, weekday=WeekDay.SUN) + assert datetime(2013, 7, 31) + offset_sunday == datetime(2013, 8, 25) + + on_offset_cases = [ + (WeekDay.SUN, datetime(2013, 1, 27), True), + (WeekDay.SAT, datetime(2013, 3, 30), True), + (WeekDay.MON, datetime(2013, 2, 18), False), # Not the last Mon + (WeekDay.SUN, datetime(2013, 2, 25), False), # Not a SUN + (WeekDay.MON, datetime(2013, 2, 25), True), + (WeekDay.SAT, datetime(2013, 11, 30), True), + + (WeekDay.SAT, datetime(2006, 8, 26), True), + (WeekDay.SAT, datetime(2007, 8, 25), True), + (WeekDay.SAT, datetime(2008, 8, 30), True), + (WeekDay.SAT, datetime(2009, 8, 29), True), + (WeekDay.SAT, datetime(2010, 8, 28), True), + (WeekDay.SAT, datetime(2011, 8, 27), True), + (WeekDay.SAT, datetime(2019, 8, 31), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + weekday, dt, expected = case + offset = LastWeekOfMonth(weekday=weekday) + assert offset.onOffset(dt) == expected + + +class TestSemiMonthEnd(Base): + _offset = SemiMonthEnd + offset1 = _offset() + offset2 = _offset(2) + + def test_offset_whole_year(self): + dates = (datetime(2007, 12, 31), + datetime(2008, 1, 15), + datetime(2008, 1, 31), + datetime(2008, 2, 15), + datetime(2008, 2, 29), + datetime(2008, 3, 15), + datetime(2008, 3, 31), + datetime(2008, 4, 15), + datetime(2008, 4, 30), + datetime(2008, 5, 15), + datetime(2008, 5, 31), + datetime(2008, 6, 15), + datetime(2008, 6, 30), + datetime(2008, 7, 15), + datetime(2008, 7, 31), + datetime(2008, 8, 15), + datetime(2008, 8, 31), + datetime(2008, 9, 15), + datetime(2008, 9, 30), + datetime(2008, 10, 15), + datetime(2008, 10, 31), + datetime(2008, 11, 15), + datetime(2008, 11, 30), + datetime(2008, 12, 15), + datetime(2008, 12, 31)) + + for base, exp_date in zip(dates[:-1], dates[1:]): + assert_offset_equal(SemiMonthEnd(), base, exp_date) + + # ensure .apply_index works as expected + s = DatetimeIndex(dates[:-1]) + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = SemiMonthEnd().apply_index(s) + + exp = DatetimeIndex(dates[1:]) + tm.assert_index_equal(result, exp) + + # ensure generating a range with DatetimeIndex gives same result + result = date_range(start=dates[0], end=dates[-1], freq='SM') + exp = DatetimeIndex(dates) + tm.assert_index_equal(result, exp) + + offset_cases = [] + offset_cases.append((SemiMonthEnd(), { + datetime(2008, 1, 1): datetime(2008, 1, 15), + datetime(2008, 1, 15): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 2, 15), + datetime(2006, 12, 14): datetime(2006, 12, 15), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2007, 1, 15), + datetime(2007, 1, 1): datetime(2007, 1, 15), + datetime(2006, 12, 1): datetime(2006, 12, 15), + datetime(2006, 12, 15): datetime(2006, 12, 31)})) + + offset_cases.append((SemiMonthEnd(day_of_month=20), { + datetime(2008, 1, 1): datetime(2008, 1, 20), + datetime(2008, 1, 15): datetime(2008, 1, 20), + datetime(2008, 1, 21): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 2, 20), + datetime(2006, 12, 14): datetime(2006, 12, 20), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2007, 1, 20), + datetime(2007, 1, 1): datetime(2007, 1, 20), + datetime(2006, 12, 1): datetime(2006, 12, 20), + datetime(2006, 12, 15): datetime(2006, 12, 20)})) + + offset_cases.append((SemiMonthEnd(0), { + datetime(2008, 1, 1): datetime(2008, 1, 15), + datetime(2008, 1, 16): datetime(2008, 1, 31), + datetime(2008, 1, 15): datetime(2008, 1, 15), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2006, 12, 31), + datetime(2007, 1, 1): datetime(2007, 1, 15)})) + + offset_cases.append((SemiMonthEnd(0, day_of_month=16), { + datetime(2008, 1, 1): datetime(2008, 1, 16), + datetime(2008, 1, 16): datetime(2008, 1, 16), + datetime(2008, 1, 15): datetime(2008, 1, 16), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2006, 12, 31), + datetime(2007, 1, 1): datetime(2007, 1, 16)})) + + offset_cases.append((SemiMonthEnd(2), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 2, 29), + datetime(2006, 12, 29): datetime(2007, 1, 15), + datetime(2006, 12, 31): datetime(2007, 1, 31), + datetime(2007, 1, 1): datetime(2007, 1, 31), + datetime(2007, 1, 16): datetime(2007, 2, 15), + datetime(2006, 11, 1): datetime(2006, 11, 30)})) + + offset_cases.append((SemiMonthEnd(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 31), + datetime(2008, 6, 30): datetime(2008, 6, 15), + datetime(2008, 12, 31): datetime(2008, 12, 15), + datetime(2006, 12, 29): datetime(2006, 12, 15), + datetime(2006, 12, 30): datetime(2006, 12, 15), + datetime(2007, 1, 1): datetime(2006, 12, 31)})) + + offset_cases.append((SemiMonthEnd(-1, day_of_month=4), { + datetime(2007, 1, 1): datetime(2006, 12, 31), + datetime(2007, 1, 4): datetime(2006, 12, 31), + datetime(2008, 6, 30): datetime(2008, 6, 4), + datetime(2008, 12, 31): datetime(2008, 12, 4), + datetime(2006, 12, 5): datetime(2006, 12, 4), + datetime(2006, 12, 30): datetime(2006, 12, 4), + datetime(2007, 1, 1): datetime(2006, 12, 31)})) + + offset_cases.append((SemiMonthEnd(-2), { + datetime(2007, 1, 1): datetime(2006, 12, 15), + datetime(2008, 6, 30): datetime(2008, 5, 31), + datetime(2008, 3, 15): datetime(2008, 2, 15), + datetime(2008, 12, 31): datetime(2008, 11, 30), + datetime(2006, 12, 29): datetime(2006, 11, 30), + datetime(2006, 12, 14): datetime(2006, 11, 15), + datetime(2007, 1, 1): datetime(2006, 12, 15)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + @pytest.mark.parametrize('case', offset_cases) + def test_apply_index(self, case): + offset, cases = case + s = DatetimeIndex(cases.keys()) + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = offset.apply_index(s) + + exp = DatetimeIndex(cases.values()) + tm.assert_index_equal(result, exp) + + on_offset_cases = [(datetime(2007, 12, 31), True), + (datetime(2007, 12, 15), True), + (datetime(2007, 12, 14), False), + (datetime(2007, 12, 1), False), + (datetime(2008, 2, 29), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + dt, expected = case + assert_onOffset(SemiMonthEnd(), dt, expected) + + @pytest.mark.parametrize('klass', [Series, DatetimeIndex]) + def test_vectorized_offset_addition(self, klass): + s = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = s + SemiMonthEnd() + result2 = SemiMonthEnd() + s + + exp = klass([Timestamp('2000-01-31 00:15:00', tz='US/Central'), + Timestamp('2000-02-29', tz='US/Central')], name='a') + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + s = klass([Timestamp('2000-01-01 00:15:00', tz='US/Central'), + Timestamp('2000-02-01', tz='US/Central')], name='a') + + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = s + SemiMonthEnd() + result2 = SemiMonthEnd() + s + + exp = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + +class TestSemiMonthBegin(Base): + _offset = SemiMonthBegin + offset1 = _offset() + offset2 = _offset(2) + + def test_offset_whole_year(self): + dates = (datetime(2007, 12, 15), + datetime(2008, 1, 1), + datetime(2008, 1, 15), + datetime(2008, 2, 1), + datetime(2008, 2, 15), + datetime(2008, 3, 1), + datetime(2008, 3, 15), + datetime(2008, 4, 1), + datetime(2008, 4, 15), + datetime(2008, 5, 1), + datetime(2008, 5, 15), + datetime(2008, 6, 1), + datetime(2008, 6, 15), + datetime(2008, 7, 1), + datetime(2008, 7, 15), + datetime(2008, 8, 1), + datetime(2008, 8, 15), + datetime(2008, 9, 1), + datetime(2008, 9, 15), + datetime(2008, 10, 1), + datetime(2008, 10, 15), + datetime(2008, 11, 1), + datetime(2008, 11, 15), + datetime(2008, 12, 1), + datetime(2008, 12, 15)) + + for base, exp_date in zip(dates[:-1], dates[1:]): + assert_offset_equal(SemiMonthBegin(), base, exp_date) + + # ensure .apply_index works as expected + s = DatetimeIndex(dates[:-1]) + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = SemiMonthBegin().apply_index(s) + + exp = DatetimeIndex(dates[1:]) + tm.assert_index_equal(result, exp) + + # ensure generating a range with DatetimeIndex gives same result + result = date_range(start=dates[0], end=dates[-1], freq='SMS') + exp = DatetimeIndex(dates) + tm.assert_index_equal(result, exp) + + offset_cases = [] + offset_cases.append((SemiMonthBegin(), { + datetime(2008, 1, 1): datetime(2008, 1, 15), + datetime(2008, 1, 15): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 14): datetime(2006, 12, 15), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2007, 1, 1): datetime(2007, 1, 15), + datetime(2006, 12, 1): datetime(2006, 12, 15), + datetime(2006, 12, 15): datetime(2007, 1, 1)})) + + offset_cases.append((SemiMonthBegin(day_of_month=20), { + datetime(2008, 1, 1): datetime(2008, 1, 20), + datetime(2008, 1, 15): datetime(2008, 1, 20), + datetime(2008, 1, 21): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 14): datetime(2006, 12, 20), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2007, 1, 1): datetime(2007, 1, 20), + datetime(2006, 12, 1): datetime(2006, 12, 20), + datetime(2006, 12, 15): datetime(2006, 12, 20)})) + + offset_cases.append((SemiMonthBegin(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 1, 16): datetime(2008, 2, 1), + datetime(2008, 1, 15): datetime(2008, 1, 15), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 2): datetime(2006, 12, 15), + datetime(2007, 1, 1): datetime(2007, 1, 1)})) + + offset_cases.append((SemiMonthBegin(0, day_of_month=16), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 1, 16): datetime(2008, 1, 16), + datetime(2008, 1, 15): datetime(2008, 1, 16), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2007, 1, 5): datetime(2007, 1, 16), + datetime(2007, 1, 1): datetime(2007, 1, 1)})) + + offset_cases.append((SemiMonthBegin(2), { + datetime(2008, 1, 1): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 15), + datetime(2006, 12, 1): datetime(2007, 1, 1), + datetime(2006, 12, 29): datetime(2007, 1, 15), + datetime(2006, 12, 15): datetime(2007, 1, 15), + datetime(2007, 1, 1): datetime(2007, 2, 1), + datetime(2007, 1, 16): datetime(2007, 2, 15), + datetime(2006, 11, 1): datetime(2006, 12, 1)})) + + offset_cases.append((SemiMonthBegin(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 15), + datetime(2008, 6, 30): datetime(2008, 6, 15), + datetime(2008, 6, 14): datetime(2008, 6, 1), + datetime(2008, 12, 31): datetime(2008, 12, 15), + datetime(2006, 12, 29): datetime(2006, 12, 15), + datetime(2006, 12, 15): datetime(2006, 12, 1), + datetime(2007, 1, 1): datetime(2006, 12, 15)})) + + offset_cases.append((SemiMonthBegin(-1, day_of_month=4), { + datetime(2007, 1, 1): datetime(2006, 12, 4), + datetime(2007, 1, 4): datetime(2007, 1, 1), + datetime(2008, 6, 30): datetime(2008, 6, 4), + datetime(2008, 12, 31): datetime(2008, 12, 4), + datetime(2006, 12, 5): datetime(2006, 12, 4), + datetime(2006, 12, 30): datetime(2006, 12, 4), + datetime(2006, 12, 2): datetime(2006, 12, 1), + datetime(2007, 1, 1): datetime(2006, 12, 4)})) + + offset_cases.append((SemiMonthBegin(-2), { + datetime(2007, 1, 1): datetime(2006, 12, 1), + datetime(2008, 6, 30): datetime(2008, 6, 1), + datetime(2008, 6, 14): datetime(2008, 5, 15), + datetime(2008, 12, 31): datetime(2008, 12, 1), + datetime(2006, 12, 29): datetime(2006, 12, 1), + datetime(2006, 12, 15): datetime(2006, 11, 15), + datetime(2007, 1, 1): datetime(2006, 12, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + @pytest.mark.parametrize('case', offset_cases) + def test_apply_index(self, case): + offset, cases = case + s = DatetimeIndex(cases.keys()) + + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = offset.apply_index(s) + + exp = DatetimeIndex(cases.values()) + tm.assert_index_equal(result, exp) + + on_offset_cases = [(datetime(2007, 12, 1), True), + (datetime(2007, 12, 15), True), + (datetime(2007, 12, 14), False), + (datetime(2007, 12, 31), False), + (datetime(2008, 2, 15), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + dt, expected = case + assert_onOffset(SemiMonthBegin(), dt, expected) + + @pytest.mark.parametrize('klass', [Series, DatetimeIndex]) + def test_vectorized_offset_addition(self, klass): + s = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = s + SemiMonthBegin() + result2 = SemiMonthBegin() + s + + exp = klass([Timestamp('2000-02-01 00:15:00', tz='US/Central'), + Timestamp('2000-03-01', tz='US/Central')], name='a') + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + s = klass([Timestamp('2000-01-01 00:15:00', tz='US/Central'), + Timestamp('2000-02-01', tz='US/Central')], name='a') + with tm.assert_produces_warning(None): + # GH#22535 check that we don't get a FutureWarning from adding + # an integer array to PeriodIndex + result = s + SemiMonthBegin() + result2 = SemiMonthBegin() + s + + exp = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), + Timestamp('2000-02-15', tz='US/Central')], name='a') + tm.assert_equal(result, exp) + tm.assert_equal(result2, exp) + + +def test_Easter(): + assert_offset_equal(Easter(), datetime(2010, 1, 1), datetime(2010, 4, 4)) + assert_offset_equal(Easter(), datetime(2010, 4, 5), datetime(2011, 4, 24)) + assert_offset_equal(Easter(2), datetime(2010, 1, 1), datetime(2011, 4, 24)) + + assert_offset_equal(Easter(), datetime(2010, 4, 4), datetime(2011, 4, 24)) + assert_offset_equal(Easter(2), datetime(2010, 4, 4), datetime(2012, 4, 8)) + + assert_offset_equal(-Easter(), datetime(2011, 1, 1), datetime(2010, 4, 4)) + assert_offset_equal(-Easter(), datetime(2010, 4, 5), datetime(2010, 4, 4)) + assert_offset_equal(-Easter(2), + datetime(2011, 1, 1), + datetime(2009, 4, 12)) + + assert_offset_equal(-Easter(), datetime(2010, 4, 4), datetime(2009, 4, 12)) + assert_offset_equal(-Easter(2), + datetime(2010, 4, 4), + datetime(2008, 3, 23)) + + +class TestOffsetNames(object): + + def test_get_offset_name(self): + assert BDay().freqstr == 'B' + assert BDay(2).freqstr == '2B' + assert BMonthEnd().freqstr == 'BM' + assert Week(weekday=0).freqstr == 'W-MON' + assert Week(weekday=1).freqstr == 'W-TUE' + assert Week(weekday=2).freqstr == 'W-WED' + assert Week(weekday=3).freqstr == 'W-THU' + assert Week(weekday=4).freqstr == 'W-FRI' + + assert LastWeekOfMonth(weekday=WeekDay.SUN).freqstr == "LWOM-SUN" + + +def test_get_offset(): + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + get_offset('gibberish') + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + get_offset('QS-JAN-B') + + pairs = [ + ('B', BDay()), ('b', BDay()), ('bm', BMonthEnd()), + ('Bm', BMonthEnd()), ('W-MON', Week(weekday=0)), + ('W-TUE', Week(weekday=1)), ('W-WED', Week(weekday=2)), + ('W-THU', Week(weekday=3)), ('W-FRI', Week(weekday=4))] + + for name, expected in pairs: + offset = get_offset(name) + assert offset == expected, ("Expected %r to yield %r (actual: %r)" % + (name, expected, offset)) + + +def test_get_offset_legacy(): + pairs = [('w@Sat', Week(weekday=5))] + for name, expected in pairs: + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + get_offset(name) + + +class TestOffsetAliases(object): + + def setup_method(self, method): + _offset_map.clear() + + def test_alias_equality(self): + for k, v in compat.iteritems(_offset_map): + if v is None: + continue + assert k == v.copy() + + def test_rule_code(self): + lst = ['M', 'MS', 'BM', 'BMS', 'D', 'B', 'H', 'T', 'S', 'L', 'U'] + for k in lst: + assert k == get_offset(k).rule_code + # should be cached - this is kind of an internals test... + assert k in _offset_map + assert k == (get_offset(k) * 3).rule_code + + suffix_lst = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + base = 'W' + for v in suffix_lst: + alias = '-'.join([base, v]) + assert alias == get_offset(alias).rule_code + assert alias == (get_offset(alias) * 5).rule_code + + suffix_lst = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', + 'SEP', 'OCT', 'NOV', 'DEC'] + base_lst = ['A', 'AS', 'BA', 'BAS', 'Q', 'QS', 'BQ', 'BQS'] + for base in base_lst: + for v in suffix_lst: + alias = '-'.join([base, v]) + assert alias == get_offset(alias).rule_code + assert alias == (get_offset(alias) * 5).rule_code + + lst = ['M', 'D', 'B', 'H', 'T', 'S', 'L', 'U'] + for k in lst: + code, stride = get_freq_code('3' + k) + assert isinstance(code, int) + assert stride == 3 + assert k == get_freq_str(code) + + +def test_dateoffset_misc(): + oset = offsets.DateOffset(months=2, days=4) + # it works + oset.freqstr + + assert (not offsets.DateOffset(months=2) == 2) + + +def test_freq_offsets(): + off = BDay(1, offset=timedelta(0, 1800)) + assert (off.freqstr == 'B+30Min') + + off = BDay(1, offset=timedelta(0, -1800)) + assert (off.freqstr == 'B-30Min') + + +class TestReprNames(object): + + def test_str_for_named_is_name(self): + # look at all the amazing combinations! + month_prefixes = ['A', 'AS', 'BA', 'BAS', 'Q', 'BQ', 'BQS', 'QS'] + names = [prefix + '-' + month + for prefix in month_prefixes + for month in ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', + 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']] + days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] + names += ['W-' + day for day in days] + names += ['WOM-' + week + day + for week in ('1', '2', '3', '4') for day in days] + _offset_map.clear() + for name in names: + offset = get_offset(name) + assert offset.freqstr == name + + +def get_utc_offset_hours(ts): + # take a Timestamp and compute total hours of utc offset + o = ts.utcoffset() + return (o.days * 24 * 3600 + o.seconds) / 3600.0 + + +class TestDST(object): + """ + test DateOffset additions over Daylight Savings Time + """ + # one microsecond before the DST transition + ts_pre_fallback = "2013-11-03 01:59:59.999999" + ts_pre_springfwd = "2013-03-10 01:59:59.999999" + + # test both basic names and dateutil timezones + timezone_utc_offsets = { + 'US/Eastern': dict(utc_offset_daylight=-4, + utc_offset_standard=-5, ), + 'dateutil/US/Pacific': dict(utc_offset_daylight=-7, + utc_offset_standard=-8, ) + } + valid_date_offsets_singular = [ + 'weekday', 'day', 'hour', 'minute', 'second', 'microsecond' + ] + valid_date_offsets_plural = [ + 'weeks', 'days', + 'hours', 'minutes', 'seconds', + 'milliseconds', 'microseconds' + ] + + def _test_all_offsets(self, n, **kwds): + valid_offsets = self.valid_date_offsets_plural if n > 1 \ + else self.valid_date_offsets_singular + + for name in valid_offsets: + self._test_offset(offset_name=name, offset_n=n, **kwds) + + def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): + offset = DateOffset(**{offset_name: offset_n}) + + t = tstart + offset + if expected_utc_offset is not None: + assert get_utc_offset_hours(t) == expected_utc_offset + + if offset_name == 'weeks': + # dates should match + assert t.date() == timedelta(days=7 * offset.kwds[ + 'weeks']) + tstart.date() + # expect the same day of week, hour of day, minute, second, ... + assert (t.dayofweek == tstart.dayofweek and + t.hour == tstart.hour and + t.minute == tstart.minute and + t.second == tstart.second) + elif offset_name == 'days': + # dates should match + assert timedelta(offset.kwds['days']) + tstart.date() == t.date() + # expect the same hour of day, minute, second, ... + assert (t.hour == tstart.hour and + t.minute == tstart.minute and + t.second == tstart.second) + elif offset_name in self.valid_date_offsets_singular: + # expect the singular offset value to match between tstart and t + datepart_offset = getattr(t, offset_name + if offset_name != 'weekday' else + 'dayofweek') + assert datepart_offset == offset.kwds[offset_name] + else: + # the offset should be the same as if it was done in UTC + assert (t == (tstart.tz_convert('UTC') + offset) + .tz_convert('US/Pacific')) + + def _make_timestamp(self, string, hrs_offset, tz): + if hrs_offset >= 0: + offset_string = '{hrs:02d}00'.format(hrs=hrs_offset) + else: + offset_string = '-{hrs:02d}00'.format(hrs=-1 * hrs_offset) + return Timestamp(string + offset_string).tz_convert(tz) + + def test_fallback_plural(self): + # test moving from daylight savings to standard time + import dateutil + for tz, utc_offsets in self.timezone_utc_offsets.items(): + hrs_pre = utc_offsets['utc_offset_daylight'] + hrs_post = utc_offsets['utc_offset_standard'] + + if LooseVersion(dateutil.__version__) < LooseVersion('2.6.0'): + # buggy ambiguous behavior in 2.6.0 + # GH 14621 + # https://github.com/dateutil/dateutil/issues/321 + self._test_all_offsets( + n=3, tstart=self._make_timestamp(self.ts_pre_fallback, + hrs_pre, tz), + expected_utc_offset=hrs_post) + elif LooseVersion(dateutil.__version__) > LooseVersion('2.6.0'): + # fixed, but skip the test + continue + + def test_springforward_plural(self): + # test moving from standard to daylight savings + for tz, utc_offsets in self.timezone_utc_offsets.items(): + hrs_pre = utc_offsets['utc_offset_standard'] + hrs_post = utc_offsets['utc_offset_daylight'] + self._test_all_offsets( + n=3, tstart=self._make_timestamp(self.ts_pre_springfwd, + hrs_pre, tz), + expected_utc_offset=hrs_post) + + def test_fallback_singular(self): + # in the case of singular offsets, we don't necessarily know which utc + # offset the new Timestamp will wind up in (the tz for 1 month may be + # different from 1 second) so we don't specify an expected_utc_offset + for tz, utc_offsets in self.timezone_utc_offsets.items(): + hrs_pre = utc_offsets['utc_offset_standard'] + self._test_all_offsets(n=1, tstart=self._make_timestamp( + self.ts_pre_fallback, hrs_pre, tz), expected_utc_offset=None) + + def test_springforward_singular(self): + for tz, utc_offsets in self.timezone_utc_offsets.items(): + hrs_pre = utc_offsets['utc_offset_standard'] + self._test_all_offsets(n=1, tstart=self._make_timestamp( + self.ts_pre_springfwd, hrs_pre, tz), expected_utc_offset=None) + + offset_classes = {MonthBegin: ['11/2/2012', '12/1/2012'], + MonthEnd: ['11/2/2012', '11/30/2012'], + BMonthBegin: ['11/2/2012', '12/3/2012'], + BMonthEnd: ['11/2/2012', '11/30/2012'], + CBMonthBegin: ['11/2/2012', '12/3/2012'], + CBMonthEnd: ['11/2/2012', '11/30/2012'], + SemiMonthBegin: ['11/2/2012', '11/15/2012'], + SemiMonthEnd: ['11/2/2012', '11/15/2012'], + Week: ['11/2/2012', '11/9/2012'], + YearBegin: ['11/2/2012', '1/1/2013'], + YearEnd: ['11/2/2012', '12/31/2012'], + BYearBegin: ['11/2/2012', '1/1/2013'], + BYearEnd: ['11/2/2012', '12/31/2012'], + QuarterBegin: ['11/2/2012', '12/1/2012'], + QuarterEnd: ['11/2/2012', '12/31/2012'], + BQuarterBegin: ['11/2/2012', '12/3/2012'], + BQuarterEnd: ['11/2/2012', '12/31/2012'], + Day: ['11/4/2012', '11/4/2012 23:00']}.items() + + @pytest.mark.parametrize('tup', offset_classes) + def test_all_offset_classes(self, tup): + offset, test_values = tup + + first = Timestamp(test_values[0], tz='US/Eastern') + offset() + second = Timestamp(test_values[1], tz='US/Eastern') + assert first == second + + +# --------------------------------------------------------------------- +def test_get_offset_day_error(): + # subclass of _BaseOffset must override _day_opt attribute, or we should + # get a NotImplementedError + + with pytest.raises(NotImplementedError): + DateOffset()._get_offset_day(datetime.now()) + + +def test_valid_default_arguments(offset_types): + # GH#19142 check that the calling the constructors without passing + # any keyword arguments produce valid offsets + cls = offset_types + cls() + + +@pytest.mark.parametrize('kwd', sorted(list(liboffsets.relativedelta_kwds))) +def test_valid_month_attributes(kwd, month_classes): + # GH#18226 + cls = month_classes + # check that we cannot create e.g. MonthEnd(weeks=3) + with pytest.raises(TypeError): + cls(**{kwd: 3}) + + +@pytest.mark.parametrize('kwd', sorted(list(liboffsets.relativedelta_kwds))) +def test_valid_relativedelta_kwargs(kwd): + # Check that all the arguments specified in liboffsets.relativedelta_kwds + # are in fact valid relativedelta keyword args + DateOffset(**{kwd: 1}) + + +@pytest.mark.parametrize('kwd', sorted(list(liboffsets.relativedelta_kwds))) +def test_valid_tick_attributes(kwd, tick_classes): + # GH#18226 + cls = tick_classes + # check that we cannot create e.g. Hour(weeks=3) + with pytest.raises(TypeError): + cls(**{kwd: 3}) + + +def test_validate_n_error(): + with pytest.raises(TypeError): + DateOffset(n='Doh!') + + with pytest.raises(TypeError): + MonthBegin(n=timedelta(1)) + + with pytest.raises(TypeError): + BDay(n=np.array([1, 2], dtype=np.int64)) + + +def test_require_integers(offset_types): + cls = offset_types + with pytest.raises(ValueError): + cls(n=1.5) + + +def test_tick_normalize_raises(tick_classes): + # check that trying to create a Tick object with normalize=True raises + # GH#21427 + cls = tick_classes + with pytest.raises(ValueError): + cls(n=3, normalize=True) + + +def test_weeks_onoffset(): + # GH#18510 Week with weekday = None, normalize = False should always + # be onOffset + offset = Week(n=2, weekday=None) + ts = Timestamp('1862-01-13 09:03:34.873477378+0210', tz='Africa/Lusaka') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + # negative n + offset = Week(n=2, weekday=None) + ts = Timestamp('1856-10-24 16:18:36.556360110-0717', tz='Pacific/Easter') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + +def test_weekofmonth_onoffset(): + # GH#18864 + # Make sure that nanoseconds don't trip up onOffset (and with it apply) + offset = WeekOfMonth(n=2, week=2, weekday=0) + ts = Timestamp('1916-05-15 01:14:49.583410462+0422', tz='Asia/Qyzylorda') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + # negative n + offset = WeekOfMonth(n=-3, week=1, weekday=0) + ts = Timestamp('1980-12-08 03:38:52.878321185+0500', tz='Asia/Oral') + fast = offset.onOffset(ts) + slow = (ts + offset) - offset == ts + assert fast == slow + + +def test_last_week_of_month_on_offset(): + # GH#19036, GH#18977 _adjust_dst was incorrect for LastWeekOfMonth + offset = LastWeekOfMonth(n=4, weekday=6) + ts = Timestamp('1917-05-27 20:55:27.084284178+0200', + tz='Europe/Warsaw') + slow = (ts + offset) - offset == ts + fast = offset.onOffset(ts) + assert fast == slow + + # negative n + offset = LastWeekOfMonth(n=-4, weekday=5) + ts = Timestamp('2005-08-27 05:01:42.799392561-0500', + tz='America/Rainy_River') + slow = (ts + offset) - offset == ts + fast = offset.onOffset(ts) + assert fast == slow diff --git a/pandas/tests/tseries/offsets/test_offsets_properties.py b/pandas/tests/tseries/offsets/test_offsets_properties.py new file mode 100644 index 0000000000000..cd5f2a2a25e58 --- /dev/null +++ b/pandas/tests/tseries/offsets/test_offsets_properties.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +""" +Behavioral based tests for offsets and date_range. + +This file is adapted from https://github.com/pandas-dev/pandas/pull/18761 - +which was more ambitious but less idiomatic in its use of Hypothesis. + +You may wish to consult the previous version for inspiration on further +tests, or when trying to pin down the bugs exposed by the tests below. +""" +import warnings + +from hypothesis import assume, given, strategies as st +from hypothesis.extra.dateutil import timezones as dateutil_timezones +from hypothesis.extra.pytz import timezones as pytz_timezones +import pytest + +import pandas as pd + +from pandas.tseries.offsets import ( + BMonthBegin, BMonthEnd, BQuarterBegin, BQuarterEnd, BYearBegin, BYearEnd, + MonthBegin, MonthEnd, QuarterBegin, QuarterEnd, YearBegin, YearEnd) + +# ---------------------------------------------------------------- +# Helpers for generating random data + +with warnings.catch_warnings(): + warnings.simplefilter('ignore') + min_dt = pd.Timestamp(1900, 1, 1).to_pydatetime(), + max_dt = pd.Timestamp(1900, 1, 1).to_pydatetime(), + +gen_date_range = st.builds( + pd.date_range, + start=st.datetimes( + # TODO: Choose the min/max values more systematically + min_value=pd.Timestamp(1900, 1, 1).to_pydatetime(), + max_value=pd.Timestamp(2100, 1, 1).to_pydatetime() + ), + periods=st.integers(min_value=2, max_value=100), + freq=st.sampled_from('Y Q M D H T s ms us ns'.split()), + tz=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()), +) + +gen_random_datetime = st.datetimes( + min_value=min_dt, + max_value=max_dt, + timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()) +) + +# The strategy for each type is registered in conftest.py, as they don't carry +# enough runtime information (e.g. type hints) to infer how to build them. +gen_yqm_offset = st.one_of(*map(st.from_type, [ + MonthBegin, MonthEnd, BMonthBegin, BMonthEnd, + QuarterBegin, QuarterEnd, BQuarterBegin, BQuarterEnd, + YearBegin, YearEnd, BYearBegin, BYearEnd +])) + + +# ---------------------------------------------------------------- +# Offset-specific behaviour tests + + +# Based on CI runs: Always passes on OSX, fails on Linux, sometimes on Windows +@pytest.mark.xfail(strict=False, reason='inconsistent between OSs, Pythons') +@given(gen_random_datetime, gen_yqm_offset) +def test_on_offset_implementations(dt, offset): + assume(not offset.normalize) + # check that the class-specific implementations of onOffset match + # the general case definition: + # (dt + offset) - offset == dt + compare = (dt + offset) - offset + assert offset.onOffset(dt) == (compare == dt) + + +@pytest.mark.xfail +@given(gen_yqm_offset, gen_date_range) +def test_apply_index_implementations(offset, rng): + # offset.apply_index(dti)[i] should match dti[i] + offset + assume(offset.n != 0) # TODO: test for that case separately + + # rng = pd.date_range(start='1/1/2000', periods=100000, freq='T') + ser = pd.Series(rng) + + res = rng + offset + res_v2 = offset.apply_index(rng) + assert (res == res_v2).all() + + assert res[0] == rng[0] + offset + assert res[-1] == rng[-1] + offset + res2 = ser + offset + # apply_index is only for indexes, not series, so no res2_v2 + assert res2.iloc[0] == ser.iloc[0] + offset + assert res2.iloc[-1] == ser.iloc[-1] + offset + # TODO: Check randomly assorted entries, not just first/last + + +@pytest.mark.xfail +@given(gen_yqm_offset) +def test_shift_across_dst(offset): + # GH#18319 check that 1) timezone is correctly normalized and + # 2) that hour is not incorrectly changed by this normalization + # Note that dti includes a transition across DST boundary + dti = pd.date_range(start='2017-10-30 12:00:00', end='2017-11-06', + freq='D', tz='US/Eastern') + assert (dti.hour == 12).all() # we haven't screwed up yet + + res = dti + offset + assert (res.hour == 12).all() diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py new file mode 100644 index 0000000000000..9a8251201f75f --- /dev/null +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +""" +Tests for offsets.Tick and subclasses +""" +from __future__ import division + +from datetime import datetime, timedelta + +from hypothesis import assume, example, given, settings, strategies as st +import numpy as np +import pytest + +from pandas import Timedelta, Timestamp +import pandas.util.testing as tm + +from pandas.tseries import offsets +from pandas.tseries.offsets import Hour, Micro, Milli, Minute, Nano, Second + +from .common import assert_offset_equal + +# --------------------------------------------------------------------- +# Test Helpers + +tick_classes = [Hour, Minute, Second, Milli, Micro, Nano] + + +# --------------------------------------------------------------------- + + +def test_apply_ticks(): + result = offsets.Hour(3).apply(offsets.Hour(4)) + exp = offsets.Hour(7) + assert (result == exp) + + +def test_delta_to_tick(): + delta = timedelta(3) + + tick = offsets._delta_to_tick(delta) + assert (tick == offsets.Day(3)) + + td = Timedelta(nanoseconds=5) + tick = offsets._delta_to_tick(td) + assert tick == Nano(5) + + +@pytest.mark.parametrize('cls', tick_classes) +@settings(deadline=None) # GH 24641 +@example(n=2, m=3) +@example(n=800, m=300) +@example(n=1000, m=5) +@given(n=st.integers(-999, 999), m=st.integers(-999, 999)) +def test_tick_add_sub(cls, n, m): + # For all Tick subclasses and all integers n, m, we should have + # tick(n) + tick(m) == tick(n+m) + # tick(n) - tick(m) == tick(n-m) + left = cls(n) + right = cls(m) + expected = cls(n + m) + + assert left + right == expected + assert left.apply(right) == expected + + expected = cls(n - m) + assert left - right == expected + + +@pytest.mark.parametrize('cls', tick_classes) +@settings(deadline=None) +@example(n=2, m=3) +@given(n=st.integers(-999, 999), m=st.integers(-999, 999)) +def test_tick_equality(cls, n, m): + assume(m != n) + # tick == tock iff tick.n == tock.n + left = cls(n) + right = cls(m) + assert left != right + assert not (left == right) + + right = cls(n) + assert left == right + assert not (left != right) + + if n != 0: + assert cls(n) != cls(-n) + + +# --------------------------------------------------------------------- + + +def test_Hour(): + assert_offset_equal(Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) + assert_offset_equal(Hour(-1), + datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) + assert_offset_equal(2 * Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) + assert_offset_equal(-1 * Hour(), + datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) + + assert Hour(3) + Hour(2) == Hour(5) + assert Hour(3) - Hour(2) == Hour() + + assert Hour(4) != Hour(1) + + +def test_Minute(): + assert_offset_equal(Minute(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 0, 1)) + assert_offset_equal(Minute(-1), + datetime(2010, 1, 1, 0, 1), datetime(2010, 1, 1)) + assert_offset_equal(2 * Minute(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 0, 2)) + assert_offset_equal(-1 * Minute(), + datetime(2010, 1, 1, 0, 1), datetime(2010, 1, 1)) + + assert Minute(3) + Minute(2) == Minute(5) + assert Minute(3) - Minute(2) == Minute() + assert Minute(5) != Minute() + + +def test_Second(): + assert_offset_equal(Second(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 1)) + assert_offset_equal(Second(-1), + datetime(2010, 1, 1, 0, 0, 1), + datetime(2010, 1, 1)) + assert_offset_equal(2 * Second(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 2)) + assert_offset_equal(-1 * Second(), + datetime(2010, 1, 1, 0, 0, 1), + datetime(2010, 1, 1)) + + assert Second(3) + Second(2) == Second(5) + assert Second(3) - Second(2) == Second() + + +def test_Millisecond(): + assert_offset_equal(Milli(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 0, 1000)) + assert_offset_equal(Milli(-1), + datetime(2010, 1, 1, 0, 0, 0, 1000), + datetime(2010, 1, 1)) + assert_offset_equal(Milli(2), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 0, 2000)) + assert_offset_equal(2 * Milli(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 0, 2000)) + assert_offset_equal(-1 * Milli(), + datetime(2010, 1, 1, 0, 0, 0, 1000), + datetime(2010, 1, 1)) + + assert Milli(3) + Milli(2) == Milli(5) + assert Milli(3) - Milli(2) == Milli() + + +def test_MillisecondTimestampArithmetic(): + assert_offset_equal(Milli(), + Timestamp('2010-01-01'), + Timestamp('2010-01-01 00:00:00.001')) + assert_offset_equal(Milli(-1), + Timestamp('2010-01-01 00:00:00.001'), + Timestamp('2010-01-01')) + + +def test_Microsecond(): + assert_offset_equal(Micro(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 0, 1)) + assert_offset_equal(Micro(-1), + datetime(2010, 1, 1, 0, 0, 0, 1), + datetime(2010, 1, 1)) + + assert_offset_equal(2 * Micro(), + datetime(2010, 1, 1), + datetime(2010, 1, 1, 0, 0, 0, 2)) + assert_offset_equal(-1 * Micro(), + datetime(2010, 1, 1, 0, 0, 0, 1), + datetime(2010, 1, 1)) + + assert Micro(3) + Micro(2) == Micro(5) + assert Micro(3) - Micro(2) == Micro() + + +def test_NanosecondGeneric(): + timestamp = Timestamp(datetime(2010, 1, 1)) + assert timestamp.nanosecond == 0 + + result = timestamp + Nano(10) + assert result.nanosecond == 10 + + reverse_result = Nano(10) + timestamp + assert reverse_result.nanosecond == 10 + + +def test_Nanosecond(): + timestamp = Timestamp(datetime(2010, 1, 1)) + assert_offset_equal(Nano(), + timestamp, + timestamp + np.timedelta64(1, 'ns')) + assert_offset_equal(Nano(-1), + timestamp + np.timedelta64(1, 'ns'), + timestamp) + assert_offset_equal(2 * Nano(), + timestamp, + timestamp + np.timedelta64(2, 'ns')) + assert_offset_equal(-1 * Nano(), + timestamp + np.timedelta64(1, 'ns'), + timestamp) + + assert Nano(3) + Nano(2) == Nano(5) + assert Nano(3) - Nano(2) == Nano() + + # GH9284 + assert Nano(1) + Nano(10) == Nano(11) + assert Nano(5) + Micro(1) == Nano(1005) + assert Micro(5) + Nano(1) == Nano(5001) + + +@pytest.mark.parametrize('kls, expected', + [(Hour, Timedelta(hours=5)), + (Minute, Timedelta(hours=2, minutes=3)), + (Second, Timedelta(hours=2, seconds=3)), + (Milli, Timedelta(hours=2, milliseconds=3)), + (Micro, Timedelta(hours=2, microseconds=3)), + (Nano, Timedelta(hours=2, nanoseconds=3))]) +def test_tick_addition(kls, expected): + offset = kls(3) + result = offset + Timedelta(hours=2) + assert isinstance(result, Timedelta) + assert result == expected + + +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_division(cls): + off = cls(10) + + assert off / cls(5) == 2 + assert off / 2 == cls(5) + assert off / 2.0 == cls(5) + + assert off / off.delta == 1 + assert off / off.delta.to_timedelta64() == 1 + + assert off / Nano(1) == off.delta / Nano(1).delta + + if cls is not Nano: + # A case where we end up with a smaller class + result = off / 1000 + assert isinstance(result, offsets.Tick) + assert not isinstance(result, cls) + assert result.delta == off.delta / 1000 + + if cls._inc < Timedelta(seconds=1): + # Case where we end up with a bigger class + result = off / .001 + assert isinstance(result, offsets.Tick) + assert not isinstance(result, cls) + assert result.delta == off.delta / .001 + + +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_rdiv(cls): + off = cls(10) + delta = off.delta + td64 = delta.to_timedelta64() + + with pytest.raises(TypeError): + 2 / off + with pytest.raises(TypeError): + 2.0 / off + + assert (td64 * 2.5) / off == 2.5 + + if cls is not Nano: + # skip pytimedelta for Nano since it gets dropped + assert (delta.to_pytimedelta() * 2) / off == 2 + + result = np.array([2 * td64, td64]) / off + expected = np.array([2., 1.]) + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize('cls1', tick_classes) +@pytest.mark.parametrize('cls2', tick_classes) +def test_tick_zero(cls1, cls2): + assert cls1(0) == cls2(0) + assert cls1(0) + cls2(0) == cls1(0) + + if cls1 is not Nano: + assert cls1(2) + cls2(0) == cls1(2) + + if cls1 is Nano: + assert cls1(2) + Nano(0) == cls1(2) + + +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_equalities(cls): + assert cls() == cls(1) + + +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_offset(cls): + assert not cls().isAnchored() + + +@pytest.mark.parametrize('cls', tick_classes) +def test_compare_ticks(cls): + three = cls(3) + four = cls(4) + + assert three < cls(4) + assert cls(3) < four + assert four > cls(3) + assert cls(4) > three + assert cls(3) == cls(3) + assert cls(3) != cls(4) + + +@pytest.mark.parametrize('cls', tick_classes) +def test_compare_ticks_to_strs(cls): + # GH#23524 + off = cls(19) + + # These tests should work with any strings, but we particularly are + # interested in "infer" as that comparison is convenient to make in + # Datetime/Timedelta Array/Index constructors + assert not off == "infer" + assert not "foo" == off + + for left, right in [("infer", off), (off, "infer")]: + with pytest.raises(TypeError): + left < right + with pytest.raises(TypeError): + left <= right + with pytest.raises(TypeError): + left > right + with pytest.raises(TypeError): + left >= right diff --git a/pandas/tests/tseries/offsets/test_yqm_offsets.py b/pandas/tests/tseries/offsets/test_yqm_offsets.py new file mode 100644 index 0000000000000..9ee03d2e886f3 --- /dev/null +++ b/pandas/tests/tseries/offsets/test_yqm_offsets.py @@ -0,0 +1,1035 @@ +# -*- coding: utf-8 -*- +""" +Tests for Year, Quarter, and Month-based DateOffset subclasses +""" +from datetime import datetime + +import pytest + +import pandas as pd +from pandas import Timestamp, compat + +from pandas.tseries.offsets import ( + BMonthBegin, BMonthEnd, BQuarterBegin, BQuarterEnd, BYearBegin, BYearEnd, + MonthBegin, MonthEnd, QuarterBegin, QuarterEnd, YearBegin, YearEnd) + +from .common import assert_offset_equal, assert_onOffset +from .test_offsets import Base + +# -------------------------------------------------------------------- +# Misc + + +def test_quarterly_dont_normalize(): + date = datetime(2012, 3, 31, 5, 30) + + offsets = (QuarterBegin, QuarterEnd, BQuarterEnd, BQuarterBegin) + + for klass in offsets: + result = date + klass() + assert (result.time() == date.time()) + + +@pytest.mark.parametrize('n', [-2, 1]) +@pytest.mark.parametrize('cls', [MonthBegin, MonthEnd, + BMonthBegin, BMonthEnd, + QuarterBegin, QuarterEnd, + BQuarterBegin, BQuarterEnd, + YearBegin, YearEnd, + BYearBegin, BYearEnd]) +def test_apply_index(cls, n): + offset = cls(n=n) + rng = pd.date_range(start='1/1/2000', periods=100000, freq='T') + ser = pd.Series(rng) + + res = rng + offset + res_v2 = offset.apply_index(rng) + assert (res == res_v2).all() + assert res[0] == rng[0] + offset + assert res[-1] == rng[-1] + offset + res2 = ser + offset + # apply_index is only for indexes, not series, so no res2_v2 + assert res2.iloc[0] == ser.iloc[0] + offset + assert res2.iloc[-1] == ser.iloc[-1] + offset + + +@pytest.mark.parametrize('offset', [QuarterBegin(), QuarterEnd(), + BQuarterBegin(), BQuarterEnd()]) +def test_on_offset(offset): + dates = [datetime(2016, m, d) + for m in [10, 11, 12] + for d in [1, 2, 3, 28, 29, 30, 31] if not (m == 11 and d == 31)] + for date in dates: + res = offset.onOffset(date) + slow_version = date == (date + offset) - offset + assert res == slow_version + + +# -------------------------------------------------------------------- +# Months + +class TestMonthBegin(Base): + _offset = MonthBegin + + offset_cases = [] + # NOTE: I'm not entirely happy with the logic here for Begin -ss + # see thread 'offset conventions' on the ML + offset_cases.append((MonthBegin(), { + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2008, 2, 1): datetime(2008, 3, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2006, 12, 1): datetime(2007, 1, 1), + datetime(2007, 1, 31): datetime(2007, 2, 1)})) + + offset_cases.append((MonthBegin(0), { + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2006, 12, 3): datetime(2007, 1, 1), + datetime(2007, 1, 31): datetime(2007, 2, 1)})) + + offset_cases.append((MonthBegin(2), { + datetime(2008, 2, 29): datetime(2008, 4, 1), + datetime(2008, 1, 31): datetime(2008, 3, 1), + datetime(2006, 12, 31): datetime(2007, 2, 1), + datetime(2007, 12, 28): datetime(2008, 2, 1), + datetime(2007, 1, 1): datetime(2007, 3, 1), + datetime(2006, 11, 1): datetime(2007, 1, 1)})) + + offset_cases.append((MonthBegin(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 1), + datetime(2008, 5, 31): datetime(2008, 5, 1), + datetime(2008, 12, 31): datetime(2008, 12, 1), + datetime(2006, 12, 29): datetime(2006, 12, 1), + datetime(2006, 1, 2): datetime(2006, 1, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + +class TestMonthEnd(Base): + _offset = MonthEnd + + def test_day_of_month(self): + dt = datetime(2007, 1, 1) + offset = MonthEnd() + + result = dt + offset + assert result == Timestamp(2007, 1, 31) + + result = result + offset + assert result == Timestamp(2007, 2, 28) + + def test_normalize(self): + dt = datetime(2007, 1, 1, 3) + + result = dt + MonthEnd(normalize=True) + expected = dt.replace(hour=0) + MonthEnd() + assert result == expected + + offset_cases = [] + offset_cases.append((MonthEnd(), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 2, 29), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2007, 1, 31), + datetime(2007, 1, 1): datetime(2007, 1, 31), + datetime(2006, 12, 1): datetime(2006, 12, 31)})) + + offset_cases.append((MonthEnd(0), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2006, 12, 29): datetime(2006, 12, 31), + datetime(2006, 12, 31): datetime(2006, 12, 31), + datetime(2007, 1, 1): datetime(2007, 1, 31)})) + + offset_cases.append((MonthEnd(2), { + datetime(2008, 1, 1): datetime(2008, 2, 29), + datetime(2008, 1, 31): datetime(2008, 3, 31), + datetime(2006, 12, 29): datetime(2007, 1, 31), + datetime(2006, 12, 31): datetime(2007, 2, 28), + datetime(2007, 1, 1): datetime(2007, 2, 28), + datetime(2006, 11, 1): datetime(2006, 12, 31)})) + + offset_cases.append((MonthEnd(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 31), + datetime(2008, 6, 30): datetime(2008, 5, 31), + datetime(2008, 12, 31): datetime(2008, 11, 30), + datetime(2006, 12, 29): datetime(2006, 11, 30), + datetime(2006, 12, 30): datetime(2006, 11, 30), + datetime(2007, 1, 1): datetime(2006, 12, 31)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(MonthEnd(), datetime(2007, 12, 31), True), + (MonthEnd(), datetime(2008, 1, 1), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestBMonthBegin(Base): + _offset = BMonthBegin + + def test_offsets_compare_equal(self): + # root cause of #456 + offset1 = BMonthBegin() + offset2 = BMonthBegin() + assert not offset1 != offset2 + + offset_cases = [] + offset_cases.append((BMonthBegin(), { + datetime(2008, 1, 1): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2006, 9, 1): datetime(2006, 10, 2), + datetime(2007, 1, 1): datetime(2007, 2, 1), + datetime(2006, 12, 1): datetime(2007, 1, 1)})) + + offset_cases.append((BMonthBegin(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2006, 10, 2): datetime(2006, 10, 2), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2006, 12, 29): datetime(2007, 1, 1), + datetime(2006, 12, 31): datetime(2007, 1, 1), + datetime(2006, 9, 15): datetime(2006, 10, 2)})) + + offset_cases.append((BMonthBegin(2), { + datetime(2008, 1, 1): datetime(2008, 3, 3), + datetime(2008, 1, 15): datetime(2008, 3, 3), + datetime(2006, 12, 29): datetime(2007, 2, 1), + datetime(2006, 12, 31): datetime(2007, 2, 1), + datetime(2007, 1, 1): datetime(2007, 3, 1), + datetime(2006, 11, 1): datetime(2007, 1, 1)})) + + offset_cases.append((BMonthBegin(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 1), + datetime(2008, 6, 30): datetime(2008, 6, 2), + datetime(2008, 6, 1): datetime(2008, 5, 1), + datetime(2008, 3, 10): datetime(2008, 3, 3), + datetime(2008, 12, 31): datetime(2008, 12, 1), + datetime(2006, 12, 29): datetime(2006, 12, 1), + datetime(2006, 12, 30): datetime(2006, 12, 1), + datetime(2007, 1, 1): datetime(2006, 12, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(BMonthBegin(), datetime(2007, 12, 31), False), + (BMonthBegin(), datetime(2008, 1, 1), True), + (BMonthBegin(), datetime(2001, 4, 2), True), + (BMonthBegin(), datetime(2008, 3, 3), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestBMonthEnd(Base): + _offset = BMonthEnd + + def test_normalize(self): + dt = datetime(2007, 1, 1, 3) + + result = dt + BMonthEnd(normalize=True) + expected = dt.replace(hour=0) + BMonthEnd() + assert result == expected + + def test_offsets_compare_equal(self): + # root cause of #456 + offset1 = BMonthEnd() + offset2 = BMonthEnd() + assert not offset1 != offset2 + + offset_cases = [] + offset_cases.append((BMonthEnd(), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 2, 29), + datetime(2006, 12, 29): datetime(2007, 1, 31), + datetime(2006, 12, 31): datetime(2007, 1, 31), + datetime(2007, 1, 1): datetime(2007, 1, 31), + datetime(2006, 12, 1): datetime(2006, 12, 29)})) + + offset_cases.append((BMonthEnd(0), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2006, 12, 29): datetime(2006, 12, 29), + datetime(2006, 12, 31): datetime(2007, 1, 31), + datetime(2007, 1, 1): datetime(2007, 1, 31)})) + + offset_cases.append((BMonthEnd(2), { + datetime(2008, 1, 1): datetime(2008, 2, 29), + datetime(2008, 1, 31): datetime(2008, 3, 31), + datetime(2006, 12, 29): datetime(2007, 2, 28), + datetime(2006, 12, 31): datetime(2007, 2, 28), + datetime(2007, 1, 1): datetime(2007, 2, 28), + datetime(2006, 11, 1): datetime(2006, 12, 29)})) + + offset_cases.append((BMonthEnd(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 29), + datetime(2008, 6, 30): datetime(2008, 5, 30), + datetime(2008, 12, 31): datetime(2008, 11, 28), + datetime(2006, 12, 29): datetime(2006, 11, 30), + datetime(2006, 12, 30): datetime(2006, 12, 29), + datetime(2007, 1, 1): datetime(2006, 12, 29)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(BMonthEnd(), datetime(2007, 12, 31), True), + (BMonthEnd(), datetime(2008, 1, 1), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + +# -------------------------------------------------------------------- +# Quarters + + +class TestQuarterBegin(Base): + + def test_repr(self): + expected = "" + assert repr(QuarterBegin()) == expected + expected = "" + assert repr(QuarterBegin(startingMonth=3)) == expected + expected = "" + assert repr(QuarterBegin(startingMonth=1)) == expected + + def test_isAnchored(self): + assert QuarterBegin(startingMonth=1).isAnchored() + assert QuarterBegin().isAnchored() + assert not QuarterBegin(2, startingMonth=1).isAnchored() + + def test_offset_corner_case(self): + # corner + offset = QuarterBegin(n=-1, startingMonth=1) + assert datetime(2010, 2, 1) + offset == datetime(2010, 1, 1) + + offset_cases = [] + offset_cases.append((QuarterBegin(startingMonth=1), { + datetime(2007, 12, 1): datetime(2008, 1, 1), + datetime(2008, 1, 1): datetime(2008, 4, 1), + datetime(2008, 2, 15): datetime(2008, 4, 1), + datetime(2008, 2, 29): datetime(2008, 4, 1), + datetime(2008, 3, 15): datetime(2008, 4, 1), + datetime(2008, 3, 31): datetime(2008, 4, 1), + datetime(2008, 4, 15): datetime(2008, 7, 1), + datetime(2008, 4, 1): datetime(2008, 7, 1)})) + + offset_cases.append((QuarterBegin(startingMonth=2), { + datetime(2008, 1, 1): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2008, 1, 15): datetime(2008, 2, 1), + datetime(2008, 2, 29): datetime(2008, 5, 1), + datetime(2008, 3, 15): datetime(2008, 5, 1), + datetime(2008, 3, 31): datetime(2008, 5, 1), + datetime(2008, 4, 15): datetime(2008, 5, 1), + datetime(2008, 4, 30): datetime(2008, 5, 1)})) + + offset_cases.append((QuarterBegin(startingMonth=1, n=0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 12, 1): datetime(2009, 1, 1), + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 2, 15): datetime(2008, 4, 1), + datetime(2008, 2, 29): datetime(2008, 4, 1), + datetime(2008, 3, 15): datetime(2008, 4, 1), + datetime(2008, 3, 31): datetime(2008, 4, 1), + datetime(2008, 4, 15): datetime(2008, 7, 1), + datetime(2008, 4, 30): datetime(2008, 7, 1)})) + + offset_cases.append((QuarterBegin(startingMonth=1, n=-1), { + datetime(2008, 1, 1): datetime(2007, 10, 1), + datetime(2008, 1, 31): datetime(2008, 1, 1), + datetime(2008, 2, 15): datetime(2008, 1, 1), + datetime(2008, 2, 29): datetime(2008, 1, 1), + datetime(2008, 3, 15): datetime(2008, 1, 1), + datetime(2008, 3, 31): datetime(2008, 1, 1), + datetime(2008, 4, 15): datetime(2008, 4, 1), + datetime(2008, 4, 30): datetime(2008, 4, 1), + datetime(2008, 7, 1): datetime(2008, 4, 1)})) + + offset_cases.append((QuarterBegin(startingMonth=1, n=2), { + datetime(2008, 1, 1): datetime(2008, 7, 1), + datetime(2008, 2, 15): datetime(2008, 7, 1), + datetime(2008, 2, 29): datetime(2008, 7, 1), + datetime(2008, 3, 15): datetime(2008, 7, 1), + datetime(2008, 3, 31): datetime(2008, 7, 1), + datetime(2008, 4, 15): datetime(2008, 10, 1), + datetime(2008, 4, 1): datetime(2008, 10, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + +class TestQuarterEnd(Base): + _offset = QuarterEnd + + def test_repr(self): + expected = "" + assert repr(QuarterEnd()) == expected + expected = "" + assert repr(QuarterEnd(startingMonth=3)) == expected + expected = "" + assert repr(QuarterEnd(startingMonth=1)) == expected + + def test_isAnchored(self): + assert QuarterEnd(startingMonth=1).isAnchored() + assert QuarterEnd().isAnchored() + assert not QuarterEnd(2, startingMonth=1).isAnchored() + + def test_offset_corner_case(self): + # corner + offset = QuarterEnd(n=-1, startingMonth=1) + assert datetime(2010, 2, 1) + offset == datetime(2010, 1, 31) + + offset_cases = [] + offset_cases.append((QuarterEnd(startingMonth=1), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 4, 30), + datetime(2008, 2, 15): datetime(2008, 4, 30), + datetime(2008, 2, 29): datetime(2008, 4, 30), + datetime(2008, 3, 15): datetime(2008, 4, 30), + datetime(2008, 3, 31): datetime(2008, 4, 30), + datetime(2008, 4, 15): datetime(2008, 4, 30), + datetime(2008, 4, 30): datetime(2008, 7, 31)})) + + offset_cases.append((QuarterEnd(startingMonth=2), { + datetime(2008, 1, 1): datetime(2008, 2, 29), + datetime(2008, 1, 31): datetime(2008, 2, 29), + datetime(2008, 2, 15): datetime(2008, 2, 29), + datetime(2008, 2, 29): datetime(2008, 5, 31), + datetime(2008, 3, 15): datetime(2008, 5, 31), + datetime(2008, 3, 31): datetime(2008, 5, 31), + datetime(2008, 4, 15): datetime(2008, 5, 31), + datetime(2008, 4, 30): datetime(2008, 5, 31)})) + + offset_cases.append((QuarterEnd(startingMonth=1, n=0), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2008, 2, 15): datetime(2008, 4, 30), + datetime(2008, 2, 29): datetime(2008, 4, 30), + datetime(2008, 3, 15): datetime(2008, 4, 30), + datetime(2008, 3, 31): datetime(2008, 4, 30), + datetime(2008, 4, 15): datetime(2008, 4, 30), + datetime(2008, 4, 30): datetime(2008, 4, 30)})) + + offset_cases.append((QuarterEnd(startingMonth=1, n=-1), { + datetime(2008, 1, 1): datetime(2007, 10, 31), + datetime(2008, 1, 31): datetime(2007, 10, 31), + datetime(2008, 2, 15): datetime(2008, 1, 31), + datetime(2008, 2, 29): datetime(2008, 1, 31), + datetime(2008, 3, 15): datetime(2008, 1, 31), + datetime(2008, 3, 31): datetime(2008, 1, 31), + datetime(2008, 4, 15): datetime(2008, 1, 31), + datetime(2008, 4, 30): datetime(2008, 1, 31), + datetime(2008, 7, 1): datetime(2008, 4, 30)})) + + offset_cases.append((QuarterEnd(startingMonth=1, n=2), { + datetime(2008, 1, 31): datetime(2008, 7, 31), + datetime(2008, 2, 15): datetime(2008, 7, 31), + datetime(2008, 2, 29): datetime(2008, 7, 31), + datetime(2008, 3, 15): datetime(2008, 7, 31), + datetime(2008, 3, 31): datetime(2008, 7, 31), + datetime(2008, 4, 15): datetime(2008, 7, 31), + datetime(2008, 4, 30): datetime(2008, 10, 31)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [ + (QuarterEnd(1, startingMonth=1), datetime(2008, 1, 31), True), + (QuarterEnd(1, startingMonth=1), datetime(2007, 12, 31), False), + (QuarterEnd(1, startingMonth=1), datetime(2008, 2, 29), False), + (QuarterEnd(1, startingMonth=1), datetime(2007, 3, 30), False), + (QuarterEnd(1, startingMonth=1), datetime(2007, 3, 31), False), + (QuarterEnd(1, startingMonth=1), datetime(2008, 4, 30), True), + (QuarterEnd(1, startingMonth=1), datetime(2008, 5, 30), False), + (QuarterEnd(1, startingMonth=1), datetime(2008, 5, 31), False), + (QuarterEnd(1, startingMonth=1), datetime(2007, 6, 29), False), + (QuarterEnd(1, startingMonth=1), datetime(2007, 6, 30), False), + (QuarterEnd(1, startingMonth=2), datetime(2008, 1, 31), False), + (QuarterEnd(1, startingMonth=2), datetime(2007, 12, 31), False), + (QuarterEnd(1, startingMonth=2), datetime(2008, 2, 29), True), + (QuarterEnd(1, startingMonth=2), datetime(2007, 3, 30), False), + (QuarterEnd(1, startingMonth=2), datetime(2007, 3, 31), False), + (QuarterEnd(1, startingMonth=2), datetime(2008, 4, 30), False), + (QuarterEnd(1, startingMonth=2), datetime(2008, 5, 30), False), + (QuarterEnd(1, startingMonth=2), datetime(2008, 5, 31), True), + (QuarterEnd(1, startingMonth=2), datetime(2007, 6, 29), False), + (QuarterEnd(1, startingMonth=2), datetime(2007, 6, 30), False), + (QuarterEnd(1, startingMonth=3), datetime(2008, 1, 31), False), + (QuarterEnd(1, startingMonth=3), datetime(2007, 12, 31), True), + (QuarterEnd(1, startingMonth=3), datetime(2008, 2, 29), False), + (QuarterEnd(1, startingMonth=3), datetime(2007, 3, 30), False), + (QuarterEnd(1, startingMonth=3), datetime(2007, 3, 31), True), + (QuarterEnd(1, startingMonth=3), datetime(2008, 4, 30), False), + (QuarterEnd(1, startingMonth=3), datetime(2008, 5, 30), False), + (QuarterEnd(1, startingMonth=3), datetime(2008, 5, 31), False), + (QuarterEnd(1, startingMonth=3), datetime(2007, 6, 29), False), + (QuarterEnd(1, startingMonth=3), datetime(2007, 6, 30), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestBQuarterBegin(Base): + _offset = BQuarterBegin + + def test_repr(self): + expected = "" + assert repr(BQuarterBegin()) == expected + expected = "" + assert repr(BQuarterBegin(startingMonth=3)) == expected + expected = "" + assert repr(BQuarterBegin(startingMonth=1)) == expected + + def test_isAnchored(self): + assert BQuarterBegin(startingMonth=1).isAnchored() + assert BQuarterBegin().isAnchored() + assert not BQuarterBegin(2, startingMonth=1).isAnchored() + + def test_offset_corner_case(self): + # corner + offset = BQuarterBegin(n=-1, startingMonth=1) + assert datetime(2007, 4, 3) + offset == datetime(2007, 4, 2) + + offset_cases = [] + offset_cases.append((BQuarterBegin(startingMonth=1), { + datetime(2008, 1, 1): datetime(2008, 4, 1), + datetime(2008, 1, 31): datetime(2008, 4, 1), + datetime(2008, 2, 15): datetime(2008, 4, 1), + datetime(2008, 2, 29): datetime(2008, 4, 1), + datetime(2008, 3, 15): datetime(2008, 4, 1), + datetime(2008, 3, 31): datetime(2008, 4, 1), + datetime(2008, 4, 15): datetime(2008, 7, 1), + datetime(2007, 3, 15): datetime(2007, 4, 2), + datetime(2007, 2, 28): datetime(2007, 4, 2), + datetime(2007, 1, 1): datetime(2007, 4, 2), + datetime(2007, 4, 15): datetime(2007, 7, 2), + datetime(2007, 7, 1): datetime(2007, 7, 2), + datetime(2007, 4, 1): datetime(2007, 4, 2), + datetime(2007, 4, 2): datetime(2007, 7, 2), + datetime(2008, 4, 30): datetime(2008, 7, 1)})) + + offset_cases.append((BQuarterBegin(startingMonth=2), { + datetime(2008, 1, 1): datetime(2008, 2, 1), + datetime(2008, 1, 31): datetime(2008, 2, 1), + datetime(2008, 1, 15): datetime(2008, 2, 1), + datetime(2008, 2, 29): datetime(2008, 5, 1), + datetime(2008, 3, 15): datetime(2008, 5, 1), + datetime(2008, 3, 31): datetime(2008, 5, 1), + datetime(2008, 4, 15): datetime(2008, 5, 1), + datetime(2008, 8, 15): datetime(2008, 11, 3), + datetime(2008, 9, 15): datetime(2008, 11, 3), + datetime(2008, 11, 1): datetime(2008, 11, 3), + datetime(2008, 4, 30): datetime(2008, 5, 1)})) + + offset_cases.append((BQuarterBegin(startingMonth=1, n=0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2007, 12, 31): datetime(2008, 1, 1), + datetime(2008, 2, 15): datetime(2008, 4, 1), + datetime(2008, 2, 29): datetime(2008, 4, 1), + datetime(2008, 1, 15): datetime(2008, 4, 1), + datetime(2008, 2, 27): datetime(2008, 4, 1), + datetime(2008, 3, 15): datetime(2008, 4, 1), + datetime(2007, 4, 1): datetime(2007, 4, 2), + datetime(2007, 4, 2): datetime(2007, 4, 2), + datetime(2007, 7, 1): datetime(2007, 7, 2), + datetime(2007, 4, 15): datetime(2007, 7, 2), + datetime(2007, 7, 2): datetime(2007, 7, 2)})) + + offset_cases.append((BQuarterBegin(startingMonth=1, n=-1), { + datetime(2008, 1, 1): datetime(2007, 10, 1), + datetime(2008, 1, 31): datetime(2008, 1, 1), + datetime(2008, 2, 15): datetime(2008, 1, 1), + datetime(2008, 2, 29): datetime(2008, 1, 1), + datetime(2008, 3, 15): datetime(2008, 1, 1), + datetime(2008, 3, 31): datetime(2008, 1, 1), + datetime(2008, 4, 15): datetime(2008, 4, 1), + datetime(2007, 7, 3): datetime(2007, 7, 2), + datetime(2007, 4, 3): datetime(2007, 4, 2), + datetime(2007, 7, 2): datetime(2007, 4, 2), + datetime(2008, 4, 1): datetime(2008, 1, 1)})) + + offset_cases.append((BQuarterBegin(startingMonth=1, n=2), { + datetime(2008, 1, 1): datetime(2008, 7, 1), + datetime(2008, 1, 15): datetime(2008, 7, 1), + datetime(2008, 2, 29): datetime(2008, 7, 1), + datetime(2008, 3, 15): datetime(2008, 7, 1), + datetime(2007, 3, 31): datetime(2007, 7, 2), + datetime(2007, 4, 15): datetime(2007, 10, 1), + datetime(2008, 4, 30): datetime(2008, 10, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + +class TestBQuarterEnd(Base): + _offset = BQuarterEnd + + def test_repr(self): + expected = "" + assert repr(BQuarterEnd()) == expected + expected = "" + assert repr(BQuarterEnd(startingMonth=3)) == expected + expected = "" + assert repr(BQuarterEnd(startingMonth=1)) == expected + + def test_isAnchored(self): + assert BQuarterEnd(startingMonth=1).isAnchored() + assert BQuarterEnd().isAnchored() + assert not BQuarterEnd(2, startingMonth=1).isAnchored() + + def test_offset_corner_case(self): + # corner + offset = BQuarterEnd(n=-1, startingMonth=1) + assert datetime(2010, 1, 31) + offset == datetime(2010, 1, 29) + + offset_cases = [] + offset_cases.append((BQuarterEnd(startingMonth=1), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 4, 30), + datetime(2008, 2, 15): datetime(2008, 4, 30), + datetime(2008, 2, 29): datetime(2008, 4, 30), + datetime(2008, 3, 15): datetime(2008, 4, 30), + datetime(2008, 3, 31): datetime(2008, 4, 30), + datetime(2008, 4, 15): datetime(2008, 4, 30), + datetime(2008, 4, 30): datetime(2008, 7, 31)})) + + offset_cases.append((BQuarterEnd(startingMonth=2), { + datetime(2008, 1, 1): datetime(2008, 2, 29), + datetime(2008, 1, 31): datetime(2008, 2, 29), + datetime(2008, 2, 15): datetime(2008, 2, 29), + datetime(2008, 2, 29): datetime(2008, 5, 30), + datetime(2008, 3, 15): datetime(2008, 5, 30), + datetime(2008, 3, 31): datetime(2008, 5, 30), + datetime(2008, 4, 15): datetime(2008, 5, 30), + datetime(2008, 4, 30): datetime(2008, 5, 30)})) + + offset_cases.append((BQuarterEnd(startingMonth=1, n=0), { + datetime(2008, 1, 1): datetime(2008, 1, 31), + datetime(2008, 1, 31): datetime(2008, 1, 31), + datetime(2008, 2, 15): datetime(2008, 4, 30), + datetime(2008, 2, 29): datetime(2008, 4, 30), + datetime(2008, 3, 15): datetime(2008, 4, 30), + datetime(2008, 3, 31): datetime(2008, 4, 30), + datetime(2008, 4, 15): datetime(2008, 4, 30), + datetime(2008, 4, 30): datetime(2008, 4, 30)})) + + offset_cases.append((BQuarterEnd(startingMonth=1, n=-1), { + datetime(2008, 1, 1): datetime(2007, 10, 31), + datetime(2008, 1, 31): datetime(2007, 10, 31), + datetime(2008, 2, 15): datetime(2008, 1, 31), + datetime(2008, 2, 29): datetime(2008, 1, 31), + datetime(2008, 3, 15): datetime(2008, 1, 31), + datetime(2008, 3, 31): datetime(2008, 1, 31), + datetime(2008, 4, 15): datetime(2008, 1, 31), + datetime(2008, 4, 30): datetime(2008, 1, 31)})) + + offset_cases.append((BQuarterEnd(startingMonth=1, n=2), { + datetime(2008, 1, 31): datetime(2008, 7, 31), + datetime(2008, 2, 15): datetime(2008, 7, 31), + datetime(2008, 2, 29): datetime(2008, 7, 31), + datetime(2008, 3, 15): datetime(2008, 7, 31), + datetime(2008, 3, 31): datetime(2008, 7, 31), + datetime(2008, 4, 15): datetime(2008, 7, 31), + datetime(2008, 4, 30): datetime(2008, 10, 31)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [ + (BQuarterEnd(1, startingMonth=1), datetime(2008, 1, 31), True), + (BQuarterEnd(1, startingMonth=1), datetime(2007, 12, 31), False), + (BQuarterEnd(1, startingMonth=1), datetime(2008, 2, 29), False), + (BQuarterEnd(1, startingMonth=1), datetime(2007, 3, 30), False), + (BQuarterEnd(1, startingMonth=1), datetime(2007, 3, 31), False), + (BQuarterEnd(1, startingMonth=1), datetime(2008, 4, 30), True), + (BQuarterEnd(1, startingMonth=1), datetime(2008, 5, 30), False), + (BQuarterEnd(1, startingMonth=1), datetime(2007, 6, 29), False), + (BQuarterEnd(1, startingMonth=1), datetime(2007, 6, 30), False), + (BQuarterEnd(1, startingMonth=2), datetime(2008, 1, 31), False), + (BQuarterEnd(1, startingMonth=2), datetime(2007, 12, 31), False), + (BQuarterEnd(1, startingMonth=2), datetime(2008, 2, 29), True), + (BQuarterEnd(1, startingMonth=2), datetime(2007, 3, 30), False), + (BQuarterEnd(1, startingMonth=2), datetime(2007, 3, 31), False), + (BQuarterEnd(1, startingMonth=2), datetime(2008, 4, 30), False), + (BQuarterEnd(1, startingMonth=2), datetime(2008, 5, 30), True), + (BQuarterEnd(1, startingMonth=2), datetime(2007, 6, 29), False), + (BQuarterEnd(1, startingMonth=2), datetime(2007, 6, 30), False), + (BQuarterEnd(1, startingMonth=3), datetime(2008, 1, 31), False), + (BQuarterEnd(1, startingMonth=3), datetime(2007, 12, 31), True), + (BQuarterEnd(1, startingMonth=3), datetime(2008, 2, 29), False), + (BQuarterEnd(1, startingMonth=3), datetime(2007, 3, 30), True), + (BQuarterEnd(1, startingMonth=3), datetime(2007, 3, 31), False), + (BQuarterEnd(1, startingMonth=3), datetime(2008, 4, 30), False), + (BQuarterEnd(1, startingMonth=3), datetime(2008, 5, 30), False), + (BQuarterEnd(1, startingMonth=3), datetime(2007, 6, 29), True), + (BQuarterEnd(1, startingMonth=3), datetime(2007, 6, 30), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + +# -------------------------------------------------------------------- +# Years + + +class TestYearBegin(Base): + _offset = YearBegin + + def test_misspecified(self): + with pytest.raises(ValueError, match="Month must go from 1 to 12"): + YearBegin(month=13) + + offset_cases = [] + offset_cases.append((YearBegin(), { + datetime(2008, 1, 1): datetime(2009, 1, 1), + datetime(2008, 6, 30): datetime(2009, 1, 1), + datetime(2008, 12, 31): datetime(2009, 1, 1), + datetime(2005, 12, 30): datetime(2006, 1, 1), + datetime(2005, 12, 31): datetime(2006, 1, 1)})) + + offset_cases.append((YearBegin(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 6, 30): datetime(2009, 1, 1), + datetime(2008, 12, 31): datetime(2009, 1, 1), + datetime(2005, 12, 30): datetime(2006, 1, 1), + datetime(2005, 12, 31): datetime(2006, 1, 1)})) + + offset_cases.append((YearBegin(3), { + datetime(2008, 1, 1): datetime(2011, 1, 1), + datetime(2008, 6, 30): datetime(2011, 1, 1), + datetime(2008, 12, 31): datetime(2011, 1, 1), + datetime(2005, 12, 30): datetime(2008, 1, 1), + datetime(2005, 12, 31): datetime(2008, 1, 1)})) + + offset_cases.append((YearBegin(-1), { + datetime(2007, 1, 1): datetime(2006, 1, 1), + datetime(2007, 1, 15): datetime(2007, 1, 1), + datetime(2008, 6, 30): datetime(2008, 1, 1), + datetime(2008, 12, 31): datetime(2008, 1, 1), + datetime(2006, 12, 29): datetime(2006, 1, 1), + datetime(2006, 12, 30): datetime(2006, 1, 1), + datetime(2007, 1, 1): datetime(2006, 1, 1)})) + + offset_cases.append((YearBegin(-2), { + datetime(2007, 1, 1): datetime(2005, 1, 1), + datetime(2008, 6, 30): datetime(2007, 1, 1), + datetime(2008, 12, 31): datetime(2007, 1, 1)})) + + offset_cases.append((YearBegin(month=4), { + datetime(2007, 4, 1): datetime(2008, 4, 1), + datetime(2007, 4, 15): datetime(2008, 4, 1), + datetime(2007, 3, 1): datetime(2007, 4, 1), + datetime(2007, 12, 15): datetime(2008, 4, 1), + datetime(2012, 1, 31): datetime(2012, 4, 1)})) + + offset_cases.append((YearBegin(0, month=4), { + datetime(2007, 4, 1): datetime(2007, 4, 1), + datetime(2007, 3, 1): datetime(2007, 4, 1), + datetime(2007, 12, 15): datetime(2008, 4, 1), + datetime(2012, 1, 31): datetime(2012, 4, 1)})) + + offset_cases.append((YearBegin(4, month=4), { + datetime(2007, 4, 1): datetime(2011, 4, 1), + datetime(2007, 4, 15): datetime(2011, 4, 1), + datetime(2007, 3, 1): datetime(2010, 4, 1), + datetime(2007, 12, 15): datetime(2011, 4, 1), + datetime(2012, 1, 31): datetime(2015, 4, 1)})) + + offset_cases.append((YearBegin(-1, month=4), { + datetime(2007, 4, 1): datetime(2006, 4, 1), + datetime(2007, 3, 1): datetime(2006, 4, 1), + datetime(2007, 12, 15): datetime(2007, 4, 1), + datetime(2012, 1, 31): datetime(2011, 4, 1)})) + + offset_cases.append((YearBegin(-3, month=4), { + datetime(2007, 4, 1): datetime(2004, 4, 1), + datetime(2007, 3, 1): datetime(2004, 4, 1), + datetime(2007, 12, 15): datetime(2005, 4, 1), + datetime(2012, 1, 31): datetime(2009, 4, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(YearBegin(), datetime(2007, 1, 3), False), + (YearBegin(), datetime(2008, 1, 1), True), + (YearBegin(), datetime(2006, 12, 31), False), + (YearBegin(), datetime(2006, 1, 2), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestYearEnd(Base): + _offset = YearEnd + + def test_misspecified(self): + with pytest.raises(ValueError, match="Month must go from 1 to 12"): + YearEnd(month=13) + + offset_cases = [] + offset_cases.append((YearEnd(), { + datetime(2008, 1, 1): datetime(2008, 12, 31), + datetime(2008, 6, 30): datetime(2008, 12, 31), + datetime(2008, 12, 31): datetime(2009, 12, 31), + datetime(2005, 12, 30): datetime(2005, 12, 31), + datetime(2005, 12, 31): datetime(2006, 12, 31)})) + + offset_cases.append((YearEnd(0), { + datetime(2008, 1, 1): datetime(2008, 12, 31), + datetime(2008, 6, 30): datetime(2008, 12, 31), + datetime(2008, 12, 31): datetime(2008, 12, 31), + datetime(2005, 12, 30): datetime(2005, 12, 31)})) + + offset_cases.append((YearEnd(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 31), + datetime(2008, 6, 30): datetime(2007, 12, 31), + datetime(2008, 12, 31): datetime(2007, 12, 31), + datetime(2006, 12, 29): datetime(2005, 12, 31), + datetime(2006, 12, 30): datetime(2005, 12, 31), + datetime(2007, 1, 1): datetime(2006, 12, 31)})) + + offset_cases.append((YearEnd(-2), { + datetime(2007, 1, 1): datetime(2005, 12, 31), + datetime(2008, 6, 30): datetime(2006, 12, 31), + datetime(2008, 12, 31): datetime(2006, 12, 31)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(YearEnd(), datetime(2007, 12, 31), True), + (YearEnd(), datetime(2008, 1, 1), False), + (YearEnd(), datetime(2006, 12, 31), True), + (YearEnd(), datetime(2006, 12, 29), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestYearEndDiffMonth(Base): + offset_cases = [] + offset_cases.append((YearEnd(month=3), + {datetime(2008, 1, 1): datetime(2008, 3, 31), + datetime(2008, 2, 15): datetime(2008, 3, 31), + datetime(2008, 3, 31): datetime(2009, 3, 31), + datetime(2008, 3, 30): datetime(2008, 3, 31), + datetime(2005, 3, 31): datetime(2006, 3, 31), + datetime(2006, 7, 30): datetime(2007, 3, 31)})) + + offset_cases.append((YearEnd(0, month=3), + {datetime(2008, 1, 1): datetime(2008, 3, 31), + datetime(2008, 2, 28): datetime(2008, 3, 31), + datetime(2008, 3, 31): datetime(2008, 3, 31), + datetime(2005, 3, 30): datetime(2005, 3, 31)})) + + offset_cases.append((YearEnd(-1, month=3), + {datetime(2007, 1, 1): datetime(2006, 3, 31), + datetime(2008, 2, 28): datetime(2007, 3, 31), + datetime(2008, 3, 31): datetime(2007, 3, 31), + datetime(2006, 3, 29): datetime(2005, 3, 31), + datetime(2006, 3, 30): datetime(2005, 3, 31), + datetime(2007, 3, 1): datetime(2006, 3, 31)})) + + offset_cases.append((YearEnd(-2, month=3), + {datetime(2007, 1, 1): datetime(2005, 3, 31), + datetime(2008, 6, 30): datetime(2007, 3, 31), + datetime(2008, 3, 31): datetime(2006, 3, 31)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(YearEnd(month=3), datetime(2007, 3, 31), True), + (YearEnd(month=3), datetime(2008, 1, 1), False), + (YearEnd(month=3), datetime(2006, 3, 31), True), + (YearEnd(month=3), datetime(2006, 3, 29), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestBYearBegin(Base): + _offset = BYearBegin + + def test_misspecified(self): + msg = "Month must go from 1 to 12" + with pytest.raises(ValueError, match=msg): + BYearBegin(month=13) + with pytest.raises(ValueError, match=msg): + BYearEnd(month=13) + + offset_cases = [] + offset_cases.append((BYearBegin(), { + datetime(2008, 1, 1): datetime(2009, 1, 1), + datetime(2008, 6, 30): datetime(2009, 1, 1), + datetime(2008, 12, 31): datetime(2009, 1, 1), + datetime(2011, 1, 1): datetime(2011, 1, 3), + datetime(2011, 1, 3): datetime(2012, 1, 2), + datetime(2005, 12, 30): datetime(2006, 1, 2), + datetime(2005, 12, 31): datetime(2006, 1, 2)})) + + offset_cases.append((BYearBegin(0), { + datetime(2008, 1, 1): datetime(2008, 1, 1), + datetime(2008, 6, 30): datetime(2009, 1, 1), + datetime(2008, 12, 31): datetime(2009, 1, 1), + datetime(2005, 12, 30): datetime(2006, 1, 2), + datetime(2005, 12, 31): datetime(2006, 1, 2)})) + + offset_cases.append((BYearBegin(-1), { + datetime(2007, 1, 1): datetime(2006, 1, 2), + datetime(2009, 1, 4): datetime(2009, 1, 1), + datetime(2009, 1, 1): datetime(2008, 1, 1), + datetime(2008, 6, 30): datetime(2008, 1, 1), + datetime(2008, 12, 31): datetime(2008, 1, 1), + datetime(2006, 12, 29): datetime(2006, 1, 2), + datetime(2006, 12, 30): datetime(2006, 1, 2), + datetime(2006, 1, 1): datetime(2005, 1, 3)})) + + offset_cases.append((BYearBegin(-2), { + datetime(2007, 1, 1): datetime(2005, 1, 3), + datetime(2007, 6, 30): datetime(2006, 1, 2), + datetime(2008, 12, 31): datetime(2007, 1, 1)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + +class TestBYearEnd(Base): + _offset = BYearEnd + + offset_cases = [] + offset_cases.append((BYearEnd(), { + datetime(2008, 1, 1): datetime(2008, 12, 31), + datetime(2008, 6, 30): datetime(2008, 12, 31), + datetime(2008, 12, 31): datetime(2009, 12, 31), + datetime(2005, 12, 30): datetime(2006, 12, 29), + datetime(2005, 12, 31): datetime(2006, 12, 29)})) + + offset_cases.append((BYearEnd(0), { + datetime(2008, 1, 1): datetime(2008, 12, 31), + datetime(2008, 6, 30): datetime(2008, 12, 31), + datetime(2008, 12, 31): datetime(2008, 12, 31), + datetime(2005, 12, 31): datetime(2006, 12, 29)})) + + offset_cases.append((BYearEnd(-1), { + datetime(2007, 1, 1): datetime(2006, 12, 29), + datetime(2008, 6, 30): datetime(2007, 12, 31), + datetime(2008, 12, 31): datetime(2007, 12, 31), + datetime(2006, 12, 29): datetime(2005, 12, 30), + datetime(2006, 12, 30): datetime(2006, 12, 29), + datetime(2007, 1, 1): datetime(2006, 12, 29)})) + + offset_cases.append((BYearEnd(-2), { + datetime(2007, 1, 1): datetime(2005, 12, 30), + datetime(2008, 6, 30): datetime(2006, 12, 29), + datetime(2008, 12, 31): datetime(2006, 12, 29)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + on_offset_cases = [(BYearEnd(), datetime(2007, 12, 31), True), + (BYearEnd(), datetime(2008, 1, 1), False), + (BYearEnd(), datetime(2006, 12, 31), False), + (BYearEnd(), datetime(2006, 12, 29), True)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) + + +class TestBYearEndLagged(Base): + _offset = BYearEnd + + def test_bad_month_fail(self): + msg = "Month must go from 1 to 12" + with pytest.raises(ValueError, match=msg): + BYearEnd(month=13) + with pytest.raises(ValueError, match=msg): + BYearEnd(month=0) + + offset_cases = [] + offset_cases.append((BYearEnd(month=6), { + datetime(2008, 1, 1): datetime(2008, 6, 30), + datetime(2007, 6, 30): datetime(2008, 6, 30)})) + + offset_cases.append((BYearEnd(n=-1, month=6), { + datetime(2008, 1, 1): datetime(2007, 6, 29), + datetime(2007, 6, 30): datetime(2007, 6, 29)})) + + @pytest.mark.parametrize('case', offset_cases) + def test_offset(self, case): + offset, cases = case + for base, expected in compat.iteritems(cases): + assert_offset_equal(offset, base, expected) + + def test_roll(self): + offset = BYearEnd(month=6) + date = datetime(2009, 11, 30) + + assert offset.rollforward(date) == datetime(2010, 6, 30) + assert offset.rollback(date) == datetime(2009, 6, 30) + + on_offset_cases = [(BYearEnd(month=2), datetime(2007, 2, 28), True), + (BYearEnd(month=6), datetime(2007, 6, 30), False)] + + @pytest.mark.parametrize('case', on_offset_cases) + def test_onOffset(self, case): + offset, dt, expected = case + assert_onOffset(offset, dt, expected) diff --git a/pandas/tests/tseries/test_converter.py b/pandas/tests/tseries/test_converter.py deleted file mode 100644 index 5351e26f0e62b..0000000000000 --- a/pandas/tests/tseries/test_converter.py +++ /dev/null @@ -1,199 +0,0 @@ -import pytest -from datetime import datetime, date - -import numpy as np -from pandas import Timestamp, Period, Index -from pandas.compat import u -import pandas.util.testing as tm -from pandas.tseries.offsets import Second, Milli, Micro, Day -from pandas.compat.numpy import np_datetime64_compat - -converter = pytest.importorskip('pandas.tseries.converter') - - -def test_timtetonum_accepts_unicode(): - assert (converter.time2num("00:01") == converter.time2num(u("00:01"))) - - -class TestDateTimeConverter(tm.TestCase): - - def setUp(self): - self.dtc = converter.DatetimeConverter() - self.tc = converter.TimeFormatter(None) - - def test_convert_accepts_unicode(self): - r1 = self.dtc.convert("12:22", None, None) - r2 = self.dtc.convert(u("12:22"), None, None) - assert (r1 == r2), "DatetimeConverter.convert should accept unicode" - - def test_conversion(self): - rs = self.dtc.convert(['2012-1-1'], None, None)[0] - xp = datetime(2012, 1, 1).toordinal() - self.assertEqual(rs, xp) - - rs = self.dtc.convert('2012-1-1', None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(date(2012, 1, 1), None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(datetime(2012, 1, 1).toordinal(), None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert('2012-1-1', None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(Timestamp('2012-1-1'), None, None) - self.assertEqual(rs, xp) - - # also testing datetime64 dtype (GH8614) - rs = self.dtc.convert(np_datetime64_compat('2012-01-01'), None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(np_datetime64_compat( - '2012-01-01 00:00:00+0000'), None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(np.array([ - np_datetime64_compat('2012-01-01 00:00:00+0000'), - np_datetime64_compat('2012-01-02 00:00:00+0000')]), None, None) - self.assertEqual(rs[0], xp) - - # we have a tz-aware date (constructed to that when we turn to utc it - # is the same as our sample) - ts = (Timestamp('2012-01-01') - .tz_localize('UTC') - .tz_convert('US/Eastern') - ) - rs = self.dtc.convert(ts, None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(ts.to_pydatetime(), None, None) - self.assertEqual(rs, xp) - - rs = self.dtc.convert(Index([ts - Day(1), ts]), None, None) - self.assertEqual(rs[1], xp) - - rs = self.dtc.convert(Index([ts - Day(1), ts]).to_pydatetime(), - None, None) - self.assertEqual(rs[1], xp) - - def test_conversion_float(self): - decimals = 9 - - rs = self.dtc.convert( - Timestamp('2012-1-1 01:02:03', tz='UTC'), None, None) - xp = converter.dates.date2num(Timestamp('2012-1-1 01:02:03', tz='UTC')) - tm.assert_almost_equal(rs, xp, decimals) - - rs = self.dtc.convert( - Timestamp('2012-1-1 09:02:03', tz='Asia/Hong_Kong'), None, None) - tm.assert_almost_equal(rs, xp, decimals) - - rs = self.dtc.convert(datetime(2012, 1, 1, 1, 2, 3), None, None) - tm.assert_almost_equal(rs, xp, decimals) - - def test_conversion_outofbounds_datetime(self): - # 2579 - values = [date(1677, 1, 1), date(1677, 1, 2)] - rs = self.dtc.convert(values, None, None) - xp = converter.dates.date2num(values) - tm.assert_numpy_array_equal(rs, xp) - rs = self.dtc.convert(values[0], None, None) - xp = converter.dates.date2num(values[0]) - self.assertEqual(rs, xp) - - values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)] - rs = self.dtc.convert(values, None, None) - xp = converter.dates.date2num(values) - tm.assert_numpy_array_equal(rs, xp) - rs = self.dtc.convert(values[0], None, None) - xp = converter.dates.date2num(values[0]) - self.assertEqual(rs, xp) - - def test_time_formatter(self): - self.tc(90000) - - def test_dateindex_conversion(self): - decimals = 9 - - for freq in ('B', 'L', 'S'): - dateindex = tm.makeDateIndex(k=10, freq=freq) - rs = self.dtc.convert(dateindex, None, None) - xp = converter.dates.date2num(dateindex._mpl_repr()) - tm.assert_almost_equal(rs, xp, decimals) - - def test_resolution(self): - def _assert_less(ts1, ts2): - val1 = self.dtc.convert(ts1, None, None) - val2 = self.dtc.convert(ts2, None, None) - if not val1 < val2: - raise AssertionError('{0} is not less than {1}.'.format(val1, - val2)) - - # Matplotlib's time representation using floats cannot distinguish - # intervals smaller than ~10 microsecond in the common range of years. - ts = Timestamp('2012-1-1') - _assert_less(ts, ts + Second()) - _assert_less(ts, ts + Milli()) - _assert_less(ts, ts + Micro(50)) - - -class TestPeriodConverter(tm.TestCase): - - def setUp(self): - self.pc = converter.PeriodConverter() - - class Axis(object): - pass - - self.axis = Axis() - self.axis.freq = 'D' - - def test_convert_accepts_unicode(self): - r1 = self.pc.convert("2012-1-1", None, self.axis) - r2 = self.pc.convert(u("2012-1-1"), None, self.axis) - self.assert_equal(r1, r2, - "PeriodConverter.convert should accept unicode") - - def test_conversion(self): - rs = self.pc.convert(['2012-1-1'], None, self.axis)[0] - xp = Period('2012-1-1').ordinal - self.assertEqual(rs, xp) - - rs = self.pc.convert('2012-1-1', None, self.axis) - self.assertEqual(rs, xp) - - rs = self.pc.convert([date(2012, 1, 1)], None, self.axis)[0] - self.assertEqual(rs, xp) - - rs = self.pc.convert(date(2012, 1, 1), None, self.axis) - self.assertEqual(rs, xp) - - rs = self.pc.convert([Timestamp('2012-1-1')], None, self.axis)[0] - self.assertEqual(rs, xp) - - rs = self.pc.convert(Timestamp('2012-1-1'), None, self.axis) - self.assertEqual(rs, xp) - - # FIXME - # rs = self.pc.convert( - # np_datetime64_compat('2012-01-01'), None, self.axis) - # self.assertEqual(rs, xp) - # - # rs = self.pc.convert( - # np_datetime64_compat('2012-01-01 00:00:00+0000'), - # None, self.axis) - # self.assertEqual(rs, xp) - # - # rs = self.pc.convert(np.array([ - # np_datetime64_compat('2012-01-01 00:00:00+0000'), - # np_datetime64_compat('2012-01-02 00:00:00+0000')]), - # None, self.axis) - # self.assertEqual(rs[0], xp) - - def test_integer_passthrough(self): - # GH9012 - rs = self.pc.convert([0, 1], None, self.axis) - xp = [0, 1] - self.assertEqual(rs, xp) diff --git a/pandas/tests/tseries/test_frequencies.py b/pandas/tests/tseries/test_frequencies.py deleted file mode 100644 index 5fbef465ca8fc..0000000000000 --- a/pandas/tests/tseries/test_frequencies.py +++ /dev/null @@ -1,845 +0,0 @@ -from datetime import datetime, timedelta -from pandas.compat import range - -import numpy as np - -from pandas import (Index, DatetimeIndex, Timestamp, Series, - date_range, period_range) - -import pandas.tseries.frequencies as frequencies -from pandas.tseries.tools import to_datetime - -import pandas.tseries.offsets as offsets -from pandas.tseries.period import PeriodIndex -import pandas.compat as compat -from pandas.compat import is_platform_windows - -import pandas.util.testing as tm -from pandas import Timedelta - - -class TestToOffset(tm.TestCase): - - def test_to_offset_multiple(self): - freqstr = '2h30min' - freqstr2 = '2h 30min' - - result = frequencies.to_offset(freqstr) - assert (result == frequencies.to_offset(freqstr2)) - expected = offsets.Minute(150) - assert (result == expected) - - freqstr = '2h30min15s' - result = frequencies.to_offset(freqstr) - expected = offsets.Second(150 * 60 + 15) - assert (result == expected) - - freqstr = '2h 60min' - result = frequencies.to_offset(freqstr) - expected = offsets.Hour(3) - assert (result == expected) - - freqstr = '2h 20.5min' - result = frequencies.to_offset(freqstr) - expected = offsets.Second(8430) - assert (result == expected) - - freqstr = '1.5min' - result = frequencies.to_offset(freqstr) - expected = offsets.Second(90) - assert (result == expected) - - freqstr = '0.5S' - result = frequencies.to_offset(freqstr) - expected = offsets.Milli(500) - assert (result == expected) - - freqstr = '15l500u' - result = frequencies.to_offset(freqstr) - expected = offsets.Micro(15500) - assert (result == expected) - - freqstr = '10s75L' - result = frequencies.to_offset(freqstr) - expected = offsets.Milli(10075) - assert (result == expected) - - freqstr = '1s0.25ms' - result = frequencies.to_offset(freqstr) - expected = offsets.Micro(1000250) - assert (result == expected) - - freqstr = '1s0.25L' - result = frequencies.to_offset(freqstr) - expected = offsets.Micro(1000250) - assert (result == expected) - - freqstr = '2800N' - result = frequencies.to_offset(freqstr) - expected = offsets.Nano(2800) - assert (result == expected) - - freqstr = '2SM' - result = frequencies.to_offset(freqstr) - expected = offsets.SemiMonthEnd(2) - assert (result == expected) - - freqstr = '2SM-16' - result = frequencies.to_offset(freqstr) - expected = offsets.SemiMonthEnd(2, day_of_month=16) - assert (result == expected) - - freqstr = '2SMS-14' - result = frequencies.to_offset(freqstr) - expected = offsets.SemiMonthBegin(2, day_of_month=14) - assert (result == expected) - - freqstr = '2SMS-15' - result = frequencies.to_offset(freqstr) - expected = offsets.SemiMonthBegin(2) - assert (result == expected) - - # malformed - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: 2h20m'): - frequencies.to_offset('2h20m') - - def test_to_offset_negative(self): - freqstr = '-1S' - result = frequencies.to_offset(freqstr) - assert (result.n == -1) - - freqstr = '-5min10s' - result = frequencies.to_offset(freqstr) - assert (result.n == -310) - - freqstr = '-2SM' - result = frequencies.to_offset(freqstr) - assert (result.n == -2) - - freqstr = '-1SMS' - result = frequencies.to_offset(freqstr) - assert (result.n == -1) - - def test_to_offset_invalid(self): - # GH 13930 - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: U1'): - frequencies.to_offset('U1') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: -U'): - frequencies.to_offset('-U') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: 3U1'): - frequencies.to_offset('3U1') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: -2-3U'): - frequencies.to_offset('-2-3U') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: -2D:3H'): - frequencies.to_offset('-2D:3H') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: 1.5.0S'): - frequencies.to_offset('1.5.0S') - - # split offsets with spaces are valid - assert frequencies.to_offset('2D 3H') == offsets.Hour(51) - assert frequencies.to_offset('2 D3 H') == offsets.Hour(51) - assert frequencies.to_offset('2 D 3 H') == offsets.Hour(51) - assert frequencies.to_offset(' 2 D 3 H ') == offsets.Hour(51) - assert frequencies.to_offset(' H ') == offsets.Hour() - assert frequencies.to_offset(' 3 H ') == offsets.Hour(3) - - # special cases - assert frequencies.to_offset('2SMS-15') == offsets.SemiMonthBegin(2) - with tm.assertRaisesRegexp(ValueError, - 'Invalid frequency: 2SMS-15-15'): - frequencies.to_offset('2SMS-15-15') - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: 2SMS-15D'): - frequencies.to_offset('2SMS-15D') - - def test_to_offset_leading_zero(self): - freqstr = '00H 00T 01S' - result = frequencies.to_offset(freqstr) - assert (result.n == 1) - - freqstr = '-00H 03T 14S' - result = frequencies.to_offset(freqstr) - assert (result.n == -194) - - def test_to_offset_pd_timedelta(self): - # Tests for #9064 - td = Timedelta(days=1, seconds=1) - result = frequencies.to_offset(td) - expected = offsets.Second(86401) - assert (expected == result) - - td = Timedelta(days=-1, seconds=1) - result = frequencies.to_offset(td) - expected = offsets.Second(-86399) - assert (expected == result) - - td = Timedelta(hours=1, minutes=10) - result = frequencies.to_offset(td) - expected = offsets.Minute(70) - assert (expected == result) - - td = Timedelta(hours=1, minutes=-10) - result = frequencies.to_offset(td) - expected = offsets.Minute(50) - assert (expected == result) - - td = Timedelta(weeks=1) - result = frequencies.to_offset(td) - expected = offsets.Day(7) - assert (expected == result) - - td1 = Timedelta(hours=1) - result1 = frequencies.to_offset(td1) - result2 = frequencies.to_offset('60min') - assert (result1 == result2) - - td = Timedelta(microseconds=1) - result = frequencies.to_offset(td) - expected = offsets.Micro(1) - assert (expected == result) - - td = Timedelta(microseconds=0) - tm.assertRaises(ValueError, lambda: frequencies.to_offset(td)) - - def test_anchored_shortcuts(self): - result = frequencies.to_offset('W') - expected = frequencies.to_offset('W-SUN') - assert (result == expected) - - result1 = frequencies.to_offset('Q') - result2 = frequencies.to_offset('Q-DEC') - expected = offsets.QuarterEnd(startingMonth=12) - assert (result1 == expected) - assert (result2 == expected) - - result1 = frequencies.to_offset('Q-MAY') - expected = offsets.QuarterEnd(startingMonth=5) - assert (result1 == expected) - - result1 = frequencies.to_offset('SM') - result2 = frequencies.to_offset('SM-15') - expected = offsets.SemiMonthEnd(day_of_month=15) - assert (result1 == expected) - assert (result2 == expected) - - result = frequencies.to_offset('SM-1') - expected = offsets.SemiMonthEnd(day_of_month=1) - assert (result == expected) - - result = frequencies.to_offset('SM-27') - expected = offsets.SemiMonthEnd(day_of_month=27) - assert (result == expected) - - result = frequencies.to_offset('SMS-2') - expected = offsets.SemiMonthBegin(day_of_month=2) - assert (result == expected) - - result = frequencies.to_offset('SMS-27') - expected = offsets.SemiMonthBegin(day_of_month=27) - assert (result == expected) - - # ensure invalid cases fail as expected - invalid_anchors = ['SM-0', 'SM-28', 'SM-29', - 'SM-FOO', 'BSM', 'SM--1' - 'SMS-1', 'SMS-28', 'SMS-30', - 'SMS-BAR', 'BSMS', 'SMS--2'] - for invalid_anchor in invalid_anchors: - with tm.assertRaisesRegexp(ValueError, 'Invalid frequency: '): - frequencies.to_offset(invalid_anchor) - - -def test_ms_vs_MS(): - left = frequencies.get_offset('ms') - right = frequencies.get_offset('MS') - assert left == offsets.Milli() - assert right == offsets.MonthBegin() - - -def test_rule_aliases(): - rule = frequencies.to_offset('10us') - assert rule == offsets.Micro(10) - - -def test_get_rule_month(): - result = frequencies._get_rule_month('W') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.Week()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('D') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.Day()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('Q') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.QuarterEnd(startingMonth=12)) - print(result == 'DEC') - - result = frequencies._get_rule_month('Q-JAN') - assert (result == 'JAN') - result = frequencies._get_rule_month(offsets.QuarterEnd(startingMonth=1)) - assert (result == 'JAN') - - result = frequencies._get_rule_month('A-DEC') - assert (result == 'DEC') - result = frequencies._get_rule_month(offsets.YearEnd()) - assert (result == 'DEC') - - result = frequencies._get_rule_month('A-MAY') - assert (result == 'MAY') - result = frequencies._get_rule_month(offsets.YearEnd(month=5)) - assert (result == 'MAY') - - -def test_period_str_to_code(): - assert (frequencies._period_str_to_code('A') == 1000) - assert (frequencies._period_str_to_code('A-DEC') == 1000) - assert (frequencies._period_str_to_code('A-JAN') == 1001) - assert (frequencies._period_str_to_code('Q') == 2000) - assert (frequencies._period_str_to_code('Q-DEC') == 2000) - assert (frequencies._period_str_to_code('Q-FEB') == 2002) - - def _assert_depr(freq, expected, aliases): - assert isinstance(aliases, list) - assert (frequencies._period_str_to_code(freq) == expected) - - msg = frequencies._INVALID_FREQ_ERROR - for alias in aliases: - with tm.assertRaisesRegexp(ValueError, msg): - frequencies._period_str_to_code(alias) - - _assert_depr("M", 3000, ["MTH", "MONTH", "MONTHLY"]) - - assert (frequencies._period_str_to_code('W') == 4000) - assert (frequencies._period_str_to_code('W-SUN') == 4000) - assert (frequencies._period_str_to_code('W-FRI') == 4005) - - _assert_depr("B", 5000, ["BUS", "BUSINESS", "BUSINESSLY", "WEEKDAY"]) - _assert_depr("D", 6000, ["DAY", "DLY", "DAILY"]) - _assert_depr("H", 7000, ["HR", "HOUR", "HRLY", "HOURLY"]) - - _assert_depr("T", 8000, ["minute", "MINUTE", "MINUTELY"]) - assert (frequencies._period_str_to_code('Min') == 8000) - - _assert_depr("S", 9000, ["sec", "SEC", "SECOND", "SECONDLY"]) - _assert_depr("L", 10000, ["MILLISECOND", "MILLISECONDLY"]) - assert (frequencies._period_str_to_code('ms') == 10000) - - _assert_depr("U", 11000, ["MICROSECOND", "MICROSECONDLY"]) - assert (frequencies._period_str_to_code('US') == 11000) - - _assert_depr("N", 12000, ["NANOSECOND", "NANOSECONDLY"]) - assert (frequencies._period_str_to_code('NS') == 12000) - - -class TestFrequencyCode(tm.TestCase): - - def test_freq_code(self): - self.assertEqual(frequencies.get_freq('A'), 1000) - self.assertEqual(frequencies.get_freq('3A'), 1000) - self.assertEqual(frequencies.get_freq('-1A'), 1000) - - self.assertEqual(frequencies.get_freq('W'), 4000) - self.assertEqual(frequencies.get_freq('W-MON'), 4001) - self.assertEqual(frequencies.get_freq('W-FRI'), 4005) - - for freqstr, code in compat.iteritems(frequencies._period_code_map): - result = frequencies.get_freq(freqstr) - self.assertEqual(result, code) - - result = frequencies.get_freq_group(freqstr) - self.assertEqual(result, code // 1000 * 1000) - - result = frequencies.get_freq_group(code) - self.assertEqual(result, code // 1000 * 1000) - - def test_freq_group(self): - self.assertEqual(frequencies.get_freq_group('A'), 1000) - self.assertEqual(frequencies.get_freq_group('3A'), 1000) - self.assertEqual(frequencies.get_freq_group('-1A'), 1000) - self.assertEqual(frequencies.get_freq_group('A-JAN'), 1000) - self.assertEqual(frequencies.get_freq_group('A-MAY'), 1000) - self.assertEqual(frequencies.get_freq_group(offsets.YearEnd()), 1000) - self.assertEqual(frequencies.get_freq_group( - offsets.YearEnd(month=1)), 1000) - self.assertEqual(frequencies.get_freq_group( - offsets.YearEnd(month=5)), 1000) - - self.assertEqual(frequencies.get_freq_group('W'), 4000) - self.assertEqual(frequencies.get_freq_group('W-MON'), 4000) - self.assertEqual(frequencies.get_freq_group('W-FRI'), 4000) - self.assertEqual(frequencies.get_freq_group(offsets.Week()), 4000) - self.assertEqual(frequencies.get_freq_group( - offsets.Week(weekday=1)), 4000) - self.assertEqual(frequencies.get_freq_group( - offsets.Week(weekday=5)), 4000) - - def test_get_to_timestamp_base(self): - tsb = frequencies.get_to_timestamp_base - - self.assertEqual(tsb(frequencies.get_freq_code('D')[0]), - frequencies.get_freq_code('D')[0]) - self.assertEqual(tsb(frequencies.get_freq_code('W')[0]), - frequencies.get_freq_code('D')[0]) - self.assertEqual(tsb(frequencies.get_freq_code('M')[0]), - frequencies.get_freq_code('D')[0]) - - self.assertEqual(tsb(frequencies.get_freq_code('S')[0]), - frequencies.get_freq_code('S')[0]) - self.assertEqual(tsb(frequencies.get_freq_code('T')[0]), - frequencies.get_freq_code('S')[0]) - self.assertEqual(tsb(frequencies.get_freq_code('H')[0]), - frequencies.get_freq_code('S')[0]) - - def test_freq_to_reso(self): - Reso = frequencies.Resolution - - self.assertEqual(Reso.get_str_from_freq('A'), 'year') - self.assertEqual(Reso.get_str_from_freq('Q'), 'quarter') - self.assertEqual(Reso.get_str_from_freq('M'), 'month') - self.assertEqual(Reso.get_str_from_freq('D'), 'day') - self.assertEqual(Reso.get_str_from_freq('H'), 'hour') - self.assertEqual(Reso.get_str_from_freq('T'), 'minute') - self.assertEqual(Reso.get_str_from_freq('S'), 'second') - self.assertEqual(Reso.get_str_from_freq('L'), 'millisecond') - self.assertEqual(Reso.get_str_from_freq('U'), 'microsecond') - self.assertEqual(Reso.get_str_from_freq('N'), 'nanosecond') - - for freq in ['A', 'Q', 'M', 'D', 'H', 'T', 'S', 'L', 'U', 'N']: - # check roundtrip - result = Reso.get_freq(Reso.get_str_from_freq(freq)) - self.assertEqual(freq, result) - - for freq in ['D', 'H', 'T', 'S', 'L', 'U']: - result = Reso.get_freq(Reso.get_str(Reso.get_reso_from_freq(freq))) - self.assertEqual(freq, result) - - def test_resolution_bumping(self): - # GH 14378 - Reso = frequencies.Resolution - - self.assertEqual(Reso.get_stride_from_decimal(1.5, 'T'), (90, 'S')) - self.assertEqual(Reso.get_stride_from_decimal(62.4, 'T'), (3744, 'S')) - self.assertEqual(Reso.get_stride_from_decimal(1.04, 'H'), (3744, 'S')) - self.assertEqual(Reso.get_stride_from_decimal(1, 'D'), (1, 'D')) - self.assertEqual(Reso.get_stride_from_decimal(0.342931, 'H'), - (1234551600, 'U')) - self.assertEqual(Reso.get_stride_from_decimal(1.2345, 'D'), - (106660800, 'L')) - - with self.assertRaises(ValueError): - Reso.get_stride_from_decimal(0.5, 'N') - - # too much precision in the input can prevent - with self.assertRaises(ValueError): - Reso.get_stride_from_decimal(0.3429324798798269273987982, 'H') - - def test_get_freq_code(self): - # freqstr - self.assertEqual(frequencies.get_freq_code('A'), - (frequencies.get_freq('A'), 1)) - self.assertEqual(frequencies.get_freq_code('3D'), - (frequencies.get_freq('D'), 3)) - self.assertEqual(frequencies.get_freq_code('-2M'), - (frequencies.get_freq('M'), -2)) - - # tuple - self.assertEqual(frequencies.get_freq_code(('D', 1)), - (frequencies.get_freq('D'), 1)) - self.assertEqual(frequencies.get_freq_code(('A', 3)), - (frequencies.get_freq('A'), 3)) - self.assertEqual(frequencies.get_freq_code(('M', -2)), - (frequencies.get_freq('M'), -2)) - # numeric tuple - self.assertEqual(frequencies.get_freq_code((1000, 1)), (1000, 1)) - - # offsets - self.assertEqual(frequencies.get_freq_code(offsets.Day()), - (frequencies.get_freq('D'), 1)) - self.assertEqual(frequencies.get_freq_code(offsets.Day(3)), - (frequencies.get_freq('D'), 3)) - self.assertEqual(frequencies.get_freq_code(offsets.Day(-2)), - (frequencies.get_freq('D'), -2)) - - self.assertEqual(frequencies.get_freq_code(offsets.MonthEnd()), - (frequencies.get_freq('M'), 1)) - self.assertEqual(frequencies.get_freq_code(offsets.MonthEnd(3)), - (frequencies.get_freq('M'), 3)) - self.assertEqual(frequencies.get_freq_code(offsets.MonthEnd(-2)), - (frequencies.get_freq('M'), -2)) - - self.assertEqual(frequencies.get_freq_code(offsets.Week()), - (frequencies.get_freq('W'), 1)) - self.assertEqual(frequencies.get_freq_code(offsets.Week(3)), - (frequencies.get_freq('W'), 3)) - self.assertEqual(frequencies.get_freq_code(offsets.Week(-2)), - (frequencies.get_freq('W'), -2)) - - # monday is weekday=0 - self.assertEqual(frequencies.get_freq_code(offsets.Week(weekday=1)), - (frequencies.get_freq('W-TUE'), 1)) - self.assertEqual(frequencies.get_freq_code(offsets.Week(3, weekday=0)), - (frequencies.get_freq('W-MON'), 3)) - self.assertEqual( - frequencies.get_freq_code(offsets.Week(-2, weekday=4)), - (frequencies.get_freq('W-FRI'), -2)) - - -_dti = DatetimeIndex - - -class TestFrequencyInference(tm.TestCase): - - def test_raise_if_period_index(self): - index = PeriodIndex(start="1/1/1990", periods=20, freq="M") - self.assertRaises(TypeError, frequencies.infer_freq, index) - - def test_raise_if_too_few(self): - index = _dti(['12/31/1998', '1/3/1999']) - self.assertRaises(ValueError, frequencies.infer_freq, index) - - def test_business_daily(self): - index = _dti(['12/31/1998', '1/3/1999', '1/4/1999']) - self.assertEqual(frequencies.infer_freq(index), 'B') - - def test_day(self): - self._check_tick(timedelta(1), 'D') - - def test_day_corner(self): - index = _dti(['1/1/2000', '1/2/2000', '1/3/2000']) - self.assertEqual(frequencies.infer_freq(index), 'D') - - def test_non_datetimeindex(self): - dates = to_datetime(['1/1/2000', '1/2/2000', '1/3/2000']) - self.assertEqual(frequencies.infer_freq(dates), 'D') - - def test_hour(self): - self._check_tick(timedelta(hours=1), 'H') - - def test_minute(self): - self._check_tick(timedelta(minutes=1), 'T') - - def test_second(self): - self._check_tick(timedelta(seconds=1), 'S') - - def test_millisecond(self): - self._check_tick(timedelta(microseconds=1000), 'L') - - def test_microsecond(self): - self._check_tick(timedelta(microseconds=1), 'U') - - def test_nanosecond(self): - self._check_tick(np.timedelta64(1, 'ns'), 'N') - - def _check_tick(self, base_delta, code): - b = Timestamp(datetime.now()) - for i in range(1, 5): - inc = base_delta * i - index = _dti([b + inc * j for j in range(3)]) - if i > 1: - exp_freq = '%d%s' % (i, code) - else: - exp_freq = code - self.assertEqual(frequencies.infer_freq(index), exp_freq) - - index = _dti([b + base_delta * 7] + [b + base_delta * j for j in range( - 3)]) - self.assertIsNone(frequencies.infer_freq(index)) - - index = _dti([b + base_delta * j for j in range(3)] + [b + base_delta * - 7]) - - self.assertIsNone(frequencies.infer_freq(index)) - - def test_weekly(self): - days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] - - for day in days: - self._check_generated_range('1/1/2000', 'W-%s' % day) - - def test_week_of_month(self): - days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] - - for day in days: - for i in range(1, 5): - self._check_generated_range('1/1/2000', 'WOM-%d%s' % (i, day)) - - def test_fifth_week_of_month(self): - # Only supports freq up to WOM-4. See #9425 - func = lambda: date_range('2014-01-01', freq='WOM-5MON') - self.assertRaises(ValueError, func) - - def test_fifth_week_of_month_infer(self): - # Only attempts to infer up to WOM-4. See #9425 - index = DatetimeIndex(["2014-03-31", "2014-06-30", "2015-03-30"]) - assert frequencies.infer_freq(index) is None - - def test_week_of_month_fake(self): - # All of these dates are on same day of week and are 4 or 5 weeks apart - index = DatetimeIndex(["2013-08-27", "2013-10-01", "2013-10-29", - "2013-11-26"]) - assert frequencies.infer_freq(index) != 'WOM-4TUE' - - def test_monthly(self): - self._check_generated_range('1/1/2000', 'M') - - def test_monthly_ambiguous(self): - rng = _dti(['1/31/2000', '2/29/2000', '3/31/2000']) - self.assertEqual(rng.inferred_freq, 'M') - - def test_business_monthly(self): - self._check_generated_range('1/1/2000', 'BM') - - def test_business_start_monthly(self): - self._check_generated_range('1/1/2000', 'BMS') - - def test_quarterly(self): - for month in ['JAN', 'FEB', 'MAR']: - self._check_generated_range('1/1/2000', 'Q-%s' % month) - - def test_annual(self): - for month in MONTHS: - self._check_generated_range('1/1/2000', 'A-%s' % month) - - def test_business_annual(self): - for month in MONTHS: - self._check_generated_range('1/1/2000', 'BA-%s' % month) - - def test_annual_ambiguous(self): - rng = _dti(['1/31/2000', '1/31/2001', '1/31/2002']) - self.assertEqual(rng.inferred_freq, 'A-JAN') - - def _check_generated_range(self, start, freq): - freq = freq.upper() - - gen = date_range(start, periods=7, freq=freq) - index = _dti(gen.values) - if not freq.startswith('Q-'): - self.assertEqual(frequencies.infer_freq(index), gen.freqstr) - else: - inf_freq = frequencies.infer_freq(index) - self.assertTrue((inf_freq == 'Q-DEC' and gen.freqstr in ( - 'Q', 'Q-DEC', 'Q-SEP', 'Q-JUN', 'Q-MAR')) or ( - inf_freq == 'Q-NOV' and gen.freqstr in ( - 'Q-NOV', 'Q-AUG', 'Q-MAY', 'Q-FEB')) or ( - inf_freq == 'Q-OCT' and gen.freqstr in ( - 'Q-OCT', 'Q-JUL', 'Q-APR', 'Q-JAN'))) - - gen = date_range(start, periods=5, freq=freq) - index = _dti(gen.values) - if not freq.startswith('Q-'): - self.assertEqual(frequencies.infer_freq(index), gen.freqstr) - else: - inf_freq = frequencies.infer_freq(index) - self.assertTrue((inf_freq == 'Q-DEC' and gen.freqstr in ( - 'Q', 'Q-DEC', 'Q-SEP', 'Q-JUN', 'Q-MAR')) or ( - inf_freq == 'Q-NOV' and gen.freqstr in ( - 'Q-NOV', 'Q-AUG', 'Q-MAY', 'Q-FEB')) or ( - inf_freq == 'Q-OCT' and gen.freqstr in ( - 'Q-OCT', 'Q-JUL', 'Q-APR', 'Q-JAN'))) - - def test_infer_freq(self): - rng = period_range('1959Q2', '2009Q3', freq='Q') - rng = Index(rng.to_timestamp('D', how='e').asobject) - self.assertEqual(rng.inferred_freq, 'Q-DEC') - - rng = period_range('1959Q2', '2009Q3', freq='Q-NOV') - rng = Index(rng.to_timestamp('D', how='e').asobject) - self.assertEqual(rng.inferred_freq, 'Q-NOV') - - rng = period_range('1959Q2', '2009Q3', freq='Q-OCT') - rng = Index(rng.to_timestamp('D', how='e').asobject) - self.assertEqual(rng.inferred_freq, 'Q-OCT') - - def test_infer_freq_tz(self): - - freqs = {'AS-JAN': - ['2009-01-01', '2010-01-01', '2011-01-01', '2012-01-01'], - 'Q-OCT': - ['2009-01-31', '2009-04-30', '2009-07-31', '2009-10-31'], - 'M': ['2010-11-30', '2010-12-31', '2011-01-31', '2011-02-28'], - 'W-SAT': - ['2010-12-25', '2011-01-01', '2011-01-08', '2011-01-15'], - 'D': ['2011-01-01', '2011-01-02', '2011-01-03', '2011-01-04'], - 'H': ['2011-12-31 22:00', '2011-12-31 23:00', - '2012-01-01 00:00', '2012-01-01 01:00']} - - # GH 7310 - for tz in [None, 'Australia/Sydney', 'Asia/Tokyo', 'Europe/Paris', - 'US/Pacific', 'US/Eastern']: - for expected, dates in compat.iteritems(freqs): - idx = DatetimeIndex(dates, tz=tz) - self.assertEqual(idx.inferred_freq, expected) - - def test_infer_freq_tz_transition(self): - # Tests for #8772 - date_pairs = [['2013-11-02', '2013-11-5'], # Fall DST - ['2014-03-08', '2014-03-11'], # Spring DST - ['2014-01-01', '2014-01-03']] # Regular Time - freqs = ['3H', '10T', '3601S', '3600001L', '3600000001U', - '3600000000001N'] - - for tz in [None, 'Australia/Sydney', 'Asia/Tokyo', 'Europe/Paris', - 'US/Pacific', 'US/Eastern']: - for date_pair in date_pairs: - for freq in freqs: - idx = date_range(date_pair[0], date_pair[ - 1], freq=freq, tz=tz) - self.assertEqual(idx.inferred_freq, freq) - - index = date_range("2013-11-03", periods=5, - freq="3H").tz_localize("America/Chicago") - self.assertIsNone(index.inferred_freq) - - def test_infer_freq_businesshour(self): - # GH 7905 - idx = DatetimeIndex( - ['2014-07-01 09:00', '2014-07-01 10:00', '2014-07-01 11:00', - '2014-07-01 12:00', '2014-07-01 13:00', '2014-07-01 14:00']) - # hourly freq in a day must result in 'H' - self.assertEqual(idx.inferred_freq, 'H') - - idx = DatetimeIndex( - ['2014-07-01 09:00', '2014-07-01 10:00', '2014-07-01 11:00', - '2014-07-01 12:00', '2014-07-01 13:00', '2014-07-01 14:00', - '2014-07-01 15:00', '2014-07-01 16:00', '2014-07-02 09:00', - '2014-07-02 10:00', '2014-07-02 11:00']) - self.assertEqual(idx.inferred_freq, 'BH') - - idx = DatetimeIndex( - ['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', - '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', - '2014-07-04 15:00', '2014-07-04 16:00', '2014-07-07 09:00', - '2014-07-07 10:00', '2014-07-07 11:00']) - self.assertEqual(idx.inferred_freq, 'BH') - - idx = DatetimeIndex( - ['2014-07-04 09:00', '2014-07-04 10:00', '2014-07-04 11:00', - '2014-07-04 12:00', '2014-07-04 13:00', '2014-07-04 14:00', - '2014-07-04 15:00', '2014-07-04 16:00', '2014-07-07 09:00', - '2014-07-07 10:00', '2014-07-07 11:00', '2014-07-07 12:00', - '2014-07-07 13:00', '2014-07-07 14:00', '2014-07-07 15:00', - '2014-07-07 16:00', '2014-07-08 09:00', '2014-07-08 10:00', - '2014-07-08 11:00', '2014-07-08 12:00', '2014-07-08 13:00', - '2014-07-08 14:00', '2014-07-08 15:00', '2014-07-08 16:00']) - self.assertEqual(idx.inferred_freq, 'BH') - - def test_not_monotonic(self): - rng = _dti(['1/31/2000', '1/31/2001', '1/31/2002']) - rng = rng[::-1] - self.assertEqual(rng.inferred_freq, '-1A-JAN') - - def test_non_datetimeindex2(self): - rng = _dti(['1/31/2000', '1/31/2001', '1/31/2002']) - - vals = rng.to_pydatetime() - - result = frequencies.infer_freq(vals) - self.assertEqual(result, rng.inferred_freq) - - def test_invalid_index_types(self): - - # test all index types - for i in [tm.makeIntIndex(10), tm.makeFloatIndex(10), - tm.makePeriodIndex(10)]: - self.assertRaises(TypeError, lambda: frequencies.infer_freq(i)) - - # GH 10822 - # odd error message on conversions to datetime for unicode - if not is_platform_windows(): - for i in [tm.makeStringIndex(10), tm.makeUnicodeIndex(10)]: - self.assertRaises(ValueError, - lambda: frequencies.infer_freq(i)) - - def test_string_datetimelike_compat(self): - - # GH 6463 - expected = frequencies.infer_freq(['2004-01', '2004-02', '2004-03', - '2004-04']) - result = frequencies.infer_freq(Index(['2004-01', '2004-02', '2004-03', - '2004-04'])) - self.assertEqual(result, expected) - - def test_series(self): - - # GH6407 - # inferring series - - # invalid type of Series - for s in [Series(np.arange(10)), Series(np.arange(10.))]: - self.assertRaises(TypeError, lambda: frequencies.infer_freq(s)) - - # a non-convertible string - self.assertRaises(ValueError, - lambda: frequencies.infer_freq( - Series(['foo', 'bar']))) - - # cannot infer on PeriodIndex - for freq in [None, 'L']: - s = Series(period_range('2013', periods=10, freq=freq)) - self.assertRaises(TypeError, lambda: frequencies.infer_freq(s)) - for freq in ['Y']: - - msg = frequencies._INVALID_FREQ_ERROR - with tm.assertRaisesRegexp(ValueError, msg): - s = Series(period_range('2013', periods=10, freq=freq)) - self.assertRaises(TypeError, lambda: frequencies.infer_freq(s)) - - # DateTimeIndex - for freq in ['M', 'L', 'S']: - s = Series(date_range('20130101', periods=10, freq=freq)) - inferred = frequencies.infer_freq(s) - self.assertEqual(inferred, freq) - - s = Series(date_range('20130101', '20130110')) - inferred = frequencies.infer_freq(s) - self.assertEqual(inferred, 'D') - - def test_legacy_offset_warnings(self): - freqs = ['WEEKDAY', 'EOM', 'W@MON', 'W@TUE', 'W@WED', 'W@THU', - 'W@FRI', 'W@SAT', 'W@SUN', 'Q@JAN', 'Q@FEB', 'Q@MAR', - 'A@JAN', 'A@FEB', 'A@MAR', 'A@APR', 'A@MAY', 'A@JUN', - 'A@JUL', 'A@AUG', 'A@SEP', 'A@OCT', 'A@NOV', 'A@DEC', - 'WOM@1MON', 'WOM@2MON', 'WOM@3MON', 'WOM@4MON', - 'WOM@1TUE', 'WOM@2TUE', 'WOM@3TUE', 'WOM@4TUE', - 'WOM@1WED', 'WOM@2WED', 'WOM@3WED', 'WOM@4WED', - 'WOM@1THU', 'WOM@2THU', 'WOM@3THU', 'WOM@4THU' - 'WOM@1FRI', 'WOM@2FRI', 'WOM@3FRI', 'WOM@4FRI'] - - msg = frequencies._INVALID_FREQ_ERROR - for freq in freqs: - with tm.assertRaisesRegexp(ValueError, msg): - frequencies.get_offset(freq) - - with tm.assertRaisesRegexp(ValueError, msg): - date_range('2011-01-01', periods=5, freq=freq) - - -MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', - 'NOV', 'DEC'] - - -def test_is_superperiod_subperiod(): - - # input validation - assert not (frequencies.is_superperiod(offsets.YearEnd(), None)) - assert not (frequencies.is_subperiod(offsets.MonthEnd(), None)) - assert not (frequencies.is_superperiod(None, offsets.YearEnd())) - assert not (frequencies.is_subperiod(None, offsets.MonthEnd())) - assert not (frequencies.is_superperiod(None, None)) - assert not (frequencies.is_subperiod(None, None)) - - assert (frequencies.is_superperiod(offsets.YearEnd(), offsets.MonthEnd())) - assert (frequencies.is_subperiod(offsets.MonthEnd(), offsets.YearEnd())) - - assert (frequencies.is_superperiod(offsets.Hour(), offsets.Minute())) - assert (frequencies.is_subperiod(offsets.Minute(), offsets.Hour())) - - assert (frequencies.is_superperiod(offsets.Second(), offsets.Milli())) - assert (frequencies.is_subperiod(offsets.Milli(), offsets.Second())) - - assert (frequencies.is_superperiod(offsets.Milli(), offsets.Micro())) - assert (frequencies.is_subperiod(offsets.Micro(), offsets.Milli())) - - assert (frequencies.is_superperiod(offsets.Micro(), offsets.Nano())) - assert (frequencies.is_subperiod(offsets.Nano(), offsets.Micro())) diff --git a/pandas/tests/tseries/test_holiday.py b/pandas/tests/tseries/test_holiday.py deleted file mode 100644 index 2adf28a506c53..0000000000000 --- a/pandas/tests/tseries/test_holiday.py +++ /dev/null @@ -1,390 +0,0 @@ -from datetime import datetime -import pandas.util.testing as tm -from pandas import compat -from pandas import DatetimeIndex -from pandas.tseries.holiday import (USFederalHolidayCalendar, USMemorialDay, - USThanksgivingDay, nearest_workday, - next_monday_or_tuesday, next_monday, - previous_friday, sunday_to_monday, Holiday, - DateOffset, MO, SA, Timestamp, - AbstractHolidayCalendar, get_calendar, - HolidayCalendarFactory, next_workday, - previous_workday, before_nearest_workday, - EasterMonday, GoodFriday, - after_nearest_workday, weekend_to_monday, - USLaborDay, USColumbusDay, - USMartinLutherKingJr, USPresidentsDay) -from pytz import utc - - -class TestCalendar(tm.TestCase): - - def setUp(self): - self.holiday_list = [ - datetime(2012, 1, 2), - datetime(2012, 1, 16), - datetime(2012, 2, 20), - datetime(2012, 5, 28), - datetime(2012, 7, 4), - datetime(2012, 9, 3), - datetime(2012, 10, 8), - datetime(2012, 11, 12), - datetime(2012, 11, 22), - datetime(2012, 12, 25)] - - self.start_date = datetime(2012, 1, 1) - self.end_date = datetime(2012, 12, 31) - - def test_calendar(self): - - calendar = USFederalHolidayCalendar() - holidays = calendar.holidays(self.start_date, self.end_date) - - holidays_1 = calendar.holidays( - self.start_date.strftime('%Y-%m-%d'), - self.end_date.strftime('%Y-%m-%d')) - holidays_2 = calendar.holidays( - Timestamp(self.start_date), - Timestamp(self.end_date)) - - self.assertEqual(list(holidays.to_pydatetime()), self.holiday_list) - self.assertEqual(list(holidays_1.to_pydatetime()), self.holiday_list) - self.assertEqual(list(holidays_2.to_pydatetime()), self.holiday_list) - - def test_calendar_caching(self): - # Test for issue #9552 - - class TestCalendar(AbstractHolidayCalendar): - - def __init__(self, name=None, rules=None): - super(TestCalendar, self).__init__(name=name, rules=rules) - - jan1 = TestCalendar(rules=[Holiday('jan1', year=2015, month=1, day=1)]) - jan2 = TestCalendar(rules=[Holiday('jan2', year=2015, month=1, day=2)]) - - tm.assert_index_equal(jan1.holidays(), DatetimeIndex(['01-Jan-2015'])) - tm.assert_index_equal(jan2.holidays(), DatetimeIndex(['02-Jan-2015'])) - - def test_calendar_observance_dates(self): - # Test for issue 11477 - USFedCal = get_calendar('USFederalHolidayCalendar') - holidays0 = USFedCal.holidays(datetime(2015, 7, 3), datetime( - 2015, 7, 3)) # <-- same start and end dates - holidays1 = USFedCal.holidays(datetime(2015, 7, 3), datetime( - 2015, 7, 6)) # <-- different start and end dates - holidays2 = USFedCal.holidays(datetime(2015, 7, 3), datetime( - 2015, 7, 3)) # <-- same start and end dates - - tm.assert_index_equal(holidays0, holidays1) - tm.assert_index_equal(holidays0, holidays2) - - def test_rule_from_name(self): - USFedCal = get_calendar('USFederalHolidayCalendar') - self.assertEqual(USFedCal.rule_from_name( - 'Thanksgiving'), USThanksgivingDay) - - -class TestHoliday(tm.TestCase): - - def setUp(self): - self.start_date = datetime(2011, 1, 1) - self.end_date = datetime(2020, 12, 31) - - def check_results(self, holiday, start, end, expected): - self.assertEqual(list(holiday.dates(start, end)), expected) - # Verify that timezone info is preserved. - self.assertEqual( - list( - holiday.dates( - utc.localize(Timestamp(start)), - utc.localize(Timestamp(end)), - ) - ), - [utc.localize(dt) for dt in expected], - ) - - def test_usmemorialday(self): - self.check_results(holiday=USMemorialDay, - start=self.start_date, - end=self.end_date, - expected=[ - datetime(2011, 5, 30), - datetime(2012, 5, 28), - datetime(2013, 5, 27), - datetime(2014, 5, 26), - datetime(2015, 5, 25), - datetime(2016, 5, 30), - datetime(2017, 5, 29), - datetime(2018, 5, 28), - datetime(2019, 5, 27), - datetime(2020, 5, 25), - ], ) - - def test_non_observed_holiday(self): - - self.check_results( - Holiday('July 4th Eve', month=7, day=3), - start="2001-01-01", - end="2003-03-03", - expected=[ - Timestamp('2001-07-03 00:00:00'), - Timestamp('2002-07-03 00:00:00') - ] - ) - - self.check_results( - Holiday('July 4th Eve', month=7, day=3, days_of_week=(0, 1, 2, 3)), - start="2001-01-01", - end="2008-03-03", - expected=[ - Timestamp('2001-07-03 00:00:00'), - Timestamp('2002-07-03 00:00:00'), - Timestamp('2003-07-03 00:00:00'), - Timestamp('2006-07-03 00:00:00'), - Timestamp('2007-07-03 00:00:00'), - ] - ) - - def test_easter(self): - - self.check_results(EasterMonday, - start=self.start_date, - end=self.end_date, - expected=[ - Timestamp('2011-04-25 00:00:00'), - Timestamp('2012-04-09 00:00:00'), - Timestamp('2013-04-01 00:00:00'), - Timestamp('2014-04-21 00:00:00'), - Timestamp('2015-04-06 00:00:00'), - Timestamp('2016-03-28 00:00:00'), - Timestamp('2017-04-17 00:00:00'), - Timestamp('2018-04-02 00:00:00'), - Timestamp('2019-04-22 00:00:00'), - Timestamp('2020-04-13 00:00:00'), - ], ) - self.check_results(GoodFriday, - start=self.start_date, - end=self.end_date, - expected=[ - Timestamp('2011-04-22 00:00:00'), - Timestamp('2012-04-06 00:00:00'), - Timestamp('2013-03-29 00:00:00'), - Timestamp('2014-04-18 00:00:00'), - Timestamp('2015-04-03 00:00:00'), - Timestamp('2016-03-25 00:00:00'), - Timestamp('2017-04-14 00:00:00'), - Timestamp('2018-03-30 00:00:00'), - Timestamp('2019-04-19 00:00:00'), - Timestamp('2020-04-10 00:00:00'), - ], ) - - def test_usthanksgivingday(self): - - self.check_results(USThanksgivingDay, - start=self.start_date, - end=self.end_date, - expected=[ - datetime(2011, 11, 24), - datetime(2012, 11, 22), - datetime(2013, 11, 28), - datetime(2014, 11, 27), - datetime(2015, 11, 26), - datetime(2016, 11, 24), - datetime(2017, 11, 23), - datetime(2018, 11, 22), - datetime(2019, 11, 28), - datetime(2020, 11, 26), - ], ) - - def test_holidays_within_dates(self): - # Fix holiday behavior found in #11477 - # where holiday.dates returned dates outside start/end date - # or observed rules could not be applied as the holiday - # was not in the original date range (e.g., 7/4/2015 -> 7/3/2015) - start_date = datetime(2015, 7, 1) - end_date = datetime(2015, 7, 1) - - calendar = get_calendar('USFederalHolidayCalendar') - new_years = calendar.rule_from_name('New Years Day') - july_4th = calendar.rule_from_name('July 4th') - veterans_day = calendar.rule_from_name('Veterans Day') - christmas = calendar.rule_from_name('Christmas') - - # Holiday: (start/end date, holiday) - holidays = {USMemorialDay: ("2015-05-25", "2015-05-25"), - USLaborDay: ("2015-09-07", "2015-09-07"), - USColumbusDay: ("2015-10-12", "2015-10-12"), - USThanksgivingDay: ("2015-11-26", "2015-11-26"), - USMartinLutherKingJr: ("2015-01-19", "2015-01-19"), - USPresidentsDay: ("2015-02-16", "2015-02-16"), - GoodFriday: ("2015-04-03", "2015-04-03"), - EasterMonday: [("2015-04-06", "2015-04-06"), - ("2015-04-05", [])], - new_years: [("2015-01-01", "2015-01-01"), - ("2011-01-01", []), - ("2010-12-31", "2010-12-31")], - july_4th: [("2015-07-03", "2015-07-03"), - ("2015-07-04", [])], - veterans_day: [("2012-11-11", []), - ("2012-11-12", "2012-11-12")], - christmas: [("2011-12-25", []), - ("2011-12-26", "2011-12-26")]} - - for rule, dates in compat.iteritems(holidays): - empty_dates = rule.dates(start_date, end_date) - self.assertEqual(empty_dates.tolist(), []) - - if isinstance(dates, tuple): - dates = [dates] - - for start, expected in dates: - if len(expected): - expected = [Timestamp(expected)] - self.check_results(rule, start, start, expected) - - def test_argument_types(self): - holidays = USThanksgivingDay.dates(self.start_date, self.end_date) - - holidays_1 = USThanksgivingDay.dates( - self.start_date.strftime('%Y-%m-%d'), - self.end_date.strftime('%Y-%m-%d')) - - holidays_2 = USThanksgivingDay.dates( - Timestamp(self.start_date), - Timestamp(self.end_date)) - - self.assert_index_equal(holidays, holidays_1) - self.assert_index_equal(holidays, holidays_2) - - def test_special_holidays(self): - base_date = [datetime(2012, 5, 28)] - holiday_1 = Holiday('One-Time', year=2012, month=5, day=28) - holiday_2 = Holiday('Range', month=5, day=28, - start_date=datetime(2012, 1, 1), - end_date=datetime(2012, 12, 31), - offset=DateOffset(weekday=MO(1))) - - self.assertEqual(base_date, - holiday_1.dates(self.start_date, self.end_date)) - self.assertEqual(base_date, - holiday_2.dates(self.start_date, self.end_date)) - - def test_get_calendar(self): - class TestCalendar(AbstractHolidayCalendar): - rules = [] - - calendar = get_calendar('TestCalendar') - self.assertEqual(TestCalendar, calendar.__class__) - - def test_factory(self): - class_1 = HolidayCalendarFactory('MemorialDay', - AbstractHolidayCalendar, - USMemorialDay) - class_2 = HolidayCalendarFactory('Thansksgiving', - AbstractHolidayCalendar, - USThanksgivingDay) - class_3 = HolidayCalendarFactory('Combined', class_1, class_2) - - self.assertEqual(len(class_1.rules), 1) - self.assertEqual(len(class_2.rules), 1) - self.assertEqual(len(class_3.rules), 2) - - -class TestObservanceRules(tm.TestCase): - - def setUp(self): - self.we = datetime(2014, 4, 9) - self.th = datetime(2014, 4, 10) - self.fr = datetime(2014, 4, 11) - self.sa = datetime(2014, 4, 12) - self.su = datetime(2014, 4, 13) - self.mo = datetime(2014, 4, 14) - self.tu = datetime(2014, 4, 15) - - def test_next_monday(self): - self.assertEqual(next_monday(self.sa), self.mo) - self.assertEqual(next_monday(self.su), self.mo) - - def test_next_monday_or_tuesday(self): - self.assertEqual(next_monday_or_tuesday(self.sa), self.mo) - self.assertEqual(next_monday_or_tuesday(self.su), self.tu) - self.assertEqual(next_monday_or_tuesday(self.mo), self.tu) - - def test_previous_friday(self): - self.assertEqual(previous_friday(self.sa), self.fr) - self.assertEqual(previous_friday(self.su), self.fr) - - def test_sunday_to_monday(self): - self.assertEqual(sunday_to_monday(self.su), self.mo) - - def test_nearest_workday(self): - self.assertEqual(nearest_workday(self.sa), self.fr) - self.assertEqual(nearest_workday(self.su), self.mo) - self.assertEqual(nearest_workday(self.mo), self.mo) - - def test_weekend_to_monday(self): - self.assertEqual(weekend_to_monday(self.sa), self.mo) - self.assertEqual(weekend_to_monday(self.su), self.mo) - self.assertEqual(weekend_to_monday(self.mo), self.mo) - - def test_next_workday(self): - self.assertEqual(next_workday(self.sa), self.mo) - self.assertEqual(next_workday(self.su), self.mo) - self.assertEqual(next_workday(self.mo), self.tu) - - def test_previous_workday(self): - self.assertEqual(previous_workday(self.sa), self.fr) - self.assertEqual(previous_workday(self.su), self.fr) - self.assertEqual(previous_workday(self.tu), self.mo) - - def test_before_nearest_workday(self): - self.assertEqual(before_nearest_workday(self.sa), self.th) - self.assertEqual(before_nearest_workday(self.su), self.fr) - self.assertEqual(before_nearest_workday(self.tu), self.mo) - - def test_after_nearest_workday(self): - self.assertEqual(after_nearest_workday(self.sa), self.mo) - self.assertEqual(after_nearest_workday(self.su), self.tu) - self.assertEqual(after_nearest_workday(self.fr), self.mo) - - -class TestFederalHolidayCalendar(tm.TestCase): - - # Test for issue 10278 - def test_no_mlk_before_1984(self): - class MLKCalendar(AbstractHolidayCalendar): - rules = [USMartinLutherKingJr] - - holidays = MLKCalendar().holidays(start='1984', - end='1988').to_pydatetime().tolist() - # Testing to make sure holiday is not incorrectly observed before 1986 - self.assertEqual(holidays, [datetime(1986, 1, 20, 0, 0), datetime( - 1987, 1, 19, 0, 0)]) - - def test_memorial_day(self): - class MemorialDay(AbstractHolidayCalendar): - rules = [USMemorialDay] - - holidays = MemorialDay().holidays(start='1971', - end='1980').to_pydatetime().tolist() - # Fixes 5/31 error and checked manually against wikipedia - self.assertEqual(holidays, [datetime(1971, 5, 31, 0, 0), - datetime(1972, 5, 29, 0, 0), - datetime(1973, 5, 28, 0, 0), - datetime(1974, 5, 27, 0, - 0), datetime(1975, 5, 26, 0, 0), - datetime(1976, 5, 31, 0, - 0), datetime(1977, 5, 30, 0, 0), - datetime(1978, 5, 29, 0, - 0), datetime(1979, 5, 28, 0, 0)]) - - -class TestHolidayConflictingArguments(tm.TestCase): - - # GH 10217 - - def test_both_offset_observance_raises(self): - with self.assertRaises(NotImplementedError): - Holiday("Cyber Monday", month=11, day=1, - offset=[DateOffset(weekday=SA(4))], - observance=next_monday) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py deleted file mode 100644 index f644c353982f6..0000000000000 --- a/pandas/tests/tseries/test_offsets.py +++ /dev/null @@ -1,4962 +0,0 @@ -import os -from distutils.version import LooseVersion -from datetime import date, datetime, timedelta -from dateutil.relativedelta import relativedelta - -import pytest -from pandas.compat import range, iteritems -from pandas import compat - -import numpy as np - -from pandas.compat.numpy import np_datetime64_compat - -from pandas.core.series import Series -from pandas.tseries.frequencies import (_offset_map, get_freq_code, - _get_freq_str, _INVALID_FREQ_ERROR, - get_offset, get_standard_freq) -from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache -from pandas.tseries.offsets import (BDay, CDay, BQuarterEnd, BMonthEnd, - BusinessHour, WeekOfMonth, CBMonthEnd, - CustomBusinessHour, WeekDay, - CBMonthBegin, BYearEnd, MonthEnd, - MonthBegin, SemiMonthBegin, SemiMonthEnd, - BYearBegin, QuarterBegin, BQuarterBegin, - BMonthBegin, DateOffset, Week, YearBegin, - YearEnd, Hour, Minute, Second, Day, Micro, - QuarterEnd, BusinessMonthEnd, FY5253, - Milli, Nano, Easter, FY5253Quarter, - LastWeekOfMonth, CacheableOffset) -from pandas.tseries.tools import (format, ole2datetime, parse_time_string, - to_datetime, DateParseError) -import pandas.tseries.offsets as offsets -from pandas.io.pickle import read_pickle -from pandas._libs.tslib import normalize_date, NaT, Timestamp, Timedelta -import pandas._libs.tslib as tslib -from pandas.util.testing import assertRaisesRegexp -import pandas.util.testing as tm -from pandas.tseries.holiday import USFederalHolidayCalendar - - -def test_monthrange(): - import calendar - for y in range(2000, 2013): - for m in range(1, 13): - assert tslib.monthrange(y, m) == calendar.monthrange(y, m) - -#### -# Misc function tests -#### - - -def test_format(): - actual = format(datetime(2008, 1, 15)) - assert actual == '20080115' - - -def test_ole2datetime(): - actual = ole2datetime(60000) - assert actual == datetime(2064, 4, 8) - - with pytest.raises(ValueError): - ole2datetime(60) - - -def test_to_datetime1(): - actual = to_datetime(datetime(2008, 1, 15)) - assert actual == datetime(2008, 1, 15) - - actual = to_datetime('20080115') - assert actual == datetime(2008, 1, 15) - - # unparseable - s = 'Month 1, 1999' - assert to_datetime(s, errors='ignore') == s - - -def test_normalize_date(): - actual = normalize_date(datetime(2007, 10, 1, 1, 12, 5, 10)) - assert actual == datetime(2007, 10, 1) - - -def test_to_m8(): - valb = datetime(2007, 10, 1) - valu = _to_m8(valb) - tm.assertIsInstance(valu, np.datetime64) - # assert valu == np.datetime64(datetime(2007,10,1)) - - # def test_datetime64_box(): - # valu = np.datetime64(datetime(2007,10,1)) - # valb = _dt_box(valu) - # assert type(valb) == datetime - # assert valb == datetime(2007,10,1) - - ##### - # DateOffset Tests - ##### - - -class Base(tm.TestCase): - _offset = None - - _offset_types = [getattr(offsets, o) for o in offsets.__all__] - - timezones = [None, 'UTC', 'Asia/Tokyo', 'US/Eastern', - 'dateutil/Asia/Tokyo', 'dateutil/US/Pacific'] - - @property - def offset_types(self): - return self._offset_types - - def _get_offset(self, klass, value=1, normalize=False): - # create instance from offset class - if klass is FY5253 or klass is FY5253Quarter: - klass = klass(n=value, startingMonth=1, weekday=1, - qtr_with_extra_week=1, variation='last', - normalize=normalize) - elif klass is LastWeekOfMonth: - klass = klass(n=value, weekday=5, normalize=normalize) - elif klass is WeekOfMonth: - klass = klass(n=value, week=1, weekday=5, normalize=normalize) - elif klass is Week: - klass = klass(n=value, weekday=5, normalize=normalize) - elif klass is DateOffset: - klass = klass(days=value, normalize=normalize) - else: - try: - klass = klass(value, normalize=normalize) - except: - klass = klass(normalize=normalize) - return klass - - def test_apply_out_of_range(self): - if self._offset is None: - return - - # try to create an out-of-bounds result timestamp; if we can't create - # the offset skip - try: - if self._offset in (BusinessHour, CustomBusinessHour): - # Using 10000 in BusinessHour fails in tz check because of DST - # difference - offset = self._get_offset(self._offset, value=100000) - else: - offset = self._get_offset(self._offset, value=10000) - - result = Timestamp('20080101') + offset - self.assertIsInstance(result, datetime) - self.assertIsNone(result.tzinfo) - - tm._skip_if_no_pytz() - tm._skip_if_no_dateutil() - # Check tz is preserved - for tz in self.timezones: - t = Timestamp('20080101', tz=tz) - result = t + offset - self.assertIsInstance(result, datetime) - self.assertEqual(t.tzinfo, result.tzinfo) - - except (tslib.OutOfBoundsDatetime): - raise - except (ValueError, KeyError) as e: - pytest.skip( - "cannot create out_of_range offset: {0} {1}".format( - str(self).split('.')[-1], e)) - - -class TestCommon(Base): - - def setUp(self): - # exected value created by Base._get_offset - # are applied to 2011/01/01 09:00 (Saturday) - # used for .apply and .rollforward - self.expecteds = {'Day': Timestamp('2011-01-02 09:00:00'), - 'DateOffset': Timestamp('2011-01-02 09:00:00'), - 'BusinessDay': Timestamp('2011-01-03 09:00:00'), - 'CustomBusinessDay': - Timestamp('2011-01-03 09:00:00'), - 'CustomBusinessMonthEnd': - Timestamp('2011-01-31 09:00:00'), - 'CustomBusinessMonthBegin': - Timestamp('2011-01-03 09:00:00'), - 'MonthBegin': Timestamp('2011-02-01 09:00:00'), - 'BusinessMonthBegin': - Timestamp('2011-01-03 09:00:00'), - 'MonthEnd': Timestamp('2011-01-31 09:00:00'), - 'SemiMonthEnd': Timestamp('2011-01-15 09:00:00'), - 'SemiMonthBegin': Timestamp('2011-01-15 09:00:00'), - 'BusinessMonthEnd': Timestamp('2011-01-31 09:00:00'), - 'YearBegin': Timestamp('2012-01-01 09:00:00'), - 'BYearBegin': Timestamp('2011-01-03 09:00:00'), - 'YearEnd': Timestamp('2011-12-31 09:00:00'), - 'BYearEnd': Timestamp('2011-12-30 09:00:00'), - 'QuarterBegin': Timestamp('2011-03-01 09:00:00'), - 'BQuarterBegin': Timestamp('2011-03-01 09:00:00'), - 'QuarterEnd': Timestamp('2011-03-31 09:00:00'), - 'BQuarterEnd': Timestamp('2011-03-31 09:00:00'), - 'BusinessHour': Timestamp('2011-01-03 10:00:00'), - 'CustomBusinessHour': - Timestamp('2011-01-03 10:00:00'), - 'WeekOfMonth': Timestamp('2011-01-08 09:00:00'), - 'LastWeekOfMonth': Timestamp('2011-01-29 09:00:00'), - 'FY5253Quarter': Timestamp('2011-01-25 09:00:00'), - 'FY5253': Timestamp('2011-01-25 09:00:00'), - 'Week': Timestamp('2011-01-08 09:00:00'), - 'Easter': Timestamp('2011-04-24 09:00:00'), - 'Hour': Timestamp('2011-01-01 10:00:00'), - 'Minute': Timestamp('2011-01-01 09:01:00'), - 'Second': Timestamp('2011-01-01 09:00:01'), - 'Milli': Timestamp('2011-01-01 09:00:00.001000'), - 'Micro': Timestamp('2011-01-01 09:00:00.000001'), - 'Nano': Timestamp(np_datetime64_compat( - '2011-01-01T09:00:00.000000001Z'))} - - def test_return_type(self): - for offset in self.offset_types: - offset = self._get_offset(offset) - - # make sure that we are returning a Timestamp - result = Timestamp('20080101') + offset - self.assertIsInstance(result, Timestamp) - - # make sure that we are returning NaT - self.assertTrue(NaT + offset is NaT) - self.assertTrue(offset + NaT is NaT) - - self.assertTrue(NaT - offset is NaT) - self.assertTrue((-offset).apply(NaT) is NaT) - - def test_offset_n(self): - for offset_klass in self.offset_types: - offset = self._get_offset(offset_klass) - self.assertEqual(offset.n, 1) - - neg_offset = offset * -1 - self.assertEqual(neg_offset.n, -1) - - mul_offset = offset * 3 - self.assertEqual(mul_offset.n, 3) - - def test_offset_freqstr(self): - for offset_klass in self.offset_types: - offset = self._get_offset(offset_klass) - - freqstr = offset.freqstr - if freqstr not in ('', - "", - 'LWOM-SAT', ): - code = get_offset(freqstr) - self.assertEqual(offset.rule_code, code) - - def _check_offsetfunc_works(self, offset, funcname, dt, expected, - normalize=False): - offset_s = self._get_offset(offset, normalize=normalize) - func = getattr(offset_s, funcname) - - result = func(dt) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected) - - result = func(Timestamp(dt)) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected) - - # see gh-14101 - exp_warning = None - ts = Timestamp(dt) + Nano(5) - - if (offset_s.__class__.__name__ == 'DateOffset' and - (funcname == 'apply' or normalize) and - ts.nanosecond > 0): - exp_warning = UserWarning - - # test nanosecond is preserved - with tm.assert_produces_warning(exp_warning, - check_stacklevel=False): - result = func(ts) - self.assertTrue(isinstance(result, Timestamp)) - if normalize is False: - self.assertEqual(result, expected + Nano(5)) - else: - self.assertEqual(result, expected) - - if isinstance(dt, np.datetime64): - # test tz when input is datetime or Timestamp - return - - tm._skip_if_no_pytz() - tm._skip_if_no_dateutil() - - for tz in self.timezones: - expected_localize = expected.tz_localize(tz) - tz_obj = tslib.maybe_get_tz(tz) - dt_tz = tslib._localize_pydatetime(dt, tz_obj) - - result = func(dt_tz) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected_localize) - - result = func(Timestamp(dt, tz=tz)) - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected_localize) - - # see gh-14101 - exp_warning = None - ts = Timestamp(dt, tz=tz) + Nano(5) - - if (offset_s.__class__.__name__ == 'DateOffset' and - (funcname == 'apply' or normalize) and - ts.nanosecond > 0): - exp_warning = UserWarning - - # test nanosecond is preserved - with tm.assert_produces_warning(exp_warning, - check_stacklevel=False): - result = func(ts) - self.assertTrue(isinstance(result, Timestamp)) - if normalize is False: - self.assertEqual(result, expected_localize + Nano(5)) - else: - self.assertEqual(result, expected_localize) - - def test_apply(self): - sdt = datetime(2011, 1, 1, 9, 0) - ndt = np_datetime64_compat('2011-01-01 09:00Z') - - for offset in self.offset_types: - for dt in [sdt, ndt]: - expected = self.expecteds[offset.__name__] - self._check_offsetfunc_works(offset, 'apply', dt, expected) - - expected = Timestamp(expected.date()) - self._check_offsetfunc_works(offset, 'apply', dt, expected, - normalize=True) - - def test_rollforward(self): - expecteds = self.expecteds.copy() - - # result will not be changed if the target is on the offset - no_changes = ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', - 'Week', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', - 'Nano', 'DateOffset'] - for n in no_changes: - expecteds[n] = Timestamp('2011/01/01 09:00') - - expecteds['BusinessHour'] = Timestamp('2011-01-03 09:00:00') - expecteds['CustomBusinessHour'] = Timestamp('2011-01-03 09:00:00') - - # but be changed when normalize=True - norm_expected = expecteds.copy() - for k in norm_expected: - norm_expected[k] = Timestamp(norm_expected[k].date()) - - normalized = {'Day': Timestamp('2011-01-02 00:00:00'), - 'DateOffset': Timestamp('2011-01-02 00:00:00'), - 'MonthBegin': Timestamp('2011-02-01 00:00:00'), - 'SemiMonthBegin': Timestamp('2011-01-15 00:00:00'), - 'YearBegin': Timestamp('2012-01-01 00:00:00'), - 'Week': Timestamp('2011-01-08 00:00:00'), - 'Hour': Timestamp('2011-01-01 00:00:00'), - 'Minute': Timestamp('2011-01-01 00:00:00'), - 'Second': Timestamp('2011-01-01 00:00:00'), - 'Milli': Timestamp('2011-01-01 00:00:00'), - 'Micro': Timestamp('2011-01-01 00:00:00')} - norm_expected.update(normalized) - - sdt = datetime(2011, 1, 1, 9, 0) - ndt = np_datetime64_compat('2011-01-01 09:00Z') - - for offset in self.offset_types: - for dt in [sdt, ndt]: - expected = expecteds[offset.__name__] - self._check_offsetfunc_works(offset, 'rollforward', dt, - expected) - expected = norm_expected[offset.__name__] - self._check_offsetfunc_works(offset, 'rollforward', dt, - expected, normalize=True) - - def test_rollback(self): - expecteds = {'BusinessDay': Timestamp('2010-12-31 09:00:00'), - 'CustomBusinessDay': Timestamp('2010-12-31 09:00:00'), - 'CustomBusinessMonthEnd': - Timestamp('2010-12-31 09:00:00'), - 'CustomBusinessMonthBegin': - Timestamp('2010-12-01 09:00:00'), - 'BusinessMonthBegin': Timestamp('2010-12-01 09:00:00'), - 'MonthEnd': Timestamp('2010-12-31 09:00:00'), - 'SemiMonthEnd': Timestamp('2010-12-31 09:00:00'), - 'BusinessMonthEnd': Timestamp('2010-12-31 09:00:00'), - 'BYearBegin': Timestamp('2010-01-01 09:00:00'), - 'YearEnd': Timestamp('2010-12-31 09:00:00'), - 'BYearEnd': Timestamp('2010-12-31 09:00:00'), - 'QuarterBegin': Timestamp('2010-12-01 09:00:00'), - 'BQuarterBegin': Timestamp('2010-12-01 09:00:00'), - 'QuarterEnd': Timestamp('2010-12-31 09:00:00'), - 'BQuarterEnd': Timestamp('2010-12-31 09:00:00'), - 'BusinessHour': Timestamp('2010-12-31 17:00:00'), - 'CustomBusinessHour': Timestamp('2010-12-31 17:00:00'), - 'WeekOfMonth': Timestamp('2010-12-11 09:00:00'), - 'LastWeekOfMonth': Timestamp('2010-12-25 09:00:00'), - 'FY5253Quarter': Timestamp('2010-10-26 09:00:00'), - 'FY5253': Timestamp('2010-01-26 09:00:00'), - 'Easter': Timestamp('2010-04-04 09:00:00')} - - # result will not be changed if the target is on the offset - for n in ['Day', 'MonthBegin', 'SemiMonthBegin', 'YearBegin', 'Week', - 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano', - 'DateOffset']: - expecteds[n] = Timestamp('2011/01/01 09:00') - - # but be changed when normalize=True - norm_expected = expecteds.copy() - for k in norm_expected: - norm_expected[k] = Timestamp(norm_expected[k].date()) - - normalized = {'Day': Timestamp('2010-12-31 00:00:00'), - 'DateOffset': Timestamp('2010-12-31 00:00:00'), - 'MonthBegin': Timestamp('2010-12-01 00:00:00'), - 'SemiMonthBegin': Timestamp('2010-12-15 00:00:00'), - 'YearBegin': Timestamp('2010-01-01 00:00:00'), - 'Week': Timestamp('2010-12-25 00:00:00'), - 'Hour': Timestamp('2011-01-01 00:00:00'), - 'Minute': Timestamp('2011-01-01 00:00:00'), - 'Second': Timestamp('2011-01-01 00:00:00'), - 'Milli': Timestamp('2011-01-01 00:00:00'), - 'Micro': Timestamp('2011-01-01 00:00:00')} - norm_expected.update(normalized) - - sdt = datetime(2011, 1, 1, 9, 0) - ndt = np_datetime64_compat('2011-01-01 09:00Z') - - for offset in self.offset_types: - for dt in [sdt, ndt]: - expected = expecteds[offset.__name__] - self._check_offsetfunc_works(offset, 'rollback', dt, expected) - - expected = norm_expected[offset.__name__] - self._check_offsetfunc_works(offset, 'rollback', dt, expected, - normalize=True) - - def test_onOffset(self): - for offset in self.offset_types: - dt = self.expecteds[offset.__name__] - offset_s = self._get_offset(offset) - self.assertTrue(offset_s.onOffset(dt)) - - # when normalize=True, onOffset checks time is 00:00:00 - offset_n = self._get_offset(offset, normalize=True) - self.assertFalse(offset_n.onOffset(dt)) - - if offset in (BusinessHour, CustomBusinessHour): - # In default BusinessHour (9:00-17:00), normalized time - # cannot be in business hour range - continue - date = datetime(dt.year, dt.month, dt.day) - self.assertTrue(offset_n.onOffset(date)) - - def test_add(self): - dt = datetime(2011, 1, 1, 9, 0) - - for offset in self.offset_types: - offset_s = self._get_offset(offset) - expected = self.expecteds[offset.__name__] - - result_dt = dt + offset_s - result_ts = Timestamp(dt) + offset_s - for result in [result_dt, result_ts]: - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected) - - tm._skip_if_no_pytz() - for tz in self.timezones: - expected_localize = expected.tz_localize(tz) - result = Timestamp(dt, tz=tz) + offset_s - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected_localize) - - # normalize=True - offset_s = self._get_offset(offset, normalize=True) - expected = Timestamp(expected.date()) - - result_dt = dt + offset_s - result_ts = Timestamp(dt) + offset_s - for result in [result_dt, result_ts]: - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected) - - for tz in self.timezones: - expected_localize = expected.tz_localize(tz) - result = Timestamp(dt, tz=tz) + offset_s - self.assertTrue(isinstance(result, Timestamp)) - self.assertEqual(result, expected_localize) - - def test_pickle_v0_15_2(self): - offsets = {'DateOffset': DateOffset(years=1), - 'MonthBegin': MonthBegin(1), - 'Day': Day(1), - 'YearBegin': YearBegin(1), - 'Week': Week(1)} - pickle_path = os.path.join(tm.get_data_path(), - 'dateoffset_0_15_2.pickle') - # This code was executed once on v0.15.2 to generate the pickle: - # with open(pickle_path, 'wb') as f: pickle.dump(offsets, f) - # - tm.assert_dict_equal(offsets, read_pickle(pickle_path)) - - -class TestDateOffset(Base): - - def setUp(self): - self.d = Timestamp(datetime(2008, 1, 2)) - _offset_map.clear() - - def test_repr(self): - repr(DateOffset()) - repr(DateOffset(2)) - repr(2 * DateOffset()) - repr(2 * DateOffset(months=2)) - - def test_mul(self): - assert DateOffset(2) == 2 * DateOffset(1) - assert DateOffset(2) == DateOffset(1) * 2 - - def test_constructor(self): - - assert ((self.d + DateOffset(months=2)) == datetime(2008, 3, 2)) - assert ((self.d - DateOffset(months=2)) == datetime(2007, 11, 2)) - - assert ((self.d + DateOffset(2)) == datetime(2008, 1, 4)) - - assert not DateOffset(2).isAnchored() - assert DateOffset(1).isAnchored() - - d = datetime(2008, 1, 31) - assert ((d + DateOffset(months=1)) == datetime(2008, 2, 29)) - - def test_copy(self): - assert (DateOffset(months=2).copy() == DateOffset(months=2)) - - def test_eq(self): - offset1 = DateOffset(days=1) - offset2 = DateOffset(days=365) - - self.assertNotEqual(offset1, offset2) - - -class TestBusinessDay(Base): - _offset = BDay - - def setUp(self): - self.d = datetime(2008, 1, 1) - - self.offset = BDay() - self.offset2 = BDay(2) - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = BDay() - offset2 = BDay() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - self.assertEqual(repr(self.offset), '') - assert repr(self.offset2) == '<2 * BusinessDays>' - - expected = '' - assert repr(self.offset + timedelta(1)) == expected - - def test_with_offset(self): - offset = self.offset + timedelta(hours=2) - - assert (self.d + offset) == datetime(2008, 1, 2, 2) - - def testEQ(self): - self.assertEqual(self.offset2, self.offset2) - - def test_mul(self): - pass - - def test_hash(self): - self.assertEqual(hash(self.offset2), hash(self.offset2)) - - def testCall(self): - self.assertEqual(self.offset2(self.d), datetime(2008, 1, 3)) - - def testRAdd(self): - self.assertEqual(self.d + self.offset2, self.offset2 + self.d) - - def testSub(self): - off = self.offset2 - self.assertRaises(Exception, off.__sub__, self.d) - self.assertEqual(2 * off - off, off) - - self.assertEqual(self.d - self.offset2, self.d + BDay(-2)) - - def testRSub(self): - self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) - - def testMult1(self): - self.assertEqual(self.d + 10 * self.offset, self.d + BDay(10)) - - def testMult2(self): - self.assertEqual(self.d + (-5 * BDay(-10)), self.d + BDay(50)) - - def testRollback1(self): - self.assertEqual(BDay(10).rollback(self.d), self.d) - - def testRollback2(self): - self.assertEqual( - BDay(10).rollback(datetime(2008, 1, 5)), datetime(2008, 1, 4)) - - def testRollforward1(self): - self.assertEqual(BDay(10).rollforward(self.d), self.d) - - def testRollforward2(self): - self.assertEqual( - BDay(10).rollforward(datetime(2008, 1, 5)), datetime(2008, 1, 7)) - - def test_roll_date_object(self): - offset = BDay() - - dt = date(2012, 9, 15) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 14)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 17)) - - offset = offsets.Day() - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - def test_onOffset(self): - tests = [(BDay(), datetime(2008, 1, 1), True), - (BDay(), datetime(2008, 1, 5), False)] - - for offset, d, expected in tests: - assertOnOffset(offset, d, expected) - - def test_apply(self): - tests = [] - - tests.append((BDay(), {datetime(2008, 1, 1): datetime(2008, 1, 2), - datetime(2008, 1, 4): datetime(2008, 1, 7), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 8)})) - - tests.append((2 * BDay(), {datetime(2008, 1, 1): datetime(2008, 1, 3), - datetime(2008, 1, 4): datetime(2008, 1, 8), - datetime(2008, 1, 5): datetime(2008, 1, 8), - datetime(2008, 1, 6): datetime(2008, 1, 8), - datetime(2008, 1, 7): datetime(2008, 1, 9)} - )) - - tests.append((-BDay(), {datetime(2008, 1, 1): datetime(2007, 12, 31), - datetime(2008, 1, 4): datetime(2008, 1, 3), - datetime(2008, 1, 5): datetime(2008, 1, 4), - datetime(2008, 1, 6): datetime(2008, 1, 4), - datetime(2008, 1, 7): datetime(2008, 1, 4), - datetime(2008, 1, 8): datetime(2008, 1, 7)} - )) - - tests.append((-2 * BDay(), { - datetime(2008, 1, 1): datetime(2007, 12, 28), - datetime(2008, 1, 4): datetime(2008, 1, 2), - datetime(2008, 1, 5): datetime(2008, 1, 3), - datetime(2008, 1, 6): datetime(2008, 1, 3), - datetime(2008, 1, 7): datetime(2008, 1, 3), - datetime(2008, 1, 8): datetime(2008, 1, 4), - datetime(2008, 1, 9): datetime(2008, 1, 7)} - )) - - tests.append((BDay(0), {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 1, 4): datetime(2008, 1, 4), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 7)} - )) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_large_n(self): - dt = datetime(2012, 10, 23) - - result = dt + BDay(10) - self.assertEqual(result, datetime(2012, 11, 6)) - - result = dt + BDay(100) - BDay(100) - self.assertEqual(result, dt) - - off = BDay() * 6 - rs = datetime(2012, 1, 1) - off - xp = datetime(2011, 12, 23) - self.assertEqual(rs, xp) - - st = datetime(2011, 12, 18) - rs = st + off - xp = datetime(2011, 12, 26) - self.assertEqual(rs, xp) - - off = BDay() * 10 - rs = datetime(2014, 1, 5) + off # see #5890 - xp = datetime(2014, 1, 17) - self.assertEqual(rs, xp) - - def test_apply_corner(self): - self.assertRaises(TypeError, BDay().apply, BMonthEnd()) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = BDay() - offset2 = BDay() - self.assertFalse(offset1 != offset2) - - -class TestBusinessHour(Base): - _offset = BusinessHour - - def setUp(self): - self.d = datetime(2014, 7, 1, 10, 00) - - self.offset1 = BusinessHour() - self.offset2 = BusinessHour(n=3) - - self.offset3 = BusinessHour(n=-1) - self.offset4 = BusinessHour(n=-4) - - from datetime import time as dt_time - self.offset5 = BusinessHour(start=dt_time(11, 0), end=dt_time(14, 30)) - self.offset6 = BusinessHour(start='20:00', end='05:00') - self.offset7 = BusinessHour(n=-2, start=dt_time(21, 30), - end=dt_time(6, 30)) - - def test_constructor_errors(self): - from datetime import time as dt_time - with tm.assertRaises(ValueError): - BusinessHour(start=dt_time(11, 0, 5)) - with tm.assertRaises(ValueError): - BusinessHour(start='AAA') - with tm.assertRaises(ValueError): - BusinessHour(start='14:00:05') - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = self._offset() - offset2 = self._offset() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - self.assertEqual(repr(self.offset1), '') - self.assertEqual(repr(self.offset2), - '<3 * BusinessHours: BH=09:00-17:00>') - self.assertEqual(repr(self.offset3), - '<-1 * BusinessHour: BH=09:00-17:00>') - self.assertEqual(repr(self.offset4), - '<-4 * BusinessHours: BH=09:00-17:00>') - - self.assertEqual(repr(self.offset5), '') - self.assertEqual(repr(self.offset6), '') - self.assertEqual(repr(self.offset7), - '<-2 * BusinessHours: BH=21:30-06:30>') - - def test_with_offset(self): - expected = Timestamp('2014-07-01 13:00') - - self.assertEqual(self.d + BusinessHour() * 3, expected) - self.assertEqual(self.d + BusinessHour(n=3), expected) - - def testEQ(self): - for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: - self.assertEqual(offset, offset) - - self.assertNotEqual(BusinessHour(), BusinessHour(-1)) - self.assertEqual(BusinessHour(start='09:00'), BusinessHour()) - self.assertNotEqual(BusinessHour(start='09:00'), - BusinessHour(start='09:01')) - self.assertNotEqual(BusinessHour(start='09:00', end='17:00'), - BusinessHour(start='17:00', end='09:01')) - - def test_hash(self): - for offset in [self.offset1, self.offset2, self.offset3, self.offset4]: - self.assertEqual(hash(offset), hash(offset)) - - def testCall(self): - self.assertEqual(self.offset1(self.d), datetime(2014, 7, 1, 11)) - self.assertEqual(self.offset2(self.d), datetime(2014, 7, 1, 13)) - self.assertEqual(self.offset3(self.d), datetime(2014, 6, 30, 17)) - self.assertEqual(self.offset4(self.d), datetime(2014, 6, 30, 14)) - - def testRAdd(self): - self.assertEqual(self.d + self.offset2, self.offset2 + self.d) - - def testSub(self): - off = self.offset2 - self.assertRaises(Exception, off.__sub__, self.d) - self.assertEqual(2 * off - off, off) - - self.assertEqual(self.d - self.offset2, self.d + self._offset(-3)) - - def testRSub(self): - self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) - - def testMult1(self): - self.assertEqual(self.d + 5 * self.offset1, self.d + self._offset(5)) - - def testMult2(self): - self.assertEqual(self.d + (-3 * self._offset(-2)), - self.d + self._offset(6)) - - def testRollback1(self): - self.assertEqual(self.offset1.rollback(self.d), self.d) - self.assertEqual(self.offset2.rollback(self.d), self.d) - self.assertEqual(self.offset3.rollback(self.d), self.d) - self.assertEqual(self.offset4.rollback(self.d), self.d) - self.assertEqual(self.offset5.rollback(self.d), - datetime(2014, 6, 30, 14, 30)) - self.assertEqual(self.offset6.rollback( - self.d), datetime(2014, 7, 1, 5, 0)) - self.assertEqual(self.offset7.rollback( - self.d), datetime(2014, 7, 1, 6, 30)) - - d = datetime(2014, 7, 1, 0) - self.assertEqual(self.offset1.rollback(d), datetime(2014, 6, 30, 17)) - self.assertEqual(self.offset2.rollback(d), datetime(2014, 6, 30, 17)) - self.assertEqual(self.offset3.rollback(d), datetime(2014, 6, 30, 17)) - self.assertEqual(self.offset4.rollback(d), datetime(2014, 6, 30, 17)) - self.assertEqual(self.offset5.rollback( - d), datetime(2014, 6, 30, 14, 30)) - self.assertEqual(self.offset6.rollback(d), d) - self.assertEqual(self.offset7.rollback(d), d) - - self.assertEqual(self._offset(5).rollback(self.d), self.d) - - def testRollback2(self): - self.assertEqual(self._offset(-3) - .rollback(datetime(2014, 7, 5, 15, 0)), - datetime(2014, 7, 4, 17, 0)) - - def testRollforward1(self): - self.assertEqual(self.offset1.rollforward(self.d), self.d) - self.assertEqual(self.offset2.rollforward(self.d), self.d) - self.assertEqual(self.offset3.rollforward(self.d), self.d) - self.assertEqual(self.offset4.rollforward(self.d), self.d) - self.assertEqual(self.offset5.rollforward( - self.d), datetime(2014, 7, 1, 11, 0)) - self.assertEqual(self.offset6.rollforward( - self.d), datetime(2014, 7, 1, 20, 0)) - self.assertEqual(self.offset7.rollforward( - self.d), datetime(2014, 7, 1, 21, 30)) - - d = datetime(2014, 7, 1, 0) - self.assertEqual(self.offset1.rollforward(d), datetime(2014, 7, 1, 9)) - self.assertEqual(self.offset2.rollforward(d), datetime(2014, 7, 1, 9)) - self.assertEqual(self.offset3.rollforward(d), datetime(2014, 7, 1, 9)) - self.assertEqual(self.offset4.rollforward(d), datetime(2014, 7, 1, 9)) - self.assertEqual(self.offset5.rollforward(d), datetime(2014, 7, 1, 11)) - self.assertEqual(self.offset6.rollforward(d), d) - self.assertEqual(self.offset7.rollforward(d), d) - - self.assertEqual(self._offset(5).rollforward(self.d), self.d) - - def testRollforward2(self): - self.assertEqual(self._offset(-3) - .rollforward(datetime(2014, 7, 5, 16, 0)), - datetime(2014, 7, 7, 9)) - - def test_roll_date_object(self): - offset = BusinessHour() - - dt = datetime(2014, 7, 6, 15, 0) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2014, 7, 4, 17)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2014, 7, 7, 9)) - - def test_normalize(self): - tests = [] - - tests.append((BusinessHour(normalize=True), - {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), - datetime(2014, 7, 1, 17): datetime(2014, 7, 2), - datetime(2014, 7, 1, 16): datetime(2014, 7, 2), - datetime(2014, 7, 1, 23): datetime(2014, 7, 2), - datetime(2014, 7, 1, 0): datetime(2014, 7, 1), - datetime(2014, 7, 4, 15): datetime(2014, 7, 4), - datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7), - datetime(2014, 7, 5, 23): datetime(2014, 7, 7), - datetime(2014, 7, 6, 10): datetime(2014, 7, 7)})) - - tests.append((BusinessHour(-1, normalize=True), - {datetime(2014, 7, 1, 8): datetime(2014, 6, 30), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1), - datetime(2014, 7, 1, 16): datetime(2014, 7, 1), - datetime(2014, 7, 1, 10): datetime(2014, 6, 30), - datetime(2014, 7, 1, 0): datetime(2014, 6, 30), - datetime(2014, 7, 7, 10): datetime(2014, 7, 4), - datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7), - datetime(2014, 7, 5, 23): datetime(2014, 7, 4), - datetime(2014, 7, 6, 10): datetime(2014, 7, 4)})) - - tests.append((BusinessHour(1, normalize=True, start='17:00', - end='04:00'), - {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1), - datetime(2014, 7, 1, 23): datetime(2014, 7, 2), - datetime(2014, 7, 2, 2): datetime(2014, 7, 2), - datetime(2014, 7, 2, 3): datetime(2014, 7, 2), - datetime(2014, 7, 4, 23): datetime(2014, 7, 5), - datetime(2014, 7, 5, 2): datetime(2014, 7, 5), - datetime(2014, 7, 7, 2): datetime(2014, 7, 7), - datetime(2014, 7, 7, 17): datetime(2014, 7, 7)})) - - for offset, cases in tests: - for dt, expected in compat.iteritems(cases): - self.assertEqual(offset.apply(dt), expected) - - def test_onOffset(self): - tests = [] - - tests.append((BusinessHour(), {datetime(2014, 7, 1, 9): True, - datetime(2014, 7, 1, 8, 59): False, - datetime(2014, 7, 1, 8): False, - datetime(2014, 7, 1, 17): True, - datetime(2014, 7, 1, 17, 1): False, - datetime(2014, 7, 1, 18): False, - datetime(2014, 7, 5, 9): False, - datetime(2014, 7, 6, 12): False})) - - tests.append((BusinessHour(start='10:00', end='15:00'), - {datetime(2014, 7, 1, 9): False, - datetime(2014, 7, 1, 10): True, - datetime(2014, 7, 1, 15): True, - datetime(2014, 7, 1, 15, 1): False, - datetime(2014, 7, 5, 12): False, - datetime(2014, 7, 6, 12): False})) - - tests.append((BusinessHour(start='19:00', end='05:00'), - {datetime(2014, 7, 1, 9, 0): False, - datetime(2014, 7, 1, 10, 0): False, - datetime(2014, 7, 1, 15): False, - datetime(2014, 7, 1, 15, 1): False, - datetime(2014, 7, 5, 12, 0): False, - datetime(2014, 7, 6, 12, 0): False, - datetime(2014, 7, 1, 19, 0): True, - datetime(2014, 7, 2, 0, 0): True, - datetime(2014, 7, 4, 23): True, - datetime(2014, 7, 5, 1): True, - datetime(2014, 7, 5, 5, 0): True, - datetime(2014, 7, 6, 23, 0): False, - datetime(2014, 7, 7, 3, 0): False})) - - for offset, cases in tests: - for dt, expected in compat.iteritems(cases): - self.assertEqual(offset.onOffset(dt), expected) - - def test_opening_time(self): - tests = [] - - # opening time should be affected by sign of n, not by n's value and - # end - tests.append(( - [BusinessHour(), BusinessHour(n=2), BusinessHour( - n=4), BusinessHour(end='10:00'), BusinessHour(n=2, end='4:00'), - BusinessHour(n=4, end='15:00')], - {datetime(2014, 7, 1, 11): (datetime(2014, 7, 2, 9), datetime( - 2014, 7, 1, 9)), - datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 9), datetime( - 2014, 7, 1, 9)), - datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 9), datetime( - 2014, 7, 1, 9)), - datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 9), datetime( - 2014, 7, 1, 9)), - # if timestamp is on opening time, next opening time is - # as it is - datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), datetime( - 2014, 7, 2, 9)), - datetime(2014, 7, 2, 10): (datetime(2014, 7, 3, 9), datetime( - 2014, 7, 2, 9)), - # 2014-07-05 is saturday - datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 9), datetime( - 2014, 7, 4, 9)), - datetime(2014, 7, 4, 10): (datetime(2014, 7, 7, 9), datetime( - 2014, 7, 4, 9)), - datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 9), datetime( - 2014, 7, 4, 9)), - datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 9), datetime( - 2014, 7, 4, 9)), - datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 9), datetime( - 2014, 7, 4, 9)), - datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 8, 9), datetime( - 2014, 7, 7, 9))})) - - tests.append(([BusinessHour(start='11:15'), - BusinessHour(n=2, start='11:15'), - BusinessHour(n=3, start='11:15'), - BusinessHour(start='11:15', end='10:00'), - BusinessHour(n=2, start='11:15', end='4:00'), - BusinessHour(n=3, start='11:15', end='15:00')], - {datetime(2014, 7, 1, 11): (datetime( - 2014, 7, 1, 11, 15), datetime(2014, 6, 30, 11, 15)), - datetime(2014, 7, 1, 18): (datetime( - 2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), - datetime(2014, 7, 1, 23): (datetime( - 2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), - datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 11, 15), - datetime(2014, 7, 1, 11, 15)), - datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 11, 15), - datetime(2014, 7, 1, 11, 15)), - datetime(2014, 7, 2, 10): (datetime( - 2014, 7, 2, 11, 15), datetime(2014, 7, 1, 11, 15)), - datetime(2014, 7, 2, 11, 15): (datetime( - 2014, 7, 2, 11, 15), datetime(2014, 7, 2, 11, 15)), - datetime(2014, 7, 2, 11, 15, 1): (datetime( - 2014, 7, 3, 11, 15), datetime(2014, 7, 2, 11, 15)), - datetime(2014, 7, 5, 10): (datetime( - 2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), - datetime(2014, 7, 4, 10): (datetime( - 2014, 7, 4, 11, 15), datetime(2014, 7, 3, 11, 15)), - datetime(2014, 7, 4, 23): (datetime( - 2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), - datetime(2014, 7, 6, 10): (datetime( - 2014, 7, 7, 11, 15), datetime(2014, 7, 4, 11, 15)), - datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 11, 15), - datetime(2014, 7, 4, 11, 15)), - datetime(2014, 7, 7, 9, 1): ( - datetime(2014, 7, 7, 11, 15), - datetime(2014, 7, 4, 11, 15))})) - - tests.append(([BusinessHour(-1), BusinessHour(n=-2), - BusinessHour(n=-4), - BusinessHour(n=-1, end='10:00'), - BusinessHour(n=-2, end='4:00'), - BusinessHour(n=-4, end='15:00')], - {datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 9), - datetime(2014, 7, 2, 9)), - datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 9), - datetime(2014, 7, 2, 9)), - datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 9), - datetime(2014, 7, 2, 9)), - datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 9), - datetime(2014, 7, 2, 9)), - datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 9), - datetime(2014, 7, 2, 9)), - datetime(2014, 7, 2, 10): (datetime(2014, 7, 2, 9), - datetime(2014, 7, 3, 9)), - datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 7, 9): (datetime(2014, 7, 7, 9), - datetime(2014, 7, 7, 9)), - datetime(2014, 7, 7, 9, 1): (datetime(2014, 7, 7, 9), - datetime(2014, 7, 8, 9))})) - - tests.append(([BusinessHour(start='17:00', end='05:00'), - BusinessHour(n=3, start='17:00', end='03:00')], - {datetime(2014, 7, 1, 11): (datetime(2014, 7, 1, 17), - datetime(2014, 6, 30, 17)), - datetime(2014, 7, 1, 18): (datetime(2014, 7, 2, 17), - datetime(2014, 7, 1, 17)), - datetime(2014, 7, 1, 23): (datetime(2014, 7, 2, 17), - datetime(2014, 7, 1, 17)), - datetime(2014, 7, 2, 8): (datetime(2014, 7, 2, 17), - datetime(2014, 7, 1, 17)), - datetime(2014, 7, 2, 9): (datetime(2014, 7, 2, 17), - datetime(2014, 7, 1, 17)), - datetime(2014, 7, 4, 17): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 5, 10): (datetime(2014, 7, 7, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 4, 10): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 3, 17)), - datetime(2014, 7, 4, 23): (datetime(2014, 7, 7, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 6, 10): (datetime(2014, 7, 7, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 7, 5): (datetime(2014, 7, 7, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 7, 17, 1): (datetime( - 2014, 7, 8, 17), datetime(2014, 7, 7, 17)), })) - - tests.append(([BusinessHour(-1, start='17:00', end='05:00'), - BusinessHour(n=-2, start='17:00', end='03:00')], - {datetime(2014, 7, 1, 11): (datetime(2014, 6, 30, 17), - datetime(2014, 7, 1, 17)), - datetime(2014, 7, 1, 18): (datetime(2014, 7, 1, 17), - datetime(2014, 7, 2, 17)), - datetime(2014, 7, 1, 23): (datetime(2014, 7, 1, 17), - datetime(2014, 7, 2, 17)), - datetime(2014, 7, 2, 8): (datetime(2014, 7, 1, 17), - datetime(2014, 7, 2, 17)), - datetime(2014, 7, 2, 9): (datetime(2014, 7, 1, 17), - datetime(2014, 7, 2, 17)), - datetime(2014, 7, 2, 16, 59): (datetime( - 2014, 7, 1, 17), datetime(2014, 7, 2, 17)), - datetime(2014, 7, 5, 10): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 7, 17)), - datetime(2014, 7, 4, 10): (datetime(2014, 7, 3, 17), - datetime(2014, 7, 4, 17)), - datetime(2014, 7, 4, 23): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 7, 17)), - datetime(2014, 7, 6, 10): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 7, 17)), - datetime(2014, 7, 7, 5): (datetime(2014, 7, 4, 17), - datetime(2014, 7, 7, 17)), - datetime(2014, 7, 7, 18): (datetime(2014, 7, 7, 17), - datetime(2014, 7, 8, 17))})) - - for _offsets, cases in tests: - for offset in _offsets: - for dt, (exp_next, exp_prev) in compat.iteritems(cases): - self.assertEqual(offset._next_opening_time(dt), exp_next) - self.assertEqual(offset._prev_opening_time(dt), exp_prev) - - def test_apply(self): - tests = [] - - tests.append(( - BusinessHour(), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12), - datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), - datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16), - datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 10), - datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 9), - datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 2, 9, 30, 15), - datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 10), - datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 12), - # out of business hours - datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 10), - datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10), - datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10), - # saturday - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30), - datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, - 30)})) - - tests.append((BusinessHour( - 4), {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), - datetime(2014, 7, 1, 13): datetime(2014, 7, 2, 9), - datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 11), - datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 12), - datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 13), - datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 15), - datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 13), - datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13), - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30), - datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, - 30)})) - - tests.append( - (BusinessHour(-1), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 10), - datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 12), - datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 14), - datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 15), - datetime(2014, 7, 1, 10): datetime(2014, 6, 30, 17), - datetime(2014, 7, 1, 16, 30, 15): datetime( - 2014, 7, 1, 15, 30, 15), - datetime(2014, 7, 1, 9, 30, 15): datetime( - 2014, 6, 30, 16, 30, 15), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 16), - datetime(2014, 7, 1, 5): datetime(2014, 6, 30, 16), - datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 10), - # out of business hours - datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 16), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 16), - datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 16), - datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 16), - # saturday - datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 16), - datetime(2014, 7, 7, 9): datetime(2014, 7, 4, 16), - datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 16, 30), - datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 16, 30, - 30)})) - - tests.append((BusinessHour( - -4), {datetime(2014, 7, 1, 11): datetime(2014, 6, 30, 15), - datetime(2014, 7, 1, 13): datetime(2014, 6, 30, 17), - datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 11), - datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 12), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), - datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), - datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), - datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 13), - datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), - datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), - datetime(2014, 7, 4, 18): datetime(2014, 7, 4, 13), - datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 13, 30), - datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 13, 30, - 30)})) - - tests.append((BusinessHour(start='13:00', end='16:00'), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 14), - datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), - datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 13), - datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 14), - datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 14), - datetime(2014, 7, 1, 15, 30, 15): datetime(2014, 7, 2, - 13, 30, 15), - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 14), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 14)})) - - tests.append((BusinessHour(n=2, start='13:00', end='16:00'), { - datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 15), - datetime(2014, 7, 2, 14): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 15), - datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 15), - datetime(2014, 7, 2, 14, 30): datetime(2014, 7, 3, 13, 30), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 15), - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 15), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 15), - datetime(2014, 7, 4, 14, 30): datetime(2014, 7, 7, 13, 30), - datetime(2014, 7, 4, 14, 30, 30): datetime(2014, 7, 7, 13, 30, 30) - })) - - tests.append((BusinessHour(n=-1, start='13:00', end='16:00'), - {datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15), - datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 15), - datetime(2014, 7, 2, 14): datetime(2014, 7, 1, 16), - datetime(2014, 7, 2, 15): datetime(2014, 7, 2, 14), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 15), - datetime(2014, 7, 2, 16): datetime(2014, 7, 2, 15), - datetime(2014, 7, 2, 13, 30, 15): datetime(2014, 7, 1, - 15, 30, 15), - datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 15), - datetime(2014, 7, 7, 11): datetime(2014, 7, 4, 15)})) - - tests.append((BusinessHour(n=-3, start='10:00', end='16:00'), { - datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13), - datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 11), - datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13), - datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 16), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13), - datetime(2014, 7, 2, 11, 30): datetime(2014, 7, 1, 14, 30), - datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13), - datetime(2014, 7, 4, 10): datetime(2014, 7, 3, 13), - datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13), - datetime(2014, 7, 4, 16): datetime(2014, 7, 4, 13), - datetime(2014, 7, 4, 12, 30): datetime(2014, 7, 3, 15, 30), - datetime(2014, 7, 4, 12, 30, 30): datetime(2014, 7, 3, 15, 30, 30) - })) - - tests.append((BusinessHour(start='19:00', end='05:00'), { - datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 20), - datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 20), - datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 20), - datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 20), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 20), - datetime(2014, 7, 2, 4, 30): datetime(2014, 7, 2, 19, 30), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 1), - datetime(2014, 7, 4, 10): datetime(2014, 7, 4, 20), - datetime(2014, 7, 4, 23): datetime(2014, 7, 5, 0), - datetime(2014, 7, 5, 0): datetime(2014, 7, 5, 1), - datetime(2014, 7, 5, 4): datetime(2014, 7, 7, 19), - datetime(2014, 7, 5, 4, 30): datetime(2014, 7, 7, 19, 30), - datetime(2014, 7, 5, 4, 30, 30): datetime(2014, 7, 7, 19, 30, 30) - })) - - tests.append((BusinessHour(n=-1, start='19:00', end='05:00'), { - datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 4), - datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 4), - datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 4), - datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 4), - datetime(2014, 7, 2, 20): datetime(2014, 7, 2, 5), - datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 4), - datetime(2014, 7, 2, 19, 30): datetime(2014, 7, 2, 4, 30), - datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 23), - datetime(2014, 7, 3, 6): datetime(2014, 7, 3, 4), - datetime(2014, 7, 4, 23): datetime(2014, 7, 4, 22), - datetime(2014, 7, 5, 0): datetime(2014, 7, 4, 23), - datetime(2014, 7, 5, 4): datetime(2014, 7, 5, 3), - datetime(2014, 7, 7, 19, 30): datetime(2014, 7, 5, 4, 30), - datetime(2014, 7, 7, 19, 30, 30): datetime(2014, 7, 5, 4, 30, 30) - })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_large_n(self): - tests = [] - - tests.append( - (BusinessHour(40), # A week later - {datetime(2014, 7, 1, 11): datetime(2014, 7, 8, 11), - datetime(2014, 7, 1, 13): datetime(2014, 7, 8, 13), - datetime(2014, 7, 1, 15): datetime(2014, 7, 8, 15), - datetime(2014, 7, 1, 16): datetime(2014, 7, 8, 16), - datetime(2014, 7, 1, 17): datetime(2014, 7, 9, 9), - datetime(2014, 7, 2, 11): datetime(2014, 7, 9, 11), - datetime(2014, 7, 2, 8): datetime(2014, 7, 9, 9), - datetime(2014, 7, 2, 19): datetime(2014, 7, 10, 9), - datetime(2014, 7, 2, 23): datetime(2014, 7, 10, 9), - datetime(2014, 7, 3, 0): datetime(2014, 7, 10, 9), - datetime(2014, 7, 5, 15): datetime(2014, 7, 14, 9), - datetime(2014, 7, 4, 18): datetime(2014, 7, 14, 9), - datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 14, 9, 30), - datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 14, 9, 30, - 30)})) - - tests.append( - (BusinessHour(-25), # 3 days and 1 hour before - {datetime(2014, 7, 1, 11): datetime(2014, 6, 26, 10), - datetime(2014, 7, 1, 13): datetime(2014, 6, 26, 12), - datetime(2014, 7, 1, 9): datetime(2014, 6, 25, 16), - datetime(2014, 7, 1, 10): datetime(2014, 6, 25, 17), - datetime(2014, 7, 3, 11): datetime(2014, 6, 30, 10), - datetime(2014, 7, 3, 8): datetime(2014, 6, 27, 16), - datetime(2014, 7, 3, 19): datetime(2014, 6, 30, 16), - datetime(2014, 7, 3, 23): datetime(2014, 6, 30, 16), - datetime(2014, 7, 4, 9): datetime(2014, 6, 30, 16), - datetime(2014, 7, 5, 15): datetime(2014, 7, 1, 16), - datetime(2014, 7, 6, 18): datetime(2014, 7, 1, 16), - datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 1, 16, 30), - datetime(2014, 7, 7, 10, 30, 30): datetime(2014, 7, 2, 9, 30, - 30)})) - - # 5 days and 3 hours later - tests.append((BusinessHour(28, start='21:00', end='02:00'), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 9, 0), - datetime(2014, 7, 1, 22): datetime(2014, 7, 9, 1), - datetime(2014, 7, 1, 23): datetime(2014, 7, 9, 21), - datetime(2014, 7, 2, 2): datetime(2014, 7, 10, 0), - datetime(2014, 7, 3, 21): datetime(2014, 7, 11, 0), - datetime(2014, 7, 4, 1): datetime(2014, 7, 11, 23), - datetime(2014, 7, 4, 2): datetime(2014, 7, 12, 0), - datetime(2014, 7, 4, 3): datetime(2014, 7, 12, 0), - datetime(2014, 7, 5, 1): datetime(2014, 7, 14, 23), - datetime(2014, 7, 5, 15): datetime(2014, 7, 15, 0), - datetime(2014, 7, 6, 18): datetime(2014, 7, 15, 0), - datetime(2014, 7, 7, 1): datetime(2014, 7, 15, 0), - datetime(2014, 7, 7, 23, 30): datetime(2014, 7, 15, 21, - 30)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_nanoseconds(self): - tests = [] - - tests.append((BusinessHour(), - {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp( - '2014-07-04 16:00') + Nano(5), - Timestamp('2014-07-04 16:00') + Nano(5): Timestamp( - '2014-07-07 09:00') + Nano(5), - Timestamp('2014-07-04 16:00') - Nano(5): Timestamp( - '2014-07-04 17:00') - Nano(5)})) - - tests.append((BusinessHour(-1), - {Timestamp('2014-07-04 15:00') + Nano(5): Timestamp( - '2014-07-04 14:00') + Nano(5), - Timestamp('2014-07-04 10:00') + Nano(5): Timestamp( - '2014-07-04 09:00') + Nano(5), - Timestamp('2014-07-04 10:00') - Nano(5): Timestamp( - '2014-07-03 17:00') - Nano(5), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = self._offset() - offset2 = self._offset() - self.assertFalse(offset1 != offset2) - - def test_datetimeindex(self): - idx1 = DatetimeIndex(start='2014-07-04 15:00', end='2014-07-08 10:00', - freq='BH') - idx2 = DatetimeIndex(start='2014-07-04 15:00', periods=12, freq='BH') - idx3 = DatetimeIndex(end='2014-07-08 10:00', periods=12, freq='BH') - expected = DatetimeIndex(['2014-07-04 15:00', '2014-07-04 16:00', - '2014-07-07 09:00', - '2014-07-07 10:00', '2014-07-07 11:00', - '2014-07-07 12:00', - '2014-07-07 13:00', '2014-07-07 14:00', - '2014-07-07 15:00', - '2014-07-07 16:00', '2014-07-08 09:00', - '2014-07-08 10:00'], - freq='BH') - for idx in [idx1, idx2, idx3]: - tm.assert_index_equal(idx, expected) - - idx1 = DatetimeIndex(start='2014-07-04 15:45', end='2014-07-08 10:45', - freq='BH') - idx2 = DatetimeIndex(start='2014-07-04 15:45', periods=12, freq='BH') - idx3 = DatetimeIndex(end='2014-07-08 10:45', periods=12, freq='BH') - - expected = DatetimeIndex(['2014-07-04 15:45', '2014-07-04 16:45', - '2014-07-07 09:45', - '2014-07-07 10:45', '2014-07-07 11:45', - '2014-07-07 12:45', - '2014-07-07 13:45', '2014-07-07 14:45', - '2014-07-07 15:45', - '2014-07-07 16:45', '2014-07-08 09:45', - '2014-07-08 10:45'], - freq='BH') - expected = idx1 - for idx in [idx1, idx2, idx3]: - tm.assert_index_equal(idx, expected) - - -class TestCustomBusinessHour(Base): - _offset = CustomBusinessHour - - def setUp(self): - # 2014 Calendar to check custom holidays - # Sun Mon Tue Wed Thu Fri Sat - # 6/22 23 24 25 26 27 28 - # 29 30 7/1 2 3 4 5 - # 6 7 8 9 10 11 12 - self.d = datetime(2014, 7, 1, 10, 00) - self.offset1 = CustomBusinessHour(weekmask='Tue Wed Thu Fri') - - self.holidays = ['2014-06-27', datetime(2014, 6, 30), - np.datetime64('2014-07-02')] - self.offset2 = CustomBusinessHour(holidays=self.holidays) - - def test_constructor_errors(self): - from datetime import time as dt_time - with tm.assertRaises(ValueError): - CustomBusinessHour(start=dt_time(11, 0, 5)) - with tm.assertRaises(ValueError): - CustomBusinessHour(start='AAA') - with tm.assertRaises(ValueError): - CustomBusinessHour(start='14:00:05') - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = self._offset() - offset2 = self._offset() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - self.assertEqual(repr(self.offset1), - '') - self.assertEqual(repr(self.offset2), - '') - - def test_with_offset(self): - expected = Timestamp('2014-07-01 13:00') - - self.assertEqual(self.d + CustomBusinessHour() * 3, expected) - self.assertEqual(self.d + CustomBusinessHour(n=3), expected) - - def testEQ(self): - for offset in [self.offset1, self.offset2]: - self.assertEqual(offset, offset) - - self.assertNotEqual(CustomBusinessHour(), CustomBusinessHour(-1)) - self.assertEqual(CustomBusinessHour(start='09:00'), - CustomBusinessHour()) - self.assertNotEqual(CustomBusinessHour(start='09:00'), - CustomBusinessHour(start='09:01')) - self.assertNotEqual(CustomBusinessHour(start='09:00', end='17:00'), - CustomBusinessHour(start='17:00', end='09:01')) - - self.assertNotEqual(CustomBusinessHour(weekmask='Tue Wed Thu Fri'), - CustomBusinessHour(weekmask='Mon Tue Wed Thu Fri')) - self.assertNotEqual(CustomBusinessHour(holidays=['2014-06-27']), - CustomBusinessHour(holidays=['2014-06-28'])) - - def test_hash(self): - self.assertEqual(hash(self.offset1), hash(self.offset1)) - self.assertEqual(hash(self.offset2), hash(self.offset2)) - - def testCall(self): - self.assertEqual(self.offset1(self.d), datetime(2014, 7, 1, 11)) - self.assertEqual(self.offset2(self.d), datetime(2014, 7, 1, 11)) - - def testRAdd(self): - self.assertEqual(self.d + self.offset2, self.offset2 + self.d) - - def testSub(self): - off = self.offset2 - self.assertRaises(Exception, off.__sub__, self.d) - self.assertEqual(2 * off - off, off) - - self.assertEqual(self.d - self.offset2, self.d - (2 * off - off)) - - def testRSub(self): - self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) - - def testMult1(self): - self.assertEqual(self.d + 5 * self.offset1, self.d + self._offset(5)) - - def testMult2(self): - self.assertEqual(self.d + (-3 * self._offset(-2)), - self.d + self._offset(6)) - - def testRollback1(self): - self.assertEqual(self.offset1.rollback(self.d), self.d) - self.assertEqual(self.offset2.rollback(self.d), self.d) - - d = datetime(2014, 7, 1, 0) - # 2014/07/01 is Tuesday, 06/30 is Monday(holiday) - self.assertEqual(self.offset1.rollback(d), datetime(2014, 6, 27, 17)) - - # 2014/6/30 and 2014/6/27 are holidays - self.assertEqual(self.offset2.rollback(d), datetime(2014, 6, 26, 17)) - - def testRollback2(self): - self.assertEqual(self._offset(-3) - .rollback(datetime(2014, 7, 5, 15, 0)), - datetime(2014, 7, 4, 17, 0)) - - def testRollforward1(self): - self.assertEqual(self.offset1.rollforward(self.d), self.d) - self.assertEqual(self.offset2.rollforward(self.d), self.d) - - d = datetime(2014, 7, 1, 0) - self.assertEqual(self.offset1.rollforward(d), datetime(2014, 7, 1, 9)) - self.assertEqual(self.offset2.rollforward(d), datetime(2014, 7, 1, 9)) - - def testRollforward2(self): - self.assertEqual(self._offset(-3) - .rollforward(datetime(2014, 7, 5, 16, 0)), - datetime(2014, 7, 7, 9)) - - def test_roll_date_object(self): - offset = BusinessHour() - - dt = datetime(2014, 7, 6, 15, 0) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2014, 7, 4, 17)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2014, 7, 7, 9)) - - def test_normalize(self): - tests = [] - - tests.append((CustomBusinessHour(normalize=True, - holidays=self.holidays), - {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), - datetime(2014, 7, 1, 17): datetime(2014, 7, 3), - datetime(2014, 7, 1, 16): datetime(2014, 7, 3), - datetime(2014, 7, 1, 23): datetime(2014, 7, 3), - datetime(2014, 7, 1, 0): datetime(2014, 7, 1), - datetime(2014, 7, 4, 15): datetime(2014, 7, 4), - datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7), - datetime(2014, 7, 5, 23): datetime(2014, 7, 7), - datetime(2014, 7, 6, 10): datetime(2014, 7, 7)})) - - tests.append((CustomBusinessHour(-1, normalize=True, - holidays=self.holidays), - {datetime(2014, 7, 1, 8): datetime(2014, 6, 26), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1), - datetime(2014, 7, 1, 16): datetime(2014, 7, 1), - datetime(2014, 7, 1, 10): datetime(2014, 6, 26), - datetime(2014, 7, 1, 0): datetime(2014, 6, 26), - datetime(2014, 7, 7, 10): datetime(2014, 7, 4), - datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7), - datetime(2014, 7, 5, 23): datetime(2014, 7, 4), - datetime(2014, 7, 6, 10): datetime(2014, 7, 4)})) - - tests.append((CustomBusinessHour(1, normalize=True, start='17:00', - end='04:00', holidays=self.holidays), - {datetime(2014, 7, 1, 8): datetime(2014, 7, 1), - datetime(2014, 7, 1, 17): datetime(2014, 7, 1), - datetime(2014, 7, 1, 23): datetime(2014, 7, 2), - datetime(2014, 7, 2, 2): datetime(2014, 7, 2), - datetime(2014, 7, 2, 3): datetime(2014, 7, 3), - datetime(2014, 7, 4, 23): datetime(2014, 7, 5), - datetime(2014, 7, 5, 2): datetime(2014, 7, 5), - datetime(2014, 7, 7, 2): datetime(2014, 7, 7), - datetime(2014, 7, 7, 17): datetime(2014, 7, 7)})) - - for offset, cases in tests: - for dt, expected in compat.iteritems(cases): - self.assertEqual(offset.apply(dt), expected) - - def test_onOffset(self): - tests = [] - - tests.append((CustomBusinessHour(start='10:00', end='15:00', - holidays=self.holidays), - {datetime(2014, 7, 1, 9): False, - datetime(2014, 7, 1, 10): True, - datetime(2014, 7, 1, 15): True, - datetime(2014, 7, 1, 15, 1): False, - datetime(2014, 7, 5, 12): False, - datetime(2014, 7, 6, 12): False})) - - for offset, cases in tests: - for dt, expected in compat.iteritems(cases): - self.assertEqual(offset.onOffset(dt), expected) - - def test_apply(self): - tests = [] - - tests.append(( - CustomBusinessHour(holidays=self.holidays), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12), - datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14), - datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16), - datetime(2014, 7, 1, 19): datetime(2014, 7, 3, 10), - datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 9), - datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 3, 9, 30, 15), - datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 10), - datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 10), - # out of business hours - datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 10), - datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10), - datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10), - # saturday - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30), - datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, - 30)})) - - tests.append(( - CustomBusinessHour(4, holidays=self.holidays), - {datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15), - datetime(2014, 7, 1, 13): datetime(2014, 7, 3, 9), - datetime(2014, 7, 1, 15): datetime(2014, 7, 3, 11), - datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 12), - datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13), - datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13), - datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13), - datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13), - datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13), - datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30), - datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, - 30)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_nanoseconds(self): - tests = [] - - tests.append((CustomBusinessHour(holidays=self.holidays), - {Timestamp('2014-07-01 15:00') + Nano(5): Timestamp( - '2014-07-01 16:00') + Nano(5), - Timestamp('2014-07-01 16:00') + Nano(5): Timestamp( - '2014-07-03 09:00') + Nano(5), - Timestamp('2014-07-01 16:00') - Nano(5): Timestamp( - '2014-07-01 17:00') - Nano(5)})) - - tests.append((CustomBusinessHour(-1, holidays=self.holidays), - {Timestamp('2014-07-01 15:00') + Nano(5): Timestamp( - '2014-07-01 14:00') + Nano(5), - Timestamp('2014-07-01 10:00') + Nano(5): Timestamp( - '2014-07-01 09:00') + Nano(5), - Timestamp('2014-07-01 10:00') - Nano(5): Timestamp( - '2014-06-26 17:00') - Nano(5), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - -class TestCustomBusinessDay(Base): - _offset = CDay - - def setUp(self): - self.d = datetime(2008, 1, 1) - self.nd = np_datetime64_compat('2008-01-01 00:00:00Z') - - self.offset = CDay() - self.offset2 = CDay(2) - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = CDay() - offset2 = CDay() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - assert repr(self.offset) == '' - assert repr(self.offset2) == '<2 * CustomBusinessDays>' - - expected = '' - assert repr(self.offset + timedelta(1)) == expected - - def test_with_offset(self): - offset = self.offset + timedelta(hours=2) - - assert (self.d + offset) == datetime(2008, 1, 2, 2) - - def testEQ(self): - self.assertEqual(self.offset2, self.offset2) - - def test_mul(self): - pass - - def test_hash(self): - self.assertEqual(hash(self.offset2), hash(self.offset2)) - - def testCall(self): - self.assertEqual(self.offset2(self.d), datetime(2008, 1, 3)) - self.assertEqual(self.offset2(self.nd), datetime(2008, 1, 3)) - - def testRAdd(self): - self.assertEqual(self.d + self.offset2, self.offset2 + self.d) - - def testSub(self): - off = self.offset2 - self.assertRaises(Exception, off.__sub__, self.d) - self.assertEqual(2 * off - off, off) - - self.assertEqual(self.d - self.offset2, self.d + CDay(-2)) - - def testRSub(self): - self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) - - def testMult1(self): - self.assertEqual(self.d + 10 * self.offset, self.d + CDay(10)) - - def testMult2(self): - self.assertEqual(self.d + (-5 * CDay(-10)), self.d + CDay(50)) - - def testRollback1(self): - self.assertEqual(CDay(10).rollback(self.d), self.d) - - def testRollback2(self): - self.assertEqual( - CDay(10).rollback(datetime(2008, 1, 5)), datetime(2008, 1, 4)) - - def testRollforward1(self): - self.assertEqual(CDay(10).rollforward(self.d), self.d) - - def testRollforward2(self): - self.assertEqual( - CDay(10).rollforward(datetime(2008, 1, 5)), datetime(2008, 1, 7)) - - def test_roll_date_object(self): - offset = CDay() - - dt = date(2012, 9, 15) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 14)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 17)) - - offset = offsets.Day() - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - def test_onOffset(self): - tests = [(CDay(), datetime(2008, 1, 1), True), - (CDay(), datetime(2008, 1, 5), False)] - - for offset, d, expected in tests: - assertOnOffset(offset, d, expected) - - def test_apply(self): - tests = [] - - tests.append((CDay(), {datetime(2008, 1, 1): datetime(2008, 1, 2), - datetime(2008, 1, 4): datetime(2008, 1, 7), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 8)})) - - tests.append((2 * CDay(), { - datetime(2008, 1, 1): datetime(2008, 1, 3), - datetime(2008, 1, 4): datetime(2008, 1, 8), - datetime(2008, 1, 5): datetime(2008, 1, 8), - datetime(2008, 1, 6): datetime(2008, 1, 8), - datetime(2008, 1, 7): datetime(2008, 1, 9)} - )) - - tests.append((-CDay(), { - datetime(2008, 1, 1): datetime(2007, 12, 31), - datetime(2008, 1, 4): datetime(2008, 1, 3), - datetime(2008, 1, 5): datetime(2008, 1, 4), - datetime(2008, 1, 6): datetime(2008, 1, 4), - datetime(2008, 1, 7): datetime(2008, 1, 4), - datetime(2008, 1, 8): datetime(2008, 1, 7)} - )) - - tests.append((-2 * CDay(), { - datetime(2008, 1, 1): datetime(2007, 12, 28), - datetime(2008, 1, 4): datetime(2008, 1, 2), - datetime(2008, 1, 5): datetime(2008, 1, 3), - datetime(2008, 1, 6): datetime(2008, 1, 3), - datetime(2008, 1, 7): datetime(2008, 1, 3), - datetime(2008, 1, 8): datetime(2008, 1, 4), - datetime(2008, 1, 9): datetime(2008, 1, 7)} - )) - - tests.append((CDay(0), {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 1, 4): datetime(2008, 1, 4), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 7)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_large_n(self): - dt = datetime(2012, 10, 23) - - result = dt + CDay(10) - self.assertEqual(result, datetime(2012, 11, 6)) - - result = dt + CDay(100) - CDay(100) - self.assertEqual(result, dt) - - off = CDay() * 6 - rs = datetime(2012, 1, 1) - off - xp = datetime(2011, 12, 23) - self.assertEqual(rs, xp) - - st = datetime(2011, 12, 18) - rs = st + off - xp = datetime(2011, 12, 26) - self.assertEqual(rs, xp) - - def test_apply_corner(self): - self.assertRaises(Exception, CDay().apply, BMonthEnd()) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = CDay() - offset2 = CDay() - self.assertFalse(offset1 != offset2) - - def test_holidays(self): - # Define a TradingDay offset - holidays = ['2012-05-01', datetime(2013, 5, 1), - np.datetime64('2014-05-01')] - tday = CDay(holidays=holidays) - for year in range(2012, 2015): - dt = datetime(year, 4, 30) - xp = datetime(year, 5, 2) - rs = dt + tday - self.assertEqual(rs, xp) - - def test_weekmask(self): - weekmask_saudi = 'Sat Sun Mon Tue Wed' # Thu-Fri Weekend - weekmask_uae = '1111001' # Fri-Sat Weekend - weekmask_egypt = [1, 1, 1, 1, 0, 0, 1] # Fri-Sat Weekend - bday_saudi = CDay(weekmask=weekmask_saudi) - bday_uae = CDay(weekmask=weekmask_uae) - bday_egypt = CDay(weekmask=weekmask_egypt) - dt = datetime(2013, 5, 1) - xp_saudi = datetime(2013, 5, 4) - xp_uae = datetime(2013, 5, 2) - xp_egypt = datetime(2013, 5, 2) - self.assertEqual(xp_saudi, dt + bday_saudi) - self.assertEqual(xp_uae, dt + bday_uae) - self.assertEqual(xp_egypt, dt + bday_egypt) - xp2 = datetime(2013, 5, 5) - self.assertEqual(xp2, dt + 2 * bday_saudi) - self.assertEqual(xp2, dt + 2 * bday_uae) - self.assertEqual(xp2, dt + 2 * bday_egypt) - - def test_weekmask_and_holidays(self): - weekmask_egypt = 'Sun Mon Tue Wed Thu' # Fri-Sat Weekend - holidays = ['2012-05-01', datetime(2013, 5, 1), - np.datetime64('2014-05-01')] - bday_egypt = CDay(holidays=holidays, weekmask=weekmask_egypt) - dt = datetime(2013, 4, 30) - xp_egypt = datetime(2013, 5, 5) - self.assertEqual(xp_egypt, dt + 2 * bday_egypt) - - def test_calendar(self): - calendar = USFederalHolidayCalendar() - dt = datetime(2014, 1, 17) - assertEq(CDay(calendar=calendar), dt, datetime(2014, 1, 21)) - - def test_roundtrip_pickle(self): - def _check_roundtrip(obj): - unpickled = self.round_trip_pickle(obj) - self.assertEqual(unpickled, obj) - - _check_roundtrip(self.offset) - _check_roundtrip(self.offset2) - _check_roundtrip(self.offset * 2) - - def test_pickle_compat_0_14_1(self): - hdays = [datetime(2013, 1, 1) for ele in range(4)] - - pth = tm.get_data_path() - - cday0_14_1 = read_pickle(os.path.join(pth, 'cday-0.14.1.pickle')) - cday = CDay(holidays=hdays) - self.assertEqual(cday, cday0_14_1) - - -class CustomBusinessMonthBase(object): - - def setUp(self): - self.d = datetime(2008, 1, 1) - - self.offset = self._object() - self.offset2 = self._object(2) - - def testEQ(self): - self.assertEqual(self.offset2, self.offset2) - - def test_mul(self): - pass - - def test_hash(self): - self.assertEqual(hash(self.offset2), hash(self.offset2)) - - def testRAdd(self): - self.assertEqual(self.d + self.offset2, self.offset2 + self.d) - - def testSub(self): - off = self.offset2 - self.assertRaises(Exception, off.__sub__, self.d) - self.assertEqual(2 * off - off, off) - - self.assertEqual(self.d - self.offset2, self.d + self._object(-2)) - - def testRSub(self): - self.assertEqual(self.d - self.offset2, (-self.offset2).apply(self.d)) - - def testMult1(self): - self.assertEqual(self.d + 10 * self.offset, self.d + self._object(10)) - - def testMult2(self): - self.assertEqual(self.d + (-5 * self._object(-10)), - self.d + self._object(50)) - - def test_offsets_compare_equal(self): - offset1 = self._object() - offset2 = self._object() - self.assertFalse(offset1 != offset2) - - def test_roundtrip_pickle(self): - def _check_roundtrip(obj): - unpickled = self.round_trip_pickle(obj) - self.assertEqual(unpickled, obj) - - _check_roundtrip(self._object()) - _check_roundtrip(self._object(2)) - _check_roundtrip(self._object() * 2) - - -class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base): - _object = CBMonthEnd - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = CBMonthEnd() - offset2 = CBMonthEnd() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - assert repr(self.offset) == '' - assert repr(self.offset2) == '<2 * CustomBusinessMonthEnds>' - - def testCall(self): - self.assertEqual(self.offset2(self.d), datetime(2008, 2, 29)) - - def testRollback1(self): - self.assertEqual( - CDay(10).rollback(datetime(2007, 12, 31)), datetime(2007, 12, 31)) - - def testRollback2(self): - self.assertEqual(CBMonthEnd(10).rollback(self.d), - datetime(2007, 12, 31)) - - def testRollforward1(self): - self.assertEqual(CBMonthEnd(10).rollforward( - self.d), datetime(2008, 1, 31)) - - def test_roll_date_object(self): - offset = CBMonthEnd() - - dt = date(2012, 9, 15) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 8, 31)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 28)) - - offset = offsets.Day() - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - def test_onOffset(self): - tests = [(CBMonthEnd(), datetime(2008, 1, 31), True), - (CBMonthEnd(), datetime(2008, 1, 1), False)] - - for offset, d, expected in tests: - assertOnOffset(offset, d, expected) - - def test_apply(self): - cbm = CBMonthEnd() - tests = [] - - tests.append((cbm, {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 2, 7): datetime(2008, 2, 29)})) - - tests.append((2 * cbm, {datetime(2008, 1, 1): datetime(2008, 2, 29), - datetime(2008, 2, 7): datetime(2008, 3, 31)})) - - tests.append((-cbm, {datetime(2008, 1, 1): datetime(2007, 12, 31), - datetime(2008, 2, 8): datetime(2008, 1, 31)})) - - tests.append((-2 * cbm, {datetime(2008, 1, 1): datetime(2007, 11, 30), - datetime(2008, 2, 9): datetime(2007, 12, 31)} - )) - - tests.append((CBMonthEnd(0), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 2, 7): datetime(2008, 2, 29)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_large_n(self): - dt = datetime(2012, 10, 23) - - result = dt + CBMonthEnd(10) - self.assertEqual(result, datetime(2013, 7, 31)) - - result = dt + CDay(100) - CDay(100) - self.assertEqual(result, dt) - - off = CBMonthEnd() * 6 - rs = datetime(2012, 1, 1) - off - xp = datetime(2011, 7, 29) - self.assertEqual(rs, xp) - - st = datetime(2011, 12, 18) - rs = st + off - xp = datetime(2012, 5, 31) - self.assertEqual(rs, xp) - - def test_holidays(self): - # Define a TradingDay offset - holidays = ['2012-01-31', datetime(2012, 2, 28), - np.datetime64('2012-02-29')] - bm_offset = CBMonthEnd(holidays=holidays) - dt = datetime(2012, 1, 1) - self.assertEqual(dt + bm_offset, datetime(2012, 1, 30)) - self.assertEqual(dt + 2 * bm_offset, datetime(2012, 2, 27)) - - def test_datetimeindex(self): - from pandas.tseries.holiday import USFederalHolidayCalendar - hcal = USFederalHolidayCalendar() - freq = CBMonthEnd(calendar=hcal) - - self.assertEqual(DatetimeIndex(start='20120101', end='20130101', - freq=freq).tolist()[0], - datetime(2012, 1, 31)) - - -class TestCustomBusinessMonthBegin(CustomBusinessMonthBase, Base): - _object = CBMonthBegin - - def test_different_normalize_equals(self): - # equivalent in this special case - offset = CBMonthBegin() - offset2 = CBMonthBegin() - offset2.normalize = True - self.assertEqual(offset, offset2) - - def test_repr(self): - assert repr(self.offset) == '' - assert repr(self.offset2) == '<2 * CustomBusinessMonthBegins>' - - def testCall(self): - self.assertEqual(self.offset2(self.d), datetime(2008, 3, 3)) - - def testRollback1(self): - self.assertEqual( - CDay(10).rollback(datetime(2007, 12, 31)), datetime(2007, 12, 31)) - - def testRollback2(self): - self.assertEqual(CBMonthBegin(10).rollback(self.d), - datetime(2008, 1, 1)) - - def testRollforward1(self): - self.assertEqual(CBMonthBegin(10).rollforward( - self.d), datetime(2008, 1, 1)) - - def test_roll_date_object(self): - offset = CBMonthBegin() - - dt = date(2012, 9, 15) - - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 3)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 10, 1)) - - offset = offsets.Day() - result = offset.rollback(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - result = offset.rollforward(dt) - self.assertEqual(result, datetime(2012, 9, 15)) - - def test_onOffset(self): - tests = [(CBMonthBegin(), datetime(2008, 1, 1), True), - (CBMonthBegin(), datetime(2008, 1, 31), False)] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_apply(self): - cbm = CBMonthBegin() - tests = [] - - tests.append((cbm, {datetime(2008, 1, 1): datetime(2008, 2, 1), - datetime(2008, 2, 7): datetime(2008, 3, 3)})) - - tests.append((2 * cbm, {datetime(2008, 1, 1): datetime(2008, 3, 3), - datetime(2008, 2, 7): datetime(2008, 4, 1)})) - - tests.append((-cbm, {datetime(2008, 1, 1): datetime(2007, 12, 3), - datetime(2008, 2, 8): datetime(2008, 2, 1)})) - - tests.append((-2 * cbm, {datetime(2008, 1, 1): datetime(2007, 11, 1), - datetime(2008, 2, 9): datetime(2008, 1, 1)})) - - tests.append((CBMonthBegin(0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 1, 7): datetime(2008, 2, 1)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_large_n(self): - dt = datetime(2012, 10, 23) - - result = dt + CBMonthBegin(10) - self.assertEqual(result, datetime(2013, 8, 1)) - - result = dt + CDay(100) - CDay(100) - self.assertEqual(result, dt) - - off = CBMonthBegin() * 6 - rs = datetime(2012, 1, 1) - off - xp = datetime(2011, 7, 1) - self.assertEqual(rs, xp) - - st = datetime(2011, 12, 18) - rs = st + off - xp = datetime(2012, 6, 1) - self.assertEqual(rs, xp) - - def test_holidays(self): - # Define a TradingDay offset - holidays = ['2012-02-01', datetime(2012, 2, 2), - np.datetime64('2012-03-01')] - bm_offset = CBMonthBegin(holidays=holidays) - dt = datetime(2012, 1, 1) - self.assertEqual(dt + bm_offset, datetime(2012, 1, 2)) - self.assertEqual(dt + 2 * bm_offset, datetime(2012, 2, 3)) - - def test_datetimeindex(self): - hcal = USFederalHolidayCalendar() - cbmb = CBMonthBegin(calendar=hcal) - self.assertEqual(DatetimeIndex(start='20120101', end='20130101', - freq=cbmb).tolist()[0], - datetime(2012, 1, 3)) - - -def assertOnOffset(offset, date, expected): - actual = offset.onOffset(date) - assert actual == expected, ("\nExpected: %s\nActual: %s\nFor Offset: %s)" - "\nAt Date: %s" % - (expected, actual, offset, date)) - - -class TestWeek(Base): - _offset = Week - - def test_repr(self): - self.assertEqual(repr(Week(weekday=0)), "") - self.assertEqual(repr(Week(n=-1, weekday=0)), "<-1 * Week: weekday=0>") - self.assertEqual(repr(Week(n=-2, weekday=0)), - "<-2 * Weeks: weekday=0>") - - def test_corner(self): - self.assertRaises(ValueError, Week, weekday=7) - assertRaisesRegexp(ValueError, "Day must be", Week, weekday=-1) - - def test_isAnchored(self): - self.assertTrue(Week(weekday=0).isAnchored()) - self.assertFalse(Week().isAnchored()) - self.assertFalse(Week(2, weekday=2).isAnchored()) - self.assertFalse(Week(2).isAnchored()) - - def test_offset(self): - tests = [] - - tests.append((Week(), # not business week - {datetime(2008, 1, 1): datetime(2008, 1, 8), - datetime(2008, 1, 4): datetime(2008, 1, 11), - datetime(2008, 1, 5): datetime(2008, 1, 12), - datetime(2008, 1, 6): datetime(2008, 1, 13), - datetime(2008, 1, 7): datetime(2008, 1, 14)})) - - tests.append((Week(weekday=0), # Mon - {datetime(2007, 12, 31): datetime(2008, 1, 7), - datetime(2008, 1, 4): datetime(2008, 1, 7), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 14)})) - - tests.append((Week(0, weekday=0), # n=0 -> roll forward. Mon - {datetime(2007, 12, 31): datetime(2007, 12, 31), - datetime(2008, 1, 4): datetime(2008, 1, 7), - datetime(2008, 1, 5): datetime(2008, 1, 7), - datetime(2008, 1, 6): datetime(2008, 1, 7), - datetime(2008, 1, 7): datetime(2008, 1, 7)})) - - tests.append((Week(-2, weekday=1), # n=0 -> roll forward. Mon - {datetime(2010, 4, 6): datetime(2010, 3, 23), - datetime(2010, 4, 8): datetime(2010, 3, 30), - datetime(2010, 4, 5): datetime(2010, 3, 23)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - for weekday in range(7): - offset = Week(weekday=weekday) - - for day in range(1, 8): - date = datetime(2008, 1, day) - - if day % 7 == weekday: - expected = True - else: - expected = False - assertOnOffset(offset, date, expected) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = Week() - offset2 = Week() - self.assertFalse(offset1 != offset2) - - -class TestWeekOfMonth(Base): - _offset = WeekOfMonth - - def test_constructor(self): - assertRaisesRegexp(ValueError, "^N cannot be 0", WeekOfMonth, n=0, - week=1, weekday=1) - assertRaisesRegexp(ValueError, "^Week", WeekOfMonth, n=1, week=4, - weekday=0) - assertRaisesRegexp(ValueError, "^Week", WeekOfMonth, n=1, week=-1, - weekday=0) - assertRaisesRegexp(ValueError, "^Day", WeekOfMonth, n=1, week=0, - weekday=-1) - assertRaisesRegexp(ValueError, "^Day", WeekOfMonth, n=1, week=0, - weekday=7) - - def test_repr(self): - self.assertEqual(repr(WeekOfMonth(weekday=1, week=2)), - "") - - def test_offset(self): - date1 = datetime(2011, 1, 4) # 1st Tuesday of Month - date2 = datetime(2011, 1, 11) # 2nd Tuesday of Month - date3 = datetime(2011, 1, 18) # 3rd Tuesday of Month - date4 = datetime(2011, 1, 25) # 4th Tuesday of Month - - # see for loop for structure - test_cases = [ - (-2, 2, 1, date1, datetime(2010, 11, 16)), - (-2, 2, 1, date2, datetime(2010, 11, 16)), - (-2, 2, 1, date3, datetime(2010, 11, 16)), - (-2, 2, 1, date4, datetime(2010, 12, 21)), - - (-1, 2, 1, date1, datetime(2010, 12, 21)), - (-1, 2, 1, date2, datetime(2010, 12, 21)), - (-1, 2, 1, date3, datetime(2010, 12, 21)), - (-1, 2, 1, date4, datetime(2011, 1, 18)), - - (1, 0, 0, date1, datetime(2011, 2, 7)), - (1, 0, 0, date2, datetime(2011, 2, 7)), - (1, 0, 0, date3, datetime(2011, 2, 7)), - (1, 0, 0, date4, datetime(2011, 2, 7)), - (1, 0, 1, date1, datetime(2011, 2, 1)), - (1, 0, 1, date2, datetime(2011, 2, 1)), - (1, 0, 1, date3, datetime(2011, 2, 1)), - (1, 0, 1, date4, datetime(2011, 2, 1)), - (1, 0, 2, date1, datetime(2011, 1, 5)), - (1, 0, 2, date2, datetime(2011, 2, 2)), - (1, 0, 2, date3, datetime(2011, 2, 2)), - (1, 0, 2, date4, datetime(2011, 2, 2)), - - (1, 2, 1, date1, datetime(2011, 1, 18)), - (1, 2, 1, date2, datetime(2011, 1, 18)), - (1, 2, 1, date3, datetime(2011, 2, 15)), - (1, 2, 1, date4, datetime(2011, 2, 15)), - - (2, 2, 1, date1, datetime(2011, 2, 15)), - (2, 2, 1, date2, datetime(2011, 2, 15)), - (2, 2, 1, date3, datetime(2011, 3, 15)), - (2, 2, 1, date4, datetime(2011, 3, 15)), - ] - - for n, week, weekday, dt, expected in test_cases: - offset = WeekOfMonth(n, week=week, weekday=weekday) - assertEq(offset, dt, expected) - - # try subtracting - result = datetime(2011, 2, 1) - WeekOfMonth(week=1, weekday=2) - self.assertEqual(result, datetime(2011, 1, 12)) - result = datetime(2011, 2, 3) - WeekOfMonth(week=0, weekday=2) - self.assertEqual(result, datetime(2011, 2, 2)) - - def test_onOffset(self): - test_cases = [ - (0, 0, datetime(2011, 2, 7), True), - (0, 0, datetime(2011, 2, 6), False), - (0, 0, datetime(2011, 2, 14), False), - (1, 0, datetime(2011, 2, 14), True), - (0, 1, datetime(2011, 2, 1), True), - (0, 1, datetime(2011, 2, 8), False), - ] - - for week, weekday, dt, expected in test_cases: - offset = WeekOfMonth(week=week, weekday=weekday) - self.assertEqual(offset.onOffset(dt), expected) - - -class TestLastWeekOfMonth(Base): - _offset = LastWeekOfMonth - - def test_constructor(self): - assertRaisesRegexp(ValueError, "^N cannot be 0", LastWeekOfMonth, n=0, - weekday=1) - - assertRaisesRegexp(ValueError, "^Day", LastWeekOfMonth, n=1, - weekday=-1) - assertRaisesRegexp(ValueError, "^Day", LastWeekOfMonth, n=1, weekday=7) - - def test_offset(self): - # Saturday - last_sat = datetime(2013, 8, 31) - next_sat = datetime(2013, 9, 28) - offset_sat = LastWeekOfMonth(n=1, weekday=5) - - one_day_before = (last_sat + timedelta(days=-1)) - self.assertEqual(one_day_before + offset_sat, last_sat) - - one_day_after = (last_sat + timedelta(days=+1)) - self.assertEqual(one_day_after + offset_sat, next_sat) - - # Test On that day - self.assertEqual(last_sat + offset_sat, next_sat) - - # Thursday - - offset_thur = LastWeekOfMonth(n=1, weekday=3) - last_thurs = datetime(2013, 1, 31) - next_thurs = datetime(2013, 2, 28) - - one_day_before = last_thurs + timedelta(days=-1) - self.assertEqual(one_day_before + offset_thur, last_thurs) - - one_day_after = last_thurs + timedelta(days=+1) - self.assertEqual(one_day_after + offset_thur, next_thurs) - - # Test on that day - self.assertEqual(last_thurs + offset_thur, next_thurs) - - three_before = last_thurs + timedelta(days=-3) - self.assertEqual(three_before + offset_thur, last_thurs) - - two_after = last_thurs + timedelta(days=+2) - self.assertEqual(two_after + offset_thur, next_thurs) - - offset_sunday = LastWeekOfMonth(n=1, weekday=WeekDay.SUN) - self.assertEqual(datetime(2013, 7, 31) + - offset_sunday, datetime(2013, 8, 25)) - - def test_onOffset(self): - test_cases = [ - (WeekDay.SUN, datetime(2013, 1, 27), True), - (WeekDay.SAT, datetime(2013, 3, 30), True), - (WeekDay.MON, datetime(2013, 2, 18), False), # Not the last Mon - (WeekDay.SUN, datetime(2013, 2, 25), False), # Not a SUN - (WeekDay.MON, datetime(2013, 2, 25), True), - (WeekDay.SAT, datetime(2013, 11, 30), True), - - (WeekDay.SAT, datetime(2006, 8, 26), True), - (WeekDay.SAT, datetime(2007, 8, 25), True), - (WeekDay.SAT, datetime(2008, 8, 30), True), - (WeekDay.SAT, datetime(2009, 8, 29), True), - (WeekDay.SAT, datetime(2010, 8, 28), True), - (WeekDay.SAT, datetime(2011, 8, 27), True), - (WeekDay.SAT, datetime(2019, 8, 31), True), - ] - - for weekday, dt, expected in test_cases: - offset = LastWeekOfMonth(weekday=weekday) - self.assertEqual(offset.onOffset(dt), expected, msg=date) - - -class TestBMonthBegin(Base): - _offset = BMonthBegin - - def test_offset(self): - tests = [] - - tests.append((BMonthBegin(), - {datetime(2008, 1, 1): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2006, 9, 1): datetime(2006, 10, 2), - datetime(2007, 1, 1): datetime(2007, 2, 1), - datetime(2006, 12, 1): datetime(2007, 1, 1)})) - - tests.append((BMonthBegin(0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2006, 10, 2): datetime(2006, 10, 2), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2006, 9, 15): datetime(2006, 10, 2)})) - - tests.append((BMonthBegin(2), - {datetime(2008, 1, 1): datetime(2008, 3, 3), - datetime(2008, 1, 15): datetime(2008, 3, 3), - datetime(2006, 12, 29): datetime(2007, 2, 1), - datetime(2006, 12, 31): datetime(2007, 2, 1), - datetime(2007, 1, 1): datetime(2007, 3, 1), - datetime(2006, 11, 1): datetime(2007, 1, 1)})) - - tests.append((BMonthBegin(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 1), - datetime(2008, 6, 30): datetime(2008, 6, 2), - datetime(2008, 6, 1): datetime(2008, 5, 1), - datetime(2008, 3, 10): datetime(2008, 3, 3), - datetime(2008, 12, 31): datetime(2008, 12, 1), - datetime(2006, 12, 29): datetime(2006, 12, 1), - datetime(2006, 12, 30): datetime(2006, 12, 1), - datetime(2007, 1, 1): datetime(2006, 12, 1)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - - tests = [(BMonthBegin(), datetime(2007, 12, 31), False), - (BMonthBegin(), datetime(2008, 1, 1), True), - (BMonthBegin(), datetime(2001, 4, 2), True), - (BMonthBegin(), datetime(2008, 3, 3), True)] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = BMonthBegin() - offset2 = BMonthBegin() - self.assertFalse(offset1 != offset2) - - -class TestBMonthEnd(Base): - _offset = BMonthEnd - - def test_offset(self): - tests = [] - - tests.append((BMonthEnd(), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 2, 29), - datetime(2006, 12, 29): datetime(2007, 1, 31), - datetime(2006, 12, 31): datetime(2007, 1, 31), - datetime(2007, 1, 1): datetime(2007, 1, 31), - datetime(2006, 12, 1): datetime(2006, 12, 29)})) - - tests.append((BMonthEnd(0), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2006, 12, 29): datetime(2006, 12, 29), - datetime(2006, 12, 31): datetime(2007, 1, 31), - datetime(2007, 1, 1): datetime(2007, 1, 31)})) - - tests.append((BMonthEnd(2), - {datetime(2008, 1, 1): datetime(2008, 2, 29), - datetime(2008, 1, 31): datetime(2008, 3, 31), - datetime(2006, 12, 29): datetime(2007, 2, 28), - datetime(2006, 12, 31): datetime(2007, 2, 28), - datetime(2007, 1, 1): datetime(2007, 2, 28), - datetime(2006, 11, 1): datetime(2006, 12, 29)})) - - tests.append((BMonthEnd(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 29), - datetime(2008, 6, 30): datetime(2008, 5, 30), - datetime(2008, 12, 31): datetime(2008, 11, 28), - datetime(2006, 12, 29): datetime(2006, 11, 30), - datetime(2006, 12, 30): datetime(2006, 12, 29), - datetime(2007, 1, 1): datetime(2006, 12, 29)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_normalize(self): - dt = datetime(2007, 1, 1, 3) - - result = dt + BMonthEnd(normalize=True) - expected = dt.replace(hour=0) + BMonthEnd() - self.assertEqual(result, expected) - - def test_onOffset(self): - - tests = [(BMonthEnd(), datetime(2007, 12, 31), True), - (BMonthEnd(), datetime(2008, 1, 1), False)] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_offsets_compare_equal(self): - # root cause of #456 - offset1 = BMonthEnd() - offset2 = BMonthEnd() - self.assertFalse(offset1 != offset2) - - -class TestMonthBegin(Base): - _offset = MonthBegin - - def test_offset(self): - tests = [] - - # NOTE: I'm not entirely happy with the logic here for Begin -ss - # see thread 'offset conventions' on the ML - tests.append((MonthBegin(), - {datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2008, 2, 1): datetime(2008, 3, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2006, 12, 1): datetime(2007, 1, 1), - datetime(2007, 1, 31): datetime(2007, 2, 1)})) - - tests.append((MonthBegin(0), - {datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2006, 12, 3): datetime(2007, 1, 1), - datetime(2007, 1, 31): datetime(2007, 2, 1)})) - - tests.append((MonthBegin(2), - {datetime(2008, 2, 29): datetime(2008, 4, 1), - datetime(2008, 1, 31): datetime(2008, 3, 1), - datetime(2006, 12, 31): datetime(2007, 2, 1), - datetime(2007, 12, 28): datetime(2008, 2, 1), - datetime(2007, 1, 1): datetime(2007, 3, 1), - datetime(2006, 11, 1): datetime(2007, 1, 1)})) - - tests.append((MonthBegin(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 1), - datetime(2008, 5, 31): datetime(2008, 5, 1), - datetime(2008, 12, 31): datetime(2008, 12, 1), - datetime(2006, 12, 29): datetime(2006, 12, 1), - datetime(2006, 1, 2): datetime(2006, 1, 1)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - -class TestMonthEnd(Base): - _offset = MonthEnd - - def test_offset(self): - tests = [] - - tests.append((MonthEnd(), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 2, 29), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2007, 1, 31), - datetime(2007, 1, 1): datetime(2007, 1, 31), - datetime(2006, 12, 1): datetime(2006, 12, 31)})) - - tests.append((MonthEnd(0), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2006, 12, 31), - datetime(2007, 1, 1): datetime(2007, 1, 31)})) - - tests.append((MonthEnd(2), - {datetime(2008, 1, 1): datetime(2008, 2, 29), - datetime(2008, 1, 31): datetime(2008, 3, 31), - datetime(2006, 12, 29): datetime(2007, 1, 31), - datetime(2006, 12, 31): datetime(2007, 2, 28), - datetime(2007, 1, 1): datetime(2007, 2, 28), - datetime(2006, 11, 1): datetime(2006, 12, 31)})) - - tests.append((MonthEnd(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 31), - datetime(2008, 6, 30): datetime(2008, 5, 31), - datetime(2008, 12, 31): datetime(2008, 11, 30), - datetime(2006, 12, 29): datetime(2006, 11, 30), - datetime(2006, 12, 30): datetime(2006, 11, 30), - datetime(2007, 1, 1): datetime(2006, 12, 31)})) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - # def test_day_of_month(self): - # dt = datetime(2007, 1, 1) - - # offset = MonthEnd(day=20) - - # result = dt + offset - # self.assertEqual(result, datetime(2007, 1, 20)) - - # result = result + offset - # self.assertEqual(result, datetime(2007, 2, 20)) - - def test_normalize(self): - dt = datetime(2007, 1, 1, 3) - - result = dt + MonthEnd(normalize=True) - expected = dt.replace(hour=0) + MonthEnd() - self.assertEqual(result, expected) - - def test_onOffset(self): - - tests = [(MonthEnd(), datetime(2007, 12, 31), True), - (MonthEnd(), datetime(2008, 1, 1), False)] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestSemiMonthEnd(Base): - _offset = SemiMonthEnd - - def _get_tests(self): - tests = [] - - tests.append((SemiMonthEnd(), - {datetime(2008, 1, 1): datetime(2008, 1, 15), - datetime(2008, 1, 15): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 2, 15), - datetime(2006, 12, 14): datetime(2006, 12, 15), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2007, 1, 15), - datetime(2007, 1, 1): datetime(2007, 1, 15), - datetime(2006, 12, 1): datetime(2006, 12, 15), - datetime(2006, 12, 15): datetime(2006, 12, 31)})) - - tests.append((SemiMonthEnd(day_of_month=20), - {datetime(2008, 1, 1): datetime(2008, 1, 20), - datetime(2008, 1, 15): datetime(2008, 1, 20), - datetime(2008, 1, 21): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 2, 20), - datetime(2006, 12, 14): datetime(2006, 12, 20), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2007, 1, 20), - datetime(2007, 1, 1): datetime(2007, 1, 20), - datetime(2006, 12, 1): datetime(2006, 12, 20), - datetime(2006, 12, 15): datetime(2006, 12, 20)})) - - tests.append((SemiMonthEnd(0), - {datetime(2008, 1, 1): datetime(2008, 1, 15), - datetime(2008, 1, 16): datetime(2008, 1, 31), - datetime(2008, 1, 15): datetime(2008, 1, 15), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2006, 12, 31), - datetime(2007, 1, 1): datetime(2007, 1, 15)})) - - tests.append((SemiMonthEnd(0, day_of_month=16), - {datetime(2008, 1, 1): datetime(2008, 1, 16), - datetime(2008, 1, 16): datetime(2008, 1, 16), - datetime(2008, 1, 15): datetime(2008, 1, 16), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2006, 12, 29): datetime(2006, 12, 31), - datetime(2006, 12, 31): datetime(2006, 12, 31), - datetime(2007, 1, 1): datetime(2007, 1, 16)})) - - tests.append((SemiMonthEnd(2), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 2, 29), - datetime(2006, 12, 29): datetime(2007, 1, 15), - datetime(2006, 12, 31): datetime(2007, 1, 31), - datetime(2007, 1, 1): datetime(2007, 1, 31), - datetime(2007, 1, 16): datetime(2007, 2, 15), - datetime(2006, 11, 1): datetime(2006, 11, 30)})) - - tests.append((SemiMonthEnd(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 31), - datetime(2008, 6, 30): datetime(2008, 6, 15), - datetime(2008, 12, 31): datetime(2008, 12, 15), - datetime(2006, 12, 29): datetime(2006, 12, 15), - datetime(2006, 12, 30): datetime(2006, 12, 15), - datetime(2007, 1, 1): datetime(2006, 12, 31)})) - - tests.append((SemiMonthEnd(-1, day_of_month=4), - {datetime(2007, 1, 1): datetime(2006, 12, 31), - datetime(2007, 1, 4): datetime(2006, 12, 31), - datetime(2008, 6, 30): datetime(2008, 6, 4), - datetime(2008, 12, 31): datetime(2008, 12, 4), - datetime(2006, 12, 5): datetime(2006, 12, 4), - datetime(2006, 12, 30): datetime(2006, 12, 4), - datetime(2007, 1, 1): datetime(2006, 12, 31)})) - - tests.append((SemiMonthEnd(-2), - {datetime(2007, 1, 1): datetime(2006, 12, 15), - datetime(2008, 6, 30): datetime(2008, 5, 31), - datetime(2008, 3, 15): datetime(2008, 2, 15), - datetime(2008, 12, 31): datetime(2008, 11, 30), - datetime(2006, 12, 29): datetime(2006, 11, 30), - datetime(2006, 12, 14): datetime(2006, 11, 15), - datetime(2007, 1, 1): datetime(2006, 12, 15)})) - - return tests - - def test_offset_whole_year(self): - dates = (datetime(2007, 12, 31), - datetime(2008, 1, 15), - datetime(2008, 1, 31), - datetime(2008, 2, 15), - datetime(2008, 2, 29), - datetime(2008, 3, 15), - datetime(2008, 3, 31), - datetime(2008, 4, 15), - datetime(2008, 4, 30), - datetime(2008, 5, 15), - datetime(2008, 5, 31), - datetime(2008, 6, 15), - datetime(2008, 6, 30), - datetime(2008, 7, 15), - datetime(2008, 7, 31), - datetime(2008, 8, 15), - datetime(2008, 8, 31), - datetime(2008, 9, 15), - datetime(2008, 9, 30), - datetime(2008, 10, 15), - datetime(2008, 10, 31), - datetime(2008, 11, 15), - datetime(2008, 11, 30), - datetime(2008, 12, 15), - datetime(2008, 12, 31)) - - for base, exp_date in zip(dates[:-1], dates[1:]): - assertEq(SemiMonthEnd(), base, exp_date) - - # ensure .apply_index works as expected - s = DatetimeIndex(dates[:-1]) - result = SemiMonthEnd().apply_index(s) - exp = DatetimeIndex(dates[1:]) - tm.assert_index_equal(result, exp) - - # ensure generating a range with DatetimeIndex gives same result - result = DatetimeIndex(start=dates[0], end=dates[-1], freq='SM') - exp = DatetimeIndex(dates) - tm.assert_index_equal(result, exp) - - def test_offset(self): - for offset, cases in self._get_tests(): - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_index(self): - for offset, cases in self._get_tests(): - s = DatetimeIndex(cases.keys()) - result = offset.apply_index(s) - exp = DatetimeIndex(cases.values()) - tm.assert_index_equal(result, exp) - - def test_onOffset(self): - - tests = [(datetime(2007, 12, 31), True), - (datetime(2007, 12, 15), True), - (datetime(2007, 12, 14), False), - (datetime(2007, 12, 1), False), - (datetime(2008, 2, 29), True)] - - for dt, expected in tests: - assertOnOffset(SemiMonthEnd(), dt, expected) - - def test_vectorized_offset_addition(self): - for klass, assert_func in zip([Series, DatetimeIndex], - [self.assert_series_equal, - tm.assert_index_equal]): - s = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - - result = s + SemiMonthEnd() - result2 = SemiMonthEnd() + s - exp = klass([Timestamp('2000-01-31 00:15:00', tz='US/Central'), - Timestamp('2000-02-29', tz='US/Central')], name='a') - assert_func(result, exp) - assert_func(result2, exp) - - s = klass([Timestamp('2000-01-01 00:15:00', tz='US/Central'), - Timestamp('2000-02-01', tz='US/Central')], name='a') - result = s + SemiMonthEnd() - result2 = SemiMonthEnd() + s - exp = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - assert_func(result, exp) - assert_func(result2, exp) - - -class TestSemiMonthBegin(Base): - _offset = SemiMonthBegin - - def _get_tests(self): - tests = [] - - tests.append((SemiMonthBegin(), - {datetime(2008, 1, 1): datetime(2008, 1, 15), - datetime(2008, 1, 15): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 14): datetime(2006, 12, 15), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2007, 1, 1): datetime(2007, 1, 15), - datetime(2006, 12, 1): datetime(2006, 12, 15), - datetime(2006, 12, 15): datetime(2007, 1, 1)})) - - tests.append((SemiMonthBegin(day_of_month=20), - {datetime(2008, 1, 1): datetime(2008, 1, 20), - datetime(2008, 1, 15): datetime(2008, 1, 20), - datetime(2008, 1, 21): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 14): datetime(2006, 12, 20), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2007, 1, 1): datetime(2007, 1, 20), - datetime(2006, 12, 1): datetime(2006, 12, 20), - datetime(2006, 12, 15): datetime(2006, 12, 20)})) - - tests.append((SemiMonthBegin(0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 1, 16): datetime(2008, 2, 1), - datetime(2008, 1, 15): datetime(2008, 1, 15), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 2): datetime(2006, 12, 15), - datetime(2007, 1, 1): datetime(2007, 1, 1)})) - - tests.append((SemiMonthBegin(0, day_of_month=16), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 1, 16): datetime(2008, 1, 16), - datetime(2008, 1, 15): datetime(2008, 1, 16), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2006, 12, 29): datetime(2007, 1, 1), - datetime(2006, 12, 31): datetime(2007, 1, 1), - datetime(2007, 1, 5): datetime(2007, 1, 16), - datetime(2007, 1, 1): datetime(2007, 1, 1)})) - - tests.append((SemiMonthBegin(2), - {datetime(2008, 1, 1): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 15), - datetime(2006, 12, 1): datetime(2007, 1, 1), - datetime(2006, 12, 29): datetime(2007, 1, 15), - datetime(2006, 12, 15): datetime(2007, 1, 15), - datetime(2007, 1, 1): datetime(2007, 2, 1), - datetime(2007, 1, 16): datetime(2007, 2, 15), - datetime(2006, 11, 1): datetime(2006, 12, 1)})) - - tests.append((SemiMonthBegin(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 15), - datetime(2008, 6, 30): datetime(2008, 6, 15), - datetime(2008, 6, 14): datetime(2008, 6, 1), - datetime(2008, 12, 31): datetime(2008, 12, 15), - datetime(2006, 12, 29): datetime(2006, 12, 15), - datetime(2006, 12, 15): datetime(2006, 12, 1), - datetime(2007, 1, 1): datetime(2006, 12, 15)})) - - tests.append((SemiMonthBegin(-1, day_of_month=4), - {datetime(2007, 1, 1): datetime(2006, 12, 4), - datetime(2007, 1, 4): datetime(2007, 1, 1), - datetime(2008, 6, 30): datetime(2008, 6, 4), - datetime(2008, 12, 31): datetime(2008, 12, 4), - datetime(2006, 12, 5): datetime(2006, 12, 4), - datetime(2006, 12, 30): datetime(2006, 12, 4), - datetime(2006, 12, 2): datetime(2006, 12, 1), - datetime(2007, 1, 1): datetime(2006, 12, 4)})) - - tests.append((SemiMonthBegin(-2), - {datetime(2007, 1, 1): datetime(2006, 12, 1), - datetime(2008, 6, 30): datetime(2008, 6, 1), - datetime(2008, 6, 14): datetime(2008, 5, 15), - datetime(2008, 12, 31): datetime(2008, 12, 1), - datetime(2006, 12, 29): datetime(2006, 12, 1), - datetime(2006, 12, 15): datetime(2006, 11, 15), - datetime(2007, 1, 1): datetime(2006, 12, 1)})) - - return tests - - def test_offset_whole_year(self): - dates = (datetime(2007, 12, 15), - datetime(2008, 1, 1), - datetime(2008, 1, 15), - datetime(2008, 2, 1), - datetime(2008, 2, 15), - datetime(2008, 3, 1), - datetime(2008, 3, 15), - datetime(2008, 4, 1), - datetime(2008, 4, 15), - datetime(2008, 5, 1), - datetime(2008, 5, 15), - datetime(2008, 6, 1), - datetime(2008, 6, 15), - datetime(2008, 7, 1), - datetime(2008, 7, 15), - datetime(2008, 8, 1), - datetime(2008, 8, 15), - datetime(2008, 9, 1), - datetime(2008, 9, 15), - datetime(2008, 10, 1), - datetime(2008, 10, 15), - datetime(2008, 11, 1), - datetime(2008, 11, 15), - datetime(2008, 12, 1), - datetime(2008, 12, 15)) - - for base, exp_date in zip(dates[:-1], dates[1:]): - assertEq(SemiMonthBegin(), base, exp_date) - - # ensure .apply_index works as expected - s = DatetimeIndex(dates[:-1]) - result = SemiMonthBegin().apply_index(s) - exp = DatetimeIndex(dates[1:]) - tm.assert_index_equal(result, exp) - - # ensure generating a range with DatetimeIndex gives same result - result = DatetimeIndex(start=dates[0], end=dates[-1], freq='SMS') - exp = DatetimeIndex(dates) - tm.assert_index_equal(result, exp) - - def test_offset(self): - for offset, cases in self._get_tests(): - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_apply_index(self): - for offset, cases in self._get_tests(): - s = DatetimeIndex(cases.keys()) - result = offset.apply_index(s) - exp = DatetimeIndex(cases.values()) - tm.assert_index_equal(result, exp) - - def test_onOffset(self): - tests = [(datetime(2007, 12, 1), True), - (datetime(2007, 12, 15), True), - (datetime(2007, 12, 14), False), - (datetime(2007, 12, 31), False), - (datetime(2008, 2, 15), True)] - - for dt, expected in tests: - assertOnOffset(SemiMonthBegin(), dt, expected) - - def test_vectorized_offset_addition(self): - for klass, assert_func in zip([Series, DatetimeIndex], - [self.assert_series_equal, - tm.assert_index_equal]): - - s = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - result = s + SemiMonthBegin() - result2 = SemiMonthBegin() + s - exp = klass([Timestamp('2000-02-01 00:15:00', tz='US/Central'), - Timestamp('2000-03-01', tz='US/Central')], name='a') - assert_func(result, exp) - assert_func(result2, exp) - - s = klass([Timestamp('2000-01-01 00:15:00', tz='US/Central'), - Timestamp('2000-02-01', tz='US/Central')], name='a') - result = s + SemiMonthBegin() - result2 = SemiMonthBegin() + s - exp = klass([Timestamp('2000-01-15 00:15:00', tz='US/Central'), - Timestamp('2000-02-15', tz='US/Central')], name='a') - assert_func(result, exp) - assert_func(result2, exp) - - -class TestBQuarterBegin(Base): - _offset = BQuarterBegin - - def test_repr(self): - self.assertEqual(repr(BQuarterBegin()), - "") - self.assertEqual(repr(BQuarterBegin(startingMonth=3)), - "") - self.assertEqual(repr(BQuarterBegin(startingMonth=1)), - "") - - def test_isAnchored(self): - self.assertTrue(BQuarterBegin(startingMonth=1).isAnchored()) - self.assertTrue(BQuarterBegin().isAnchored()) - self.assertFalse(BQuarterBegin(2, startingMonth=1).isAnchored()) - - def test_offset(self): - tests = [] - - tests.append((BQuarterBegin(startingMonth=1), - {datetime(2008, 1, 1): datetime(2008, 4, 1), - datetime(2008, 1, 31): datetime(2008, 4, 1), - datetime(2008, 2, 15): datetime(2008, 4, 1), - datetime(2008, 2, 29): datetime(2008, 4, 1), - datetime(2008, 3, 15): datetime(2008, 4, 1), - datetime(2008, 3, 31): datetime(2008, 4, 1), - datetime(2008, 4, 15): datetime(2008, 7, 1), - datetime(2007, 3, 15): datetime(2007, 4, 2), - datetime(2007, 2, 28): datetime(2007, 4, 2), - datetime(2007, 1, 1): datetime(2007, 4, 2), - datetime(2007, 4, 15): datetime(2007, 7, 2), - datetime(2007, 7, 1): datetime(2007, 7, 2), - datetime(2007, 4, 1): datetime(2007, 4, 2), - datetime(2007, 4, 2): datetime(2007, 7, 2), - datetime(2008, 4, 30): datetime(2008, 7, 1), })) - - tests.append((BQuarterBegin(startingMonth=2), - {datetime(2008, 1, 1): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2008, 1, 15): datetime(2008, 2, 1), - datetime(2008, 2, 29): datetime(2008, 5, 1), - datetime(2008, 3, 15): datetime(2008, 5, 1), - datetime(2008, 3, 31): datetime(2008, 5, 1), - datetime(2008, 4, 15): datetime(2008, 5, 1), - datetime(2008, 8, 15): datetime(2008, 11, 3), - datetime(2008, 9, 15): datetime(2008, 11, 3), - datetime(2008, 11, 1): datetime(2008, 11, 3), - datetime(2008, 4, 30): datetime(2008, 5, 1), })) - - tests.append((BQuarterBegin(startingMonth=1, n=0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2007, 12, 31): datetime(2008, 1, 1), - datetime(2008, 2, 15): datetime(2008, 4, 1), - datetime(2008, 2, 29): datetime(2008, 4, 1), - datetime(2008, 1, 15): datetime(2008, 4, 1), - datetime(2008, 2, 27): datetime(2008, 4, 1), - datetime(2008, 3, 15): datetime(2008, 4, 1), - datetime(2007, 4, 1): datetime(2007, 4, 2), - datetime(2007, 4, 2): datetime(2007, 4, 2), - datetime(2007, 7, 1): datetime(2007, 7, 2), - datetime(2007, 4, 15): datetime(2007, 7, 2), - datetime(2007, 7, 2): datetime(2007, 7, 2), })) - - tests.append((BQuarterBegin(startingMonth=1, n=-1), - {datetime(2008, 1, 1): datetime(2007, 10, 1), - datetime(2008, 1, 31): datetime(2008, 1, 1), - datetime(2008, 2, 15): datetime(2008, 1, 1), - datetime(2008, 2, 29): datetime(2008, 1, 1), - datetime(2008, 3, 15): datetime(2008, 1, 1), - datetime(2008, 3, 31): datetime(2008, 1, 1), - datetime(2008, 4, 15): datetime(2008, 4, 1), - datetime(2007, 7, 3): datetime(2007, 7, 2), - datetime(2007, 4, 3): datetime(2007, 4, 2), - datetime(2007, 7, 2): datetime(2007, 4, 2), - datetime(2008, 4, 1): datetime(2008, 1, 1), })) - - tests.append((BQuarterBegin(startingMonth=1, n=2), - {datetime(2008, 1, 1): datetime(2008, 7, 1), - datetime(2008, 1, 15): datetime(2008, 7, 1), - datetime(2008, 2, 29): datetime(2008, 7, 1), - datetime(2008, 3, 15): datetime(2008, 7, 1), - datetime(2007, 3, 31): datetime(2007, 7, 2), - datetime(2007, 4, 15): datetime(2007, 10, 1), - datetime(2008, 4, 30): datetime(2008, 10, 1), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - # corner - offset = BQuarterBegin(n=-1, startingMonth=1) - self.assertEqual(datetime(2007, 4, 3) + offset, datetime(2007, 4, 2)) - - -class TestBQuarterEnd(Base): - _offset = BQuarterEnd - - def test_repr(self): - self.assertEqual(repr(BQuarterEnd()), - "") - self.assertEqual(repr(BQuarterEnd(startingMonth=3)), - "") - self.assertEqual(repr(BQuarterEnd(startingMonth=1)), - "") - - def test_isAnchored(self): - self.assertTrue(BQuarterEnd(startingMonth=1).isAnchored()) - self.assertTrue(BQuarterEnd().isAnchored()) - self.assertFalse(BQuarterEnd(2, startingMonth=1).isAnchored()) - - def test_offset(self): - tests = [] - - tests.append((BQuarterEnd(startingMonth=1), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 4, 30), - datetime(2008, 2, 15): datetime(2008, 4, 30), - datetime(2008, 2, 29): datetime(2008, 4, 30), - datetime(2008, 3, 15): datetime(2008, 4, 30), - datetime(2008, 3, 31): datetime(2008, 4, 30), - datetime(2008, 4, 15): datetime(2008, 4, 30), - datetime(2008, 4, 30): datetime(2008, 7, 31), })) - - tests.append((BQuarterEnd(startingMonth=2), - {datetime(2008, 1, 1): datetime(2008, 2, 29), - datetime(2008, 1, 31): datetime(2008, 2, 29), - datetime(2008, 2, 15): datetime(2008, 2, 29), - datetime(2008, 2, 29): datetime(2008, 5, 30), - datetime(2008, 3, 15): datetime(2008, 5, 30), - datetime(2008, 3, 31): datetime(2008, 5, 30), - datetime(2008, 4, 15): datetime(2008, 5, 30), - datetime(2008, 4, 30): datetime(2008, 5, 30), })) - - tests.append((BQuarterEnd(startingMonth=1, n=0), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2008, 2, 15): datetime(2008, 4, 30), - datetime(2008, 2, 29): datetime(2008, 4, 30), - datetime(2008, 3, 15): datetime(2008, 4, 30), - datetime(2008, 3, 31): datetime(2008, 4, 30), - datetime(2008, 4, 15): datetime(2008, 4, 30), - datetime(2008, 4, 30): datetime(2008, 4, 30), })) - - tests.append((BQuarterEnd(startingMonth=1, n=-1), - {datetime(2008, 1, 1): datetime(2007, 10, 31), - datetime(2008, 1, 31): datetime(2007, 10, 31), - datetime(2008, 2, 15): datetime(2008, 1, 31), - datetime(2008, 2, 29): datetime(2008, 1, 31), - datetime(2008, 3, 15): datetime(2008, 1, 31), - datetime(2008, 3, 31): datetime(2008, 1, 31), - datetime(2008, 4, 15): datetime(2008, 1, 31), - datetime(2008, 4, 30): datetime(2008, 1, 31), })) - - tests.append((BQuarterEnd(startingMonth=1, n=2), - {datetime(2008, 1, 31): datetime(2008, 7, 31), - datetime(2008, 2, 15): datetime(2008, 7, 31), - datetime(2008, 2, 29): datetime(2008, 7, 31), - datetime(2008, 3, 15): datetime(2008, 7, 31), - datetime(2008, 3, 31): datetime(2008, 7, 31), - datetime(2008, 4, 15): datetime(2008, 7, 31), - datetime(2008, 4, 30): datetime(2008, 10, 31), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - # corner - offset = BQuarterEnd(n=-1, startingMonth=1) - self.assertEqual(datetime(2010, 1, 31) + offset, datetime(2010, 1, 29)) - - def test_onOffset(self): - - tests = [ - (BQuarterEnd(1, startingMonth=1), datetime(2008, 1, 31), True), - (BQuarterEnd(1, startingMonth=1), datetime(2007, 12, 31), False), - (BQuarterEnd(1, startingMonth=1), datetime(2008, 2, 29), False), - (BQuarterEnd(1, startingMonth=1), datetime(2007, 3, 30), False), - (BQuarterEnd(1, startingMonth=1), datetime(2007, 3, 31), False), - (BQuarterEnd(1, startingMonth=1), datetime(2008, 4, 30), True), - (BQuarterEnd(1, startingMonth=1), datetime(2008, 5, 30), False), - (BQuarterEnd(1, startingMonth=1), datetime(2007, 6, 29), False), - (BQuarterEnd(1, startingMonth=1), datetime(2007, 6, 30), False), - (BQuarterEnd(1, startingMonth=2), datetime(2008, 1, 31), False), - (BQuarterEnd(1, startingMonth=2), datetime(2007, 12, 31), False), - (BQuarterEnd(1, startingMonth=2), datetime(2008, 2, 29), True), - (BQuarterEnd(1, startingMonth=2), datetime(2007, 3, 30), False), - (BQuarterEnd(1, startingMonth=2), datetime(2007, 3, 31), False), - (BQuarterEnd(1, startingMonth=2), datetime(2008, 4, 30), False), - (BQuarterEnd(1, startingMonth=2), datetime(2008, 5, 30), True), - (BQuarterEnd(1, startingMonth=2), datetime(2007, 6, 29), False), - (BQuarterEnd(1, startingMonth=2), datetime(2007, 6, 30), False), - (BQuarterEnd(1, startingMonth=3), datetime(2008, 1, 31), False), - (BQuarterEnd(1, startingMonth=3), datetime(2007, 12, 31), True), - (BQuarterEnd(1, startingMonth=3), datetime(2008, 2, 29), False), - (BQuarterEnd(1, startingMonth=3), datetime(2007, 3, 30), True), - (BQuarterEnd(1, startingMonth=3), datetime(2007, 3, 31), False), - (BQuarterEnd(1, startingMonth=3), datetime(2008, 4, 30), False), - (BQuarterEnd(1, startingMonth=3), datetime(2008, 5, 30), False), - (BQuarterEnd(1, startingMonth=3), datetime(2007, 6, 29), True), - (BQuarterEnd(1, startingMonth=3), datetime(2007, 6, 30), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -def makeFY5253LastOfMonthQuarter(*args, **kwds): - return FY5253Quarter(*args, variation="last", **kwds) - - -def makeFY5253NearestEndMonthQuarter(*args, **kwds): - return FY5253Quarter(*args, variation="nearest", **kwds) - - -def makeFY5253NearestEndMonth(*args, **kwds): - return FY5253(*args, variation="nearest", **kwds) - - -def makeFY5253LastOfMonth(*args, **kwds): - return FY5253(*args, variation="last", **kwds) - - -class TestFY5253LastOfMonth(Base): - - def test_onOffset(self): - - offset_lom_sat_aug = makeFY5253LastOfMonth(1, startingMonth=8, - weekday=WeekDay.SAT) - offset_lom_sat_sep = makeFY5253LastOfMonth(1, startingMonth=9, - weekday=WeekDay.SAT) - - tests = [ - # From Wikipedia (see: - # http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar#Last_Saturday_of_the_month_at_fiscal_year_end) - (offset_lom_sat_aug, datetime(2006, 8, 26), True), - (offset_lom_sat_aug, datetime(2007, 8, 25), True), - (offset_lom_sat_aug, datetime(2008, 8, 30), True), - (offset_lom_sat_aug, datetime(2009, 8, 29), True), - (offset_lom_sat_aug, datetime(2010, 8, 28), True), - (offset_lom_sat_aug, datetime(2011, 8, 27), True), - (offset_lom_sat_aug, datetime(2012, 8, 25), True), - (offset_lom_sat_aug, datetime(2013, 8, 31), True), - (offset_lom_sat_aug, datetime(2014, 8, 30), True), - (offset_lom_sat_aug, datetime(2015, 8, 29), True), - (offset_lom_sat_aug, datetime(2016, 8, 27), True), - (offset_lom_sat_aug, datetime(2017, 8, 26), True), - (offset_lom_sat_aug, datetime(2018, 8, 25), True), - (offset_lom_sat_aug, datetime(2019, 8, 31), True), - - (offset_lom_sat_aug, datetime(2006, 8, 27), False), - (offset_lom_sat_aug, datetime(2007, 8, 28), False), - (offset_lom_sat_aug, datetime(2008, 8, 31), False), - (offset_lom_sat_aug, datetime(2009, 8, 30), False), - (offset_lom_sat_aug, datetime(2010, 8, 29), False), - (offset_lom_sat_aug, datetime(2011, 8, 28), False), - - (offset_lom_sat_aug, datetime(2006, 8, 25), False), - (offset_lom_sat_aug, datetime(2007, 8, 24), False), - (offset_lom_sat_aug, datetime(2008, 8, 29), False), - (offset_lom_sat_aug, datetime(2009, 8, 28), False), - (offset_lom_sat_aug, datetime(2010, 8, 27), False), - (offset_lom_sat_aug, datetime(2011, 8, 26), False), - (offset_lom_sat_aug, datetime(2019, 8, 30), False), - - # From GMCR (see for example: - # http://yahoo.brand.edgar-online.com/Default.aspx? - # companyid=3184&formtypeID=7) - (offset_lom_sat_sep, datetime(2010, 9, 25), True), - (offset_lom_sat_sep, datetime(2011, 9, 24), True), - (offset_lom_sat_sep, datetime(2012, 9, 29), True), - - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_apply(self): - offset_lom_aug_sat = makeFY5253LastOfMonth(startingMonth=8, - weekday=WeekDay.SAT) - offset_lom_aug_sat_1 = makeFY5253LastOfMonth(n=1, startingMonth=8, - weekday=WeekDay.SAT) - - date_seq_lom_aug_sat = [datetime(2006, 8, 26), datetime(2007, 8, 25), - datetime(2008, 8, 30), datetime(2009, 8, 29), - datetime(2010, 8, 28), datetime(2011, 8, 27), - datetime(2012, 8, 25), datetime(2013, 8, 31), - datetime(2014, 8, 30), datetime(2015, 8, 29), - datetime(2016, 8, 27)] - - tests = [ - (offset_lom_aug_sat, date_seq_lom_aug_sat), - (offset_lom_aug_sat_1, date_seq_lom_aug_sat), - (offset_lom_aug_sat, [ - datetime(2006, 8, 25)] + date_seq_lom_aug_sat), - (offset_lom_aug_sat_1, [ - datetime(2006, 8, 27)] + date_seq_lom_aug_sat[1:]), - (makeFY5253LastOfMonth(n=-1, startingMonth=8, - weekday=WeekDay.SAT), - list(reversed(date_seq_lom_aug_sat))), - ] - for test in tests: - offset, data = test - current = data[0] - for datum in data[1:]: - current = current + offset - self.assertEqual(current, datum) - - -class TestFY5253NearestEndMonth(Base): - - def test_get_target_month_end(self): - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, - weekday=WeekDay.SAT) - .get_target_month_end( - datetime(2013, 1, 1)), datetime(2013, 8, 31)) - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=12, - weekday=WeekDay.SAT) - .get_target_month_end(datetime(2013, 1, 1)), - datetime(2013, 12, 31)) - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=2, - weekday=WeekDay.SAT) - .get_target_month_end(datetime(2013, 1, 1)), - datetime(2013, 2, 28)) - - def test_get_year_end(self): - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, - weekday=WeekDay.SAT) - .get_year_end(datetime(2013, 1, 1)), - datetime(2013, 8, 31)) - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, - weekday=WeekDay.SUN) - .get_year_end(datetime(2013, 1, 1)), - datetime(2013, 9, 1)) - self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, - weekday=WeekDay.FRI) - .get_year_end(datetime(2013, 1, 1)), - datetime(2013, 8, 30)) - - offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, - variation="nearest") - self.assertEqual(offset_n.get_year_end( - datetime(2012, 1, 1)), datetime(2013, 1, 1)) - self.assertEqual(offset_n.get_year_end( - datetime(2012, 1, 10)), datetime(2013, 1, 1)) - - self.assertEqual(offset_n.get_year_end( - datetime(2013, 1, 1)), datetime(2013, 12, 31)) - self.assertEqual(offset_n.get_year_end( - datetime(2013, 1, 2)), datetime(2013, 12, 31)) - self.assertEqual(offset_n.get_year_end( - datetime(2013, 1, 3)), datetime(2013, 12, 31)) - self.assertEqual(offset_n.get_year_end( - datetime(2013, 1, 10)), datetime(2013, 12, 31)) - - JNJ = FY5253(n=1, startingMonth=12, weekday=6, variation="nearest") - self.assertEqual(JNJ.get_year_end( - datetime(2006, 1, 1)), datetime(2006, 12, 31)) - - def test_onOffset(self): - offset_lom_aug_sat = makeFY5253NearestEndMonth(1, startingMonth=8, - weekday=WeekDay.SAT) - offset_lom_aug_thu = makeFY5253NearestEndMonth(1, startingMonth=8, - weekday=WeekDay.THU) - offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, - variation="nearest") - - tests = [ - # From Wikipedia (see: - # http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar - # #Saturday_nearest_the_end_of_month) - # 2006-09-02 2006 September 2 - # 2007-09-01 2007 September 1 - # 2008-08-30 2008 August 30 (leap year) - # 2009-08-29 2009 August 29 - # 2010-08-28 2010 August 28 - # 2011-09-03 2011 September 3 - # 2012-09-01 2012 September 1 (leap year) - # 2013-08-31 2013 August 31 - # 2014-08-30 2014 August 30 - # 2015-08-29 2015 August 29 - # 2016-09-03 2016 September 3 (leap year) - # 2017-09-02 2017 September 2 - # 2018-09-01 2018 September 1 - # 2019-08-31 2019 August 31 - (offset_lom_aug_sat, datetime(2006, 9, 2), True), - (offset_lom_aug_sat, datetime(2007, 9, 1), True), - (offset_lom_aug_sat, datetime(2008, 8, 30), True), - (offset_lom_aug_sat, datetime(2009, 8, 29), True), - (offset_lom_aug_sat, datetime(2010, 8, 28), True), - (offset_lom_aug_sat, datetime(2011, 9, 3), True), - - (offset_lom_aug_sat, datetime(2016, 9, 3), True), - (offset_lom_aug_sat, datetime(2017, 9, 2), True), - (offset_lom_aug_sat, datetime(2018, 9, 1), True), - (offset_lom_aug_sat, datetime(2019, 8, 31), True), - - (offset_lom_aug_sat, datetime(2006, 8, 27), False), - (offset_lom_aug_sat, datetime(2007, 8, 28), False), - (offset_lom_aug_sat, datetime(2008, 8, 31), False), - (offset_lom_aug_sat, datetime(2009, 8, 30), False), - (offset_lom_aug_sat, datetime(2010, 8, 29), False), - (offset_lom_aug_sat, datetime(2011, 8, 28), False), - - (offset_lom_aug_sat, datetime(2006, 8, 25), False), - (offset_lom_aug_sat, datetime(2007, 8, 24), False), - (offset_lom_aug_sat, datetime(2008, 8, 29), False), - (offset_lom_aug_sat, datetime(2009, 8, 28), False), - (offset_lom_aug_sat, datetime(2010, 8, 27), False), - (offset_lom_aug_sat, datetime(2011, 8, 26), False), - (offset_lom_aug_sat, datetime(2019, 8, 30), False), - - # From Micron, see: - # http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 - (offset_lom_aug_thu, datetime(2012, 8, 30), True), - (offset_lom_aug_thu, datetime(2011, 9, 1), True), - - (offset_n, datetime(2012, 12, 31), False), - (offset_n, datetime(2013, 1, 1), True), - (offset_n, datetime(2013, 1, 2), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_apply(self): - date_seq_nem_8_sat = [datetime(2006, 9, 2), datetime(2007, 9, 1), - datetime(2008, 8, 30), datetime(2009, 8, 29), - datetime(2010, 8, 28), datetime(2011, 9, 3)] - - JNJ = [datetime(2005, 1, 2), datetime(2006, 1, 1), - datetime(2006, 12, 31), datetime(2007, 12, 30), - datetime(2008, 12, 28), datetime(2010, 1, 3), - datetime(2011, 1, 2), datetime(2012, 1, 1), - datetime(2012, 12, 30)] - - DEC_SAT = FY5253(n=-1, startingMonth=12, weekday=5, - variation="nearest") - - tests = [ - (makeFY5253NearestEndMonth(startingMonth=8, - weekday=WeekDay.SAT), - date_seq_nem_8_sat), - (makeFY5253NearestEndMonth(n=1, startingMonth=8, - weekday=WeekDay.SAT), - date_seq_nem_8_sat), - (makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT), - [datetime(2006, 9, 1)] + date_seq_nem_8_sat), - (makeFY5253NearestEndMonth(n=1, startingMonth=8, - weekday=WeekDay.SAT), - [datetime(2006, 9, 3)] + date_seq_nem_8_sat[1:]), - (makeFY5253NearestEndMonth(n=-1, startingMonth=8, - weekday=WeekDay.SAT), - list(reversed(date_seq_nem_8_sat))), - (makeFY5253NearestEndMonth(n=1, startingMonth=12, - weekday=WeekDay.SUN), JNJ), - (makeFY5253NearestEndMonth(n=-1, startingMonth=12, - weekday=WeekDay.SUN), - list(reversed(JNJ))), - (makeFY5253NearestEndMonth(n=1, startingMonth=12, - weekday=WeekDay.SUN), - [datetime(2005, 1, 2), datetime(2006, 1, 1)]), - (makeFY5253NearestEndMonth(n=1, startingMonth=12, - weekday=WeekDay.SUN), - [datetime(2006, 1, 2), datetime(2006, 12, 31)]), - (DEC_SAT, [datetime(2013, 1, 15), datetime(2012, 12, 29)]) - ] - for test in tests: - offset, data = test - current = data[0] - for datum in data[1:]: - current = current + offset - self.assertEqual(current, datum) - - -class TestFY5253LastOfMonthQuarter(Base): - - def test_isAnchored(self): - self.assertTrue( - makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, - qtr_with_extra_week=4).isAnchored()) - self.assertTrue( - makeFY5253LastOfMonthQuarter(weekday=WeekDay.SAT, startingMonth=3, - qtr_with_extra_week=4).isAnchored()) - self.assertFalse(makeFY5253LastOfMonthQuarter( - 2, startingMonth=1, weekday=WeekDay.SAT, - qtr_with_extra_week=4).isAnchored()) - - def test_equality(self): - self.assertEqual(makeFY5253LastOfMonthQuarter(startingMonth=1, - weekday=WeekDay.SAT, - qtr_with_extra_week=4), - makeFY5253LastOfMonthQuarter(startingMonth=1, - weekday=WeekDay.SAT, - qtr_with_extra_week=4)) - self.assertNotEqual( - makeFY5253LastOfMonthQuarter( - startingMonth=1, weekday=WeekDay.SAT, - qtr_with_extra_week=4), - makeFY5253LastOfMonthQuarter( - startingMonth=1, weekday=WeekDay.SUN, - qtr_with_extra_week=4)) - self.assertNotEqual( - makeFY5253LastOfMonthQuarter( - startingMonth=1, weekday=WeekDay.SAT, - qtr_with_extra_week=4), - makeFY5253LastOfMonthQuarter( - startingMonth=2, weekday=WeekDay.SAT, - qtr_with_extra_week=4)) - - def test_offset(self): - offset = makeFY5253LastOfMonthQuarter(1, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - offset2 = makeFY5253LastOfMonthQuarter(2, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - offset4 = makeFY5253LastOfMonthQuarter(4, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - - offset_neg1 = makeFY5253LastOfMonthQuarter(-1, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - offset_neg2 = makeFY5253LastOfMonthQuarter(-2, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - - GMCR = [datetime(2010, 3, 27), datetime(2010, 6, 26), - datetime(2010, 9, 25), datetime(2010, 12, 25), - datetime(2011, 3, 26), datetime(2011, 6, 25), - datetime(2011, 9, 24), datetime(2011, 12, 24), - datetime(2012, 3, 24), datetime(2012, 6, 23), - datetime(2012, 9, 29), datetime(2012, 12, 29), - datetime(2013, 3, 30), datetime(2013, 6, 29)] - - assertEq(offset, base=GMCR[0], expected=GMCR[1]) - assertEq(offset, base=GMCR[0] + relativedelta(days=-1), - expected=GMCR[0]) - assertEq(offset, base=GMCR[1], expected=GMCR[2]) - - assertEq(offset2, base=GMCR[0], expected=GMCR[2]) - assertEq(offset4, base=GMCR[0], expected=GMCR[4]) - - assertEq(offset_neg1, base=GMCR[-1], expected=GMCR[-2]) - assertEq(offset_neg1, base=GMCR[-1] + relativedelta(days=+1), - expected=GMCR[-1]) - assertEq(offset_neg2, base=GMCR[-1], expected=GMCR[-3]) - - date = GMCR[0] + relativedelta(days=-1) - for expected in GMCR: - assertEq(offset, date, expected) - date = date + offset - - date = GMCR[-1] + relativedelta(days=+1) - for expected in reversed(GMCR): - assertEq(offset_neg1, date, expected) - date = date + offset_neg1 - - def test_onOffset(self): - lomq_aug_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=8, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - lomq_sep_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=9, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - - tests = [ - # From Wikipedia - (lomq_aug_sat_4, datetime(2006, 8, 26), True), - (lomq_aug_sat_4, datetime(2007, 8, 25), True), - (lomq_aug_sat_4, datetime(2008, 8, 30), True), - (lomq_aug_sat_4, datetime(2009, 8, 29), True), - (lomq_aug_sat_4, datetime(2010, 8, 28), True), - (lomq_aug_sat_4, datetime(2011, 8, 27), True), - (lomq_aug_sat_4, datetime(2019, 8, 31), True), - - (lomq_aug_sat_4, datetime(2006, 8, 27), False), - (lomq_aug_sat_4, datetime(2007, 8, 28), False), - (lomq_aug_sat_4, datetime(2008, 8, 31), False), - (lomq_aug_sat_4, datetime(2009, 8, 30), False), - (lomq_aug_sat_4, datetime(2010, 8, 29), False), - (lomq_aug_sat_4, datetime(2011, 8, 28), False), - - (lomq_aug_sat_4, datetime(2006, 8, 25), False), - (lomq_aug_sat_4, datetime(2007, 8, 24), False), - (lomq_aug_sat_4, datetime(2008, 8, 29), False), - (lomq_aug_sat_4, datetime(2009, 8, 28), False), - (lomq_aug_sat_4, datetime(2010, 8, 27), False), - (lomq_aug_sat_4, datetime(2011, 8, 26), False), - (lomq_aug_sat_4, datetime(2019, 8, 30), False), - - # From GMCR - (lomq_sep_sat_4, datetime(2010, 9, 25), True), - (lomq_sep_sat_4, datetime(2011, 9, 24), True), - (lomq_sep_sat_4, datetime(2012, 9, 29), True), - - (lomq_sep_sat_4, datetime(2013, 6, 29), True), - (lomq_sep_sat_4, datetime(2012, 6, 23), True), - (lomq_sep_sat_4, datetime(2012, 6, 30), False), - - (lomq_sep_sat_4, datetime(2013, 3, 30), True), - (lomq_sep_sat_4, datetime(2012, 3, 24), True), - - (lomq_sep_sat_4, datetime(2012, 12, 29), True), - (lomq_sep_sat_4, datetime(2011, 12, 24), True), - - # INTC (extra week in Q1) - # See: http://www.intc.com/releasedetail.cfm?ReleaseID=542844 - (makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1), - datetime(2011, 4, 2), True), - - # see: http://google.brand.edgar-online.com/?sym=INTC&formtypeID=7 - (makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1), - datetime(2012, 12, 29), True), - (makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1), - datetime(2011, 12, 31), True), - (makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1), - datetime(2010, 12, 25), True), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_year_has_extra_week(self): - # End of long Q1 - self.assertTrue( - makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(2011, 4, 2))) - - # Start of long Q1 - self.assertTrue( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(2010, 12, 26))) - - # End of year before year with long Q1 - self.assertFalse( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(2010, 12, 25))) - - for year in [x - for x in range(1994, 2011 + 1) - if x not in [2011, 2005, 2000, 1994]]: - self.assertFalse( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(year, 4, 2))) - - # Other long years - self.assertTrue( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(2005, 4, 2))) - - self.assertTrue( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(2000, 4, 2))) - - self.assertTrue( - makeFY5253LastOfMonthQuarter( - 1, startingMonth=12, weekday=WeekDay.SAT, - qtr_with_extra_week=1) - .year_has_extra_week(datetime(1994, 4, 2))) - - def test_get_weeks(self): - sat_dec_1 = makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=1) - sat_dec_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=12, - weekday=WeekDay.SAT, - qtr_with_extra_week=4) - - self.assertEqual(sat_dec_1.get_weeks( - datetime(2011, 4, 2)), [14, 13, 13, 13]) - self.assertEqual(sat_dec_4.get_weeks( - datetime(2011, 4, 2)), [13, 13, 13, 14]) - self.assertEqual(sat_dec_1.get_weeks( - datetime(2010, 12, 25)), [13, 13, 13, 13]) - - -class TestFY5253NearestEndMonthQuarter(Base): - - def test_onOffset(self): - - offset_nem_sat_aug_4 = makeFY5253NearestEndMonthQuarter( - 1, startingMonth=8, weekday=WeekDay.SAT, - qtr_with_extra_week=4) - offset_nem_thu_aug_4 = makeFY5253NearestEndMonthQuarter( - 1, startingMonth=8, weekday=WeekDay.THU, - qtr_with_extra_week=4) - offset_n = FY5253(weekday=WeekDay.TUE, startingMonth=12, - variation="nearest", qtr_with_extra_week=4) - - tests = [ - # From Wikipedia - (offset_nem_sat_aug_4, datetime(2006, 9, 2), True), - (offset_nem_sat_aug_4, datetime(2007, 9, 1), True), - (offset_nem_sat_aug_4, datetime(2008, 8, 30), True), - (offset_nem_sat_aug_4, datetime(2009, 8, 29), True), - (offset_nem_sat_aug_4, datetime(2010, 8, 28), True), - (offset_nem_sat_aug_4, datetime(2011, 9, 3), True), - - (offset_nem_sat_aug_4, datetime(2016, 9, 3), True), - (offset_nem_sat_aug_4, datetime(2017, 9, 2), True), - (offset_nem_sat_aug_4, datetime(2018, 9, 1), True), - (offset_nem_sat_aug_4, datetime(2019, 8, 31), True), - - (offset_nem_sat_aug_4, datetime(2006, 8, 27), False), - (offset_nem_sat_aug_4, datetime(2007, 8, 28), False), - (offset_nem_sat_aug_4, datetime(2008, 8, 31), False), - (offset_nem_sat_aug_4, datetime(2009, 8, 30), False), - (offset_nem_sat_aug_4, datetime(2010, 8, 29), False), - (offset_nem_sat_aug_4, datetime(2011, 8, 28), False), - - (offset_nem_sat_aug_4, datetime(2006, 8, 25), False), - (offset_nem_sat_aug_4, datetime(2007, 8, 24), False), - (offset_nem_sat_aug_4, datetime(2008, 8, 29), False), - (offset_nem_sat_aug_4, datetime(2009, 8, 28), False), - (offset_nem_sat_aug_4, datetime(2010, 8, 27), False), - (offset_nem_sat_aug_4, datetime(2011, 8, 26), False), - (offset_nem_sat_aug_4, datetime(2019, 8, 30), False), - - # From Micron, see: - # http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 - (offset_nem_thu_aug_4, datetime(2012, 8, 30), True), - (offset_nem_thu_aug_4, datetime(2011, 9, 1), True), - - # See: http://google.brand.edgar-online.com/?sym=MU&formtypeID=13 - (offset_nem_thu_aug_4, datetime(2013, 5, 30), True), - (offset_nem_thu_aug_4, datetime(2013, 2, 28), True), - (offset_nem_thu_aug_4, datetime(2012, 11, 29), True), - (offset_nem_thu_aug_4, datetime(2012, 5, 31), True), - (offset_nem_thu_aug_4, datetime(2007, 3, 1), True), - (offset_nem_thu_aug_4, datetime(1994, 3, 3), True), - - (offset_n, datetime(2012, 12, 31), False), - (offset_n, datetime(2013, 1, 1), True), - (offset_n, datetime(2013, 1, 2), False) - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - def test_offset(self): - offset = makeFY5253NearestEndMonthQuarter(1, startingMonth=8, - weekday=WeekDay.THU, - qtr_with_extra_week=4) - - MU = [datetime(2012, 5, 31), datetime(2012, 8, 30), datetime(2012, 11, - 29), - datetime(2013, 2, 28), datetime(2013, 5, 30)] - - date = MU[0] + relativedelta(days=-1) - for expected in MU: - assertEq(offset, date, expected) - date = date + offset - - assertEq(offset, datetime(2012, 5, 31), datetime(2012, 8, 30)) - assertEq(offset, datetime(2012, 5, 30), datetime(2012, 5, 31)) - - offset2 = FY5253Quarter(weekday=5, startingMonth=12, variation="last", - qtr_with_extra_week=4) - - assertEq(offset2, datetime(2013, 1, 15), datetime(2013, 3, 30)) - - -class TestQuarterBegin(Base): - - def test_repr(self): - self.assertEqual(repr(QuarterBegin()), - "") - self.assertEqual(repr(QuarterBegin(startingMonth=3)), - "") - self.assertEqual(repr(QuarterBegin(startingMonth=1)), - "") - - def test_isAnchored(self): - self.assertTrue(QuarterBegin(startingMonth=1).isAnchored()) - self.assertTrue(QuarterBegin().isAnchored()) - self.assertFalse(QuarterBegin(2, startingMonth=1).isAnchored()) - - def test_offset(self): - tests = [] - - tests.append((QuarterBegin(startingMonth=1), - {datetime(2007, 12, 1): datetime(2008, 1, 1), - datetime(2008, 1, 1): datetime(2008, 4, 1), - datetime(2008, 2, 15): datetime(2008, 4, 1), - datetime(2008, 2, 29): datetime(2008, 4, 1), - datetime(2008, 3, 15): datetime(2008, 4, 1), - datetime(2008, 3, 31): datetime(2008, 4, 1), - datetime(2008, 4, 15): datetime(2008, 7, 1), - datetime(2008, 4, 1): datetime(2008, 7, 1), })) - - tests.append((QuarterBegin(startingMonth=2), - {datetime(2008, 1, 1): datetime(2008, 2, 1), - datetime(2008, 1, 31): datetime(2008, 2, 1), - datetime(2008, 1, 15): datetime(2008, 2, 1), - datetime(2008, 2, 29): datetime(2008, 5, 1), - datetime(2008, 3, 15): datetime(2008, 5, 1), - datetime(2008, 3, 31): datetime(2008, 5, 1), - datetime(2008, 4, 15): datetime(2008, 5, 1), - datetime(2008, 4, 30): datetime(2008, 5, 1), })) - - tests.append((QuarterBegin(startingMonth=1, n=0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 12, 1): datetime(2009, 1, 1), - datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 2, 15): datetime(2008, 4, 1), - datetime(2008, 2, 29): datetime(2008, 4, 1), - datetime(2008, 3, 15): datetime(2008, 4, 1), - datetime(2008, 3, 31): datetime(2008, 4, 1), - datetime(2008, 4, 15): datetime(2008, 7, 1), - datetime(2008, 4, 30): datetime(2008, 7, 1), })) - - tests.append((QuarterBegin(startingMonth=1, n=-1), - {datetime(2008, 1, 1): datetime(2007, 10, 1), - datetime(2008, 1, 31): datetime(2008, 1, 1), - datetime(2008, 2, 15): datetime(2008, 1, 1), - datetime(2008, 2, 29): datetime(2008, 1, 1), - datetime(2008, 3, 15): datetime(2008, 1, 1), - datetime(2008, 3, 31): datetime(2008, 1, 1), - datetime(2008, 4, 15): datetime(2008, 4, 1), - datetime(2008, 4, 30): datetime(2008, 4, 1), - datetime(2008, 7, 1): datetime(2008, 4, 1)})) - - tests.append((QuarterBegin(startingMonth=1, n=2), - {datetime(2008, 1, 1): datetime(2008, 7, 1), - datetime(2008, 2, 15): datetime(2008, 7, 1), - datetime(2008, 2, 29): datetime(2008, 7, 1), - datetime(2008, 3, 15): datetime(2008, 7, 1), - datetime(2008, 3, 31): datetime(2008, 7, 1), - datetime(2008, 4, 15): datetime(2008, 10, 1), - datetime(2008, 4, 1): datetime(2008, 10, 1), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - # corner - offset = QuarterBegin(n=-1, startingMonth=1) - self.assertEqual(datetime(2010, 2, 1) + offset, datetime(2010, 1, 1)) - - -class TestQuarterEnd(Base): - _offset = QuarterEnd - - def test_repr(self): - self.assertEqual(repr(QuarterEnd()), "") - self.assertEqual(repr(QuarterEnd(startingMonth=3)), - "") - self.assertEqual(repr(QuarterEnd(startingMonth=1)), - "") - - def test_isAnchored(self): - self.assertTrue(QuarterEnd(startingMonth=1).isAnchored()) - self.assertTrue(QuarterEnd().isAnchored()) - self.assertFalse(QuarterEnd(2, startingMonth=1).isAnchored()) - - def test_offset(self): - tests = [] - - tests.append((QuarterEnd(startingMonth=1), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 4, 30), - datetime(2008, 2, 15): datetime(2008, 4, 30), - datetime(2008, 2, 29): datetime(2008, 4, 30), - datetime(2008, 3, 15): datetime(2008, 4, 30), - datetime(2008, 3, 31): datetime(2008, 4, 30), - datetime(2008, 4, 15): datetime(2008, 4, 30), - datetime(2008, 4, 30): datetime(2008, 7, 31), })) - - tests.append((QuarterEnd(startingMonth=2), - {datetime(2008, 1, 1): datetime(2008, 2, 29), - datetime(2008, 1, 31): datetime(2008, 2, 29), - datetime(2008, 2, 15): datetime(2008, 2, 29), - datetime(2008, 2, 29): datetime(2008, 5, 31), - datetime(2008, 3, 15): datetime(2008, 5, 31), - datetime(2008, 3, 31): datetime(2008, 5, 31), - datetime(2008, 4, 15): datetime(2008, 5, 31), - datetime(2008, 4, 30): datetime(2008, 5, 31), })) - - tests.append((QuarterEnd(startingMonth=1, n=0), - {datetime(2008, 1, 1): datetime(2008, 1, 31), - datetime(2008, 1, 31): datetime(2008, 1, 31), - datetime(2008, 2, 15): datetime(2008, 4, 30), - datetime(2008, 2, 29): datetime(2008, 4, 30), - datetime(2008, 3, 15): datetime(2008, 4, 30), - datetime(2008, 3, 31): datetime(2008, 4, 30), - datetime(2008, 4, 15): datetime(2008, 4, 30), - datetime(2008, 4, 30): datetime(2008, 4, 30), })) - - tests.append((QuarterEnd(startingMonth=1, n=-1), - {datetime(2008, 1, 1): datetime(2007, 10, 31), - datetime(2008, 1, 31): datetime(2007, 10, 31), - datetime(2008, 2, 15): datetime(2008, 1, 31), - datetime(2008, 2, 29): datetime(2008, 1, 31), - datetime(2008, 3, 15): datetime(2008, 1, 31), - datetime(2008, 3, 31): datetime(2008, 1, 31), - datetime(2008, 4, 15): datetime(2008, 1, 31), - datetime(2008, 4, 30): datetime(2008, 1, 31), - datetime(2008, 7, 1): datetime(2008, 4, 30)})) - - tests.append((QuarterEnd(startingMonth=1, n=2), - {datetime(2008, 1, 31): datetime(2008, 7, 31), - datetime(2008, 2, 15): datetime(2008, 7, 31), - datetime(2008, 2, 29): datetime(2008, 7, 31), - datetime(2008, 3, 15): datetime(2008, 7, 31), - datetime(2008, 3, 31): datetime(2008, 7, 31), - datetime(2008, 4, 15): datetime(2008, 7, 31), - datetime(2008, 4, 30): datetime(2008, 10, 31), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - # corner - offset = QuarterEnd(n=-1, startingMonth=1) - self.assertEqual(datetime(2010, 2, 1) + offset, datetime(2010, 1, 31)) - - def test_onOffset(self): - - tests = [(QuarterEnd(1, startingMonth=1), datetime(2008, 1, 31), True), - (QuarterEnd(1, startingMonth=1), datetime(2007, 12, 31), - False), - (QuarterEnd(1, startingMonth=1), datetime(2008, 2, 29), - False), - (QuarterEnd(1, startingMonth=1), datetime(2007, 3, 30), - False), - (QuarterEnd(1, startingMonth=1), datetime(2007, 3, 31), - False), - (QuarterEnd(1, startingMonth=1), datetime(2008, 4, 30), True), - (QuarterEnd(1, startingMonth=1), datetime(2008, 5, 30), - False), - (QuarterEnd(1, startingMonth=1), datetime(2008, 5, 31), - False), - (QuarterEnd(1, startingMonth=1), datetime(2007, 6, 29), - False), - (QuarterEnd(1, startingMonth=1), datetime(2007, 6, 30), - False), - (QuarterEnd(1, startingMonth=2), datetime(2008, 1, 31), - False), - (QuarterEnd(1, startingMonth=2), datetime(2007, 12, 31), - False), - (QuarterEnd(1, startingMonth=2), datetime(2008, 2, 29), True), - (QuarterEnd(1, startingMonth=2), datetime(2007, 3, 30), - False), - (QuarterEnd(1, startingMonth=2), datetime(2007, 3, 31), - False), - (QuarterEnd(1, startingMonth=2), datetime(2008, 4, 30), - False), - (QuarterEnd(1, startingMonth=2), datetime(2008, 5, 30), - False), - (QuarterEnd(1, startingMonth=2), datetime(2008, 5, 31), True), - (QuarterEnd(1, startingMonth=2), datetime(2007, 6, 29), - False), - (QuarterEnd(1, startingMonth=2), datetime(2007, 6, 30), - False), - (QuarterEnd(1, startingMonth=3), datetime(2008, 1, 31), - False), - (QuarterEnd(1, startingMonth=3), datetime(2007, 12, 31), - True), - (QuarterEnd(1, startingMonth=3), datetime(2008, 2, 29), - False), - (QuarterEnd(1, startingMonth=3), datetime(2007, 3, 30), - False), - (QuarterEnd(1, startingMonth=3), datetime(2007, 3, 31), True), - (QuarterEnd(1, startingMonth=3), datetime(2008, 4, 30), - False), - (QuarterEnd(1, startingMonth=3), datetime(2008, 5, 30), - False), - (QuarterEnd(1, startingMonth=3), datetime(2008, 5, 31), - False), - (QuarterEnd(1, startingMonth=3), datetime(2007, 6, 29), - False), - (QuarterEnd(1, startingMonth=3), datetime(2007, 6, 30), - True), ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestBYearBegin(Base): - _offset = BYearBegin - - def test_misspecified(self): - self.assertRaises(ValueError, BYearBegin, month=13) - self.assertRaises(ValueError, BYearEnd, month=13) - - def test_offset(self): - tests = [] - - tests.append((BYearBegin(), - {datetime(2008, 1, 1): datetime(2009, 1, 1), - datetime(2008, 6, 30): datetime(2009, 1, 1), - datetime(2008, 12, 31): datetime(2009, 1, 1), - datetime(2011, 1, 1): datetime(2011, 1, 3), - datetime(2011, 1, 3): datetime(2012, 1, 2), - datetime(2005, 12, 30): datetime(2006, 1, 2), - datetime(2005, 12, 31): datetime(2006, 1, 2)})) - - tests.append((BYearBegin(0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 6, 30): datetime(2009, 1, 1), - datetime(2008, 12, 31): datetime(2009, 1, 1), - datetime(2005, 12, 30): datetime(2006, 1, 2), - datetime(2005, 12, 31): datetime(2006, 1, 2), })) - - tests.append((BYearBegin(-1), - {datetime(2007, 1, 1): datetime(2006, 1, 2), - datetime(2009, 1, 4): datetime(2009, 1, 1), - datetime(2009, 1, 1): datetime(2008, 1, 1), - datetime(2008, 6, 30): datetime(2008, 1, 1), - datetime(2008, 12, 31): datetime(2008, 1, 1), - datetime(2006, 12, 29): datetime(2006, 1, 2), - datetime(2006, 12, 30): datetime(2006, 1, 2), - datetime(2006, 1, 1): datetime(2005, 1, 3), })) - - tests.append((BYearBegin(-2), - {datetime(2007, 1, 1): datetime(2005, 1, 3), - datetime(2007, 6, 30): datetime(2006, 1, 2), - datetime(2008, 12, 31): datetime(2007, 1, 1), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - -class TestYearBegin(Base): - _offset = YearBegin - - def test_misspecified(self): - self.assertRaises(ValueError, YearBegin, month=13) - - def test_offset(self): - tests = [] - - tests.append((YearBegin(), - {datetime(2008, 1, 1): datetime(2009, 1, 1), - datetime(2008, 6, 30): datetime(2009, 1, 1), - datetime(2008, 12, 31): datetime(2009, 1, 1), - datetime(2005, 12, 30): datetime(2006, 1, 1), - datetime(2005, 12, 31): datetime(2006, 1, 1), })) - - tests.append((YearBegin(0), - {datetime(2008, 1, 1): datetime(2008, 1, 1), - datetime(2008, 6, 30): datetime(2009, 1, 1), - datetime(2008, 12, 31): datetime(2009, 1, 1), - datetime(2005, 12, 30): datetime(2006, 1, 1), - datetime(2005, 12, 31): datetime(2006, 1, 1), })) - - tests.append((YearBegin(3), - {datetime(2008, 1, 1): datetime(2011, 1, 1), - datetime(2008, 6, 30): datetime(2011, 1, 1), - datetime(2008, 12, 31): datetime(2011, 1, 1), - datetime(2005, 12, 30): datetime(2008, 1, 1), - datetime(2005, 12, 31): datetime(2008, 1, 1), })) - - tests.append((YearBegin(-1), - {datetime(2007, 1, 1): datetime(2006, 1, 1), - datetime(2007, 1, 15): datetime(2007, 1, 1), - datetime(2008, 6, 30): datetime(2008, 1, 1), - datetime(2008, 12, 31): datetime(2008, 1, 1), - datetime(2006, 12, 29): datetime(2006, 1, 1), - datetime(2006, 12, 30): datetime(2006, 1, 1), - datetime(2007, 1, 1): datetime(2006, 1, 1), })) - - tests.append((YearBegin(-2), - {datetime(2007, 1, 1): datetime(2005, 1, 1), - datetime(2008, 6, 30): datetime(2007, 1, 1), - datetime(2008, 12, 31): datetime(2007, 1, 1), })) - - tests.append((YearBegin(month=4), - {datetime(2007, 4, 1): datetime(2008, 4, 1), - datetime(2007, 4, 15): datetime(2008, 4, 1), - datetime(2007, 3, 1): datetime(2007, 4, 1), - datetime(2007, 12, 15): datetime(2008, 4, 1), - datetime(2012, 1, 31): datetime(2012, 4, 1), })) - - tests.append((YearBegin(0, month=4), - {datetime(2007, 4, 1): datetime(2007, 4, 1), - datetime(2007, 3, 1): datetime(2007, 4, 1), - datetime(2007, 12, 15): datetime(2008, 4, 1), - datetime(2012, 1, 31): datetime(2012, 4, 1), })) - - tests.append((YearBegin(4, month=4), - {datetime(2007, 4, 1): datetime(2011, 4, 1), - datetime(2007, 4, 15): datetime(2011, 4, 1), - datetime(2007, 3, 1): datetime(2010, 4, 1), - datetime(2007, 12, 15): datetime(2011, 4, 1), - datetime(2012, 1, 31): datetime(2015, 4, 1), })) - - tests.append((YearBegin(-1, month=4), - {datetime(2007, 4, 1): datetime(2006, 4, 1), - datetime(2007, 3, 1): datetime(2006, 4, 1), - datetime(2007, 12, 15): datetime(2007, 4, 1), - datetime(2012, 1, 31): datetime(2011, 4, 1), })) - - tests.append((YearBegin(-3, month=4), - {datetime(2007, 4, 1): datetime(2004, 4, 1), - datetime(2007, 3, 1): datetime(2004, 4, 1), - datetime(2007, 12, 15): datetime(2005, 4, 1), - datetime(2012, 1, 31): datetime(2009, 4, 1), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - - tests = [ - (YearBegin(), datetime(2007, 1, 3), False), - (YearBegin(), datetime(2008, 1, 1), True), - (YearBegin(), datetime(2006, 12, 31), False), - (YearBegin(), datetime(2006, 1, 2), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestBYearEndLagged(Base): - - def test_bad_month_fail(self): - self.assertRaises(Exception, BYearEnd, month=13) - self.assertRaises(Exception, BYearEnd, month=0) - - def test_offset(self): - tests = [] - - tests.append((BYearEnd(month=6), - {datetime(2008, 1, 1): datetime(2008, 6, 30), - datetime(2007, 6, 30): datetime(2008, 6, 30)}, )) - - tests.append((BYearEnd(n=-1, month=6), - {datetime(2008, 1, 1): datetime(2007, 6, 29), - datetime(2007, 6, 30): datetime(2007, 6, 29)}, )) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - self.assertEqual(base + offset, expected) - - def test_roll(self): - offset = BYearEnd(month=6) - date = datetime(2009, 11, 30) - - self.assertEqual(offset.rollforward(date), datetime(2010, 6, 30)) - self.assertEqual(offset.rollback(date), datetime(2009, 6, 30)) - - def test_onOffset(self): - - tests = [ - (BYearEnd(month=2), datetime(2007, 2, 28), True), - (BYearEnd(month=6), datetime(2007, 6, 30), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestBYearEnd(Base): - _offset = BYearEnd - - def test_offset(self): - tests = [] - - tests.append((BYearEnd(), - {datetime(2008, 1, 1): datetime(2008, 12, 31), - datetime(2008, 6, 30): datetime(2008, 12, 31), - datetime(2008, 12, 31): datetime(2009, 12, 31), - datetime(2005, 12, 30): datetime(2006, 12, 29), - datetime(2005, 12, 31): datetime(2006, 12, 29), })) - - tests.append((BYearEnd(0), - {datetime(2008, 1, 1): datetime(2008, 12, 31), - datetime(2008, 6, 30): datetime(2008, 12, 31), - datetime(2008, 12, 31): datetime(2008, 12, 31), - datetime(2005, 12, 31): datetime(2006, 12, 29), })) - - tests.append((BYearEnd(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 29), - datetime(2008, 6, 30): datetime(2007, 12, 31), - datetime(2008, 12, 31): datetime(2007, 12, 31), - datetime(2006, 12, 29): datetime(2005, 12, 30), - datetime(2006, 12, 30): datetime(2006, 12, 29), - datetime(2007, 1, 1): datetime(2006, 12, 29), })) - - tests.append((BYearEnd(-2), - {datetime(2007, 1, 1): datetime(2005, 12, 30), - datetime(2008, 6, 30): datetime(2006, 12, 29), - datetime(2008, 12, 31): datetime(2006, 12, 29), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - - tests = [ - (BYearEnd(), datetime(2007, 12, 31), True), - (BYearEnd(), datetime(2008, 1, 1), False), - (BYearEnd(), datetime(2006, 12, 31), False), - (BYearEnd(), datetime(2006, 12, 29), True), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestYearEnd(Base): - _offset = YearEnd - - def test_misspecified(self): - self.assertRaises(ValueError, YearEnd, month=13) - - def test_offset(self): - tests = [] - - tests.append((YearEnd(), - {datetime(2008, 1, 1): datetime(2008, 12, 31), - datetime(2008, 6, 30): datetime(2008, 12, 31), - datetime(2008, 12, 31): datetime(2009, 12, 31), - datetime(2005, 12, 30): datetime(2005, 12, 31), - datetime(2005, 12, 31): datetime(2006, 12, 31), })) - - tests.append((YearEnd(0), - {datetime(2008, 1, 1): datetime(2008, 12, 31), - datetime(2008, 6, 30): datetime(2008, 12, 31), - datetime(2008, 12, 31): datetime(2008, 12, 31), - datetime(2005, 12, 30): datetime(2005, 12, 31), })) - - tests.append((YearEnd(-1), - {datetime(2007, 1, 1): datetime(2006, 12, 31), - datetime(2008, 6, 30): datetime(2007, 12, 31), - datetime(2008, 12, 31): datetime(2007, 12, 31), - datetime(2006, 12, 29): datetime(2005, 12, 31), - datetime(2006, 12, 30): datetime(2005, 12, 31), - datetime(2007, 1, 1): datetime(2006, 12, 31), })) - - tests.append((YearEnd(-2), - {datetime(2007, 1, 1): datetime(2005, 12, 31), - datetime(2008, 6, 30): datetime(2006, 12, 31), - datetime(2008, 12, 31): datetime(2006, 12, 31), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - - tests = [ - (YearEnd(), datetime(2007, 12, 31), True), - (YearEnd(), datetime(2008, 1, 1), False), - (YearEnd(), datetime(2006, 12, 31), True), - (YearEnd(), datetime(2006, 12, 29), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -class TestYearEndDiffMonth(Base): - - def test_offset(self): - tests = [] - - tests.append((YearEnd(month=3), - {datetime(2008, 1, 1): datetime(2008, 3, 31), - datetime(2008, 2, 15): datetime(2008, 3, 31), - datetime(2008, 3, 31): datetime(2009, 3, 31), - datetime(2008, 3, 30): datetime(2008, 3, 31), - datetime(2005, 3, 31): datetime(2006, 3, 31), - datetime(2006, 7, 30): datetime(2007, 3, 31)})) - - tests.append((YearEnd(0, month=3), - {datetime(2008, 1, 1): datetime(2008, 3, 31), - datetime(2008, 2, 28): datetime(2008, 3, 31), - datetime(2008, 3, 31): datetime(2008, 3, 31), - datetime(2005, 3, 30): datetime(2005, 3, 31), })) - - tests.append((YearEnd(-1, month=3), - {datetime(2007, 1, 1): datetime(2006, 3, 31), - datetime(2008, 2, 28): datetime(2007, 3, 31), - datetime(2008, 3, 31): datetime(2007, 3, 31), - datetime(2006, 3, 29): datetime(2005, 3, 31), - datetime(2006, 3, 30): datetime(2005, 3, 31), - datetime(2007, 3, 1): datetime(2006, 3, 31), })) - - tests.append((YearEnd(-2, month=3), - {datetime(2007, 1, 1): datetime(2005, 3, 31), - datetime(2008, 6, 30): datetime(2007, 3, 31), - datetime(2008, 3, 31): datetime(2006, 3, 31), })) - - for offset, cases in tests: - for base, expected in compat.iteritems(cases): - assertEq(offset, base, expected) - - def test_onOffset(self): - - tests = [ - (YearEnd(month=3), datetime(2007, 3, 31), True), - (YearEnd(month=3), datetime(2008, 1, 1), False), - (YearEnd(month=3), datetime(2006, 3, 31), True), - (YearEnd(month=3), datetime(2006, 3, 29), False), - ] - - for offset, dt, expected in tests: - assertOnOffset(offset, dt, expected) - - -def assertEq(offset, base, expected): - actual = offset + base - actual_swapped = base + offset - actual_apply = offset.apply(base) - try: - assert actual == expected - assert actual_swapped == expected - assert actual_apply == expected - except AssertionError: - raise AssertionError("\nExpected: %s\nActual: %s\nFor Offset: %s)" - "\nAt Date: %s" % - (expected, actual, offset, base)) - - -def test_Easter(): - assertEq(Easter(), datetime(2010, 1, 1), datetime(2010, 4, 4)) - assertEq(Easter(), datetime(2010, 4, 5), datetime(2011, 4, 24)) - assertEq(Easter(2), datetime(2010, 1, 1), datetime(2011, 4, 24)) - - assertEq(Easter(), datetime(2010, 4, 4), datetime(2011, 4, 24)) - assertEq(Easter(2), datetime(2010, 4, 4), datetime(2012, 4, 8)) - - assertEq(-Easter(), datetime(2011, 1, 1), datetime(2010, 4, 4)) - assertEq(-Easter(), datetime(2010, 4, 5), datetime(2010, 4, 4)) - assertEq(-Easter(2), datetime(2011, 1, 1), datetime(2009, 4, 12)) - - assertEq(-Easter(), datetime(2010, 4, 4), datetime(2009, 4, 12)) - assertEq(-Easter(2), datetime(2010, 4, 4), datetime(2008, 3, 23)) - - -class TestTicks(tm.TestCase): - - ticks = [Hour, Minute, Second, Milli, Micro, Nano] - - def test_ticks(self): - offsets = [(Hour, Timedelta(hours=5)), - (Minute, Timedelta(hours=2, minutes=3)), - (Second, Timedelta(hours=2, seconds=3)), - (Milli, Timedelta(hours=2, milliseconds=3)), - (Micro, Timedelta(hours=2, microseconds=3)), - (Nano, Timedelta(hours=2, nanoseconds=3))] - - for kls, expected in offsets: - offset = kls(3) - result = offset + Timedelta(hours=2) - self.assertTrue(isinstance(result, Timedelta)) - self.assertEqual(result, expected) - - def test_Hour(self): - assertEq(Hour(), datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) - assertEq(Hour(-1), datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) - assertEq(2 * Hour(), datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) - assertEq(-1 * Hour(), datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) - - self.assertEqual(Hour(3) + Hour(2), Hour(5)) - self.assertEqual(Hour(3) - Hour(2), Hour()) - - self.assertNotEqual(Hour(4), Hour(1)) - - def test_Minute(self): - assertEq(Minute(), datetime(2010, 1, 1), datetime(2010, 1, 1, 0, 1)) - assertEq(Minute(-1), datetime(2010, 1, 1, 0, 1), datetime(2010, 1, 1)) - assertEq(2 * Minute(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 2)) - assertEq(-1 * Minute(), datetime(2010, 1, 1, 0, 1), - datetime(2010, 1, 1)) - - self.assertEqual(Minute(3) + Minute(2), Minute(5)) - self.assertEqual(Minute(3) - Minute(2), Minute()) - self.assertNotEqual(Minute(5), Minute()) - - def test_Second(self): - assertEq(Second(), datetime(2010, 1, 1), datetime(2010, 1, 1, 0, 0, 1)) - assertEq(Second(-1), datetime(2010, 1, 1, - 0, 0, 1), datetime(2010, 1, 1)) - assertEq(2 * Second(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 2)) - assertEq(-1 * Second(), datetime(2010, 1, 1, 0, 0, 1), - datetime(2010, 1, 1)) - - self.assertEqual(Second(3) + Second(2), Second(5)) - self.assertEqual(Second(3) - Second(2), Second()) - - def test_Millisecond(self): - assertEq(Milli(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 0, 1000)) - assertEq(Milli(-1), datetime(2010, 1, 1, 0, - 0, 0, 1000), datetime(2010, 1, 1)) - assertEq(Milli(2), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 0, 2000)) - assertEq(2 * Milli(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 0, 2000)) - assertEq(-1 * Milli(), datetime(2010, 1, 1, 0, 0, 0, 1000), - datetime(2010, 1, 1)) - - self.assertEqual(Milli(3) + Milli(2), Milli(5)) - self.assertEqual(Milli(3) - Milli(2), Milli()) - - def test_MillisecondTimestampArithmetic(self): - assertEq(Milli(), Timestamp('2010-01-01'), - Timestamp('2010-01-01 00:00:00.001')) - assertEq(Milli(-1), Timestamp('2010-01-01 00:00:00.001'), - Timestamp('2010-01-01')) - - def test_Microsecond(self): - assertEq(Micro(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 0, 1)) - assertEq(Micro(-1), datetime(2010, 1, 1, - 0, 0, 0, 1), datetime(2010, 1, 1)) - assertEq(2 * Micro(), datetime(2010, 1, 1), - datetime(2010, 1, 1, 0, 0, 0, 2)) - assertEq(-1 * Micro(), datetime(2010, 1, 1, 0, 0, 0, 1), - datetime(2010, 1, 1)) - - self.assertEqual(Micro(3) + Micro(2), Micro(5)) - self.assertEqual(Micro(3) - Micro(2), Micro()) - - def test_NanosecondGeneric(self): - timestamp = Timestamp(datetime(2010, 1, 1)) - self.assertEqual(timestamp.nanosecond, 0) - - result = timestamp + Nano(10) - self.assertEqual(result.nanosecond, 10) - - reverse_result = Nano(10) + timestamp - self.assertEqual(reverse_result.nanosecond, 10) - - def test_Nanosecond(self): - timestamp = Timestamp(datetime(2010, 1, 1)) - assertEq(Nano(), timestamp, timestamp + np.timedelta64(1, 'ns')) - assertEq(Nano(-1), timestamp + np.timedelta64(1, 'ns'), timestamp) - assertEq(2 * Nano(), timestamp, timestamp + np.timedelta64(2, 'ns')) - assertEq(-1 * Nano(), timestamp + np.timedelta64(1, 'ns'), timestamp) - - self.assertEqual(Nano(3) + Nano(2), Nano(5)) - self.assertEqual(Nano(3) - Nano(2), Nano()) - - # GH9284 - self.assertEqual(Nano(1) + Nano(10), Nano(11)) - self.assertEqual(Nano(5) + Micro(1), Nano(1005)) - self.assertEqual(Micro(5) + Nano(1), Nano(5001)) - - def test_tick_zero(self): - for t1 in self.ticks: - for t2 in self.ticks: - self.assertEqual(t1(0), t2(0)) - self.assertEqual(t1(0) + t2(0), t1(0)) - - if t1 is not Nano: - self.assertEqual(t1(2) + t2(0), t1(2)) - if t1 is Nano: - self.assertEqual(t1(2) + Nano(0), t1(2)) - - def test_tick_equalities(self): - for t in self.ticks: - self.assertEqual(t(3), t(3)) - self.assertEqual(t(), t(1)) - - # not equals - self.assertNotEqual(t(3), t(2)) - self.assertNotEqual(t(3), t(-3)) - - def test_tick_operators(self): - for t in self.ticks: - self.assertEqual(t(3) + t(2), t(5)) - self.assertEqual(t(3) - t(2), t(1)) - self.assertEqual(t(800) + t(300), t(1100)) - self.assertEqual(t(1000) - t(5), t(995)) - - def test_tick_offset(self): - for t in self.ticks: - self.assertFalse(t().isAnchored()) - - def test_compare_ticks(self): - for kls in self.ticks: - three = kls(3) - four = kls(4) - - for _ in range(10): - self.assertTrue(three < kls(4)) - self.assertTrue(kls(3) < four) - self.assertTrue(four > kls(3)) - self.assertTrue(kls(4) > three) - self.assertTrue(kls(3) == kls(3)) - self.assertTrue(kls(3) != kls(4)) - - -class TestOffsetNames(tm.TestCase): - - def test_get_offset_name(self): - self.assertEqual(BDay().freqstr, 'B') - self.assertEqual(BDay(2).freqstr, '2B') - self.assertEqual(BMonthEnd().freqstr, 'BM') - self.assertEqual(Week(weekday=0).freqstr, 'W-MON') - self.assertEqual(Week(weekday=1).freqstr, 'W-TUE') - self.assertEqual(Week(weekday=2).freqstr, 'W-WED') - self.assertEqual(Week(weekday=3).freqstr, 'W-THU') - self.assertEqual(Week(weekday=4).freqstr, 'W-FRI') - - self.assertEqual(LastWeekOfMonth( - weekday=WeekDay.SUN).freqstr, "LWOM-SUN") - self.assertEqual( - makeFY5253LastOfMonthQuarter(weekday=1, startingMonth=3, - qtr_with_extra_week=4).freqstr, - "REQ-L-MAR-TUE-4") - self.assertEqual( - makeFY5253NearestEndMonthQuarter(weekday=1, startingMonth=3, - qtr_with_extra_week=3).freqstr, - "REQ-N-MAR-TUE-3") - - -def test_get_offset(): - with tm.assertRaisesRegexp(ValueError, _INVALID_FREQ_ERROR): - get_offset('gibberish') - with tm.assertRaisesRegexp(ValueError, _INVALID_FREQ_ERROR): - get_offset('QS-JAN-B') - - pairs = [ - ('B', BDay()), ('b', BDay()), ('bm', BMonthEnd()), - ('Bm', BMonthEnd()), ('W-MON', Week(weekday=0)), - ('W-TUE', Week(weekday=1)), ('W-WED', Week(weekday=2)), - ('W-THU', Week(weekday=3)), ('W-FRI', Week(weekday=4)), - ("RE-N-DEC-MON", makeFY5253NearestEndMonth(weekday=0, - startingMonth=12)), - ("RE-L-DEC-TUE", makeFY5253LastOfMonth(weekday=1, startingMonth=12)), - ("REQ-L-MAR-TUE-4", makeFY5253LastOfMonthQuarter( - weekday=1, startingMonth=3, qtr_with_extra_week=4)), - ("REQ-L-DEC-MON-3", makeFY5253LastOfMonthQuarter( - weekday=0, startingMonth=12, qtr_with_extra_week=3)), - ("REQ-N-DEC-MON-3", makeFY5253NearestEndMonthQuarter( - weekday=0, startingMonth=12, qtr_with_extra_week=3)), - ] - - for name, expected in pairs: - offset = get_offset(name) - assert offset == expected, ("Expected %r to yield %r (actual: %r)" % - (name, expected, offset)) - - -def test_get_offset_legacy(): - pairs = [('w@Sat', Week(weekday=5))] - for name, expected in pairs: - with tm.assertRaisesRegexp(ValueError, _INVALID_FREQ_ERROR): - get_offset(name) - - -class TestParseTimeString(tm.TestCase): - - def test_parse_time_string(self): - (date, parsed, reso) = parse_time_string('4Q1984') - (date_lower, parsed_lower, reso_lower) = parse_time_string('4q1984') - self.assertEqual(date, date_lower) - self.assertEqual(parsed, parsed_lower) - self.assertEqual(reso, reso_lower) - - def test_parse_time_quarter_w_dash(self): - # https://github.com/pandas-dev/pandas/issue/9688 - pairs = [('1988-Q2', '1988Q2'), ('2Q-1988', '2Q1988'), ] - - for dashed, normal in pairs: - (date_dash, parsed_dash, reso_dash) = parse_time_string(dashed) - (date, parsed, reso) = parse_time_string(normal) - - self.assertEqual(date_dash, date) - self.assertEqual(parsed_dash, parsed) - self.assertEqual(reso_dash, reso) - - self.assertRaises(DateParseError, parse_time_string, "-2Q1992") - self.assertRaises(DateParseError, parse_time_string, "2-Q1992") - self.assertRaises(DateParseError, parse_time_string, "4-4Q1992") - - -def test_get_standard_freq(): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - fstr = get_standard_freq('W') - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - assert fstr == get_standard_freq('w') - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - assert fstr == get_standard_freq('1w') - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - assert fstr == get_standard_freq(('W', 1)) - - with tm.assertRaisesRegexp(ValueError, _INVALID_FREQ_ERROR): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - get_standard_freq('WeEk') - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - fstr = get_standard_freq('5Q') - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - assert fstr == get_standard_freq('5q') - - with tm.assertRaisesRegexp(ValueError, _INVALID_FREQ_ERROR): - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - get_standard_freq('5QuarTer') - - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - assert fstr == get_standard_freq(('q', 5)) - - -def test_quarterly_dont_normalize(): - date = datetime(2012, 3, 31, 5, 30) - - offsets = (QuarterBegin, QuarterEnd, BQuarterEnd, BQuarterBegin) - - for klass in offsets: - result = date + klass() - assert (result.time() == date.time()) - - -class TestOffsetAliases(tm.TestCase): - - def setUp(self): - _offset_map.clear() - - def test_alias_equality(self): - for k, v in compat.iteritems(_offset_map): - if v is None: - continue - self.assertEqual(k, v.copy()) - - def test_rule_code(self): - lst = ['M', 'MS', 'BM', 'BMS', 'D', 'B', 'H', 'T', 'S', 'L', 'U'] - for k in lst: - self.assertEqual(k, get_offset(k).rule_code) - # should be cached - this is kind of an internals test... - assert k in _offset_map - self.assertEqual(k, (get_offset(k) * 3).rule_code) - - suffix_lst = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] - base = 'W' - for v in suffix_lst: - alias = '-'.join([base, v]) - self.assertEqual(alias, get_offset(alias).rule_code) - self.assertEqual(alias, (get_offset(alias) * 5).rule_code) - - suffix_lst = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', - 'SEP', 'OCT', 'NOV', 'DEC'] - base_lst = ['A', 'AS', 'BA', 'BAS', 'Q', 'QS', 'BQ', 'BQS'] - for base in base_lst: - for v in suffix_lst: - alias = '-'.join([base, v]) - self.assertEqual(alias, get_offset(alias).rule_code) - self.assertEqual(alias, (get_offset(alias) * 5).rule_code) - - lst = ['M', 'D', 'B', 'H', 'T', 'S', 'L', 'U'] - for k in lst: - code, stride = get_freq_code('3' + k) - self.assertTrue(isinstance(code, int)) - self.assertEqual(stride, 3) - self.assertEqual(k, _get_freq_str(code)) - - -def test_apply_ticks(): - result = offsets.Hour(3).apply(offsets.Hour(4)) - exp = offsets.Hour(7) - assert (result == exp) - - -def test_delta_to_tick(): - delta = timedelta(3) - - tick = offsets._delta_to_tick(delta) - assert (tick == offsets.Day(3)) - - -def test_dateoffset_misc(): - oset = offsets.DateOffset(months=2, days=4) - # it works - oset.freqstr - - assert (not offsets.DateOffset(months=2) == 2) - - -def test_freq_offsets(): - off = BDay(1, offset=timedelta(0, 1800)) - assert (off.freqstr == 'B+30Min') - - off = BDay(1, offset=timedelta(0, -1800)) - assert (off.freqstr == 'B-30Min') - - -def get_all_subclasses(cls): - ret = set() - this_subclasses = cls.__subclasses__() - ret = ret | set(this_subclasses) - for this_subclass in this_subclasses: - ret | get_all_subclasses(this_subclass) - return ret - - -class TestCaching(tm.TestCase): - - # as of GH 6479 (in 0.14.0), offset caching is turned off - # as of v0.12.0 only BusinessMonth/Quarter were actually caching - - def setUp(self): - _daterange_cache.clear() - _offset_map.clear() - - def run_X_index_creation(self, cls): - inst1 = cls() - if not inst1.isAnchored(): - self.assertFalse(inst1._should_cache(), cls) - return - - self.assertTrue(inst1._should_cache(), cls) - - DatetimeIndex(start=datetime(2013, 1, 31), end=datetime(2013, 3, 31), - freq=inst1, normalize=True) - self.assertTrue(cls() in _daterange_cache, cls) - - def test_should_cache_month_end(self): - self.assertFalse(MonthEnd()._should_cache()) - - def test_should_cache_bmonth_end(self): - self.assertFalse(BusinessMonthEnd()._should_cache()) - - def test_should_cache_week_month(self): - self.assertFalse(WeekOfMonth(weekday=1, week=2)._should_cache()) - - def test_all_cacheableoffsets(self): - for subclass in get_all_subclasses(CacheableOffset): - if subclass.__name__[0] == "_" \ - or subclass in TestCaching.no_simple_ctr: - continue - self.run_X_index_creation(subclass) - - def test_month_end_index_creation(self): - DatetimeIndex(start=datetime(2013, 1, 31), end=datetime(2013, 3, 31), - freq=MonthEnd(), normalize=True) - self.assertFalse(MonthEnd() in _daterange_cache) - - def test_bmonth_end_index_creation(self): - DatetimeIndex(start=datetime(2013, 1, 31), end=datetime(2013, 3, 29), - freq=BusinessMonthEnd(), normalize=True) - self.assertFalse(BusinessMonthEnd() in _daterange_cache) - - def test_week_of_month_index_creation(self): - inst1 = WeekOfMonth(weekday=1, week=2) - DatetimeIndex(start=datetime(2013, 1, 31), end=datetime(2013, 3, 29), - freq=inst1, normalize=True) - inst2 = WeekOfMonth(weekday=1, week=2) - self.assertFalse(inst2 in _daterange_cache) - - -class TestReprNames(tm.TestCase): - - def test_str_for_named_is_name(self): - # look at all the amazing combinations! - month_prefixes = ['A', 'AS', 'BA', 'BAS', 'Q', 'BQ', 'BQS', 'QS'] - names = [prefix + '-' + month - for prefix in month_prefixes - for month in ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', - 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']] - days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] - names += ['W-' + day for day in days] - names += ['WOM-' + week + day - for week in ('1', '2', '3', '4') for day in days] - _offset_map.clear() - for name in names: - offset = get_offset(name) - self.assertEqual(offset.freqstr, name) - - -def get_utc_offset_hours(ts): - # take a Timestamp and compute total hours of utc offset - o = ts.utcoffset() - return (o.days * 24 * 3600 + o.seconds) / 3600.0 - - -class TestDST(tm.TestCase): - """ - test DateOffset additions over Daylight Savings Time - """ - # one microsecond before the DST transition - ts_pre_fallback = "2013-11-03 01:59:59.999999" - ts_pre_springfwd = "2013-03-10 01:59:59.999999" - - # test both basic names and dateutil timezones - timezone_utc_offsets = { - 'US/Eastern': dict(utc_offset_daylight=-4, - utc_offset_standard=-5, ), - 'dateutil/US/Pacific': dict(utc_offset_daylight=-7, - utc_offset_standard=-8, ) - } - valid_date_offsets_singular = [ - 'weekday', 'day', 'hour', 'minute', 'second', 'microsecond' - ] - valid_date_offsets_plural = [ - 'weeks', 'days', - 'hours', 'minutes', 'seconds', - 'milliseconds', 'microseconds' - ] - - def _test_all_offsets(self, n, **kwds): - valid_offsets = self.valid_date_offsets_plural if n > 1 \ - else self.valid_date_offsets_singular - - for name in valid_offsets: - self._test_offset(offset_name=name, offset_n=n, **kwds) - - def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset): - offset = DateOffset(**{offset_name: offset_n}) - - t = tstart + offset - if expected_utc_offset is not None: - self.assertTrue(get_utc_offset_hours(t) == expected_utc_offset) - - if offset_name == 'weeks': - # dates should match - self.assertTrue(t.date() == timedelta(days=7 * offset.kwds[ - 'weeks']) + tstart.date()) - # expect the same day of week, hour of day, minute, second, ... - self.assertTrue(t.dayofweek == tstart.dayofweek and t.hour == - tstart.hour and t.minute == tstart.minute and - t.second == tstart.second) - elif offset_name == 'days': - # dates should match - self.assertTrue(timedelta(offset.kwds['days']) + tstart.date() == - t.date()) - # expect the same hour of day, minute, second, ... - self.assertTrue(t.hour == tstart.hour and - t.minute == tstart.minute and - t.second == tstart.second) - elif offset_name in self.valid_date_offsets_singular: - # expect the signular offset value to match between tstart and t - datepart_offset = getattr(t, offset_name - if offset_name != 'weekday' else - 'dayofweek') - self.assertTrue(datepart_offset == offset.kwds[offset_name]) - else: - # the offset should be the same as if it was done in UTC - self.assertTrue(t == (tstart.tz_convert('UTC') + offset - ).tz_convert('US/Pacific')) - - def _make_timestamp(self, string, hrs_offset, tz): - if hrs_offset >= 0: - offset_string = '{hrs:02d}00'.format(hrs=hrs_offset) - else: - offset_string = '-{hrs:02d}00'.format(hrs=-1 * hrs_offset) - return Timestamp(string + offset_string).tz_convert(tz) - - def test_fallback_plural(self): - # test moving from daylight savings to standard time - import dateutil - for tz, utc_offsets in self.timezone_utc_offsets.items(): - hrs_pre = utc_offsets['utc_offset_daylight'] - hrs_post = utc_offsets['utc_offset_standard'] - - if dateutil.__version__ != LooseVersion('2.6.0'): - # buggy ambiguous behavior in 2.6.0 - # GH 14621 - # https://github.com/dateutil/dateutil/issues/321 - self._test_all_offsets( - n=3, tstart=self._make_timestamp(self.ts_pre_fallback, - hrs_pre, tz), - expected_utc_offset=hrs_post) - - def test_springforward_plural(self): - # test moving from standard to daylight savings - for tz, utc_offsets in self.timezone_utc_offsets.items(): - hrs_pre = utc_offsets['utc_offset_standard'] - hrs_post = utc_offsets['utc_offset_daylight'] - self._test_all_offsets( - n=3, tstart=self._make_timestamp(self.ts_pre_springfwd, - hrs_pre, tz), - expected_utc_offset=hrs_post) - - def test_fallback_singular(self): - # in the case of signular offsets, we dont neccesarily know which utc - # offset the new Timestamp will wind up in (the tz for 1 month may be - # different from 1 second) so we don't specify an expected_utc_offset - for tz, utc_offsets in self.timezone_utc_offsets.items(): - hrs_pre = utc_offsets['utc_offset_standard'] - self._test_all_offsets(n=1, tstart=self._make_timestamp( - self.ts_pre_fallback, hrs_pre, tz), expected_utc_offset=None) - - def test_springforward_singular(self): - for tz, utc_offsets in self.timezone_utc_offsets.items(): - hrs_pre = utc_offsets['utc_offset_standard'] - self._test_all_offsets(n=1, tstart=self._make_timestamp( - self.ts_pre_springfwd, hrs_pre, tz), expected_utc_offset=None) - - def test_all_offset_classes(self): - tests = {MonthBegin: ['11/2/2012', '12/1/2012'], - MonthEnd: ['11/2/2012', '11/30/2012'], - BMonthBegin: ['11/2/2012', '12/3/2012'], - BMonthEnd: ['11/2/2012', '11/30/2012'], - CBMonthBegin: ['11/2/2012', '12/3/2012'], - CBMonthEnd: ['11/2/2012', '11/30/2012'], - SemiMonthBegin: ['11/2/2012', '11/15/2012'], - SemiMonthEnd: ['11/2/2012', '11/15/2012'], - Week: ['11/2/2012', '11/9/2012'], - YearBegin: ['11/2/2012', '1/1/2013'], - YearEnd: ['11/2/2012', '12/31/2012'], - BYearBegin: ['11/2/2012', '1/1/2013'], - BYearEnd: ['11/2/2012', '12/31/2012'], - QuarterBegin: ['11/2/2012', '12/1/2012'], - QuarterEnd: ['11/2/2012', '12/31/2012'], - BQuarterBegin: ['11/2/2012', '12/3/2012'], - BQuarterEnd: ['11/2/2012', '12/31/2012'], - Day: ['11/4/2012', '11/4/2012 23:00']} - - for offset, test_values in iteritems(tests): - first = Timestamp(test_values[0], tz='US/Eastern') + offset() - second = Timestamp(test_values[1], tz='US/Eastern') - self.assertEqual(first, second, msg=str(offset)) diff --git a/pandas/tests/tseries/test_resample.py b/pandas/tests/tseries/test_resample.py deleted file mode 100755 index 57a655b0b7610..0000000000000 --- a/pandas/tests/tseries/test_resample.py +++ /dev/null @@ -1,3206 +0,0 @@ -# pylint: disable=E1101 - -from datetime import datetime, timedelta -from functools import partial - -import numpy as np - -import pandas as pd -import pandas.tseries.offsets as offsets -import pandas.util.testing as tm -from pandas import (Series, DataFrame, Panel, Index, isnull, - notnull, Timestamp) - -from pandas.types.generic import ABCSeries, ABCDataFrame -from pandas.compat import range, lrange, zip, product, OrderedDict -from pandas.core.base import SpecificationError -from pandas.core.common import UnsupportedFunctionCall -from pandas.core.groupby import DataError -from pandas.tseries.frequencies import MONTHS, DAYS -from pandas.tseries.frequencies import to_offset -from pandas.tseries.index import date_range -from pandas.tseries.offsets import Minute, BDay -from pandas.tseries.period import period_range, PeriodIndex, Period -from pandas.tseries.resample import (DatetimeIndex, TimeGrouper, - DatetimeIndexResampler) -from pandas.tseries.tdi import timedelta_range, TimedeltaIndex -from pandas.util.testing import (assert_series_equal, assert_almost_equal, - assert_frame_equal, assert_index_equal) -from pandas._libs.period import IncompatibleFrequency - -bday = BDay() - -# The various methods we support -downsample_methods = ['min', 'max', 'first', 'last', 'sum', 'mean', 'sem', - 'median', 'prod', 'var', 'ohlc'] -upsample_methods = ['count', 'size'] -series_methods = ['nunique'] -resample_methods = downsample_methods + upsample_methods + series_methods - - -def _simple_ts(start, end, freq='D'): - rng = date_range(start, end, freq=freq) - return Series(np.random.randn(len(rng)), index=rng) - - -def _simple_pts(start, end, freq='D'): - rng = period_range(start, end, freq=freq) - return Series(np.random.randn(len(rng)), index=rng) - - -class TestResampleAPI(tm.TestCase): - - def setUp(self): - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='Min') - - self.series = Series(np.random.rand(len(dti)), dti) - self.frame = DataFrame( - {'A': self.series, 'B': self.series, 'C': np.arange(len(dti))}) - - def test_str(self): - - r = self.series.resample('H') - self.assertTrue( - 'DatetimeIndexResampler [freq=, axis=0, closed=left, ' - 'label=left, convention=start, base=0]' in str(r)) - - def test_api(self): - - r = self.series.resample('H') - result = r.mean() - self.assertIsInstance(result, Series) - self.assertEqual(len(result), 217) - - r = self.series.to_frame().resample('H') - result = r.mean() - self.assertIsInstance(result, DataFrame) - self.assertEqual(len(result), 217) - - def test_api_changes_v018(self): - - # change from .resample(....., how=...) - # to .resample(......).how() - - r = self.series.resample('H') - self.assertIsInstance(r, DatetimeIndexResampler) - - for how in ['sum', 'mean', 'prod', 'min', 'max', 'var', 'std']: - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = self.series.resample('H', how=how) - expected = getattr(self.series.resample('H'), how)() - tm.assert_series_equal(result, expected) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = self.series.resample('H', how='ohlc') - expected = self.series.resample('H').ohlc() - tm.assert_frame_equal(result, expected) - - # compat for pandas-like methods - for how in ['sort_values', 'isnull']: - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - getattr(r, how)() - - # invalids as these can be setting operations - r = self.series.resample('H') - self.assertRaises(ValueError, lambda: r.iloc[0]) - self.assertRaises(ValueError, lambda: r.iat[0]) - self.assertRaises(ValueError, lambda: r.loc[0]) - self.assertRaises(ValueError, lambda: r.loc[ - Timestamp('2013-01-01 00:00:00', offset='H')]) - self.assertRaises(ValueError, lambda: r.at[ - Timestamp('2013-01-01 00:00:00', offset='H')]) - - def f(): - r[0] = 5 - - self.assertRaises(ValueError, f) - - # str/repr - r = self.series.resample('H') - with tm.assert_produces_warning(None): - str(r) - with tm.assert_produces_warning(None): - repr(r) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - tm.assert_numpy_array_equal(np.array(r), np.array(r.mean())) - - # masquerade as Series/DataFrame as needed for API compat - self.assertTrue(isinstance(self.series.resample('H'), ABCSeries)) - self.assertFalse(isinstance(self.frame.resample('H'), ABCSeries)) - self.assertFalse(isinstance(self.series.resample('H'), ABCDataFrame)) - self.assertTrue(isinstance(self.frame.resample('H'), ABCDataFrame)) - - # bin numeric ops - for op in ['__add__', '__mul__', '__truediv__', '__div__', '__sub__']: - - if getattr(self.series, op, None) is None: - continue - r = self.series.resample('H') - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - self.assertIsInstance(getattr(r, op)(2), pd.Series) - - # unary numeric ops - for op in ['__pos__', '__neg__', '__abs__', '__inv__']: - - if getattr(self.series, op, None) is None: - continue - r = self.series.resample('H') - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - self.assertIsInstance(getattr(r, op)(), pd.Series) - - # comparison ops - for op in ['__lt__', '__le__', '__gt__', '__ge__', '__eq__', '__ne__']: - r = self.series.resample('H') - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - self.assertIsInstance(getattr(r, op)(2), pd.Series) - - # IPython introspection shouldn't trigger warning GH 13618 - for op in ['_repr_json', '_repr_latex', - '_ipython_canary_method_should_not_exist_']: - r = self.series.resample('H') - with tm.assert_produces_warning(None): - getattr(r, op, None) - - # getitem compat - df = self.series.to_frame('foo') - - # same as prior versions for DataFrame - self.assertRaises(KeyError, lambda: df.resample('H')[0]) - - # compat for Series - # but we cannot be sure that we need a warning here - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = self.series.resample('H')[0] - expected = self.series.resample('H').mean()[0] - self.assertEqual(result, expected) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = self.series.resample('H')['2005-01-09 23:00:00'] - expected = self.series.resample('H').mean()['2005-01-09 23:00:00'] - self.assertEqual(result, expected) - - def test_groupby_resample_api(self): - - # GH 12448 - # .groupby(...).resample(...) hitting warnings - # when appropriate - df = DataFrame({'date': pd.date_range(start='2016-01-01', - periods=4, - freq='W'), - 'group': [1, 1, 2, 2], - 'val': [5, 6, 7, 8]}).set_index('date') - - # replication step - i = pd.date_range('2016-01-03', periods=8).tolist() + \ - pd.date_range('2016-01-17', periods=8).tolist() - index = pd.MultiIndex.from_arrays([[1] * 8 + [2] * 8, i], - names=['group', 'date']) - expected = DataFrame({'val': [5] * 7 + [6] + [7] * 7 + [8]}, - index=index) - result = df.groupby('group').apply( - lambda x: x.resample('1D').ffill())[['val']] - assert_frame_equal(result, expected) - - def test_groupby_resample_on_api(self): - - # GH 15021 - # .groupby(...).resample(on=...) results in an unexpected - # keyword warning. - df = pd.DataFrame({'key': ['A', 'B'] * 5, - 'dates': pd.date_range('2016-01-01', periods=10), - 'values': np.random.randn(10)}) - - expected = df.set_index('dates').groupby('key').resample('D').mean() - - result = df.groupby('key').resample('D', on='dates').mean() - assert_frame_equal(result, expected) - - def test_plot_api(self): - tm._skip_if_no_mpl() - - # .resample(....).plot(...) - # hitting warnings - # GH 12448 - s = Series(np.random.randn(60), - index=date_range('2016-01-01', periods=60, freq='1min')) - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = s.resample('15min').plot() - tm.assert_is_valid_plot_return_object(result) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = s.resample('15min', how='sum').plot() - tm.assert_is_valid_plot_return_object(result) - - def test_getitem(self): - - r = self.frame.resample('H') - tm.assert_index_equal(r._selected_obj.columns, self.frame.columns) - - r = self.frame.resample('H')['B'] - self.assertEqual(r._selected_obj.name, self.frame.columns[1]) - - # technically this is allowed - r = self.frame.resample('H')['A', 'B'] - tm.assert_index_equal(r._selected_obj.columns, - self.frame.columns[[0, 1]]) - - r = self.frame.resample('H')['A', 'B'] - tm.assert_index_equal(r._selected_obj.columns, - self.frame.columns[[0, 1]]) - - def test_select_bad_cols(self): - - g = self.frame.resample('H') - self.assertRaises(KeyError, g.__getitem__, ['D']) - - self.assertRaises(KeyError, g.__getitem__, ['A', 'D']) - with tm.assertRaisesRegexp(KeyError, '^[^A]+$'): - # A should not be referenced as a bad column... - # will have to rethink regex if you change message! - g[['A', 'D']] - - def test_attribute_access(self): - - r = self.frame.resample('H') - tm.assert_series_equal(r.A.sum(), r['A'].sum()) - - # getting - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - self.assertRaises(AttributeError, lambda: r.F) - - # setting - def f(): - r.F = 'bah' - - self.assertRaises(ValueError, f) - - def test_api_compat_before_use(self): - - # make sure that we are setting the binner - # on these attributes - for attr in ['groups', 'ngroups', 'indices']: - rng = pd.date_range('1/1/2012', periods=100, freq='S') - ts = pd.Series(np.arange(len(rng)), index=rng) - rs = ts.resample('30s') - - # before use - getattr(rs, attr) - - # after grouper is initialized is ok - rs.mean() - getattr(rs, attr) - - def tests_skip_nuisance(self): - - df = self.frame - df['D'] = 'foo' - r = df.resample('H') - result = r[['A', 'B']].sum() - expected = pd.concat([r.A.sum(), r.B.sum()], axis=1) - assert_frame_equal(result, expected) - - expected = r[['A', 'B', 'C']].sum() - result = r.sum() - assert_frame_equal(result, expected) - - def test_downsample_but_actually_upsampling(self): - - # this is reindex / asfreq - rng = pd.date_range('1/1/2012', periods=100, freq='S') - ts = pd.Series(np.arange(len(rng), dtype='int64'), index=rng) - result = ts.resample('20s').asfreq() - expected = Series([0, 20, 40, 60, 80], - index=pd.date_range('2012-01-01 00:00:00', - freq='20s', - periods=5)) - assert_series_equal(result, expected) - - def test_combined_up_downsampling_of_irregular(self): - - # since we are reallydoing an operation like this - # ts2.resample('2s').mean().ffill() - # preserve these semantics - - rng = pd.date_range('1/1/2012', periods=100, freq='S') - ts = pd.Series(np.arange(len(rng)), index=rng) - ts2 = ts.iloc[[0, 1, 2, 3, 5, 7, 11, 15, 16, 25, 30]] - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = ts2.resample('2s', how='mean', fill_method='ffill') - expected = ts2.resample('2s').mean().ffill() - assert_series_equal(result, expected) - - def test_transform(self): - - r = self.series.resample('20min') - expected = self.series.groupby( - pd.Grouper(freq='20min')).transform('mean') - result = r.transform('mean') - assert_series_equal(result, expected) - - def test_fillna(self): - - # need to upsample here - rng = pd.date_range('1/1/2012', periods=10, freq='2S') - ts = pd.Series(np.arange(len(rng), dtype='int64'), index=rng) - r = ts.resample('s') - - expected = r.ffill() - result = r.fillna(method='ffill') - assert_series_equal(result, expected) - - expected = r.bfill() - result = r.fillna(method='bfill') - assert_series_equal(result, expected) - - with self.assertRaises(ValueError): - r.fillna(0) - - def test_apply_without_aggregation(self): - - # both resample and groupby should work w/o aggregation - r = self.series.resample('20min') - g = self.series.groupby(pd.Grouper(freq='20min')) - - for t in [g, r]: - result = t.apply(lambda x: x) - assert_series_equal(result, self.series) - - def test_agg_consistency(self): - - # make sure that we are consistent across - # similar aggregations with and w/o selection list - df = DataFrame(np.random.randn(1000, 3), - index=pd.date_range('1/1/2012', freq='S', periods=1000), - columns=['A', 'B', 'C']) - - r = df.resample('3T') - - expected = r[['A', 'B', 'C']].agg({'r1': 'mean', 'r2': 'sum'}) - result = r.agg({'r1': 'mean', 'r2': 'sum'}) - assert_frame_equal(result, expected) - - # TODO: once GH 14008 is fixed, move these tests into - # `Base` test class - def test_agg(self): - # test with all three Resampler apis and TimeGrouper - - np.random.seed(1234) - index = date_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - index.name = 'date' - df = pd.DataFrame(np.random.rand(10, 2), - columns=list('AB'), - index=index) - df_col = df.reset_index() - df_mult = df_col.copy() - df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], - names=['index', 'date']) - r = df.resample('2D') - cases = [ - r, - df_col.resample('2D', on='date'), - df_mult.resample('2D', level='date'), - df.groupby(pd.Grouper(freq='2D')) - ] - - a_mean = r['A'].mean() - a_std = r['A'].std() - a_sum = r['A'].sum() - b_mean = r['B'].mean() - b_std = r['B'].std() - b_sum = r['B'].sum() - - expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) - expected.columns = pd.MultiIndex.from_product([['A', 'B'], - ['mean', 'std']]) - for t in cases: - result = t.aggregate([np.mean, np.std]) - assert_frame_equal(result, expected) - - expected = pd.concat([a_mean, b_std], axis=1) - for t in cases: - result = t.aggregate({'A': np.mean, - 'B': np.std}) - assert_frame_equal(result, expected, check_like=True) - - expected = pd.concat([a_mean, a_std], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), - ('A', 'std')]) - for t in cases: - result = t.aggregate({'A': ['mean', 'std']}) - assert_frame_equal(result, expected) - - expected = pd.concat([a_mean, a_sum], axis=1) - expected.columns = ['mean', 'sum'] - for t in cases: - result = t['A'].aggregate(['mean', 'sum']) - assert_frame_equal(result, expected) - - expected = pd.concat([a_mean, a_sum], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), - ('A', 'sum')]) - for t in cases: - result = t.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}}) - assert_frame_equal(result, expected, check_like=True) - - expected = pd.concat([a_mean, a_sum, b_mean, b_sum], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), - ('A', 'sum'), - ('B', 'mean2'), - ('B', 'sum2')]) - for t in cases: - result = t.aggregate({'A': {'mean': 'mean', 'sum': 'sum'}, - 'B': {'mean2': 'mean', 'sum2': 'sum'}}) - assert_frame_equal(result, expected, check_like=True) - - expected = pd.concat([a_mean, a_std, b_mean, b_std], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'mean'), - ('A', 'std'), - ('B', 'mean'), - ('B', 'std')]) - for t in cases: - result = t.aggregate({'A': ['mean', 'std'], - 'B': ['mean', 'std']}) - assert_frame_equal(result, expected, check_like=True) - - expected = pd.concat([a_mean, a_sum, b_mean, b_sum], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('r1', 'A', 'mean'), - ('r1', 'A', 'sum'), - ('r2', 'B', 'mean'), - ('r2', 'B', 'sum')]) - - def test_agg_misc(self): - # test with all three Resampler apis and TimeGrouper - - np.random.seed(1234) - index = date_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - index.name = 'date' - df = pd.DataFrame(np.random.rand(10, 2), - columns=list('AB'), - index=index) - df_col = df.reset_index() - df_mult = df_col.copy() - df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], - names=['index', 'date']) - - r = df.resample('2D') - cases = [ - r, - df_col.resample('2D', on='date'), - df_mult.resample('2D', level='date'), - df.groupby(pd.Grouper(freq='2D')) - ] - - # passed lambda - for t in cases: - result = t.agg({'A': np.sum, - 'B': lambda x: np.std(x, ddof=1)}) - rcustom = t['B'].apply(lambda x: np.std(x, ddof=1)) - expected = pd.concat([r['A'].sum(), rcustom], axis=1) - assert_frame_equal(result, expected, check_like=True) - - # agg with renamers - expected = pd.concat([t['A'].sum(), - t['B'].sum(), - t['A'].mean(), - t['B'].mean()], - axis=1) - expected.columns = pd.MultiIndex.from_tuples([('result1', 'A'), - ('result1', 'B'), - ('result2', 'A'), - ('result2', 'B')]) - for t in cases: - result = t[['A', 'B']].agg(OrderedDict([('result1', np.sum), - ('result2', np.mean)])) - assert_frame_equal(result, expected, check_like=True) - - # agg with different hows - expected = pd.concat([t['A'].sum(), - t['A'].std(), - t['B'].mean(), - t['B'].std()], - axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), - ('A', 'std'), - ('B', 'mean'), - ('B', 'std')]) - for t in cases: - result = t.agg(OrderedDict([('A', ['sum', 'std']), - ('B', ['mean', 'std'])])) - assert_frame_equal(result, expected, check_like=True) - - # equivalent of using a selection list / or not - for t in cases: - result = t[['A', 'B']].agg({'A': ['sum', 'std'], - 'B': ['mean', 'std']}) - assert_frame_equal(result, expected, check_like=True) - - # series like aggs - for t in cases: - result = t['A'].agg({'A': ['sum', 'std']}) - expected = pd.concat([t['A'].sum(), - t['A'].std()], - axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), - ('A', 'std')]) - assert_frame_equal(result, expected, check_like=True) - - expected = pd.concat([t['A'].agg(['sum', 'std']), - t['A'].agg(['mean', 'std'])], - axis=1) - expected.columns = pd.MultiIndex.from_tuples([('A', 'sum'), - ('A', 'std'), - ('B', 'mean'), - ('B', 'std')]) - result = t['A'].agg({'A': ['sum', 'std'], 'B': ['mean', 'std']}) - assert_frame_equal(result, expected, check_like=True) - - # errors - # invalid names in the agg specification - for t in cases: - def f(): - t[['A']].agg({'A': ['sum', 'std'], - 'B': ['mean', 'std']}) - - self.assertRaises(SpecificationError, f) - - def test_agg_nested_dicts(self): - - np.random.seed(1234) - index = date_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - index.name = 'date' - df = pd.DataFrame(np.random.rand(10, 2), - columns=list('AB'), - index=index) - df_col = df.reset_index() - df_mult = df_col.copy() - df_mult.index = pd.MultiIndex.from_arrays([range(10), df.index], - names=['index', 'date']) - r = df.resample('2D') - cases = [ - r, - df_col.resample('2D', on='date'), - df_mult.resample('2D', level='date'), - df.groupby(pd.Grouper(freq='2D')) - ] - - for t in cases: - def f(): - t.aggregate({'r1': {'A': ['mean', 'sum']}, - 'r2': {'B': ['mean', 'sum']}}) - self.assertRaises(ValueError, f) - - for t in cases: - expected = pd.concat([t['A'].mean(), t['A'].std(), t['B'].mean(), - t['B'].std()], axis=1) - expected.columns = pd.MultiIndex.from_tuples([('ra', 'mean'), ( - 'ra', 'std'), ('rb', 'mean'), ('rb', 'std')]) - - result = t[['A', 'B']].agg({'A': {'ra': ['mean', 'std']}, - 'B': {'rb': ['mean', 'std']}}) - assert_frame_equal(result, expected, check_like=True) - - result = t.agg({'A': {'ra': ['mean', 'std']}, - 'B': {'rb': ['mean', 'std']}}) - assert_frame_equal(result, expected, check_like=True) - - def test_selection_api_validation(self): - # GH 13500 - index = date_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - df = pd.DataFrame({'date': index, - 'a': np.arange(len(index), dtype=np.int64)}, - index=pd.MultiIndex.from_arrays([ - np.arange(len(index), dtype=np.int64), - index], names=['v', 'd'])) - df_exp = pd.DataFrame({'a': np.arange(len(index), dtype=np.int64)}, - index=index) - - # non DatetimeIndex - with tm.assertRaises(TypeError): - df.resample('2D', level='v') - - with tm.assertRaises(ValueError): - df.resample('2D', on='date', level='d') - - with tm.assertRaises(TypeError): - df.resample('2D', on=['a', 'date']) - - with tm.assertRaises(KeyError): - df.resample('2D', level=['a', 'date']) - - # upsampling not allowed - with tm.assertRaises(ValueError): - df.resample('2D', level='d').asfreq() - - with tm.assertRaises(ValueError): - df.resample('2D', on='date').asfreq() - - exp = df_exp.resample('2D').sum() - exp.index.name = 'date' - assert_frame_equal(exp, df.resample('2D', on='date').sum()) - - exp.index.name = 'd' - assert_frame_equal(exp, df.resample('2D', level='d').sum()) - - -class Base(object): - """ - base class for resampling testing, calling - .create_series() generates a series of each index type - """ - - def create_index(self, *args, **kwargs): - """ return the _index_factory created using the args, kwargs """ - factory = self._index_factory() - return factory(*args, **kwargs) - - def test_asfreq_downsample(self): - s = self.create_series() - - result = s.resample('2D').asfreq() - expected = s.reindex(s.index.take(np.arange(0, len(s.index), 2))) - expected.index.freq = to_offset('2D') - assert_series_equal(result, expected) - - frame = s.to_frame('value') - result = frame.resample('2D').asfreq() - expected = frame.reindex( - frame.index.take(np.arange(0, len(frame.index), 2))) - expected.index.freq = to_offset('2D') - assert_frame_equal(result, expected) - - def test_asfreq_upsample(self): - s = self.create_series() - - result = s.resample('1H').asfreq() - new_index = self.create_index(s.index[0], s.index[-1], freq='1H') - expected = s.reindex(new_index) - assert_series_equal(result, expected) - - frame = s.to_frame('value') - result = frame.resample('1H').asfreq() - new_index = self.create_index(frame.index[0], - frame.index[-1], freq='1H') - expected = frame.reindex(new_index) - assert_frame_equal(result, expected) - - def test_asfreq_fill_value(self): - # test for fill value during resampling, issue 3715 - - s = self.create_series() - - result = s.resample('1H').asfreq() - new_index = self.create_index(s.index[0], s.index[-1], freq='1H') - expected = s.reindex(new_index) - assert_series_equal(result, expected) - - frame = s.to_frame('value') - frame.iloc[1] = None - result = frame.resample('1H').asfreq(fill_value=4.0) - new_index = self.create_index(frame.index[0], - frame.index[-1], freq='1H') - expected = frame.reindex(new_index, fill_value=4.0) - assert_frame_equal(result, expected) - - def test_resample_interpolate(self): - # # 12925 - df = self.create_series().to_frame('value') - assert_frame_equal( - df.resample('1T').asfreq().interpolate(), - df.resample('1T').interpolate()) - - def test_raises_on_non_datetimelike_index(self): - # this is a non datetimelike index - xp = DataFrame() - self.assertRaises(TypeError, lambda: xp.resample('A').mean()) - - def test_resample_empty_series(self): - # GH12771 & GH12868 - - s = self.create_series()[:0] - - for freq in ['M', 'D', 'H']: - # need to test for ohlc from GH13083 - methods = [method for method in resample_methods - if method != 'ohlc'] - for method in methods: - result = getattr(s.resample(freq), method)() - - expected = s.copy() - expected.index = s.index._shallow_copy(freq=freq) - assert_index_equal(result.index, expected.index) - self.assertEqual(result.index.freq, expected.index.freq) - - if (method == 'size' and - isinstance(result.index, PeriodIndex) and - freq in ['M', 'D']): - # GH12871 - TODO: name should propagate, but currently - # doesn't on lower / same frequency with PeriodIndex - assert_series_equal(result, expected, check_dtype=False) - - else: - assert_series_equal(result, expected, check_dtype=False) - - def test_resample_empty_dataframe(self): - # GH13212 - index = self.create_series().index[:0] - f = DataFrame(index=index) - - for freq in ['M', 'D', 'H']: - # count retains dimensions too - methods = downsample_methods + ['count'] - for method in methods: - result = getattr(f.resample(freq), method)() - - expected = f.copy() - expected.index = f.index._shallow_copy(freq=freq) - assert_index_equal(result.index, expected.index) - self.assertEqual(result.index.freq, expected.index.freq) - assert_frame_equal(result, expected, check_dtype=False) - - # test size for GH13212 (currently stays as df) - - def test_resample_empty_dtypes(self): - - # Empty series were sometimes causing a segfault (for the functions - # with Cython bounds-checking disabled) or an IndexError. We just run - # them to ensure they no longer do. (GH #10228) - for index in tm.all_timeseries_index_generator(0): - for dtype in (np.float, np.int, np.object, 'datetime64[ns]'): - for how in downsample_methods + upsample_methods: - empty_series = pd.Series([], index, dtype) - try: - getattr(empty_series.resample('d'), how)() - except DataError: - # Ignore these since some combinations are invalid - # (ex: doing mean with dtype of np.object) - pass - - def test_resample_loffset_arg_type(self): - # GH 13218, 15002 - df = self.create_series().to_frame('value') - expected_means = [df.values[i:i + 2].mean() - for i in range(0, len(df.values), 2)] - expected_index = self.create_index(df.index[0], - periods=len(df.index) / 2, - freq='2D') - - # loffset coreces PeriodIndex to DateTimeIndex - if isinstance(expected_index, PeriodIndex): - expected_index = expected_index.to_timestamp() - - expected_index += timedelta(hours=2) - expected = DataFrame({'value': expected_means}, index=expected_index) - - for arg in ['mean', {'value': 'mean'}, ['mean']]: - - result_agg = df.resample('2D', loffset='2H').agg(arg) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result_how = df.resample('2D', how=arg, loffset='2H') - - if isinstance(arg, list): - expected.columns = pd.MultiIndex.from_tuples([('value', - 'mean')]) - - # GH 13022, 7687 - TODO: fix resample w/ TimedeltaIndex - if isinstance(expected.index, TimedeltaIndex): - with tm.assertRaises(AssertionError): - assert_frame_equal(result_agg, expected) - assert_frame_equal(result_how, expected) - else: - assert_frame_equal(result_agg, expected) - assert_frame_equal(result_how, expected) - - -class TestDatetimeIndex(Base, tm.TestCase): - _index_factory = lambda x: date_range - - def setUp(self): - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='Min') - - self.series = Series(np.random.rand(len(dti)), dti) - - def create_series(self): - i = date_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - - return Series(np.arange(len(i)), index=i, name='dti') - - def test_custom_grouper(self): - - dti = DatetimeIndex(freq='Min', start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10)) - - s = Series(np.array([1] * len(dti)), index=dti, dtype='int64') - - b = TimeGrouper(Minute(5)) - g = s.groupby(b) - - # check all cython functions work - funcs = ['add', 'mean', 'prod', 'ohlc', 'min', 'max', 'var'] - for f in funcs: - g._cython_agg_general(f) - - b = TimeGrouper(Minute(5), closed='right', label='right') - g = s.groupby(b) - # check all cython functions work - funcs = ['add', 'mean', 'prod', 'ohlc', 'min', 'max', 'var'] - for f in funcs: - g._cython_agg_general(f) - - self.assertEqual(g.ngroups, 2593) - self.assertTrue(notnull(g.mean()).all()) - - # construct expected val - arr = [1] + [5] * 2592 - idx = dti[0:-1:5] - idx = idx.append(dti[-1:]) - expect = Series(arr, index=idx) - - # GH2763 - return in put dtype if we can - result = g.agg(np.sum) - assert_series_equal(result, expect) - - df = DataFrame(np.random.rand(len(dti), 10), - index=dti, dtype='float64') - r = df.groupby(b).agg(np.sum) - - self.assertEqual(len(r.columns), 10) - self.assertEqual(len(r.index), 2593) - - def test_resample_basic(self): - rng = date_range('1/1/2000 00:00:00', '1/1/2000 00:13:00', freq='min', - name='index') - s = Series(np.random.randn(14), index=rng) - result = s.resample('5min', closed='right', label='right').mean() - - exp_idx = date_range('1/1/2000', periods=4, freq='5min', name='index') - expected = Series([s[0], s[1:6].mean(), s[6:11].mean(), s[11:].mean()], - index=exp_idx) - assert_series_equal(result, expected) - self.assertEqual(result.index.name, 'index') - - result = s.resample('5min', closed='left', label='right').mean() - - exp_idx = date_range('1/1/2000 00:05', periods=3, freq='5min', - name='index') - expected = Series([s[:5].mean(), s[5:10].mean(), - s[10:].mean()], index=exp_idx) - assert_series_equal(result, expected) - - s = self.series - result = s.resample('5Min').last() - grouper = TimeGrouper(Minute(5), closed='left', label='left') - expect = s.groupby(grouper).agg(lambda x: x[-1]) - assert_series_equal(result, expect) - - def test_resample_how(self): - rng = date_range('1/1/2000 00:00:00', '1/1/2000 00:13:00', freq='min', - name='index') - s = Series(np.random.randn(14), index=rng) - grouplist = np.ones_like(s) - grouplist[0] = 0 - grouplist[1:6] = 1 - grouplist[6:11] = 2 - grouplist[11:] = 3 - args = downsample_methods - - def _ohlc(group): - if isnull(group).all(): - return np.repeat(np.nan, 4) - return [group[0], group.max(), group.min(), group[-1]] - - inds = date_range('1/1/2000', periods=4, freq='5min', name='index') - - for arg in args: - if arg == 'ohlc': - func = _ohlc - else: - func = arg - try: - result = getattr(s.resample( - '5min', closed='right', label='right'), arg)() - - expected = s.groupby(grouplist).agg(func) - self.assertEqual(result.index.name, 'index') - if arg == 'ohlc': - expected = DataFrame(expected.values.tolist()) - expected.columns = ['open', 'high', 'low', 'close'] - expected.index = Index(inds, name='index') - assert_frame_equal(result, expected) - else: - expected.index = inds - assert_series_equal(result, expected) - except BaseException as exc: - - exc.args += ('how=%s' % arg,) - raise - - def test_numpy_compat(self): - # see gh-12811 - s = Series([1, 2, 3, 4, 5], index=date_range( - '20130101', periods=5, freq='s')) - r = s.resample('2s') - - msg = "numpy operations are not valid with resample" - - for func in ('min', 'max', 'sum', 'prod', - 'mean', 'var', 'std'): - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(r, func), - func, 1, 2, 3) - tm.assertRaisesRegexp(UnsupportedFunctionCall, msg, - getattr(r, func), axis=1) - - def test_resample_how_callables(self): - # GH 7929 - data = np.arange(5, dtype=np.int64) - ind = pd.DatetimeIndex(start='2014-01-01', periods=len(data), freq='d') - df = pd.DataFrame({"A": data, "B": data}, index=ind) - - def fn(x, a=1): - return str(type(x)) - - class fn_class: - - def __call__(self, x): - return str(type(x)) - - df_standard = df.resample("M").apply(fn) - df_lambda = df.resample("M").apply(lambda x: str(type(x))) - df_partial = df.resample("M").apply(partial(fn)) - df_partial2 = df.resample("M").apply(partial(fn, a=2)) - df_class = df.resample("M").apply(fn_class()) - - assert_frame_equal(df_standard, df_lambda) - assert_frame_equal(df_standard, df_partial) - assert_frame_equal(df_standard, df_partial2) - assert_frame_equal(df_standard, df_class) - - def test_resample_with_timedeltas(self): - - expected = DataFrame({'A': np.arange(1480)}) - expected = expected.groupby(expected.index // 30).sum() - expected.index = pd.timedelta_range('0 days', freq='30T', periods=50) - - df = DataFrame({'A': np.arange(1480)}, index=pd.to_timedelta( - np.arange(1480), unit='T')) - result = df.resample('30T').sum() - - assert_frame_equal(result, expected) - - s = df['A'] - result = s.resample('30T').sum() - assert_series_equal(result, expected['A']) - - def test_resample_single_period_timedelta(self): - - s = Series(list(range(5)), index=pd.timedelta_range( - '1 day', freq='s', periods=5)) - result = s.resample('2s').sum() - expected = Series([1, 5, 4], index=pd.timedelta_range( - '1 day', freq='2s', periods=3)) - assert_series_equal(result, expected) - - def test_resample_timedelta_idempotency(self): - - # GH 12072 - index = pd.timedelta_range('0', periods=9, freq='10L') - series = pd.Series(range(9), index=index) - result = series.resample('10L').mean() - expected = series - assert_series_equal(result, expected) - - def test_resample_rounding(self): - # GH 8371 - # odd results when rounding is needed - - data = """date,time,value -11-08-2014,00:00:01.093,1 -11-08-2014,00:00:02.159,1 -11-08-2014,00:00:02.667,1 -11-08-2014,00:00:03.175,1 -11-08-2014,00:00:07.058,1 -11-08-2014,00:00:07.362,1 -11-08-2014,00:00:08.324,1 -11-08-2014,00:00:08.830,1 -11-08-2014,00:00:08.982,1 -11-08-2014,00:00:09.815,1 -11-08-2014,00:00:10.540,1 -11-08-2014,00:00:11.061,1 -11-08-2014,00:00:11.617,1 -11-08-2014,00:00:13.607,1 -11-08-2014,00:00:14.535,1 -11-08-2014,00:00:15.525,1 -11-08-2014,00:00:17.960,1 -11-08-2014,00:00:20.674,1 -11-08-2014,00:00:21.191,1""" - - from pandas.compat import StringIO - df = pd.read_csv(StringIO(data), parse_dates={'timestamp': [ - 'date', 'time']}, index_col='timestamp') - df.index.name = None - result = df.resample('6s').sum() - expected = DataFrame({'value': [ - 4, 9, 4, 2 - ]}, index=date_range('2014-11-08', freq='6s', periods=4)) - assert_frame_equal(result, expected) - - result = df.resample('7s').sum() - expected = DataFrame({'value': [ - 4, 10, 4, 1 - ]}, index=date_range('2014-11-08', freq='7s', periods=4)) - assert_frame_equal(result, expected) - - result = df.resample('11s').sum() - expected = DataFrame({'value': [ - 11, 8 - ]}, index=date_range('2014-11-08', freq='11s', periods=2)) - assert_frame_equal(result, expected) - - result = df.resample('13s').sum() - expected = DataFrame({'value': [ - 13, 6 - ]}, index=date_range('2014-11-08', freq='13s', periods=2)) - assert_frame_equal(result, expected) - - result = df.resample('17s').sum() - expected = DataFrame({'value': [ - 16, 3 - ]}, index=date_range('2014-11-08', freq='17s', periods=2)) - assert_frame_equal(result, expected) - - def test_resample_basic_from_daily(self): - # from daily - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='D', name='index') - - s = Series(np.random.rand(len(dti)), dti) - - # to weekly - result = s.resample('w-sun').last() - - self.assertEqual(len(result), 3) - self.assertTrue((result.index.dayofweek == [6, 6, 6]).all()) - self.assertEqual(result.iloc[0], s['1/2/2005']) - self.assertEqual(result.iloc[1], s['1/9/2005']) - self.assertEqual(result.iloc[2], s.iloc[-1]) - - result = s.resample('W-MON').last() - self.assertEqual(len(result), 2) - self.assertTrue((result.index.dayofweek == [0, 0]).all()) - self.assertEqual(result.iloc[0], s['1/3/2005']) - self.assertEqual(result.iloc[1], s['1/10/2005']) - - result = s.resample('W-TUE').last() - self.assertEqual(len(result), 2) - self.assertTrue((result.index.dayofweek == [1, 1]).all()) - self.assertEqual(result.iloc[0], s['1/4/2005']) - self.assertEqual(result.iloc[1], s['1/10/2005']) - - result = s.resample('W-WED').last() - self.assertEqual(len(result), 2) - self.assertTrue((result.index.dayofweek == [2, 2]).all()) - self.assertEqual(result.iloc[0], s['1/5/2005']) - self.assertEqual(result.iloc[1], s['1/10/2005']) - - result = s.resample('W-THU').last() - self.assertEqual(len(result), 2) - self.assertTrue((result.index.dayofweek == [3, 3]).all()) - self.assertEqual(result.iloc[0], s['1/6/2005']) - self.assertEqual(result.iloc[1], s['1/10/2005']) - - result = s.resample('W-FRI').last() - self.assertEqual(len(result), 2) - self.assertTrue((result.index.dayofweek == [4, 4]).all()) - self.assertEqual(result.iloc[0], s['1/7/2005']) - self.assertEqual(result.iloc[1], s['1/10/2005']) - - # to biz day - result = s.resample('B').last() - self.assertEqual(len(result), 7) - self.assertTrue((result.index.dayofweek == [ - 4, 0, 1, 2, 3, 4, 0 - ]).all()) - self.assertEqual(result.iloc[0], s['1/2/2005']) - self.assertEqual(result.iloc[1], s['1/3/2005']) - self.assertEqual(result.iloc[5], s['1/9/2005']) - self.assertEqual(result.index.name, 'index') - - def test_resample_upsampling_picked_but_not_correct(self): - - # Test for issue #3020 - dates = date_range('01-Jan-2014', '05-Jan-2014', freq='D') - series = Series(1, index=dates) - - result = series.resample('D').mean() - self.assertEqual(result.index[0], dates[0]) - - # GH 5955 - # incorrect deciding to upsample when the axis frequency matches the - # resample frequency - - import datetime - s = Series(np.arange(1., 6), index=[datetime.datetime( - 1975, 1, i, 12, 0) for i in range(1, 6)]) - expected = Series(np.arange(1., 6), index=date_range( - '19750101', periods=5, freq='D')) - - result = s.resample('D').count() - assert_series_equal(result, Series(1, index=expected.index)) - - result1 = s.resample('D').sum() - result2 = s.resample('D').mean() - assert_series_equal(result1, expected) - assert_series_equal(result2, expected) - - def test_resample_frame_basic(self): - df = tm.makeTimeDataFrame() - - b = TimeGrouper('M') - g = df.groupby(b) - - # check all cython functions work - funcs = ['add', 'mean', 'prod', 'min', 'max', 'var'] - for f in funcs: - g._cython_agg_general(f) - - result = df.resample('A').mean() - assert_series_equal(result['A'], df['A'].resample('A').mean()) - - result = df.resample('M').mean() - assert_series_equal(result['A'], df['A'].resample('M').mean()) - - df.resample('M', kind='period').mean() - df.resample('W-WED', kind='period').mean() - - def test_resample_loffset(self): - rng = date_range('1/1/2000 00:00:00', '1/1/2000 00:13:00', freq='min') - s = Series(np.random.randn(14), index=rng) - - result = s.resample('5min', closed='right', label='right', - loffset=timedelta(minutes=1)).mean() - idx = date_range('1/1/2000', periods=4, freq='5min') - expected = Series([s[0], s[1:6].mean(), s[6:11].mean(), s[11:].mean()], - index=idx + timedelta(minutes=1)) - assert_series_equal(result, expected) - - expected = s.resample( - '5min', closed='right', label='right', - loffset='1min').mean() - assert_series_equal(result, expected) - - expected = s.resample( - '5min', closed='right', label='right', - loffset=Minute(1)).mean() - assert_series_equal(result, expected) - - self.assertEqual(result.index.freq, Minute(5)) - - # from daily - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='D') - ser = Series(np.random.rand(len(dti)), dti) - - # to weekly - result = ser.resample('w-sun').last() - expected = ser.resample('w-sun', loffset=-bday).last() - self.assertEqual(result.index[0] - bday, expected.index[0]) - - def test_resample_loffset_count(self): - # GH 12725 - start_time = '1/1/2000 00:00:00' - rng = date_range(start_time, periods=100, freq='S') - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('10S', loffset='1s').count() - - expected_index = ( - date_range(start_time, periods=10, freq='10S') + - timedelta(seconds=1) - ) - expected = pd.Series(10, index=expected_index) - - assert_series_equal(result, expected) - - # Same issue should apply to .size() since it goes through - # same code path - result = ts.resample('10S', loffset='1s').size() - - assert_series_equal(result, expected) - - def test_resample_upsample(self): - # from daily - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='D', name='index') - - s = Series(np.random.rand(len(dti)), dti) - - # to minutely, by padding - result = s.resample('Min').pad() - self.assertEqual(len(result), 12961) - self.assertEqual(result[0], s[0]) - self.assertEqual(result[-1], s[-1]) - - self.assertEqual(result.index.name, 'index') - - def test_resample_how_method(self): - # GH9915 - s = pd.Series([11, 22], - index=[Timestamp('2015-03-31 21:48:52.672000'), - Timestamp('2015-03-31 21:49:52.739000')]) - expected = pd.Series([11, np.NaN, np.NaN, np.NaN, np.NaN, np.NaN, 22], - index=[Timestamp('2015-03-31 21:48:50'), - Timestamp('2015-03-31 21:49:00'), - Timestamp('2015-03-31 21:49:10'), - Timestamp('2015-03-31 21:49:20'), - Timestamp('2015-03-31 21:49:30'), - Timestamp('2015-03-31 21:49:40'), - Timestamp('2015-03-31 21:49:50')]) - assert_series_equal(s.resample("10S").mean(), expected) - - def test_resample_extra_index_point(self): - # GH 9756 - index = DatetimeIndex(start='20150101', end='20150331', freq='BM') - expected = DataFrame({'A': Series([21, 41, 63], index=index)}) - - index = DatetimeIndex(start='20150101', end='20150331', freq='B') - df = DataFrame( - {'A': Series(range(len(index)), index=index)}, dtype='int64') - result = df.resample('BM').last() - assert_frame_equal(result, expected) - - def test_upsample_with_limit(self): - rng = date_range('1/1/2000', periods=3, freq='5t') - ts = Series(np.random.randn(len(rng)), rng) - - result = ts.resample('t').ffill(limit=2) - expected = ts.reindex(result.index, method='ffill', limit=2) - assert_series_equal(result, expected) - - def test_resample_ohlc(self): - s = self.series - - grouper = TimeGrouper(Minute(5)) - expect = s.groupby(grouper).agg(lambda x: x[-1]) - result = s.resample('5Min').ohlc() - - self.assertEqual(len(result), len(expect)) - self.assertEqual(len(result.columns), 4) - - xs = result.iloc[-2] - self.assertEqual(xs['open'], s[-6]) - self.assertEqual(xs['high'], s[-6:-1].max()) - self.assertEqual(xs['low'], s[-6:-1].min()) - self.assertEqual(xs['close'], s[-2]) - - xs = result.iloc[0] - self.assertEqual(xs['open'], s[0]) - self.assertEqual(xs['high'], s[:5].max()) - self.assertEqual(xs['low'], s[:5].min()) - self.assertEqual(xs['close'], s[4]) - - def test_resample_ohlc_result(self): - - # GH 12332 - index = pd.date_range('1-1-2000', '2-15-2000', freq='h') - index = index.union(pd.date_range('4-15-2000', '5-15-2000', freq='h')) - s = Series(range(len(index)), index=index) - - a = s.loc[:'4-15-2000'].resample('30T').ohlc() - self.assertIsInstance(a, DataFrame) - - b = s.loc[:'4-14-2000'].resample('30T').ohlc() - self.assertIsInstance(b, DataFrame) - - # GH12348 - # raising on odd period - rng = date_range('2013-12-30', '2014-01-07') - index = rng.drop([Timestamp('2014-01-01'), - Timestamp('2013-12-31'), - Timestamp('2014-01-04'), - Timestamp('2014-01-05')]) - df = DataFrame(data=np.arange(len(index)), index=index) - result = df.resample('B').mean() - expected = df.reindex(index=date_range(rng[0], rng[-1], freq='B')) - assert_frame_equal(result, expected) - - def test_resample_ohlc_dataframe(self): - df = ( - pd.DataFrame({ - 'PRICE': { - Timestamp('2011-01-06 10:59:05', tz=None): 24990, - Timestamp('2011-01-06 12:43:33', tz=None): 25499, - Timestamp('2011-01-06 12:54:09', tz=None): 25499}, - 'VOLUME': { - Timestamp('2011-01-06 10:59:05', tz=None): 1500000000, - Timestamp('2011-01-06 12:43:33', tz=None): 5000000000, - Timestamp('2011-01-06 12:54:09', tz=None): 100000000}}) - ).reindex_axis(['VOLUME', 'PRICE'], axis=1) - res = df.resample('H').ohlc() - exp = pd.concat([df['VOLUME'].resample('H').ohlc(), - df['PRICE'].resample('H').ohlc()], - axis=1, - keys=['VOLUME', 'PRICE']) - assert_frame_equal(exp, res) - - df.columns = [['a', 'b'], ['c', 'd']] - res = df.resample('H').ohlc() - exp.columns = pd.MultiIndex.from_tuples([ - ('a', 'c', 'open'), ('a', 'c', 'high'), ('a', 'c', 'low'), - ('a', 'c', 'close'), ('b', 'd', 'open'), ('b', 'd', 'high'), - ('b', 'd', 'low'), ('b', 'd', 'close')]) - assert_frame_equal(exp, res) - - # dupe columns fail atm - # df.columns = ['PRICE', 'PRICE'] - - def test_resample_dup_index(self): - - # GH 4812 - # dup columns with resample raising - df = DataFrame(np.random.randn(4, 12), index=[2000, 2000, 2000, 2000], - columns=[Period(year=2000, month=i + 1, freq='M') - for i in range(12)]) - df.iloc[3, :] = np.nan - result = df.resample('Q', axis=1).mean() - expected = df.groupby(lambda x: int((x.month - 1) / 3), axis=1).mean() - expected.columns = [ - Period(year=2000, quarter=i + 1, freq='Q') for i in range(4)] - assert_frame_equal(result, expected) - - def test_resample_reresample(self): - dti = DatetimeIndex(start=datetime(2005, 1, 1), - end=datetime(2005, 1, 10), freq='D') - s = Series(np.random.rand(len(dti)), dti) - bs = s.resample('B', closed='right', label='right').mean() - result = bs.resample('8H').mean() - self.assertEqual(len(result), 22) - tm.assertIsInstance(result.index.freq, offsets.DateOffset) - self.assertEqual(result.index.freq, offsets.Hour(8)) - - def test_resample_timestamp_to_period(self): - ts = _simple_ts('1/1/1990', '1/1/2000') - - result = ts.resample('A-DEC', kind='period').mean() - expected = ts.resample('A-DEC').mean() - expected.index = period_range('1990', '2000', freq='a-dec') - assert_series_equal(result, expected) - - result = ts.resample('A-JUN', kind='period').mean() - expected = ts.resample('A-JUN').mean() - expected.index = period_range('1990', '2000', freq='a-jun') - assert_series_equal(result, expected) - - result = ts.resample('M', kind='period').mean() - expected = ts.resample('M').mean() - expected.index = period_range('1990-01', '2000-01', freq='M') - assert_series_equal(result, expected) - - result = ts.resample('M', kind='period').mean() - expected = ts.resample('M').mean() - expected.index = period_range('1990-01', '2000-01', freq='M') - assert_series_equal(result, expected) - - def test_ohlc_5min(self): - def _ohlc(group): - if isnull(group).all(): - return np.repeat(np.nan, 4) - return [group[0], group.max(), group.min(), group[-1]] - - rng = date_range('1/1/2000 00:00:00', '1/1/2000 5:59:50', freq='10s') - ts = Series(np.random.randn(len(rng)), index=rng) - - resampled = ts.resample('5min', closed='right', - label='right').ohlc() - - self.assertTrue((resampled.loc['1/1/2000 00:00'] == ts[0]).all()) - - exp = _ohlc(ts[1:31]) - self.assertTrue((resampled.loc['1/1/2000 00:05'] == exp).all()) - - exp = _ohlc(ts['1/1/2000 5:55:01':]) - self.assertTrue((resampled.loc['1/1/2000 6:00:00'] == exp).all()) - - def test_downsample_non_unique(self): - rng = date_range('1/1/2000', '2/29/2000') - rng2 = rng.repeat(5).values - ts = Series(np.random.randn(len(rng2)), index=rng2) - - result = ts.resample('M').mean() - - expected = ts.groupby(lambda x: x.month).mean() - self.assertEqual(len(result), 2) - assert_almost_equal(result[0], expected[1]) - assert_almost_equal(result[1], expected[2]) - - def test_asfreq_non_unique(self): - # GH #1077 - rng = date_range('1/1/2000', '2/29/2000') - rng2 = rng.repeat(2).values - ts = Series(np.random.randn(len(rng2)), index=rng2) - - self.assertRaises(Exception, ts.asfreq, 'B') - - def test_resample_axis1(self): - rng = date_range('1/1/2000', '2/29/2000') - df = DataFrame(np.random.randn(3, len(rng)), columns=rng, - index=['a', 'b', 'c']) - - result = df.resample('M', axis=1).mean() - expected = df.T.resample('M').mean().T - tm.assert_frame_equal(result, expected) - - def test_resample_panel(self): - rng = date_range('1/1/2000', '6/30/2000') - n = len(rng) - - panel = Panel(np.random.randn(3, n, 5), - items=['one', 'two', 'three'], - major_axis=rng, - minor_axis=['a', 'b', 'c', 'd', 'e']) - - result = panel.resample('M', axis=1).mean() - - def p_apply(panel, f): - result = {} - for item in panel.items: - result[item] = f(panel[item]) - return Panel(result, items=panel.items) - - expected = p_apply(panel, lambda x: x.resample('M').mean()) - tm.assert_panel_equal(result, expected) - - panel2 = panel.swapaxes(1, 2) - result = panel2.resample('M', axis=2).mean() - expected = p_apply(panel2, lambda x: x.resample('M', axis=1).mean()) - tm.assert_panel_equal(result, expected) - - def test_resample_panel_numpy(self): - rng = date_range('1/1/2000', '6/30/2000') - n = len(rng) - - panel = Panel(np.random.randn(3, n, 5), - items=['one', 'two', 'three'], - major_axis=rng, - minor_axis=['a', 'b', 'c', 'd', 'e']) - - result = panel.resample('M', axis=1).apply(lambda x: x.mean(1)) - expected = panel.resample('M', axis=1).mean() - tm.assert_panel_equal(result, expected) - - panel = panel.swapaxes(1, 2) - result = panel.resample('M', axis=2).apply(lambda x: x.mean(2)) - expected = panel.resample('M', axis=2).mean() - tm.assert_panel_equal(result, expected) - - def test_resample_anchored_ticks(self): - # If a fixed delta (5 minute, 4 hour) evenly divides a day, we should - # "anchor" the origin at midnight so we get regular intervals rather - # than starting from the first timestamp which might start in the - # middle of a desired interval - - rng = date_range('1/1/2000 04:00:00', periods=86400, freq='s') - ts = Series(np.random.randn(len(rng)), index=rng) - ts[:2] = np.nan # so results are the same - - freqs = ['t', '5t', '15t', '30t', '4h', '12h'] - for freq in freqs: - result = ts[2:].resample(freq, closed='left', label='left').mean() - expected = ts.resample(freq, closed='left', label='left').mean() - assert_series_equal(result, expected) - - def test_resample_single_group(self): - mysum = lambda x: x.sum() - - rng = date_range('2000-1-1', '2000-2-10', freq='D') - ts = Series(np.random.randn(len(rng)), index=rng) - assert_series_equal(ts.resample('M').sum(), - ts.resample('M').apply(mysum)) - - rng = date_range('2000-1-1', '2000-1-10', freq='D') - ts = Series(np.random.randn(len(rng)), index=rng) - assert_series_equal(ts.resample('M').sum(), - ts.resample('M').apply(mysum)) - - # GH 3849 - s = Series([30.1, 31.6], index=[Timestamp('20070915 15:30:00'), - Timestamp('20070915 15:40:00')]) - expected = Series([0.75], index=[Timestamp('20070915')]) - result = s.resample('D').apply(lambda x: np.std(x)) - assert_series_equal(result, expected) - - def test_resample_base(self): - rng = date_range('1/1/2000 00:00:00', '1/1/2000 02:00', freq='s') - ts = Series(np.random.randn(len(rng)), index=rng) - - resampled = ts.resample('5min', base=2).mean() - exp_rng = date_range('12/31/1999 23:57:00', '1/1/2000 01:57', - freq='5min') - self.assert_index_equal(resampled.index, exp_rng) - - def test_resample_base_with_timedeltaindex(self): - - # GH 10530 - rng = timedelta_range(start='0s', periods=25, freq='s') - ts = Series(np.random.randn(len(rng)), index=rng) - - with_base = ts.resample('2s', base=5).mean() - without_base = ts.resample('2s').mean() - - exp_without_base = timedelta_range(start='0s', end='25s', freq='2s') - exp_with_base = timedelta_range(start='5s', end='29s', freq='2s') - - self.assert_index_equal(without_base.index, exp_without_base) - self.assert_index_equal(with_base.index, exp_with_base) - - def test_resample_categorical_data_with_timedeltaindex(self): - # GH #12169 - df = DataFrame({'Group_obj': 'A'}, - index=pd.to_timedelta(list(range(20)), unit='s')) - df['Group'] = df['Group_obj'].astype('category') - result = df.resample('10s').agg(lambda x: (x.value_counts().index[0])) - expected = DataFrame({'Group_obj': ['A', 'A'], - 'Group': ['A', 'A']}, - index=pd.to_timedelta([0, 10], unit='s')) - expected = expected.reindex_axis(['Group_obj', 'Group'], 1) - tm.assert_frame_equal(result, expected) - - def test_resample_daily_anchored(self): - rng = date_range('1/1/2000 0:00:00', periods=10000, freq='T') - ts = Series(np.random.randn(len(rng)), index=rng) - ts[:2] = np.nan # so results are the same - - result = ts[2:].resample('D', closed='left', label='left').mean() - expected = ts.resample('D', closed='left', label='left').mean() - assert_series_equal(result, expected) - - def test_resample_to_period_monthly_buglet(self): - # GH #1259 - - rng = date_range('1/1/2000', '12/31/2000') - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('M', kind='period').mean() - exp_index = period_range('Jan-2000', 'Dec-2000', freq='M') - self.assert_index_equal(result.index, exp_index) - - def test_period_with_agg(self): - - # aggregate a period resampler with a lambda - s2 = pd.Series(np.random.randint(0, 5, 50), - index=pd.period_range('2012-01-01', - freq='H', - periods=50), - dtype='float64') - - expected = s2.to_timestamp().resample('D').mean().to_period() - result = s2.resample('D').agg(lambda x: x.mean()) - assert_series_equal(result, expected) - - def test_resample_segfault(self): - # GH 8573 - # segfaulting in older versions - all_wins_and_wagers = [ - (1, datetime(2013, 10, 1, 16, 20), 1, 0), - (2, datetime(2013, 10, 1, 16, 10), 1, 0), - (2, datetime(2013, 10, 1, 18, 15), 1, 0), - (2, datetime(2013, 10, 1, 16, 10, 31), 1, 0)] - - df = pd.DataFrame.from_records(all_wins_and_wagers, - columns=("ID", "timestamp", "A", "B") - ).set_index("timestamp") - result = df.groupby("ID").resample("5min").sum() - expected = df.groupby("ID").apply(lambda x: x.resample("5min").sum()) - assert_frame_equal(result, expected) - - def test_resample_dtype_preservation(self): - - # GH 12202 - # validation tests for dtype preservation - - df = DataFrame({'date': pd.date_range(start='2016-01-01', - periods=4, freq='W'), - 'group': [1, 1, 2, 2], - 'val': Series([5, 6, 7, 8], - dtype='int32')} - ).set_index('date') - - result = df.resample('1D').ffill() - self.assertEqual(result.val.dtype, np.int32) - - result = df.groupby('group').resample('1D').ffill() - self.assertEqual(result.val.dtype, np.int32) - - def test_weekly_resample_buglet(self): - # #1327 - rng = date_range('1/1/2000', freq='B', periods=20) - ts = Series(np.random.randn(len(rng)), index=rng) - - resampled = ts.resample('W').mean() - expected = ts.resample('W-SUN').mean() - assert_series_equal(resampled, expected) - - def test_monthly_resample_error(self): - # #1451 - dates = date_range('4/16/2012 20:00', periods=5000, freq='h') - ts = Series(np.random.randn(len(dates)), index=dates) - # it works! - ts.resample('M') - - def test_nanosecond_resample_error(self): - # GH 12307 - Values falls after last bin when - # Resampling using pd.tseries.offsets.Nano as period - start = 1443707890427 - exp_start = 1443707890400 - indx = pd.date_range( - start=pd.to_datetime(start), - periods=10, - freq='100n' - ) - ts = pd.Series(range(len(indx)), index=indx) - r = ts.resample(pd.tseries.offsets.Nano(100)) - result = r.agg('mean') - - exp_indx = pd.date_range( - start=pd.to_datetime(exp_start), - periods=10, - freq='100n' - ) - exp = pd.Series(range(len(exp_indx)), index=exp_indx) - - assert_series_equal(result, exp) - - def test_resample_anchored_intraday(self): - # #1471, #1458 - - rng = date_range('1/1/2012', '4/1/2012', freq='100min') - df = DataFrame(rng.month, index=rng) - - result = df.resample('M').mean() - expected = df.resample( - 'M', kind='period').mean().to_timestamp(how='end') - tm.assert_frame_equal(result, expected) - - result = df.resample('M', closed='left').mean() - exp = df.tshift(1, freq='D').resample('M', kind='period').mean() - exp = exp.to_timestamp(how='end') - - tm.assert_frame_equal(result, exp) - - rng = date_range('1/1/2012', '4/1/2012', freq='100min') - df = DataFrame(rng.month, index=rng) - - result = df.resample('Q').mean() - expected = df.resample( - 'Q', kind='period').mean().to_timestamp(how='end') - tm.assert_frame_equal(result, expected) - - result = df.resample('Q', closed='left').mean() - expected = df.tshift(1, freq='D').resample('Q', kind='period', - closed='left').mean() - expected = expected.to_timestamp(how='end') - tm.assert_frame_equal(result, expected) - - ts = _simple_ts('2012-04-29 23:00', '2012-04-30 5:00', freq='h') - resampled = ts.resample('M').mean() - self.assertEqual(len(resampled), 1) - - def test_resample_anchored_monthstart(self): - ts = _simple_ts('1/1/2000', '12/31/2002') - - freqs = ['MS', 'BMS', 'QS-MAR', 'AS-DEC', 'AS-JUN'] - - for freq in freqs: - ts.resample(freq).mean() - - def test_resample_anchored_multiday(self): - # When resampling a range spanning multiple days, ensure that the - # start date gets used to determine the offset. Fixes issue where - # a one day period is not a multiple of the frequency. - # - # See: https://github.com/pandas-dev/pandas/issues/8683 - - index = pd.date_range( - '2014-10-14 23:06:23.206', periods=3, freq='400L' - ) | pd.date_range( - '2014-10-15 23:00:00', periods=2, freq='2200L') - - s = pd.Series(np.random.randn(5), index=index) - - # Ensure left closing works - result = s.resample('2200L').mean() - self.assertEqual(result.index[-1], - pd.Timestamp('2014-10-15 23:00:02.000')) - - # Ensure right closing works - result = s.resample('2200L', label='right').mean() - self.assertEqual(result.index[-1], - pd.Timestamp('2014-10-15 23:00:04.200')) - - def test_corner_cases(self): - # miscellaneous test coverage - - rng = date_range('1/1/2000', periods=12, freq='t') - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('5t', closed='right', label='left').mean() - ex_index = date_range('1999-12-31 23:55', periods=4, freq='5t') - self.assert_index_equal(result.index, ex_index) - - len0pts = _simple_pts('2007-01', '2010-05', freq='M')[:0] - # it works - result = len0pts.resample('A-DEC').mean() - self.assertEqual(len(result), 0) - - # resample to periods - ts = _simple_ts('2000-04-28', '2000-04-30 11:00', freq='h') - result = ts.resample('M', kind='period').mean() - self.assertEqual(len(result), 1) - self.assertEqual(result.index[0], Period('2000-04', freq='M')) - - def test_anchored_lowercase_buglet(self): - dates = date_range('4/16/2012 20:00', periods=50000, freq='s') - ts = Series(np.random.randn(len(dates)), index=dates) - # it works! - ts.resample('d').mean() - - def test_upsample_apply_functions(self): - # #1596 - rng = pd.date_range('2012-06-12', periods=4, freq='h') - - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('20min').aggregate(['mean', 'sum']) - tm.assertIsInstance(result, DataFrame) - - def test_resample_not_monotonic(self): - rng = pd.date_range('2012-06-12', periods=200, freq='h') - ts = Series(np.random.randn(len(rng)), index=rng) - - ts = ts.take(np.random.permutation(len(ts))) - - result = ts.resample('D').sum() - exp = ts.sort_index().resample('D').sum() - assert_series_equal(result, exp) - - def test_resample_median_bug_1688(self): - - for dtype in ['int64', 'int32', 'float64', 'float32']: - df = DataFrame([1, 2], index=[datetime(2012, 1, 1, 0, 0, 0), - datetime(2012, 1, 1, 0, 5, 0)], - dtype=dtype) - - result = df.resample("T").apply(lambda x: x.mean()) - exp = df.asfreq('T') - tm.assert_frame_equal(result, exp) - - result = df.resample("T").median() - exp = df.asfreq('T') - tm.assert_frame_equal(result, exp) - - def test_how_lambda_functions(self): - - ts = _simple_ts('1/1/2000', '4/1/2000') - - result = ts.resample('M').apply(lambda x: x.mean()) - exp = ts.resample('M').mean() - tm.assert_series_equal(result, exp) - - foo_exp = ts.resample('M').mean() - foo_exp.name = 'foo' - bar_exp = ts.resample('M').std() - bar_exp.name = 'bar' - - result = ts.resample('M').apply( - [lambda x: x.mean(), lambda x: x.std(ddof=1)]) - result.columns = ['foo', 'bar'] - tm.assert_series_equal(result['foo'], foo_exp) - tm.assert_series_equal(result['bar'], bar_exp) - - result = ts.resample('M').aggregate({'foo': lambda x: x.mean(), - 'bar': lambda x: x.std(ddof=1)}) - tm.assert_series_equal(result['foo'], foo_exp) - tm.assert_series_equal(result['bar'], bar_exp) - - def test_resample_unequal_times(self): - # #1772 - start = datetime(1999, 3, 1, 5) - # end hour is less than start - end = datetime(2012, 7, 31, 4) - bad_ind = date_range(start, end, freq="30min") - df = DataFrame({'close': 1}, index=bad_ind) - - # it works! - df.resample('AS').sum() - - def test_resample_consistency(self): - - # GH 6418 - # resample with bfill / limit / reindex consistency - - i30 = pd.date_range('2002-02-02', periods=4, freq='30T') - s = pd.Series(np.arange(4.), index=i30) - s[2] = np.NaN - - # Upsample by factor 3 with reindex() and resample() methods: - i10 = pd.date_range(i30[0], i30[-1], freq='10T') - - s10 = s.reindex(index=i10, method='bfill') - s10_2 = s.reindex(index=i10, method='bfill', limit=2) - rl = s.reindex_like(s10, method='bfill', limit=2) - r10_2 = s.resample('10Min').bfill(limit=2) - r10 = s.resample('10Min').bfill() - - # s10_2, r10, r10_2, rl should all be equal - assert_series_equal(s10_2, r10) - assert_series_equal(s10_2, r10_2) - assert_series_equal(s10_2, rl) - - def test_resample_timegrouper(self): - # GH 7227 - dates1 = [datetime(2014, 10, 1), datetime(2014, 9, 3), - datetime(2014, 11, 5), datetime(2014, 9, 5), - datetime(2014, 10, 8), datetime(2014, 7, 15)] - - dates2 = dates1[:2] + [pd.NaT] + dates1[2:4] + [pd.NaT] + dates1[4:] - dates3 = [pd.NaT] + dates1 + [pd.NaT] - - for dates in [dates1, dates2, dates3]: - df = DataFrame(dict(A=dates, B=np.arange(len(dates)))) - result = df.set_index('A').resample('M').count() - exp_idx = pd.DatetimeIndex(['2014-07-31', '2014-08-31', - '2014-09-30', - '2014-10-31', '2014-11-30'], - freq='M', name='A') - expected = DataFrame({'B': [1, 0, 2, 2, 1]}, index=exp_idx) - assert_frame_equal(result, expected) - - result = df.groupby(pd.Grouper(freq='M', key='A')).count() - assert_frame_equal(result, expected) - - df = DataFrame(dict(A=dates, B=np.arange(len(dates)), C=np.arange( - len(dates)))) - result = df.set_index('A').resample('M').count() - expected = DataFrame({'B': [1, 0, 2, 2, 1], 'C': [1, 0, 2, 2, 1]}, - index=exp_idx, columns=['B', 'C']) - assert_frame_equal(result, expected) - - result = df.groupby(pd.Grouper(freq='M', key='A')).count() - assert_frame_equal(result, expected) - - def test_resample_nunique(self): - - # GH 12352 - df = DataFrame({ - 'ID': {pd.Timestamp('2015-06-05 00:00:00'): '0010100903', - pd.Timestamp('2015-06-08 00:00:00'): '0010150847'}, - 'DATE': {pd.Timestamp('2015-06-05 00:00:00'): '2015-06-05', - pd.Timestamp('2015-06-08 00:00:00'): '2015-06-08'}}) - r = df.resample('D') - g = df.groupby(pd.Grouper(freq='D')) - expected = df.groupby(pd.TimeGrouper('D')).ID.apply(lambda x: - x.nunique()) - self.assertEqual(expected.name, 'ID') - - for t in [r, g]: - result = r.ID.nunique() - assert_series_equal(result, expected) - - result = df.ID.resample('D').nunique() - assert_series_equal(result, expected) - - result = df.ID.groupby(pd.Grouper(freq='D')).nunique() - assert_series_equal(result, expected) - - def test_resample_nunique_with_date_gap(self): - # GH 13453 - index = pd.date_range('1-1-2000', '2-15-2000', freq='h') - index2 = pd.date_range('4-15-2000', '5-15-2000', freq='h') - index3 = index.append(index2) - s = pd.Series(range(len(index3)), index=index3, dtype='int64') - r = s.resample('M') - - # Since all elements are unique, these should all be the same - results = [ - r.count(), - r.nunique(), - r.agg(pd.Series.nunique), - r.agg('nunique') - ] - - assert_series_equal(results[0], results[1]) - assert_series_equal(results[0], results[2]) - assert_series_equal(results[0], results[3]) - - def test_resample_group_info(self): # GH10914 - for n, k in product((10000, 100000), (10, 100, 1000)): - dr = date_range(start='2015-08-27', periods=n // 10, freq='T') - ts = Series(np.random.randint(0, n // k, n).astype('int64'), - index=np.random.choice(dr, n)) - - left = ts.resample('30T').nunique() - ix = date_range(start=ts.index.min(), end=ts.index.max(), - freq='30T') - - vals = ts.values - bins = np.searchsorted(ix.values, ts.index, side='right') - - sorter = np.lexsort((vals, bins)) - vals, bins = vals[sorter], bins[sorter] - - mask = np.r_[True, vals[1:] != vals[:-1]] - mask |= np.r_[True, bins[1:] != bins[:-1]] - - arr = np.bincount(bins[mask] - 1, - minlength=len(ix)).astype('int64', copy=False) - right = Series(arr, index=ix) - - assert_series_equal(left, right) - - def test_resample_size(self): - n = 10000 - dr = date_range('2015-09-19', periods=n, freq='T') - ts = Series(np.random.randn(n), index=np.random.choice(dr, n)) - - left = ts.resample('7T').size() - ix = date_range(start=left.index.min(), end=ts.index.max(), freq='7T') - - bins = np.searchsorted(ix.values, ts.index.values, side='right') - val = np.bincount(bins, minlength=len(ix) + 1)[1:].astype('int64', - copy=False) - - right = Series(val, index=ix) - assert_series_equal(left, right) - - def test_resample_across_dst(self): - # The test resamples a DatetimeIndex with values before and after a - # DST change - # Issue: 14682 - - # The DatetimeIndex we will start with - # (note that DST happens at 03:00+02:00 -> 02:00+01:00) - # 2016-10-30 02:23:00+02:00, 2016-10-30 02:23:00+01:00 - df1 = DataFrame([1477786980, 1477790580], columns=['ts']) - dti1 = DatetimeIndex(pd.to_datetime(df1.ts, unit='s') - .dt.tz_localize('UTC') - .dt.tz_convert('Europe/Madrid')) - - # The expected DatetimeIndex after resampling. - # 2016-10-30 02:00:00+02:00, 2016-10-30 02:00:00+01:00 - df2 = DataFrame([1477785600, 1477789200], columns=['ts']) - dti2 = DatetimeIndex(pd.to_datetime(df2.ts, unit='s') - .dt.tz_localize('UTC') - .dt.tz_convert('Europe/Madrid')) - df = DataFrame([5, 5], index=dti1) - - result = df.resample(rule='H').sum() - expected = DataFrame([5, 5], index=dti2) - - assert_frame_equal(result, expected) - - def test_resample_dst_anchor(self): - # 5172 - dti = DatetimeIndex([datetime(2012, 11, 4, 23)], tz='US/Eastern') - df = DataFrame([5], index=dti) - assert_frame_equal(df.resample(rule='D').sum(), - DataFrame([5], index=df.index.normalize())) - df.resample(rule='MS').sum() - assert_frame_equal( - df.resample(rule='MS').sum(), - DataFrame([5], index=DatetimeIndex([datetime(2012, 11, 1)], - tz='US/Eastern'))) - - dti = date_range('2013-09-30', '2013-11-02', freq='30Min', - tz='Europe/Paris') - values = range(dti.size) - df = DataFrame({"a": values, - "b": values, - "c": values}, index=dti, dtype='int64') - how = {"a": "min", "b": "max", "c": "count"} - - assert_frame_equal( - df.resample("W-MON").agg(how)[["a", "b", "c"]], - DataFrame({"a": [0, 48, 384, 720, 1056, 1394], - "b": [47, 383, 719, 1055, 1393, 1586], - "c": [48, 336, 336, 336, 338, 193]}, - index=date_range('9/30/2013', '11/4/2013', - freq='W-MON', tz='Europe/Paris')), - 'W-MON Frequency') - - assert_frame_equal( - df.resample("2W-MON").agg(how)[["a", "b", "c"]], - DataFrame({"a": [0, 48, 720, 1394], - "b": [47, 719, 1393, 1586], - "c": [48, 672, 674, 193]}, - index=date_range('9/30/2013', '11/11/2013', - freq='2W-MON', tz='Europe/Paris')), - '2W-MON Frequency') - - assert_frame_equal( - df.resample("MS").agg(how)[["a", "b", "c"]], - DataFrame({"a": [0, 48, 1538], - "b": [47, 1537, 1586], - "c": [48, 1490, 49]}, - index=date_range('9/1/2013', '11/1/2013', - freq='MS', tz='Europe/Paris')), - 'MS Frequency') - - assert_frame_equal( - df.resample("2MS").agg(how)[["a", "b", "c"]], - DataFrame({"a": [0, 1538], - "b": [1537, 1586], - "c": [1538, 49]}, - index=date_range('9/1/2013', '11/1/2013', - freq='2MS', tz='Europe/Paris')), - '2MS Frequency') - - df_daily = df['10/26/2013':'10/29/2013'] - assert_frame_equal( - df_daily.resample("D").agg({"a": "min", "b": "max", "c": "count"}) - [["a", "b", "c"]], - DataFrame({"a": [1248, 1296, 1346, 1394], - "b": [1295, 1345, 1393, 1441], - "c": [48, 50, 48, 48]}, - index=date_range('10/26/2013', '10/29/2013', - freq='D', tz='Europe/Paris')), - 'D Frequency') - - def test_resample_with_nat(self): - # GH 13020 - index = DatetimeIndex([pd.NaT, - '1970-01-01 00:00:00', - pd.NaT, - '1970-01-01 00:00:01', - '1970-01-01 00:00:02']) - frame = DataFrame([2, 3, 5, 7, 11], index=index) - - index_1s = DatetimeIndex(['1970-01-01 00:00:00', - '1970-01-01 00:00:01', - '1970-01-01 00:00:02']) - frame_1s = DataFrame([3, 7, 11], index=index_1s) - assert_frame_equal(frame.resample('1s').mean(), frame_1s) - - index_2s = DatetimeIndex(['1970-01-01 00:00:00', - '1970-01-01 00:00:02']) - frame_2s = DataFrame([5, 11], index=index_2s) - assert_frame_equal(frame.resample('2s').mean(), frame_2s) - - index_3s = DatetimeIndex(['1970-01-01 00:00:00']) - frame_3s = DataFrame([7], index=index_3s) - assert_frame_equal(frame.resample('3s').mean(), frame_3s) - - assert_frame_equal(frame.resample('60s').mean(), frame_3s) - - def test_resample_timedelta_values(self): - # GH 13119 - # check that timedelta dtype is preserved when NaT values are - # introduced by the resampling - - times = timedelta_range('1 day', '4 day', freq='4D') - df = DataFrame({'time': times}, index=times) - - times2 = timedelta_range('1 day', '4 day', freq='2D') - exp = Series(times2, index=times2, name='time') - exp.iloc[1] = pd.NaT - - res = df.resample('2D').first()['time'] - tm.assert_series_equal(res, exp) - res = df['time'].resample('2D').first() - tm.assert_series_equal(res, exp) - - def test_resample_datetime_values(self): - # GH 13119 - # check that datetime dtype is preserved when NaT values are - # introduced by the resampling - - dates = [datetime(2016, 1, 15), datetime(2016, 1, 19)] - df = DataFrame({'timestamp': dates}, index=dates) - - exp = Series([datetime(2016, 1, 15), pd.NaT, datetime(2016, 1, 19)], - index=date_range('2016-01-15', periods=3, freq='2D'), - name='timestamp') - - res = df.resample('2D').first()['timestamp'] - tm.assert_series_equal(res, exp) - res = df['timestamp'].resample('2D').first() - tm.assert_series_equal(res, exp) - - -class TestPeriodIndex(Base, tm.TestCase): - _index_factory = lambda x: period_range - - def create_series(self): - i = period_range(datetime(2005, 1, 1), - datetime(2005, 1, 10), freq='D') - - return Series(np.arange(len(i)), index=i, name='pi') - - def test_asfreq_downsample(self): - - # series - s = self.create_series() - expected = s.reindex(s.index.take(np.arange(0, len(s.index), 2))) - expected.index = expected.index.to_timestamp() - expected.index.freq = to_offset('2D') - - # this is a bug, this *should* return a PeriodIndex - # directly - # GH 12884 - result = s.resample('2D').asfreq() - assert_series_equal(result, expected) - - # frame - frame = s.to_frame('value') - expected = frame.reindex( - frame.index.take(np.arange(0, len(frame.index), 2))) - expected.index = expected.index.to_timestamp() - expected.index.freq = to_offset('2D') - result = frame.resample('2D').asfreq() - assert_frame_equal(result, expected) - - def test_asfreq_upsample(self): - - # this is a bug, this *should* return a PeriodIndex - # directly - # GH 12884 - s = self.create_series() - new_index = date_range(s.index[0].to_timestamp(how='start'), - (s.index[-1] + 1).to_timestamp(how='start'), - freq='1H', - closed='left') - expected = s.to_timestamp().reindex(new_index).to_period() - result = s.resample('1H').asfreq() - assert_series_equal(result, expected) - - frame = s.to_frame('value') - new_index = date_range(frame.index[0].to_timestamp(how='start'), - (frame.index[-1] + 1).to_timestamp(how='start'), - freq='1H', - closed='left') - expected = frame.to_timestamp().reindex(new_index).to_period() - result = frame.resample('1H').asfreq() - assert_frame_equal(result, expected) - - def test_asfreq_fill_value(self): - # test for fill value during resampling, issue 3715 - - s = self.create_series() - new_index = date_range(s.index[0].to_timestamp(how='start'), - (s.index[-1]).to_timestamp(how='start'), - freq='1H') - expected = s.to_timestamp().reindex(new_index, fill_value=4.0) - result = s.resample('1H', kind='timestamp').asfreq(fill_value=4.0) - assert_series_equal(result, expected) - - frame = s.to_frame('value') - new_index = date_range(frame.index[0].to_timestamp(how='start'), - (frame.index[-1]).to_timestamp(how='start'), - freq='1H') - expected = frame.to_timestamp().reindex(new_index, fill_value=3.0) - result = frame.resample('1H', kind='timestamp').asfreq(fill_value=3.0) - assert_frame_equal(result, expected) - - def test_selection(self): - index = self.create_series().index - # This is a bug, these should be implemented - # GH 14008 - df = pd.DataFrame({'date': index, - 'a': np.arange(len(index), dtype=np.int64)}, - index=pd.MultiIndex.from_arrays([ - np.arange(len(index), dtype=np.int64), - index], names=['v', 'd'])) - - with tm.assertRaises(NotImplementedError): - df.resample('2D', on='date') - - with tm.assertRaises(NotImplementedError): - df.resample('2D', level='d') - - def test_annual_upsample_D_s_f(self): - self._check_annual_upsample_cases('D', 'start', 'ffill') - - def test_annual_upsample_D_e_f(self): - self._check_annual_upsample_cases('D', 'end', 'ffill') - - def test_annual_upsample_D_s_b(self): - self._check_annual_upsample_cases('D', 'start', 'bfill') - - def test_annual_upsample_D_e_b(self): - self._check_annual_upsample_cases('D', 'end', 'bfill') - - def test_annual_upsample_B_s_f(self): - self._check_annual_upsample_cases('B', 'start', 'ffill') - - def test_annual_upsample_B_e_f(self): - self._check_annual_upsample_cases('B', 'end', 'ffill') - - def test_annual_upsample_B_s_b(self): - self._check_annual_upsample_cases('B', 'start', 'bfill') - - def test_annual_upsample_B_e_b(self): - self._check_annual_upsample_cases('B', 'end', 'bfill') - - def test_annual_upsample_M_s_f(self): - self._check_annual_upsample_cases('M', 'start', 'ffill') - - def test_annual_upsample_M_e_f(self): - self._check_annual_upsample_cases('M', 'end', 'ffill') - - def test_annual_upsample_M_s_b(self): - self._check_annual_upsample_cases('M', 'start', 'bfill') - - def test_annual_upsample_M_e_b(self): - self._check_annual_upsample_cases('M', 'end', 'bfill') - - def _check_annual_upsample_cases(self, targ, conv, meth, end='12/31/1991'): - for month in MONTHS: - ts = _simple_pts('1/1/1990', end, freq='A-%s' % month) - - result = getattr(ts.resample(targ, convention=conv), meth)() - expected = result.to_timestamp(targ, how=conv) - expected = expected.asfreq(targ, meth).to_period() - assert_series_equal(result, expected) - - def test_basic_downsample(self): - ts = _simple_pts('1/1/1990', '6/30/1995', freq='M') - result = ts.resample('a-dec').mean() - - expected = ts.groupby(ts.index.year).mean() - expected.index = period_range('1/1/1990', '6/30/1995', freq='a-dec') - assert_series_equal(result, expected) - - # this is ok - assert_series_equal(ts.resample('a-dec').mean(), result) - assert_series_equal(ts.resample('a').mean(), result) - - def test_not_subperiod(self): - # These are incompatible period rules for resampling - ts = _simple_pts('1/1/1990', '6/30/1995', freq='w-wed') - self.assertRaises(ValueError, lambda: ts.resample('a-dec').mean()) - self.assertRaises(ValueError, lambda: ts.resample('q-mar').mean()) - self.assertRaises(ValueError, lambda: ts.resample('M').mean()) - self.assertRaises(ValueError, lambda: ts.resample('w-thu').mean()) - - def test_basic_upsample(self): - ts = _simple_pts('1/1/1990', '6/30/1995', freq='M') - result = ts.resample('a-dec').mean() - - resampled = result.resample('D', convention='end').ffill() - - expected = result.to_timestamp('D', how='end') - expected = expected.asfreq('D', 'ffill').to_period() - - assert_series_equal(resampled, expected) - - def test_upsample_with_limit(self): - rng = period_range('1/1/2000', periods=5, freq='A') - ts = Series(np.random.randn(len(rng)), rng) - - result = ts.resample('M', convention='end').ffill(limit=2) - expected = ts.asfreq('M').reindex(result.index, method='ffill', - limit=2) - assert_series_equal(result, expected) - - def test_annual_upsample(self): - ts = _simple_pts('1/1/1990', '12/31/1995', freq='A-DEC') - df = DataFrame({'a': ts}) - rdf = df.resample('D').ffill() - exp = df['a'].resample('D').ffill() - assert_series_equal(rdf['a'], exp) - - rng = period_range('2000', '2003', freq='A-DEC') - ts = Series([1, 2, 3, 4], index=rng) - - result = ts.resample('M').ffill() - ex_index = period_range('2000-01', '2003-12', freq='M') - - expected = ts.asfreq('M', how='start').reindex(ex_index, - method='ffill') - assert_series_equal(result, expected) - - def test_quarterly_upsample(self): - targets = ['D', 'B', 'M'] - - for month in MONTHS: - ts = _simple_pts('1/1/1990', '12/31/1995', freq='Q-%s' % month) - - for targ, conv in product(targets, ['start', 'end']): - result = ts.resample(targ, convention=conv).ffill() - expected = result.to_timestamp(targ, how=conv) - expected = expected.asfreq(targ, 'ffill').to_period() - assert_series_equal(result, expected) - - def test_monthly_upsample(self): - targets = ['D', 'B'] - - ts = _simple_pts('1/1/1990', '12/31/1995', freq='M') - - for targ, conv in product(targets, ['start', 'end']): - result = ts.resample(targ, convention=conv).ffill() - expected = result.to_timestamp(targ, how=conv) - expected = expected.asfreq(targ, 'ffill').to_period() - assert_series_equal(result, expected) - - def test_resample_basic(self): - # GH3609 - s = Series(range(100), index=date_range( - '20130101', freq='s', periods=100, name='idx'), dtype='float') - s[10:30] = np.nan - index = PeriodIndex([ - Period('2013-01-01 00:00', 'T'), - Period('2013-01-01 00:01', 'T')], name='idx') - expected = Series([34.5, 79.5], index=index) - result = s.to_period().resample('T', kind='period').mean() - assert_series_equal(result, expected) - result2 = s.resample('T', kind='period').mean() - assert_series_equal(result2, expected) - - def test_resample_count(self): - - # GH12774 - series = pd.Series(1, index=pd.period_range(start='2000', - periods=100)) - result = series.resample('M').count() - - expected_index = pd.period_range(start='2000', freq='M', periods=4) - expected = pd.Series([31, 29, 31, 9], index=expected_index) - - assert_series_equal(result, expected) - - def test_resample_same_freq(self): - - # GH12770 - series = pd.Series(range(3), index=pd.period_range( - start='2000', periods=3, freq='M')) - expected = series - - for method in resample_methods: - result = getattr(series.resample('M'), method)() - assert_series_equal(result, expected) - - def test_resample_incompat_freq(self): - - with self.assertRaises(IncompatibleFrequency): - pd.Series(range(3), index=pd.period_range( - start='2000', periods=3, freq='M')).resample('W').mean() - - def test_with_local_timezone_pytz(self): - # GH5430 - tm._skip_if_no_pytz() - import pytz - - local_timezone = pytz.timezone('America/Los_Angeles') - - start = datetime(year=2013, month=11, day=1, hour=0, minute=0, - tzinfo=pytz.utc) - # 1 day later - end = datetime(year=2013, month=11, day=2, hour=0, minute=0, - tzinfo=pytz.utc) - - index = pd.date_range(start, end, freq='H') - - series = pd.Series(1, index=index) - series = series.tz_convert(local_timezone) - result = series.resample('D', kind='period').mean() - - # Create the expected series - # Index is moved back a day with the timezone conversion from UTC to - # Pacific - expected_index = (pd.period_range(start=start, end=end, freq='D') - 1) - expected = pd.Series(1, index=expected_index) - assert_series_equal(result, expected) - - def test_with_local_timezone_dateutil(self): - # GH5430 - tm._skip_if_no_dateutil() - import dateutil - - local_timezone = 'dateutil/America/Los_Angeles' - - start = datetime(year=2013, month=11, day=1, hour=0, minute=0, - tzinfo=dateutil.tz.tzutc()) - # 1 day later - end = datetime(year=2013, month=11, day=2, hour=0, minute=0, - tzinfo=dateutil.tz.tzutc()) - - index = pd.date_range(start, end, freq='H', name='idx') - - series = pd.Series(1, index=index) - series = series.tz_convert(local_timezone) - result = series.resample('D', kind='period').mean() - - # Create the expected series - # Index is moved back a day with the timezone conversion from UTC to - # Pacific - expected_index = (pd.period_range(start=start, end=end, freq='D', - name='idx') - 1) - expected = pd.Series(1, index=expected_index) - assert_series_equal(result, expected) - - def test_fill_method_and_how_upsample(self): - # GH2073 - s = Series(np.arange(9, dtype='int64'), - index=date_range('2010-01-01', periods=9, freq='Q')) - last = s.resample('M').ffill() - both = s.resample('M').ffill().resample('M').last().astype('int64') - assert_series_equal(last, both) - - def test_weekly_upsample(self): - targets = ['D', 'B'] - - for day in DAYS: - ts = _simple_pts('1/1/1990', '12/31/1995', freq='W-%s' % day) - - for targ, conv in product(targets, ['start', 'end']): - result = ts.resample(targ, convention=conv).ffill() - expected = result.to_timestamp(targ, how=conv) - expected = expected.asfreq(targ, 'ffill').to_period() - assert_series_equal(result, expected) - - def test_resample_to_timestamps(self): - ts = _simple_pts('1/1/1990', '12/31/1995', freq='M') - - result = ts.resample('A-DEC', kind='timestamp').mean() - expected = ts.to_timestamp(how='end').resample('A-DEC').mean() - assert_series_equal(result, expected) - - def test_resample_to_quarterly(self): - for month in MONTHS: - ts = _simple_pts('1990', '1992', freq='A-%s' % month) - quar_ts = ts.resample('Q-%s' % month).ffill() - - stamps = ts.to_timestamp('D', how='start') - qdates = period_range(ts.index[0].asfreq('D', 'start'), - ts.index[-1].asfreq('D', 'end'), - freq='Q-%s' % month) - - expected = stamps.reindex(qdates.to_timestamp('D', 's'), - method='ffill') - expected.index = qdates - - assert_series_equal(quar_ts, expected) - - # conforms, but different month - ts = _simple_pts('1990', '1992', freq='A-JUN') - - for how in ['start', 'end']: - result = ts.resample('Q-MAR', convention=how).ffill() - expected = ts.asfreq('Q-MAR', how=how) - expected = expected.reindex(result.index, method='ffill') - - # .to_timestamp('D') - # expected = expected.resample('Q-MAR').ffill() - - assert_series_equal(result, expected) - - def test_resample_fill_missing(self): - rng = PeriodIndex([2000, 2005, 2007, 2009], freq='A') - - s = Series(np.random.randn(4), index=rng) - - stamps = s.to_timestamp() - filled = s.resample('A').ffill() - expected = stamps.resample('A').ffill().to_period('A') - assert_series_equal(filled, expected) - - def test_cant_fill_missing_dups(self): - rng = PeriodIndex([2000, 2005, 2005, 2007, 2007], freq='A') - s = Series(np.random.randn(5), index=rng) - self.assertRaises(Exception, lambda: s.resample('A').ffill()) - - def test_resample_5minute(self): - rng = period_range('1/1/2000', '1/5/2000', freq='T') - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('5min').mean() - expected = ts.to_timestamp().resample('5min').mean() - assert_series_equal(result, expected) - - def test_upsample_daily_business_daily(self): - ts = _simple_pts('1/1/2000', '2/1/2000', freq='B') - - result = ts.resample('D').asfreq() - expected = ts.asfreq('D').reindex(period_range('1/3/2000', '2/1/2000')) - assert_series_equal(result, expected) - - ts = _simple_pts('1/1/2000', '2/1/2000') - result = ts.resample('H', convention='s').asfreq() - exp_rng = period_range('1/1/2000', '2/1/2000 23:00', freq='H') - expected = ts.asfreq('H', how='s').reindex(exp_rng) - assert_series_equal(result, expected) - - def test_resample_irregular_sparse(self): - dr = date_range(start='1/1/2012', freq='5min', periods=1000) - s = Series(np.array(100), index=dr) - # subset the data. - subset = s[:'2012-01-04 06:55'] - - result = subset.resample('10min').apply(len) - expected = s.resample('10min').apply(len).loc[result.index] - assert_series_equal(result, expected) - - def test_resample_weekly_all_na(self): - rng = date_range('1/1/2000', periods=10, freq='W-WED') - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts.resample('W-THU').asfreq() - - self.assertTrue(result.isnull().all()) - - result = ts.resample('W-THU').asfreq().ffill()[:-1] - expected = ts.asfreq('W-THU').ffill() - assert_series_equal(result, expected) - - def test_resample_tz_localized(self): - dr = date_range(start='2012-4-13', end='2012-5-1') - ts = Series(lrange(len(dr)), dr) - - ts_utc = ts.tz_localize('UTC') - ts_local = ts_utc.tz_convert('America/Los_Angeles') - - result = ts_local.resample('W').mean() - - ts_local_naive = ts_local.copy() - ts_local_naive.index = [x.replace(tzinfo=None) - for x in ts_local_naive.index.to_pydatetime()] - - exp = ts_local_naive.resample( - 'W').mean().tz_localize('America/Los_Angeles') - - assert_series_equal(result, exp) - - # it works - result = ts_local.resample('D').mean() - - # #2245 - idx = date_range('2001-09-20 15:59', '2001-09-20 16:00', freq='T', - tz='Australia/Sydney') - s = Series([1, 2], index=idx) - - result = s.resample('D', closed='right', label='right').mean() - ex_index = date_range('2001-09-21', periods=1, freq='D', - tz='Australia/Sydney') - expected = Series([1.5], index=ex_index) - - assert_series_equal(result, expected) - - # for good measure - result = s.resample('D', kind='period').mean() - ex_index = period_range('2001-09-20', periods=1, freq='D') - expected = Series([1.5], index=ex_index) - assert_series_equal(result, expected) - - # GH 6397 - # comparing an offset that doesn't propagate tz's - rng = date_range('1/1/2011', periods=20000, freq='H') - rng = rng.tz_localize('EST') - ts = DataFrame(index=rng) - ts['first'] = np.random.randn(len(rng)) - ts['second'] = np.cumsum(np.random.randn(len(rng))) - expected = DataFrame( - { - 'first': ts.resample('A').sum()['first'], - 'second': ts.resample('A').mean()['second']}, - columns=['first', 'second']) - result = ts.resample( - 'A').agg({'first': np.sum, - 'second': np.mean}).reindex(columns=['first', 'second']) - assert_frame_equal(result, expected) - - def test_closed_left_corner(self): - # #1465 - s = Series(np.random.randn(21), - index=date_range(start='1/1/2012 9:30', - freq='1min', periods=21)) - s[0] = np.nan - - result = s.resample('10min', closed='left', label='right').mean() - exp = s[1:].resample('10min', closed='left', label='right').mean() - assert_series_equal(result, exp) - - result = s.resample('10min', closed='left', label='left').mean() - exp = s[1:].resample('10min', closed='left', label='left').mean() - - ex_index = date_range(start='1/1/2012 9:30', freq='10min', periods=3) - - self.assert_index_equal(result.index, ex_index) - assert_series_equal(result, exp) - - def test_quarterly_resampling(self): - rng = period_range('2000Q1', periods=10, freq='Q-DEC') - ts = Series(np.arange(10), index=rng) - - result = ts.resample('A').mean() - exp = ts.to_timestamp().resample('A').mean().to_period() - assert_series_equal(result, exp) - - def test_resample_weekly_bug_1726(self): - # 8/6/12 is a Monday - ind = DatetimeIndex(start="8/6/2012", end="8/26/2012", freq="D") - n = len(ind) - data = [[x] * 5 for x in range(n)] - df = DataFrame(data, columns=['open', 'high', 'low', 'close', 'vol'], - index=ind) - - # it works! - df.resample('W-MON', closed='left', label='left').first() - - def test_resample_bms_2752(self): - # GH2753 - foo = pd.Series(index=pd.bdate_range('20000101', '20000201')) - res1 = foo.resample("BMS").mean() - res2 = foo.resample("BMS").mean().resample("B").mean() - self.assertEqual(res1.index[0], Timestamp('20000103')) - self.assertEqual(res1.index[0], res2.index[0]) - - # def test_monthly_convention_span(self): - # rng = period_range('2000-01', periods=3, freq='M') - # ts = Series(np.arange(3), index=rng) - - # # hacky way to get same thing - # exp_index = period_range('2000-01-01', '2000-03-31', freq='D') - # expected = ts.asfreq('D', how='end').reindex(exp_index) - # expected = expected.fillna(method='bfill') - - # result = ts.resample('D', convention='span').mean() - - # assert_series_equal(result, expected) - - def test_default_right_closed_label(self): - end_freq = ['D', 'Q', 'M', 'D'] - end_types = ['M', 'A', 'Q', 'W'] - - for from_freq, to_freq in zip(end_freq, end_types): - idx = DatetimeIndex(start='8/15/2012', periods=100, freq=from_freq) - df = DataFrame(np.random.randn(len(idx), 2), idx) - - resampled = df.resample(to_freq).mean() - assert_frame_equal(resampled, df.resample(to_freq, closed='right', - label='right').mean()) - - def test_default_left_closed_label(self): - others = ['MS', 'AS', 'QS', 'D', 'H'] - others_freq = ['D', 'Q', 'M', 'H', 'T'] - - for from_freq, to_freq in zip(others_freq, others): - idx = DatetimeIndex(start='8/15/2012', periods=100, freq=from_freq) - df = DataFrame(np.random.randn(len(idx), 2), idx) - - resampled = df.resample(to_freq).mean() - assert_frame_equal(resampled, df.resample(to_freq, closed='left', - label='left').mean()) - - def test_all_values_single_bin(self): - # 2070 - index = period_range(start="2012-01-01", end="2012-12-31", freq="M") - s = Series(np.random.randn(len(index)), index=index) - - result = s.resample("A").mean() - tm.assert_almost_equal(result[0], s.mean()) - - def test_evenly_divisible_with_no_extra_bins(self): - # 4076 - # when the frequency is evenly divisible, sometimes extra bins - - df = DataFrame(np.random.randn(9, 3), - index=date_range('2000-1-1', periods=9)) - result = df.resample('5D').mean() - expected = pd.concat( - [df.iloc[0:5].mean(), df.iloc[5:].mean()], axis=1).T - expected.index = [Timestamp('2000-1-1'), Timestamp('2000-1-6')] - assert_frame_equal(result, expected) - - index = date_range(start='2001-5-4', periods=28) - df = DataFrame( - [{'REST_KEY': 1, 'DLY_TRN_QT': 80, 'DLY_SLS_AMT': 90, - 'COOP_DLY_TRN_QT': 30, 'COOP_DLY_SLS_AMT': 20}] * 28 + - [{'REST_KEY': 2, 'DLY_TRN_QT': 70, 'DLY_SLS_AMT': 10, - 'COOP_DLY_TRN_QT': 50, 'COOP_DLY_SLS_AMT': 20}] * 28, - index=index.append(index)).sort_index() - - index = date_range('2001-5-4', periods=4, freq='7D') - expected = DataFrame( - [{'REST_KEY': 14, 'DLY_TRN_QT': 14, 'DLY_SLS_AMT': 14, - 'COOP_DLY_TRN_QT': 14, 'COOP_DLY_SLS_AMT': 14}] * 4, - index=index) - result = df.resample('7D').count() - assert_frame_equal(result, expected) - - expected = DataFrame( - [{'REST_KEY': 21, 'DLY_TRN_QT': 1050, 'DLY_SLS_AMT': 700, - 'COOP_DLY_TRN_QT': 560, 'COOP_DLY_SLS_AMT': 280}] * 4, - index=index) - result = df.resample('7D').sum() - assert_frame_equal(result, expected) - - -class TestTimedeltaIndex(Base, tm.TestCase): - _index_factory = lambda x: timedelta_range - - def create_series(self): - i = timedelta_range('1 day', - '10 day', freq='D') - - return Series(np.arange(len(i)), index=i, name='tdi') - - def test_asfreq_bug(self): - import datetime as dt - df = DataFrame(data=[1, 3], - index=[dt.timedelta(), dt.timedelta(minutes=3)]) - result = df.resample('1T').asfreq() - expected = DataFrame(data=[1, np.nan, np.nan, 3], - index=timedelta_range('0 day', - periods=4, - freq='1T')) - assert_frame_equal(result, expected) - - -class TestResamplerGrouper(tm.TestCase): - - def setUp(self): - self.frame = DataFrame({'A': [1] * 20 + [2] * 12 + [3] * 8, - 'B': np.arange(40)}, - index=date_range('1/1/2000', - freq='s', - periods=40)) - - def test_back_compat_v180(self): - - df = self.frame - for how in ['sum', 'mean', 'prod', 'min', 'max', 'var', 'std']: - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = df.groupby('A').resample('4s', how=how) - expected = getattr(df.groupby('A').resample('4s'), how)() - assert_frame_equal(result, expected) - - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - result = df.groupby('A').resample('4s', how='mean', - fill_method='ffill') - expected = df.groupby('A').resample('4s').mean().ffill() - assert_frame_equal(result, expected) - - def test_deferred_with_groupby(self): - - # GH 12486 - # support deferred resample ops with groupby - data = [['2010-01-01', 'A', 2], ['2010-01-02', 'A', 3], - ['2010-01-05', 'A', 8], ['2010-01-10', 'A', 7], - ['2010-01-13', 'A', 3], ['2010-01-01', 'B', 5], - ['2010-01-03', 'B', 2], ['2010-01-04', 'B', 1], - ['2010-01-11', 'B', 7], ['2010-01-14', 'B', 3]] - - df = DataFrame(data, columns=['date', 'id', 'score']) - df.date = pd.to_datetime(df.date) - f = lambda x: x.set_index('date').resample('D').asfreq() - expected = df.groupby('id').apply(f) - result = df.set_index('date').groupby('id').resample('D').asfreq() - assert_frame_equal(result, expected) - - df = DataFrame({'date': pd.date_range(start='2016-01-01', - periods=4, - freq='W'), - 'group': [1, 1, 2, 2], - 'val': [5, 6, 7, 8]}).set_index('date') - - f = lambda x: x.resample('1D').ffill() - expected = df.groupby('group').apply(f) - result = df.groupby('group').resample('1D').ffill() - assert_frame_equal(result, expected) - - def test_getitem(self): - g = self.frame.groupby('A') - - expected = g.B.apply(lambda x: x.resample('2s').mean()) - - result = g.resample('2s').B.mean() - assert_series_equal(result, expected) - - result = g.B.resample('2s').mean() - assert_series_equal(result, expected) - - result = g.resample('2s').mean().B - assert_series_equal(result, expected) - - def test_getitem_multiple(self): - - # GH 13174 - # multiple calls after selection causing an issue with aliasing - data = [{'id': 1, 'buyer': 'A'}, {'id': 2, 'buyer': 'B'}] - df = pd.DataFrame(data, index=pd.date_range('2016-01-01', periods=2)) - r = df.groupby('id').resample('1D') - result = r['buyer'].count() - expected = pd.Series([1, 1], - index=pd.MultiIndex.from_tuples( - [(1, pd.Timestamp('2016-01-01')), - (2, pd.Timestamp('2016-01-02'))], - names=['id', None]), - name='buyer') - assert_series_equal(result, expected) - - result = r['buyer'].count() - assert_series_equal(result, expected) - - def test_methods(self): - g = self.frame.groupby('A') - r = g.resample('2s') - - for f in ['first', 'last', 'median', 'sem', 'sum', 'mean', - 'min', 'max']: - result = getattr(r, f)() - expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) - assert_frame_equal(result, expected) - - for f in ['size']: - result = getattr(r, f)() - expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) - assert_series_equal(result, expected) - - for f in ['count']: - result = getattr(r, f)() - expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) - assert_frame_equal(result, expected) - - # series only - for f in ['nunique']: - result = getattr(r.B, f)() - expected = g.B.apply(lambda x: getattr(x.resample('2s'), f)()) - assert_series_equal(result, expected) - - for f in ['backfill', 'ffill', 'asfreq']: - result = getattr(r, f)() - expected = g.apply(lambda x: getattr(x.resample('2s'), f)()) - assert_frame_equal(result, expected) - - result = r.ohlc() - expected = g.apply(lambda x: x.resample('2s').ohlc()) - assert_frame_equal(result, expected) - - for f in ['std', 'var']: - result = getattr(r, f)(ddof=1) - expected = g.apply(lambda x: getattr(x.resample('2s'), f)(ddof=1)) - assert_frame_equal(result, expected) - - def test_apply(self): - - g = self.frame.groupby('A') - r = g.resample('2s') - - # reduction - expected = g.resample('2s').sum() - - def f(x): - return x.resample('2s').sum() - - result = r.apply(f) - assert_frame_equal(result, expected) - - def f(x): - return x.resample('2s').apply(lambda y: y.sum()) - - result = g.apply(f) - assert_frame_equal(result, expected) - - def test_resample_groupby_with_label(self): - # GH 13235 - index = date_range('2000-01-01', freq='2D', periods=5) - df = DataFrame(index=index, - data={'col0': [0, 0, 1, 1, 2], 'col1': [1, 1, 1, 1, 1]} - ) - result = df.groupby('col0').resample('1W', label='left').sum() - - mi = [np.array([0, 0, 1, 2]), - pd.to_datetime(np.array(['1999-12-26', '2000-01-02', - '2000-01-02', '2000-01-02']) - ) - ] - mindex = pd.MultiIndex.from_arrays(mi, names=['col0', None]) - expected = DataFrame(data={'col0': [0, 0, 2, 2], 'col1': [1, 1, 2, 1]}, - index=mindex - ) - - assert_frame_equal(result, expected) - - def test_consistency_with_window(self): - - # consistent return values with window - df = self.frame - expected = pd.Int64Index([1, 2, 3], name='A') - result = df.groupby('A').resample('2s').mean() - self.assertEqual(result.index.nlevels, 2) - tm.assert_index_equal(result.index.levels[0], expected) - - result = df.groupby('A').rolling(20).mean() - self.assertEqual(result.index.nlevels, 2) - tm.assert_index_equal(result.index.levels[0], expected) - - def test_median_duplicate_columns(self): - # GH 14233 - - df = pd.DataFrame(np.random.randn(20, 3), - columns=list('aaa'), - index=pd.date_range('2012-01-01', - periods=20, freq='s')) - df2 = df.copy() - df2.columns = ['a', 'b', 'c'] - expected = df2.resample('5s').median() - result = df.resample('5s').median() - expected.columns = result.columns - assert_frame_equal(result, expected) - - -class TestTimeGrouper(tm.TestCase): - - def setUp(self): - self.ts = Series(np.random.randn(1000), - index=date_range('1/1/2000', periods=1000)) - - def test_apply(self): - grouper = TimeGrouper('A', label='right', closed='right') - - grouped = self.ts.groupby(grouper) - - f = lambda x: x.sort_values()[-3:] - - applied = grouped.apply(f) - expected = self.ts.groupby(lambda x: x.year).apply(f) - - applied.index = applied.index.droplevel(0) - expected.index = expected.index.droplevel(0) - assert_series_equal(applied, expected) - - def test_count(self): - self.ts[::3] = np.nan - - expected = self.ts.groupby(lambda x: x.year).count() - - grouper = TimeGrouper('A', label='right', closed='right') - result = self.ts.groupby(grouper).count() - expected.index = result.index - assert_series_equal(result, expected) - - result = self.ts.resample('A').count() - expected.index = result.index - assert_series_equal(result, expected) - - def test_numpy_reduction(self): - result = self.ts.resample('A', closed='right').prod() - - expected = self.ts.groupby(lambda x: x.year).agg(np.prod) - expected.index = result.index - - assert_series_equal(result, expected) - - def test_apply_iteration(self): - # #2300 - N = 1000 - ind = pd.date_range(start="2000-01-01", freq="D", periods=N) - df = DataFrame({'open': 1, 'close': 2}, index=ind) - tg = TimeGrouper('M') - - _, grouper, _ = tg._get_grouper(df) - - # Errors - grouped = df.groupby(grouper, group_keys=False) - f = lambda df: df['close'] / df['open'] - - # it works! - result = grouped.apply(f) - self.assert_index_equal(result.index, df.index) - - def test_panel_aggregation(self): - ind = pd.date_range('1/1/2000', periods=100) - data = np.random.randn(2, len(ind), 4) - wp = pd.Panel(data, items=['Item1', 'Item2'], major_axis=ind, - minor_axis=['A', 'B', 'C', 'D']) - - tg = TimeGrouper('M', axis=1) - _, grouper, _ = tg._get_grouper(wp) - bingrouped = wp.groupby(grouper) - binagg = bingrouped.mean() - - def f(x): - assert (isinstance(x, Panel)) - return x.mean(1) - - result = bingrouped.agg(f) - tm.assert_panel_equal(result, binagg) - - def test_fails_on_no_datetime_index(self): - index_names = ('Int64Index', 'Index', 'Float64Index', 'MultiIndex') - index_funcs = (tm.makeIntIndex, - tm.makeUnicodeIndex, tm.makeFloatIndex, - lambda m: tm.makeCustomIndex(m, 2)) - n = 2 - for name, func in zip(index_names, index_funcs): - index = func(n) - df = DataFrame({'a': np.random.randn(n)}, index=index) - with tm.assertRaisesRegexp(TypeError, - "Only valid with DatetimeIndex, " - "TimedeltaIndex or PeriodIndex, " - "but got an instance of %r" % name): - df.groupby(TimeGrouper('D')) - - # PeriodIndex gives a specific error message - df = DataFrame({'a': np.random.randn(n)}, index=tm.makePeriodIndex(n)) - with tm.assertRaisesRegexp(TypeError, - "axis must be a DatetimeIndex, but " - "got an instance of 'PeriodIndex'"): - df.groupby(TimeGrouper('D')) - - def test_aaa_group_order(self): - # GH 12840 - # check TimeGrouper perform stable sorts - n = 20 - data = np.random.randn(n, 4) - df = DataFrame(data, columns=['A', 'B', 'C', 'D']) - df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), - datetime(2013, 1, 3), datetime(2013, 1, 4), - datetime(2013, 1, 5)] * 4 - grouped = df.groupby(TimeGrouper(key='key', freq='D')) - - tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 1)), - df[::5]) - tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 2)), - df[1::5]) - tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 3)), - df[2::5]) - tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 4)), - df[3::5]) - tm.assert_frame_equal(grouped.get_group(datetime(2013, 1, 5)), - df[4::5]) - - def test_aggregate_normal(self): - # check TimeGrouper's aggregation is identical as normal groupby - - n = 20 - data = np.random.randn(n, 4) - normal_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) - normal_df['key'] = [1, 2, 3, 4, 5] * 4 - - dt_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) - dt_df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), - datetime(2013, 1, 3), datetime(2013, 1, 4), - datetime(2013, 1, 5)] * 4 - - normal_grouped = normal_df.groupby('key') - dt_grouped = dt_df.groupby(TimeGrouper(key='key', freq='D')) - - for func in ['min', 'max', 'prod', 'var', 'std', 'mean']: - expected = getattr(normal_grouped, func)() - dt_result = getattr(dt_grouped, func)() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - assert_frame_equal(expected, dt_result) - - for func in ['count', 'sum']: - expected = getattr(normal_grouped, func)() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - dt_result = getattr(dt_grouped, func)() - assert_frame_equal(expected, dt_result) - - # GH 7453 - for func in ['size']: - expected = getattr(normal_grouped, func)() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - dt_result = getattr(dt_grouped, func)() - assert_series_equal(expected, dt_result) - - # GH 7453 - for func in ['first', 'last']: - expected = getattr(normal_grouped, func)() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - dt_result = getattr(dt_grouped, func)() - assert_frame_equal(expected, dt_result) - - # if TimeGrouper is used included, 'nth' doesn't work yet - - """ - for func in ['nth']: - expected = getattr(normal_grouped, func)(3) - expected.index = date_range(start='2013-01-01', - freq='D', periods=5, name='key') - dt_result = getattr(dt_grouped, func)(3) - assert_frame_equal(expected, dt_result) - """ - - def test_aggregate_with_nat(self): - # check TimeGrouper's aggregation is identical as normal groupby - - n = 20 - data = np.random.randn(n, 4).astype('int64') - normal_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) - normal_df['key'] = [1, 2, np.nan, 4, 5] * 4 - - dt_df = DataFrame(data, columns=['A', 'B', 'C', 'D']) - dt_df['key'] = [datetime(2013, 1, 1), datetime(2013, 1, 2), pd.NaT, - datetime(2013, 1, 4), datetime(2013, 1, 5)] * 4 - - normal_grouped = normal_df.groupby('key') - dt_grouped = dt_df.groupby(TimeGrouper(key='key', freq='D')) - - for func in ['min', 'max', 'sum', 'prod']: - normal_result = getattr(normal_grouped, func)() - dt_result = getattr(dt_grouped, func)() - pad = DataFrame([[np.nan, np.nan, np.nan, np.nan]], index=[3], - columns=['A', 'B', 'C', 'D']) - expected = normal_result.append(pad) - expected = expected.sort_index() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - assert_frame_equal(expected, dt_result) - - for func in ['count']: - normal_result = getattr(normal_grouped, func)() - pad = DataFrame([[0, 0, 0, 0]], index=[3], - columns=['A', 'B', 'C', 'D']) - expected = normal_result.append(pad) - expected = expected.sort_index() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - dt_result = getattr(dt_grouped, func)() - assert_frame_equal(expected, dt_result) - - for func in ['size']: - normal_result = getattr(normal_grouped, func)() - pad = Series([0], index=[3]) - expected = normal_result.append(pad) - expected = expected.sort_index() - expected.index = date_range(start='2013-01-01', freq='D', - periods=5, name='key') - dt_result = getattr(dt_grouped, func)() - assert_series_equal(expected, dt_result) - # GH 9925 - self.assertEqual(dt_result.index.name, 'key') - - # if NaT is included, 'var', 'std', 'mean', 'first','last' - # and 'nth' doesn't work yet diff --git a/pandas/tests/tseries/test_timezones.py b/pandas/tests/tseries/test_timezones.py deleted file mode 100644 index 1fc0e1b73df6b..0000000000000 --- a/pandas/tests/tseries/test_timezones.py +++ /dev/null @@ -1,1722 +0,0 @@ -# pylint: disable-msg=E1101,W0612 -import pytz -import numpy as np -from distutils.version import LooseVersion -from datetime import datetime, timedelta, tzinfo, date -from pytz import NonExistentTimeError - -import pandas.util.testing as tm -import pandas.tseries.tools as tools -import pandas.tseries.offsets as offsets -from pandas.compat import lrange, zip -from pandas.tseries.index import bdate_range, date_range -from pandas.types.dtypes import DatetimeTZDtype -from pandas._libs import tslib -from pandas import (Index, Series, DataFrame, isnull, Timestamp, NaT, - DatetimeIndex, to_datetime) -from pandas.util.testing import (assert_frame_equal, assert_series_equal, - set_timezone) - -try: - import pytz # noqa -except ImportError: - pass - -try: - import dateutil -except ImportError: - pass - - -class FixedOffset(tzinfo): - """Fixed offset in minutes east from UTC.""" - - def __init__(self, offset, name): - self.__offset = timedelta(minutes=offset) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return timedelta(0) - - -fixed_off = FixedOffset(-420, '-07:00') -fixed_off_no_name = FixedOffset(-330, None) - - -class TestTimeZoneSupportPytz(tm.TestCase): - - def setUp(self): - tm._skip_if_no_pytz() - - def tz(self, tz): - # Construct a timezone object from a string. Overridden in subclass to - # parameterize tests. - return pytz.timezone(tz) - - def tzstr(self, tz): - # Construct a timezone string from a string. Overridden in subclass to - # parameterize tests. - return tz - - def localize(self, tz, x): - return tz.localize(x) - - def cmptz(self, tz1, tz2): - # Compare two timezones. Overridden in subclass to parameterize - # tests. - return tz1.zone == tz2.zone - - def test_utc_to_local_no_modify(self): - rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) - - # Values are unmodified - self.assertTrue(np.array_equal(rng.asi8, rng_eastern.asi8)) - - self.assertTrue(self.cmptz(rng_eastern.tz, self.tz('US/Eastern'))) - - def test_utc_to_local_no_modify_explicit(self): - rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert(self.tz('US/Eastern')) - - # Values are unmodified - self.assert_numpy_array_equal(rng.asi8, rng_eastern.asi8) - - self.assertEqual(rng_eastern.tz, self.tz('US/Eastern')) - - def test_localize_utc_conversion(self): - # Localizing to time zone should: - # 1) check for DST ambiguities - # 2) convert to UTC - - rng = date_range('3/10/2012', '3/11/2012', freq='30T') - - converted = rng.tz_localize(self.tzstr('US/Eastern')) - expected_naive = rng + offsets.Hour(5) - self.assert_numpy_array_equal(converted.asi8, expected_naive.asi8) - - # DST ambiguity, this should fail - rng = date_range('3/11/2012', '3/12/2012', freq='30T') - # Is this really how it should fail?? - self.assertRaises(NonExistentTimeError, rng.tz_localize, - self.tzstr('US/Eastern')) - - def test_localize_utc_conversion_explicit(self): - # Localizing to time zone should: - # 1) check for DST ambiguities - # 2) convert to UTC - - rng = date_range('3/10/2012', '3/11/2012', freq='30T') - converted = rng.tz_localize(self.tz('US/Eastern')) - expected_naive = rng + offsets.Hour(5) - self.assertTrue(np.array_equal(converted.asi8, expected_naive.asi8)) - - # DST ambiguity, this should fail - rng = date_range('3/11/2012', '3/12/2012', freq='30T') - # Is this really how it should fail?? - self.assertRaises(NonExistentTimeError, rng.tz_localize, - self.tz('US/Eastern')) - - def test_timestamp_tz_localize(self): - stamp = Timestamp('3/11/2012 04:00') - - result = stamp.tz_localize(self.tzstr('US/Eastern')) - expected = Timestamp('3/11/2012 04:00', tz=self.tzstr('US/Eastern')) - self.assertEqual(result.hour, expected.hour) - self.assertEqual(result, expected) - - def test_timestamp_tz_localize_explicit(self): - stamp = Timestamp('3/11/2012 04:00') - - result = stamp.tz_localize(self.tz('US/Eastern')) - expected = Timestamp('3/11/2012 04:00', tz=self.tz('US/Eastern')) - self.assertEqual(result.hour, expected.hour) - self.assertEqual(result, expected) - - def test_timestamp_constructed_by_date_and_tz(self): - # Fix Issue 2993, Timestamp cannot be constructed by datetime.date - # and tz correctly - - result = Timestamp(date(2012, 3, 11), tz=self.tzstr('US/Eastern')) - - expected = Timestamp('3/11/2012', tz=self.tzstr('US/Eastern')) - self.assertEqual(result.hour, expected.hour) - self.assertEqual(result, expected) - - def test_timestamp_constructed_by_date_and_tz_explicit(self): - # Fix Issue 2993, Timestamp cannot be constructed by datetime.date - # and tz correctly - - result = Timestamp(date(2012, 3, 11), tz=self.tz('US/Eastern')) - - expected = Timestamp('3/11/2012', tz=self.tz('US/Eastern')) - self.assertEqual(result.hour, expected.hour) - self.assertEqual(result, expected) - - def test_timestamp_to_datetime_tzoffset(self): - # tzoffset - from dateutil.tz import tzoffset - tzinfo = tzoffset(None, 7200) - expected = Timestamp('3/11/2012 04:00', tz=tzinfo) - result = Timestamp(expected.to_pydatetime()) - self.assertEqual(expected, result) - - def test_timedelta_push_over_dst_boundary(self): - # #1389 - - # 4 hours before DST transition - stamp = Timestamp('3/10/2012 22:00', tz=self.tzstr('US/Eastern')) - - result = stamp + timedelta(hours=6) - - # spring forward, + "7" hours - expected = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern')) - - self.assertEqual(result, expected) - - def test_timedelta_push_over_dst_boundary_explicit(self): - # #1389 - - # 4 hours before DST transition - stamp = Timestamp('3/10/2012 22:00', tz=self.tz('US/Eastern')) - - result = stamp + timedelta(hours=6) - - # spring forward, + "7" hours - expected = Timestamp('3/11/2012 05:00', tz=self.tz('US/Eastern')) - - self.assertEqual(result, expected) - - def test_tz_localize_dti(self): - dti = DatetimeIndex(start='1/1/2005', end='1/1/2005 0:00:30.256', - freq='L') - dti2 = dti.tz_localize(self.tzstr('US/Eastern')) - - dti_utc = DatetimeIndex(start='1/1/2005 05:00', - end='1/1/2005 5:00:30.256', freq='L', tz='utc') - - self.assert_numpy_array_equal(dti2.values, dti_utc.values) - - dti3 = dti2.tz_convert(self.tzstr('US/Pacific')) - self.assert_numpy_array_equal(dti3.values, dti_utc.values) - - dti = DatetimeIndex(start='11/6/2011 1:59', end='11/6/2011 2:00', - freq='L') - self.assertRaises(pytz.AmbiguousTimeError, dti.tz_localize, - self.tzstr('US/Eastern')) - - dti = DatetimeIndex(start='3/13/2011 1:59', end='3/13/2011 2:00', - freq='L') - self.assertRaises(pytz.NonExistentTimeError, dti.tz_localize, - self.tzstr('US/Eastern')) - - def test_tz_localize_empty_series(self): - # #2248 - - ts = Series() - - ts2 = ts.tz_localize('utc') - self.assertTrue(ts2.index.tz == pytz.utc) - - ts2 = ts.tz_localize(self.tzstr('US/Eastern')) - self.assertTrue(self.cmptz(ts2.index.tz, self.tz('US/Eastern'))) - - def test_astimezone(self): - utc = Timestamp('3/11/2012 22:00', tz='UTC') - expected = utc.tz_convert(self.tzstr('US/Eastern')) - result = utc.astimezone(self.tzstr('US/Eastern')) - self.assertEqual(expected, result) - tm.assertIsInstance(result, Timestamp) - - def test_create_with_tz(self): - stamp = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern')) - self.assertEqual(stamp.hour, 5) - - rng = date_range('3/11/2012 04:00', periods=10, freq='H', - tz=self.tzstr('US/Eastern')) - - self.assertEqual(stamp, rng[1]) - - utc_stamp = Timestamp('3/11/2012 05:00', tz='utc') - self.assertIs(utc_stamp.tzinfo, pytz.utc) - self.assertEqual(utc_stamp.hour, 5) - - stamp = Timestamp('3/11/2012 05:00').tz_localize('utc') - self.assertEqual(utc_stamp.hour, 5) - - def test_create_with_fixed_tz(self): - off = FixedOffset(420, '+07:00') - start = datetime(2012, 3, 11, 5, 0, 0, tzinfo=off) - end = datetime(2012, 6, 11, 5, 0, 0, tzinfo=off) - rng = date_range(start=start, end=end) - self.assertEqual(off, rng.tz) - - rng2 = date_range(start, periods=len(rng), tz=off) - self.assert_index_equal(rng, rng2) - - rng3 = date_range('3/11/2012 05:00:00+07:00', - '6/11/2012 05:00:00+07:00') - self.assertTrue((rng.values == rng3.values).all()) - - def test_create_with_fixedoffset_noname(self): - off = fixed_off_no_name - start = datetime(2012, 3, 11, 5, 0, 0, tzinfo=off) - end = datetime(2012, 6, 11, 5, 0, 0, tzinfo=off) - rng = date_range(start=start, end=end) - self.assertEqual(off, rng.tz) - - idx = Index([start, end]) - self.assertEqual(off, idx.tz) - - def test_date_range_localize(self): - rng = date_range('3/11/2012 03:00', periods=15, freq='H', - tz='US/Eastern') - rng2 = DatetimeIndex(['3/11/2012 03:00', '3/11/2012 04:00'], - tz='US/Eastern') - rng3 = date_range('3/11/2012 03:00', periods=15, freq='H') - rng3 = rng3.tz_localize('US/Eastern') - - self.assert_index_equal(rng, rng3) - - # DST transition time - val = rng[0] - exp = Timestamp('3/11/2012 03:00', tz='US/Eastern') - - self.assertEqual(val.hour, 3) - self.assertEqual(exp.hour, 3) - self.assertEqual(val, exp) # same UTC value - self.assert_index_equal(rng[:2], rng2) - - # Right before the DST transition - rng = date_range('3/11/2012 00:00', periods=2, freq='H', - tz='US/Eastern') - rng2 = DatetimeIndex(['3/11/2012 00:00', '3/11/2012 01:00'], - tz='US/Eastern') - self.assert_index_equal(rng, rng2) - exp = Timestamp('3/11/2012 00:00', tz='US/Eastern') - self.assertEqual(exp.hour, 0) - self.assertEqual(rng[0], exp) - exp = Timestamp('3/11/2012 01:00', tz='US/Eastern') - self.assertEqual(exp.hour, 1) - self.assertEqual(rng[1], exp) - - rng = date_range('3/11/2012 00:00', periods=10, freq='H', - tz='US/Eastern') - self.assertEqual(rng[2].hour, 3) - - def test_utc_box_timestamp_and_localize(self): - rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) - - tz = self.tz('US/Eastern') - expected = rng[-1].astimezone(tz) - - stamp = rng_eastern[-1] - self.assertEqual(stamp, expected) - self.assertEqual(stamp.tzinfo, expected.tzinfo) - - # right tzinfo - rng = date_range('3/13/2012', '3/14/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) - # test not valid for dateutil timezones. - # self.assertIn('EDT', repr(rng_eastern[0].tzinfo)) - self.assertTrue('EDT' in repr(rng_eastern[0].tzinfo) or 'tzfile' in - repr(rng_eastern[0].tzinfo)) - - def test_timestamp_tz_convert(self): - strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] - idx = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) - - conv = idx[0].tz_convert(self.tzstr('US/Pacific')) - expected = idx.tz_convert(self.tzstr('US/Pacific'))[0] - - self.assertEqual(conv, expected) - - def test_pass_dates_localize_to_utc(self): - strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] - - idx = DatetimeIndex(strdates) - conv = idx.tz_localize(self.tzstr('US/Eastern')) - - fromdates = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) - - self.assertEqual(conv.tz, fromdates.tz) - self.assert_numpy_array_equal(conv.values, fromdates.values) - - def test_field_access_localize(self): - strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] - rng = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) - self.assertTrue((rng.hour == 0).all()) - - # a more unusual time zone, #1946 - dr = date_range('2011-10-02 00:00', freq='h', periods=10, - tz=self.tzstr('America/Atikokan')) - - expected = Index(np.arange(10, dtype=np.int64)) - self.assert_index_equal(dr.hour, expected) - - def test_with_tz(self): - tz = self.tz('US/Central') - - # just want it to work - start = datetime(2011, 3, 12, tzinfo=pytz.utc) - dr = bdate_range(start, periods=50, freq=offsets.Hour()) - self.assertIs(dr.tz, pytz.utc) - - # DateRange with naive datetimes - dr = bdate_range('1/1/2005', '1/1/2009', tz=pytz.utc) - dr = bdate_range('1/1/2005', '1/1/2009', tz=tz) - - # normalized - central = dr.tz_convert(tz) - self.assertIs(central.tz, tz) - comp = self.localize(tz, central[0].to_pydatetime().replace( - tzinfo=None)).tzinfo - self.assertIs(central[0].tz, comp) - - # compare vs a localized tz - comp = self.localize(tz, - dr[0].to_pydatetime().replace(tzinfo=None)).tzinfo - self.assertIs(central[0].tz, comp) - - # datetimes with tzinfo set - dr = bdate_range(datetime(2005, 1, 1, tzinfo=pytz.utc), - '1/1/2009', tz=pytz.utc) - - self.assertRaises(Exception, bdate_range, - datetime(2005, 1, 1, tzinfo=pytz.utc), '1/1/2009', - tz=tz) - - def test_tz_localize(self): - dr = bdate_range('1/1/2009', '1/1/2010') - dr_utc = bdate_range('1/1/2009', '1/1/2010', tz=pytz.utc) - localized = dr.tz_localize(pytz.utc) - self.assert_index_equal(dr_utc, localized) - - def test_with_tz_ambiguous_times(self): - tz = self.tz('US/Eastern') - - # March 13, 2011, spring forward, skip from 2 AM to 3 AM - dr = date_range(datetime(2011, 3, 13, 1, 30), periods=3, - freq=offsets.Hour()) - self.assertRaises(pytz.NonExistentTimeError, dr.tz_localize, tz) - - # after dst transition, it works - dr = date_range(datetime(2011, 3, 13, 3, 30), periods=3, - freq=offsets.Hour(), tz=tz) - - # November 6, 2011, fall back, repeat 2 AM hour - dr = date_range(datetime(2011, 11, 6, 1, 30), periods=3, - freq=offsets.Hour()) - self.assertRaises(pytz.AmbiguousTimeError, dr.tz_localize, tz) - - # UTC is OK - dr = date_range(datetime(2011, 3, 13), periods=48, - freq=offsets.Minute(30), tz=pytz.utc) - - def test_ambiguous_infer(self): - # November 6, 2011, fall back, repeat 2 AM hour - # With no repeated hours, we cannot infer the transition - tz = self.tz('US/Eastern') - dr = date_range(datetime(2011, 11, 6, 0), periods=5, - freq=offsets.Hour()) - self.assertRaises(pytz.AmbiguousTimeError, dr.tz_localize, tz) - - # With repeated hours, we can infer the transition - dr = date_range(datetime(2011, 11, 6, 0), periods=5, - freq=offsets.Hour(), tz=tz) - times = ['11/06/2011 00:00', '11/06/2011 01:00', '11/06/2011 01:00', - '11/06/2011 02:00', '11/06/2011 03:00'] - di = DatetimeIndex(times) - localized = di.tz_localize(tz, ambiguous='infer') - self.assert_index_equal(dr, localized) - with tm.assert_produces_warning(FutureWarning): - localized_old = di.tz_localize(tz, infer_dst=True) - self.assert_index_equal(dr, localized_old) - self.assert_index_equal(dr, DatetimeIndex(times, tz=tz, - ambiguous='infer')) - - # When there is no dst transition, nothing special happens - dr = date_range(datetime(2011, 6, 1, 0), periods=10, - freq=offsets.Hour()) - localized = dr.tz_localize(tz) - localized_infer = dr.tz_localize(tz, ambiguous='infer') - self.assert_index_equal(localized, localized_infer) - with tm.assert_produces_warning(FutureWarning): - localized_infer_old = dr.tz_localize(tz, infer_dst=True) - self.assert_index_equal(localized, localized_infer_old) - - def test_ambiguous_flags(self): - # November 6, 2011, fall back, repeat 2 AM hour - tz = self.tz('US/Eastern') - - # Pass in flags to determine right dst transition - dr = date_range(datetime(2011, 11, 6, 0), periods=5, - freq=offsets.Hour(), tz=tz) - times = ['11/06/2011 00:00', '11/06/2011 01:00', '11/06/2011 01:00', - '11/06/2011 02:00', '11/06/2011 03:00'] - - # Test tz_localize - di = DatetimeIndex(times) - is_dst = [1, 1, 0, 0, 0] - localized = di.tz_localize(tz, ambiguous=is_dst) - self.assert_index_equal(dr, localized) - self.assert_index_equal(dr, DatetimeIndex(times, tz=tz, - ambiguous=is_dst)) - - localized = di.tz_localize(tz, ambiguous=np.array(is_dst)) - self.assert_index_equal(dr, localized) - - localized = di.tz_localize(tz, - ambiguous=np.array(is_dst).astype('bool')) - self.assert_index_equal(dr, localized) - - # Test constructor - localized = DatetimeIndex(times, tz=tz, ambiguous=is_dst) - self.assert_index_equal(dr, localized) - - # Test duplicate times where infer_dst fails - times += times - di = DatetimeIndex(times) - - # When the sizes are incompatible, make sure error is raised - self.assertRaises(Exception, di.tz_localize, tz, ambiguous=is_dst) - - # When sizes are compatible and there are repeats ('infer' won't work) - is_dst = np.hstack((is_dst, is_dst)) - localized = di.tz_localize(tz, ambiguous=is_dst) - dr = dr.append(dr) - self.assert_index_equal(dr, localized) - - # When there is no dst transition, nothing special happens - dr = date_range(datetime(2011, 6, 1, 0), periods=10, - freq=offsets.Hour()) - is_dst = np.array([1] * 10) - localized = dr.tz_localize(tz) - localized_is_dst = dr.tz_localize(tz, ambiguous=is_dst) - self.assert_index_equal(localized, localized_is_dst) - - # construction with an ambiguous end-point - # GH 11626 - tz = self.tzstr("Europe/London") - - def f(): - date_range("2013-10-26 23:00", "2013-10-27 01:00", - tz="Europe/London", freq="H") - self.assertRaises(pytz.AmbiguousTimeError, f) - - times = date_range("2013-10-26 23:00", "2013-10-27 01:00", freq="H", - tz=tz, ambiguous='infer') - self.assertEqual(times[0], Timestamp('2013-10-26 23:00', tz=tz, - freq="H")) - if dateutil.__version__ != LooseVersion('2.6.0'): - # GH 14621 - self.assertEqual(times[-1], Timestamp('2013-10-27 01:00', tz=tz, - freq="H")) - - def test_ambiguous_nat(self): - tz = self.tz('US/Eastern') - times = ['11/06/2011 00:00', '11/06/2011 01:00', '11/06/2011 01:00', - '11/06/2011 02:00', '11/06/2011 03:00'] - di = DatetimeIndex(times) - localized = di.tz_localize(tz, ambiguous='NaT') - - times = ['11/06/2011 00:00', np.NaN, np.NaN, '11/06/2011 02:00', - '11/06/2011 03:00'] - di_test = DatetimeIndex(times, tz='US/Eastern') - - # left dtype is datetime64[ns, US/Eastern] - # right is datetime64[ns, tzfile('/usr/share/zoneinfo/US/Eastern')] - self.assert_numpy_array_equal(di_test.values, localized.values) - - def test_ambiguous_bool(self): - # make sure that we are correctly accepting bool values as ambiguous - - # gh-14402 - t = Timestamp('2015-11-01 01:00:03') - expected0 = Timestamp('2015-11-01 01:00:03-0500', tz='US/Central') - expected1 = Timestamp('2015-11-01 01:00:03-0600', tz='US/Central') - - def f(): - t.tz_localize('US/Central') - self.assertRaises(pytz.AmbiguousTimeError, f) - - result = t.tz_localize('US/Central', ambiguous=True) - self.assertEqual(result, expected0) - - result = t.tz_localize('US/Central', ambiguous=False) - self.assertEqual(result, expected1) - - s = Series([t]) - expected0 = Series([expected0]) - expected1 = Series([expected1]) - - def f(): - s.dt.tz_localize('US/Central') - self.assertRaises(pytz.AmbiguousTimeError, f) - - result = s.dt.tz_localize('US/Central', ambiguous=True) - assert_series_equal(result, expected0) - - result = s.dt.tz_localize('US/Central', ambiguous=[True]) - assert_series_equal(result, expected0) - - result = s.dt.tz_localize('US/Central', ambiguous=False) - assert_series_equal(result, expected1) - - result = s.dt.tz_localize('US/Central', ambiguous=[False]) - assert_series_equal(result, expected1) - - def test_nonexistent_raise_coerce(self): - # See issue 13057 - from pytz.exceptions import NonExistentTimeError - times = ['2015-03-08 01:00', '2015-03-08 02:00', '2015-03-08 03:00'] - index = DatetimeIndex(times) - tz = 'US/Eastern' - self.assertRaises(NonExistentTimeError, - index.tz_localize, tz=tz) - self.assertRaises(NonExistentTimeError, - index.tz_localize, tz=tz, errors='raise') - result = index.tz_localize(tz=tz, errors='coerce') - test_times = ['2015-03-08 01:00-05:00', 'NaT', - '2015-03-08 03:00-04:00'] - expected = DatetimeIndex(test_times)\ - .tz_localize('UTC').tz_convert('US/Eastern') - tm.assert_index_equal(result, expected) - - # test utility methods - def test_infer_tz(self): - eastern = self.tz('US/Eastern') - utc = pytz.utc - - _start = datetime(2001, 1, 1) - _end = datetime(2009, 1, 1) - - start = self.localize(eastern, _start) - end = self.localize(eastern, _end) - assert (tools._infer_tzinfo(start, end) is self.localize( - eastern, _start).tzinfo) - assert (tools._infer_tzinfo(start, None) is self.localize( - eastern, _start).tzinfo) - assert (tools._infer_tzinfo(None, end) is self.localize(eastern, - _end).tzinfo) - - start = utc.localize(_start) - end = utc.localize(_end) - assert (tools._infer_tzinfo(start, end) is utc) - - end = self.localize(eastern, _end) - self.assertRaises(Exception, tools._infer_tzinfo, start, end) - self.assertRaises(Exception, tools._infer_tzinfo, end, start) - - def test_tz_string(self): - result = date_range('1/1/2000', periods=10, - tz=self.tzstr('US/Eastern')) - expected = date_range('1/1/2000', periods=10, tz=self.tz('US/Eastern')) - - self.assert_index_equal(result, expected) - - def test_take_dont_lose_meta(self): - tm._skip_if_no_pytz() - rng = date_range('1/1/2000', periods=20, tz=self.tzstr('US/Eastern')) - - result = rng.take(lrange(5)) - self.assertEqual(result.tz, rng.tz) - self.assertEqual(result.freq, rng.freq) - - def test_index_with_timezone_repr(self): - rng = date_range('4/13/2010', '5/6/2010') - - rng_eastern = rng.tz_localize(self.tzstr('US/Eastern')) - - rng_repr = repr(rng_eastern) - self.assertIn('2010-04-13 00:00:00', rng_repr) - - def test_index_astype_asobject_tzinfos(self): - # #1345 - - # dates around a dst transition - rng = date_range('2/13/2010', '5/6/2010', tz=self.tzstr('US/Eastern')) - - objs = rng.asobject - for i, x in enumerate(objs): - exval = rng[i] - self.assertEqual(x, exval) - self.assertEqual(x.tzinfo, exval.tzinfo) - - objs = rng.astype(object) - for i, x in enumerate(objs): - exval = rng[i] - self.assertEqual(x, exval) - self.assertEqual(x.tzinfo, exval.tzinfo) - - def test_localized_at_time_between_time(self): - from datetime import time - - rng = date_range('4/16/2012', '5/1/2012', freq='H') - ts = Series(np.random.randn(len(rng)), index=rng) - - ts_local = ts.tz_localize(self.tzstr('US/Eastern')) - - result = ts_local.at_time(time(10, 0)) - expected = ts.at_time(time(10, 0)).tz_localize(self.tzstr( - 'US/Eastern')) - assert_series_equal(result, expected) - self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern'))) - - t1, t2 = time(10, 0), time(11, 0) - result = ts_local.between_time(t1, t2) - expected = ts.between_time(t1, - t2).tz_localize(self.tzstr('US/Eastern')) - assert_series_equal(result, expected) - self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern'))) - - def test_string_index_alias_tz_aware(self): - rng = date_range('1/1/2000', periods=10, tz=self.tzstr('US/Eastern')) - ts = Series(np.random.randn(len(rng)), index=rng) - - result = ts['1/3/2000'] - self.assertAlmostEqual(result, ts[2]) - - def test_fixed_offset(self): - dates = [datetime(2000, 1, 1, tzinfo=fixed_off), - datetime(2000, 1, 2, tzinfo=fixed_off), - datetime(2000, 1, 3, tzinfo=fixed_off)] - result = to_datetime(dates) - self.assertEqual(result.tz, fixed_off) - - def test_fixedtz_topydatetime(self): - dates = np.array([datetime(2000, 1, 1, tzinfo=fixed_off), - datetime(2000, 1, 2, tzinfo=fixed_off), - datetime(2000, 1, 3, tzinfo=fixed_off)]) - result = to_datetime(dates).to_pydatetime() - self.assert_numpy_array_equal(dates, result) - result = to_datetime(dates)._mpl_repr() - self.assert_numpy_array_equal(dates, result) - - def test_convert_tz_aware_datetime_datetime(self): - # #1581 - - tz = self.tz('US/Eastern') - - dates = [datetime(2000, 1, 1), datetime(2000, 1, 2), - datetime(2000, 1, 3)] - - dates_aware = [self.localize(tz, x) for x in dates] - result = to_datetime(dates_aware) - self.assertTrue(self.cmptz(result.tz, self.tz('US/Eastern'))) - - converted = to_datetime(dates_aware, utc=True) - ex_vals = np.array([Timestamp(x).value for x in dates_aware]) - self.assert_numpy_array_equal(converted.asi8, ex_vals) - self.assertIs(converted.tz, pytz.utc) - - def test_to_datetime_utc(self): - from dateutil.parser import parse - arr = np.array([parse('2012-06-13T01:39:00Z')], dtype=object) - - result = to_datetime(arr, utc=True) - self.assertIs(result.tz, pytz.utc) - - def test_to_datetime_tzlocal(self): - from dateutil.parser import parse - from dateutil.tz import tzlocal - dt = parse('2012-06-13T01:39:00Z') - dt = dt.replace(tzinfo=tzlocal()) - - arr = np.array([dt], dtype=object) - - result = to_datetime(arr, utc=True) - self.assertIs(result.tz, pytz.utc) - - rng = date_range('2012-11-03 03:00', '2012-11-05 03:00', tz=tzlocal()) - arr = rng.to_pydatetime() - result = to_datetime(arr, utc=True) - self.assertIs(result.tz, pytz.utc) - - def test_frame_no_datetime64_dtype(self): - - # after 7822 - # these retain the timezones on dict construction - - dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI') - dr_tz = dr.tz_localize(self.tzstr('US/Eastern')) - e = DataFrame({'A': 'foo', 'B': dr_tz}, index=dr) - tz_expected = DatetimeTZDtype('ns', dr_tz.tzinfo) - self.assertEqual(e['B'].dtype, tz_expected) - - # GH 2810 (with timezones) - datetimes_naive = [ts.to_pydatetime() for ts in dr] - datetimes_with_tz = [ts.to_pydatetime() for ts in dr_tz] - df = DataFrame({'dr': dr, - 'dr_tz': dr_tz, - 'datetimes_naive': datetimes_naive, - 'datetimes_with_tz': datetimes_with_tz}) - result = df.get_dtype_counts().sort_index() - expected = Series({'datetime64[ns]': 2, - str(tz_expected): 2}).sort_index() - assert_series_equal(result, expected) - - def test_hongkong_tz_convert(self): - # #1673 - dr = date_range('2012-01-01', '2012-01-10', freq='D', tz='Hongkong') - - # it works! - dr.hour - - def test_tz_convert_unsorted(self): - dr = date_range('2012-03-09', freq='H', periods=100, tz='utc') - dr = dr.tz_convert(self.tzstr('US/Eastern')) - - result = dr[::-1].hour - exp = dr.hour[::-1] - tm.assert_almost_equal(result, exp) - - def test_shift_localized(self): - dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI') - dr_tz = dr.tz_localize(self.tzstr('US/Eastern')) - - result = dr_tz.shift(1, '10T') - self.assertEqual(result.tz, dr_tz.tz) - - def test_tz_aware_asfreq(self): - dr = date_range('2011-12-01', '2012-07-20', freq='D', - tz=self.tzstr('US/Eastern')) - - s = Series(np.random.randn(len(dr)), index=dr) - - # it works! - s.asfreq('T') - - def test_static_tzinfo(self): - # it works! - index = DatetimeIndex([datetime(2012, 1, 1)], tz=self.tzstr('EST')) - index.hour - index[0] - - def test_tzaware_datetime_to_index(self): - d = [datetime(2012, 8, 19, tzinfo=self.tz('US/Eastern'))] - - index = DatetimeIndex(d) - self.assertTrue(self.cmptz(index.tz, self.tz('US/Eastern'))) - - def test_date_range_span_dst_transition(self): - # #1778 - - # Standard -> Daylight Savings Time - dr = date_range('03/06/2012 00:00', periods=200, freq='W-FRI', - tz='US/Eastern') - - self.assertTrue((dr.hour == 0).all()) - - dr = date_range('2012-11-02', periods=10, tz=self.tzstr('US/Eastern')) - self.assertTrue((dr.hour == 0).all()) - - def test_convert_datetime_list(self): - dr = date_range('2012-06-02', periods=10, - tz=self.tzstr('US/Eastern'), name='foo') - dr2 = DatetimeIndex(list(dr), name='foo') - self.assert_index_equal(dr, dr2) - self.assertEqual(dr.tz, dr2.tz) - self.assertEqual(dr2.name, 'foo') - - def test_frame_from_records_utc(self): - rec = {'datum': 1.5, - 'begin_time': datetime(2006, 4, 27, tzinfo=pytz.utc)} - - # it works - DataFrame.from_records([rec], index='begin_time') - - def test_frame_reset_index(self): - dr = date_range('2012-06-02', periods=10, tz=self.tzstr('US/Eastern')) - df = DataFrame(np.random.randn(len(dr)), dr) - roundtripped = df.reset_index().set_index('index') - xp = df.index.tz - rs = roundtripped.index.tz - self.assertEqual(xp, rs) - - def test_dateutil_tzoffset_support(self): - from dateutil.tz import tzoffset - values = [188.5, 328.25] - tzinfo = tzoffset(None, 7200) - index = [datetime(2012, 5, 11, 11, tzinfo=tzinfo), - datetime(2012, 5, 11, 12, tzinfo=tzinfo)] - series = Series(data=values, index=index) - - self.assertEqual(series.index.tz, tzinfo) - - # it works! #2443 - repr(series.index[0]) - - def test_getitem_pydatetime_tz(self): - index = date_range(start='2012-12-24 16:00', end='2012-12-24 18:00', - freq='H', tz=self.tzstr('Europe/Berlin')) - ts = Series(index=index, data=index.hour) - time_pandas = Timestamp('2012-12-24 17:00', - tz=self.tzstr('Europe/Berlin')) - time_datetime = self.localize( - self.tz('Europe/Berlin'), datetime(2012, 12, 24, 17, 0)) - self.assertEqual(ts[time_pandas], ts[time_datetime]) - - def test_index_drop_dont_lose_tz(self): - # #2621 - ind = date_range("2012-12-01", periods=10, tz="utc") - ind = ind.drop(ind[-1]) - - self.assertTrue(ind.tz is not None) - - def test_datetimeindex_tz(self): - """ Test different DatetimeIndex constructions with timezone - Follow-up of #4229 - """ - - arr = ['11/10/2005 08:00:00', '11/10/2005 09:00:00'] - - idx1 = to_datetime(arr).tz_localize(self.tzstr('US/Eastern')) - idx2 = DatetimeIndex(start="2005-11-10 08:00:00", freq='H', periods=2, - tz=self.tzstr('US/Eastern')) - idx3 = DatetimeIndex(arr, tz=self.tzstr('US/Eastern')) - idx4 = DatetimeIndex(np.array(arr), tz=self.tzstr('US/Eastern')) - - for other in [idx2, idx3, idx4]: - self.assert_index_equal(idx1, other) - - def test_datetimeindex_tz_nat(self): - idx = to_datetime([Timestamp("2013-1-1", tz=self.tzstr('US/Eastern')), - NaT]) - - self.assertTrue(isnull(idx[1])) - self.assertTrue(idx[0].tzinfo is not None) - - -class TestTimeZoneSupportDateutil(TestTimeZoneSupportPytz): - - def setUp(self): - tm._skip_if_no_dateutil() - - def tz(self, tz): - """ - Construct a dateutil timezone. - Use tslib.maybe_get_tz so that we get the filename on the tz right - on windows. See #7337. - """ - return tslib.maybe_get_tz('dateutil/' + tz) - - def tzstr(self, tz): - """ Construct a timezone string from a string. Overridden in subclass - to parameterize tests. """ - return 'dateutil/' + tz - - def cmptz(self, tz1, tz2): - """ Compare two timezones. Overridden in subclass to parameterize - tests. """ - return tz1 == tz2 - - def localize(self, tz, x): - return x.replace(tzinfo=tz) - - def test_utc_with_system_utc(self): - # Skipped on win32 due to dateutil bug - tm._skip_if_windows() - - from pandas._libs.tslib import maybe_get_tz - - # from system utc to real utc - ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC')) - # check that the time hasn't changed. - self.assertEqual(ts, ts.tz_convert(dateutil.tz.tzutc())) - - # from system utc to real utc - ts = Timestamp('2001-01-05 11:56', tz=maybe_get_tz('dateutil/UTC')) - # check that the time hasn't changed. - self.assertEqual(ts, ts.tz_convert(dateutil.tz.tzutc())) - - def test_tz_convert_hour_overflow_dst(self): - # Regression test for: - # https://github.com/pandas-dev/pandas/issues/13306 - - # sorted case US/Eastern -> UTC - ts = ['2008-05-12 09:50:00', - '2008-12-12 09:50:35', - '2009-05-12 09:50:32'] - tt = to_datetime(ts).tz_localize('US/Eastern') - ut = tt.tz_convert('UTC') - expected = Index([13, 14, 13]) - self.assert_index_equal(ut.hour, expected) - - # sorted case UTC -> US/Eastern - ts = ['2008-05-12 13:50:00', - '2008-12-12 14:50:35', - '2009-05-12 13:50:32'] - tt = to_datetime(ts).tz_localize('UTC') - ut = tt.tz_convert('US/Eastern') - expected = Index([9, 9, 9]) - self.assert_index_equal(ut.hour, expected) - - # unsorted case US/Eastern -> UTC - ts = ['2008-05-12 09:50:00', - '2008-12-12 09:50:35', - '2008-05-12 09:50:32'] - tt = to_datetime(ts).tz_localize('US/Eastern') - ut = tt.tz_convert('UTC') - expected = Index([13, 14, 13]) - self.assert_index_equal(ut.hour, expected) - - # unsorted case UTC -> US/Eastern - ts = ['2008-05-12 13:50:00', - '2008-12-12 14:50:35', - '2008-05-12 13:50:32'] - tt = to_datetime(ts).tz_localize('UTC') - ut = tt.tz_convert('US/Eastern') - expected = Index([9, 9, 9]) - self.assert_index_equal(ut.hour, expected) - - def test_tz_convert_hour_overflow_dst_timestamps(self): - # Regression test for: - # https://github.com/pandas-dev/pandas/issues/13306 - - tz = self.tzstr('US/Eastern') - - # sorted case US/Eastern -> UTC - ts = [Timestamp('2008-05-12 09:50:00', tz=tz), - Timestamp('2008-12-12 09:50:35', tz=tz), - Timestamp('2009-05-12 09:50:32', tz=tz)] - tt = to_datetime(ts) - ut = tt.tz_convert('UTC') - expected = Index([13, 14, 13]) - self.assert_index_equal(ut.hour, expected) - - # sorted case UTC -> US/Eastern - ts = [Timestamp('2008-05-12 13:50:00', tz='UTC'), - Timestamp('2008-12-12 14:50:35', tz='UTC'), - Timestamp('2009-05-12 13:50:32', tz='UTC')] - tt = to_datetime(ts) - ut = tt.tz_convert('US/Eastern') - expected = Index([9, 9, 9]) - self.assert_index_equal(ut.hour, expected) - - # unsorted case US/Eastern -> UTC - ts = [Timestamp('2008-05-12 09:50:00', tz=tz), - Timestamp('2008-12-12 09:50:35', tz=tz), - Timestamp('2008-05-12 09:50:32', tz=tz)] - tt = to_datetime(ts) - ut = tt.tz_convert('UTC') - expected = Index([13, 14, 13]) - self.assert_index_equal(ut.hour, expected) - - # unsorted case UTC -> US/Eastern - ts = [Timestamp('2008-05-12 13:50:00', tz='UTC'), - Timestamp('2008-12-12 14:50:35', tz='UTC'), - Timestamp('2008-05-12 13:50:32', tz='UTC')] - tt = to_datetime(ts) - ut = tt.tz_convert('US/Eastern') - expected = Index([9, 9, 9]) - self.assert_index_equal(ut.hour, expected) - - def test_tslib_tz_convert_trans_pos_plus_1__bug(self): - # Regression test for tslib.tz_convert(vals, tz1, tz2). - # See https://github.com/pandas-dev/pandas/issues/4496 for details. - for freq, n in [('H', 1), ('T', 60), ('S', 3600)]: - idx = date_range(datetime(2011, 3, 26, 23), - datetime(2011, 3, 27, 1), freq=freq) - idx = idx.tz_localize('UTC') - idx = idx.tz_convert('Europe/Moscow') - - expected = np.repeat(np.array([3, 4, 5]), np.array([n, n, 1])) - self.assert_index_equal(idx.hour, Index(expected)) - - def test_tslib_tz_convert_dst(self): - for freq, n in [('H', 1), ('T', 60), ('S', 3600)]: - # Start DST - idx = date_range('2014-03-08 23:00', '2014-03-09 09:00', freq=freq, - tz='UTC') - idx = idx.tz_convert('US/Eastern') - expected = np.repeat(np.array([18, 19, 20, 21, 22, 23, - 0, 1, 3, 4, 5]), - np.array([n, n, n, n, n, n, n, n, n, n, 1])) - self.assert_index_equal(idx.hour, Index(expected)) - - idx = date_range('2014-03-08 18:00', '2014-03-09 05:00', freq=freq, - tz='US/Eastern') - idx = idx.tz_convert('UTC') - expected = np.repeat(np.array([23, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), - np.array([n, n, n, n, n, n, n, n, n, n, 1])) - self.assert_index_equal(idx.hour, Index(expected)) - - # End DST - idx = date_range('2014-11-01 23:00', '2014-11-02 09:00', freq=freq, - tz='UTC') - idx = idx.tz_convert('US/Eastern') - expected = np.repeat(np.array([19, 20, 21, 22, 23, - 0, 1, 1, 2, 3, 4]), - np.array([n, n, n, n, n, n, n, n, n, n, 1])) - self.assert_index_equal(idx.hour, Index(expected)) - - idx = date_range('2014-11-01 18:00', '2014-11-02 05:00', freq=freq, - tz='US/Eastern') - idx = idx.tz_convert('UTC') - expected = np.repeat(np.array([22, 23, 0, 1, 2, 3, 4, 5, 6, - 7, 8, 9, 10]), - np.array([n, n, n, n, n, n, n, n, n, - n, n, n, 1])) - self.assert_index_equal(idx.hour, Index(expected)) - - # daily - # Start DST - idx = date_range('2014-03-08 00:00', '2014-03-09 00:00', freq='D', - tz='UTC') - idx = idx.tz_convert('US/Eastern') - self.assert_index_equal(idx.hour, Index([19, 19])) - - idx = date_range('2014-03-08 00:00', '2014-03-09 00:00', freq='D', - tz='US/Eastern') - idx = idx.tz_convert('UTC') - self.assert_index_equal(idx.hour, Index([5, 5])) - - # End DST - idx = date_range('2014-11-01 00:00', '2014-11-02 00:00', freq='D', - tz='UTC') - idx = idx.tz_convert('US/Eastern') - self.assert_index_equal(idx.hour, Index([20, 20])) - - idx = date_range('2014-11-01 00:00', '2014-11-02 000:00', freq='D', - tz='US/Eastern') - idx = idx.tz_convert('UTC') - self.assert_index_equal(idx.hour, Index([4, 4])) - - def test_tzlocal(self): - # GH 13583 - ts = Timestamp('2011-01-01', tz=dateutil.tz.tzlocal()) - self.assertEqual(ts.tz, dateutil.tz.tzlocal()) - self.assertTrue("tz='tzlocal()')" in repr(ts)) - - tz = tslib.maybe_get_tz('tzlocal()') - self.assertEqual(tz, dateutil.tz.tzlocal()) - - # get offset using normal datetime for test - offset = dateutil.tz.tzlocal().utcoffset(datetime(2011, 1, 1)) - offset = offset.total_seconds() * 1000000000 - self.assertEqual(ts.value + offset, Timestamp('2011-01-01').value) - - def test_tz_localize_tzlocal(self): - # GH 13583 - offset = dateutil.tz.tzlocal().utcoffset(datetime(2011, 1, 1)) - offset = int(offset.total_seconds() * 1000000000) - - dti = date_range(start='2001-01-01', end='2001-03-01') - dti2 = dti.tz_localize(dateutil.tz.tzlocal()) - tm.assert_numpy_array_equal(dti2.asi8 + offset, dti.asi8) - - dti = date_range(start='2001-01-01', end='2001-03-01', - tz=dateutil.tz.tzlocal()) - dti2 = dti.tz_localize(None) - tm.assert_numpy_array_equal(dti2.asi8 - offset, dti.asi8) - - def test_tz_convert_tzlocal(self): - # GH 13583 - # tz_convert doesn't affect to internal - dti = date_range(start='2001-01-01', end='2001-03-01', tz='UTC') - dti2 = dti.tz_convert(dateutil.tz.tzlocal()) - tm.assert_numpy_array_equal(dti2.asi8, dti.asi8) - - dti = date_range(start='2001-01-01', end='2001-03-01', - tz=dateutil.tz.tzlocal()) - dti2 = dti.tz_convert(None) - tm.assert_numpy_array_equal(dti2.asi8, dti.asi8) - - -class TestTimeZoneCacheKey(tm.TestCase): - - def test_cache_keys_are_distinct_for_pytz_vs_dateutil(self): - tzs = pytz.common_timezones - for tz_name in tzs: - if tz_name == 'UTC': - # skip utc as it's a special case in dateutil - continue - tz_p = tslib.maybe_get_tz(tz_name) - tz_d = tslib.maybe_get_tz('dateutil/' + tz_name) - if tz_d is None: - # skip timezones that dateutil doesn't know about. - continue - self.assertNotEqual(tslib._p_tz_cache_key( - tz_p), tslib._p_tz_cache_key(tz_d)) - - -class TestTimeZones(tm.TestCase): - timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern', 'dateutil/US/Pacific'] - - def setUp(self): - tm._skip_if_no_pytz() - - def test_replace(self): - # GH 14621 - # GH 7825 - # replacing datetime components with and w/o presence of a timezone - dt = Timestamp('2016-01-01 09:00:00') - result = dt.replace(hour=0) - expected = Timestamp('2016-01-01 00:00:00') - self.assertEqual(result, expected) - - for tz in self.timezones: - dt = Timestamp('2016-01-01 09:00:00', tz=tz) - result = dt.replace(hour=0) - expected = Timestamp('2016-01-01 00:00:00', tz=tz) - self.assertEqual(result, expected) - - # we preserve nanoseconds - dt = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) - result = dt.replace(hour=0) - expected = Timestamp('2016-01-01 00:00:00.000000123', tz=tz) - self.assertEqual(result, expected) - - # test all - dt = Timestamp('2016-01-01 09:00:00.000000123', tz=tz) - result = dt.replace(year=2015, month=2, day=2, hour=0, minute=5, - second=5, microsecond=5, nanosecond=5) - expected = Timestamp('2015-02-02 00:05:05.000005005', tz=tz) - self.assertEqual(result, expected) - - # error - def f(): - dt.replace(foo=5) - self.assertRaises(TypeError, f) - - def f(): - dt.replace(hour=0.1) - self.assertRaises(ValueError, f) - - # assert conversion to naive is the same as replacing tzinfo with None - dt = Timestamp('2013-11-03 01:59:59.999999-0400', tz='US/Eastern') - self.assertEqual(dt.tz_localize(None), dt.replace(tzinfo=None)) - - def test_ambiguous_compat(self): - # validate that pytz and dateutil are compat for dst - # when the transition happens - tm._skip_if_no_dateutil() - tm._skip_if_no_pytz() - - pytz_zone = 'Europe/London' - dateutil_zone = 'dateutil/Europe/London' - result_pytz = (Timestamp('2013-10-27 01:00:00') - .tz_localize(pytz_zone, ambiguous=0)) - result_dateutil = (Timestamp('2013-10-27 01:00:00') - .tz_localize(dateutil_zone, ambiguous=0)) - self.assertEqual(result_pytz.value, result_dateutil.value) - self.assertEqual(result_pytz.value, 1382835600000000000) - - # dateutil 2.6 buggy w.r.t. ambiguous=0 - if dateutil.__version__ != LooseVersion('2.6.0'): - # GH 14621 - # https://github.com/dateutil/dateutil/issues/321 - self.assertEqual(result_pytz.to_pydatetime().tzname(), - result_dateutil.to_pydatetime().tzname()) - self.assertEqual(str(result_pytz), str(result_dateutil)) - - # 1 hour difference - result_pytz = (Timestamp('2013-10-27 01:00:00') - .tz_localize(pytz_zone, ambiguous=1)) - result_dateutil = (Timestamp('2013-10-27 01:00:00') - .tz_localize(dateutil_zone, ambiguous=1)) - self.assertEqual(result_pytz.value, result_dateutil.value) - self.assertEqual(result_pytz.value, 1382832000000000000) - - # dateutil < 2.6 is buggy w.r.t. ambiguous timezones - if dateutil.__version__ > LooseVersion('2.5.3'): - # GH 14621 - self.assertEqual(str(result_pytz), str(result_dateutil)) - self.assertEqual(result_pytz.to_pydatetime().tzname(), - result_dateutil.to_pydatetime().tzname()) - - def test_index_equals_with_tz(self): - left = date_range('1/1/2011', periods=100, freq='H', tz='utc') - right = date_range('1/1/2011', periods=100, freq='H', tz='US/Eastern') - - self.assertFalse(left.equals(right)) - - def test_tz_localize_naive(self): - rng = date_range('1/1/2011', periods=100, freq='H') - - conv = rng.tz_localize('US/Pacific') - exp = date_range('1/1/2011', periods=100, freq='H', tz='US/Pacific') - - self.assert_index_equal(conv, exp) - - def test_tz_localize_roundtrip(self): - for tz in self.timezones: - idx1 = date_range(start='2014-01-01', end='2014-12-31', freq='M') - idx2 = date_range(start='2014-01-01', end='2014-12-31', freq='D') - idx3 = date_range(start='2014-01-01', end='2014-03-01', freq='H') - idx4 = date_range(start='2014-08-01', end='2014-10-31', freq='T') - for idx in [idx1, idx2, idx3, idx4]: - localized = idx.tz_localize(tz) - expected = date_range(start=idx[0], end=idx[-1], freq=idx.freq, - tz=tz) - tm.assert_index_equal(localized, expected) - - with tm.assertRaises(TypeError): - localized.tz_localize(tz) - - reset = localized.tz_localize(None) - tm.assert_index_equal(reset, idx) - self.assertTrue(reset.tzinfo is None) - - def test_series_frame_tz_localize(self): - - rng = date_range('1/1/2011', periods=100, freq='H') - ts = Series(1, index=rng) - - result = ts.tz_localize('utc') - self.assertEqual(result.index.tz.zone, 'UTC') - - df = DataFrame({'a': 1}, index=rng) - result = df.tz_localize('utc') - expected = DataFrame({'a': 1}, rng.tz_localize('UTC')) - self.assertEqual(result.index.tz.zone, 'UTC') - assert_frame_equal(result, expected) - - df = df.T - result = df.tz_localize('utc', axis=1) - self.assertEqual(result.columns.tz.zone, 'UTC') - assert_frame_equal(result, expected.T) - - # Can't localize if already tz-aware - rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') - ts = Series(1, index=rng) - tm.assertRaisesRegexp(TypeError, 'Already tz-aware', ts.tz_localize, - 'US/Eastern') - - def test_series_frame_tz_convert(self): - rng = date_range('1/1/2011', periods=200, freq='D', tz='US/Eastern') - ts = Series(1, index=rng) - - result = ts.tz_convert('Europe/Berlin') - self.assertEqual(result.index.tz.zone, 'Europe/Berlin') - - df = DataFrame({'a': 1}, index=rng) - result = df.tz_convert('Europe/Berlin') - expected = DataFrame({'a': 1}, rng.tz_convert('Europe/Berlin')) - self.assertEqual(result.index.tz.zone, 'Europe/Berlin') - assert_frame_equal(result, expected) - - df = df.T - result = df.tz_convert('Europe/Berlin', axis=1) - self.assertEqual(result.columns.tz.zone, 'Europe/Berlin') - assert_frame_equal(result, expected.T) - - # can't convert tz-naive - rng = date_range('1/1/2011', periods=200, freq='D') - ts = Series(1, index=rng) - tm.assertRaisesRegexp(TypeError, "Cannot convert tz-naive", - ts.tz_convert, 'US/Eastern') - - def test_tz_convert_roundtrip(self): - for tz in self.timezones: - idx1 = date_range(start='2014-01-01', end='2014-12-31', freq='M', - tz='UTC') - exp1 = date_range(start='2014-01-01', end='2014-12-31', freq='M') - - idx2 = date_range(start='2014-01-01', end='2014-12-31', freq='D', - tz='UTC') - exp2 = date_range(start='2014-01-01', end='2014-12-31', freq='D') - - idx3 = date_range(start='2014-01-01', end='2014-03-01', freq='H', - tz='UTC') - exp3 = date_range(start='2014-01-01', end='2014-03-01', freq='H') - - idx4 = date_range(start='2014-08-01', end='2014-10-31', freq='T', - tz='UTC') - exp4 = date_range(start='2014-08-01', end='2014-10-31', freq='T') - - for idx, expected in [(idx1, exp1), (idx2, exp2), (idx3, exp3), - (idx4, exp4)]: - converted = idx.tz_convert(tz) - reset = converted.tz_convert(None) - tm.assert_index_equal(reset, expected) - self.assertTrue(reset.tzinfo is None) - tm.assert_index_equal(reset, converted.tz_convert( - 'UTC').tz_localize(None)) - - def test_join_utc_convert(self): - rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') - - left = rng.tz_convert('US/Eastern') - right = rng.tz_convert('Europe/Berlin') - - for how in ['inner', 'outer', 'left', 'right']: - result = left.join(left[:-5], how=how) - tm.assertIsInstance(result, DatetimeIndex) - self.assertEqual(result.tz, left.tz) - - result = left.join(right[:-5], how=how) - tm.assertIsInstance(result, DatetimeIndex) - self.assertEqual(result.tz.zone, 'UTC') - - def test_join_aware(self): - rng = date_range('1/1/2011', periods=10, freq='H') - ts = Series(np.random.randn(len(rng)), index=rng) - - ts_utc = ts.tz_localize('utc') - - self.assertRaises(Exception, ts.__add__, ts_utc) - self.assertRaises(Exception, ts_utc.__add__, ts) - - test1 = DataFrame(np.zeros((6, 3)), - index=date_range("2012-11-15 00:00:00", periods=6, - freq="100L", tz="US/Central")) - test2 = DataFrame(np.zeros((3, 3)), - index=date_range("2012-11-15 00:00:00", periods=3, - freq="250L", tz="US/Central"), - columns=lrange(3, 6)) - - result = test1.join(test2, how='outer') - ex_index = test1.index.union(test2.index) - - self.assert_index_equal(result.index, ex_index) - self.assertTrue(result.index.tz.zone == 'US/Central') - - # non-overlapping - rng = date_range("2012-11-15 00:00:00", periods=6, freq="H", - tz="US/Central") - - rng2 = date_range("2012-11-15 12:00:00", periods=6, freq="H", - tz="US/Eastern") - - result = rng.union(rng2) - self.assertTrue(result.tz.zone == 'UTC') - - def test_align_aware(self): - idx1 = date_range('2001', periods=5, freq='H', tz='US/Eastern') - idx2 = date_range('2001', periods=5, freq='2H', tz='US/Eastern') - df1 = DataFrame(np.random.randn(len(idx1), 3), idx1) - df2 = DataFrame(np.random.randn(len(idx2), 3), idx2) - new1, new2 = df1.align(df2) - self.assertEqual(df1.index.tz, new1.index.tz) - self.assertEqual(df2.index.tz, new2.index.tz) - - # # different timezones convert to UTC - - # frame - df1_central = df1.tz_convert('US/Central') - new1, new2 = df1.align(df1_central) - self.assertEqual(new1.index.tz, pytz.UTC) - self.assertEqual(new2.index.tz, pytz.UTC) - - # series - new1, new2 = df1[0].align(df1_central[0]) - self.assertEqual(new1.index.tz, pytz.UTC) - self.assertEqual(new2.index.tz, pytz.UTC) - - # combination - new1, new2 = df1.align(df1_central[0], axis=0) - self.assertEqual(new1.index.tz, pytz.UTC) - self.assertEqual(new2.index.tz, pytz.UTC) - - df1[0].align(df1_central, axis=0) - self.assertEqual(new1.index.tz, pytz.UTC) - self.assertEqual(new2.index.tz, pytz.UTC) - - def test_append_aware(self): - rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', - tz='US/Eastern') - rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', - tz='US/Eastern') - ts1 = Series([1], index=rng1) - ts2 = Series([2], index=rng2) - ts_result = ts1.append(ts2) - - exp_index = DatetimeIndex(['2011-01-01 01:00', '2011-01-01 02:00'], - tz='US/Eastern') - exp = Series([1, 2], index=exp_index) - assert_series_equal(ts_result, exp) - self.assertEqual(ts_result.index.tz, rng1.tz) - - rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', tz='UTC') - rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', tz='UTC') - ts1 = Series([1], index=rng1) - ts2 = Series([2], index=rng2) - ts_result = ts1.append(ts2) - - exp_index = DatetimeIndex(['2011-01-01 01:00', '2011-01-01 02:00'], - tz='UTC') - exp = Series([1, 2], index=exp_index) - assert_series_equal(ts_result, exp) - utc = rng1.tz - self.assertEqual(utc, ts_result.index.tz) - - # GH 7795 - # different tz coerces to object dtype, not UTC - rng1 = date_range('1/1/2011 01:00', periods=1, freq='H', - tz='US/Eastern') - rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', - tz='US/Central') - ts1 = Series([1], index=rng1) - ts2 = Series([2], index=rng2) - ts_result = ts1.append(ts2) - exp_index = Index([Timestamp('1/1/2011 01:00', tz='US/Eastern'), - Timestamp('1/1/2011 02:00', tz='US/Central')]) - exp = Series([1, 2], index=exp_index) - assert_series_equal(ts_result, exp) - - def test_append_dst(self): - rng1 = date_range('1/1/2016 01:00', periods=3, freq='H', - tz='US/Eastern') - rng2 = date_range('8/1/2016 01:00', periods=3, freq='H', - tz='US/Eastern') - ts1 = Series([1, 2, 3], index=rng1) - ts2 = Series([10, 11, 12], index=rng2) - ts_result = ts1.append(ts2) - - exp_index = DatetimeIndex(['2016-01-01 01:00', '2016-01-01 02:00', - '2016-01-01 03:00', '2016-08-01 01:00', - '2016-08-01 02:00', '2016-08-01 03:00'], - tz='US/Eastern') - exp = Series([1, 2, 3, 10, 11, 12], index=exp_index) - assert_series_equal(ts_result, exp) - self.assertEqual(ts_result.index.tz, rng1.tz) - - def test_append_aware_naive(self): - rng1 = date_range('1/1/2011 01:00', periods=1, freq='H') - rng2 = date_range('1/1/2011 02:00', periods=1, freq='H', - tz='US/Eastern') - ts1 = Series(np.random.randn(len(rng1)), index=rng1) - ts2 = Series(np.random.randn(len(rng2)), index=rng2) - ts_result = ts1.append(ts2) - - self.assertTrue(ts_result.index.equals(ts1.index.asobject.append( - ts2.index.asobject))) - - # mixed - rng1 = date_range('1/1/2011 01:00', periods=1, freq='H') - rng2 = lrange(100) - ts1 = Series(np.random.randn(len(rng1)), index=rng1) - ts2 = Series(np.random.randn(len(rng2)), index=rng2) - ts_result = ts1.append(ts2) - self.assertTrue(ts_result.index.equals(ts1.index.asobject.append( - ts2.index))) - - def test_equal_join_ensure_utc(self): - rng = date_range('1/1/2011', periods=10, freq='H', tz='US/Eastern') - ts = Series(np.random.randn(len(rng)), index=rng) - - ts_moscow = ts.tz_convert('Europe/Moscow') - - result = ts + ts_moscow - self.assertIs(result.index.tz, pytz.utc) - - result = ts_moscow + ts - self.assertIs(result.index.tz, pytz.utc) - - df = DataFrame({'a': ts}) - df_moscow = df.tz_convert('Europe/Moscow') - result = df + df_moscow - self.assertIs(result.index.tz, pytz.utc) - - result = df_moscow + df - self.assertIs(result.index.tz, pytz.utc) - - def test_arith_utc_convert(self): - rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') - - perm = np.random.permutation(100)[:90] - ts1 = Series(np.random.randn(90), - index=rng.take(perm).tz_convert('US/Eastern')) - - perm = np.random.permutation(100)[:90] - ts2 = Series(np.random.randn(90), - index=rng.take(perm).tz_convert('Europe/Berlin')) - - result = ts1 + ts2 - - uts1 = ts1.tz_convert('utc') - uts2 = ts2.tz_convert('utc') - expected = uts1 + uts2 - - self.assertEqual(result.index.tz, pytz.UTC) - assert_series_equal(result, expected) - - def test_intersection(self): - rng = date_range('1/1/2011', periods=100, freq='H', tz='utc') - - left = rng[10:90][::-1] - right = rng[20:80][::-1] - - self.assertEqual(left.tz, rng.tz) - result = left.intersection(right) - self.assertEqual(result.tz, left.tz) - - def test_timestamp_equality_different_timezones(self): - utc_range = date_range('1/1/2000', periods=20, tz='UTC') - eastern_range = utc_range.tz_convert('US/Eastern') - berlin_range = utc_range.tz_convert('Europe/Berlin') - - for a, b, c in zip(utc_range, eastern_range, berlin_range): - self.assertEqual(a, b) - self.assertEqual(b, c) - self.assertEqual(a, c) - - self.assertTrue((utc_range == eastern_range).all()) - self.assertTrue((utc_range == berlin_range).all()) - self.assertTrue((berlin_range == eastern_range).all()) - - def test_datetimeindex_tz(self): - rng = date_range('03/12/2012 00:00', periods=10, freq='W-FRI', - tz='US/Eastern') - rng2 = DatetimeIndex(data=rng, tz='US/Eastern') - self.assert_index_equal(rng, rng2) - - def test_normalize_tz(self): - rng = date_range('1/1/2000 9:30', periods=10, freq='D', - tz='US/Eastern') - - result = rng.normalize() - expected = date_range('1/1/2000', periods=10, freq='D', - tz='US/Eastern') - self.assert_index_equal(result, expected) - - self.assertTrue(result.is_normalized) - self.assertFalse(rng.is_normalized) - - rng = date_range('1/1/2000 9:30', periods=10, freq='D', tz='UTC') - - result = rng.normalize() - expected = date_range('1/1/2000', periods=10, freq='D', tz='UTC') - self.assert_index_equal(result, expected) - - self.assertTrue(result.is_normalized) - self.assertFalse(rng.is_normalized) - - from dateutil.tz import tzlocal - rng = date_range('1/1/2000 9:30', periods=10, freq='D', tz=tzlocal()) - result = rng.normalize() - expected = date_range('1/1/2000', periods=10, freq='D', tz=tzlocal()) - self.assert_index_equal(result, expected) - - self.assertTrue(result.is_normalized) - self.assertFalse(rng.is_normalized) - - def test_normalize_tz_local(self): - # GH 13459 - from dateutil.tz import tzlocal - - timezones = ['US/Pacific', 'US/Eastern', 'UTC', 'Asia/Kolkata', - 'Asia/Shanghai', 'Australia/Canberra'] - - for timezone in timezones: - with set_timezone(timezone): - rng = date_range('1/1/2000 9:30', periods=10, freq='D', - tz=tzlocal()) - - result = rng.normalize() - expected = date_range('1/1/2000', periods=10, freq='D', - tz=tzlocal()) - self.assert_index_equal(result, expected) - - self.assertTrue(result.is_normalized) - self.assertFalse(rng.is_normalized) - - def test_tzaware_offset(self): - dates = date_range('2012-11-01', periods=3, tz='US/Pacific') - offset = dates + offsets.Hour(5) - self.assertEqual(dates[0] + offsets.Hour(5), offset[0]) - - # GH 6818 - for tz in ['UTC', 'US/Pacific', 'Asia/Tokyo']: - dates = date_range('2010-11-01 00:00', periods=3, tz=tz, freq='H') - expected = DatetimeIndex(['2010-11-01 05:00', '2010-11-01 06:00', - '2010-11-01 07:00'], freq='H', tz=tz) - - offset = dates + offsets.Hour(5) - self.assert_index_equal(offset, expected) - offset = dates + np.timedelta64(5, 'h') - self.assert_index_equal(offset, expected) - offset = dates + timedelta(hours=5) - self.assert_index_equal(offset, expected) - - def test_nat(self): - # GH 5546 - dates = [NaT] - idx = DatetimeIndex(dates) - idx = idx.tz_localize('US/Pacific') - self.assert_index_equal(idx, DatetimeIndex(dates, tz='US/Pacific')) - idx = idx.tz_convert('US/Eastern') - self.assert_index_equal(idx, DatetimeIndex(dates, tz='US/Eastern')) - idx = idx.tz_convert('UTC') - self.assert_index_equal(idx, DatetimeIndex(dates, tz='UTC')) - - dates = ['2010-12-01 00:00', '2010-12-02 00:00', NaT] - idx = DatetimeIndex(dates) - idx = idx.tz_localize('US/Pacific') - self.assert_index_equal(idx, DatetimeIndex(dates, tz='US/Pacific')) - idx = idx.tz_convert('US/Eastern') - expected = ['2010-12-01 03:00', '2010-12-02 03:00', NaT] - self.assert_index_equal(idx, DatetimeIndex(expected, tz='US/Eastern')) - - idx = idx + offsets.Hour(5) - expected = ['2010-12-01 08:00', '2010-12-02 08:00', NaT] - self.assert_index_equal(idx, DatetimeIndex(expected, tz='US/Eastern')) - idx = idx.tz_convert('US/Pacific') - expected = ['2010-12-01 05:00', '2010-12-02 05:00', NaT] - self.assert_index_equal(idx, DatetimeIndex(expected, tz='US/Pacific')) - - idx = idx + np.timedelta64(3, 'h') - expected = ['2010-12-01 08:00', '2010-12-02 08:00', NaT] - self.assert_index_equal(idx, DatetimeIndex(expected, tz='US/Pacific')) - - idx = idx.tz_convert('US/Eastern') - expected = ['2010-12-01 11:00', '2010-12-02 11:00', NaT] - self.assert_index_equal(idx, DatetimeIndex(expected, tz='US/Eastern')) - - -class TestTslib(tm.TestCase): - - def test_tslib_tz_convert(self): - def compare_utc_to_local(tz_didx, utc_didx): - f = lambda x: tslib.tz_convert_single(x, 'UTC', tz_didx.tz) - result = tslib.tz_convert(tz_didx.asi8, 'UTC', tz_didx.tz) - result_single = np.vectorize(f)(tz_didx.asi8) - self.assert_numpy_array_equal(result, result_single) - - def compare_local_to_utc(tz_didx, utc_didx): - f = lambda x: tslib.tz_convert_single(x, tz_didx.tz, 'UTC') - result = tslib.tz_convert(utc_didx.asi8, tz_didx.tz, 'UTC') - result_single = np.vectorize(f)(utc_didx.asi8) - self.assert_numpy_array_equal(result, result_single) - - for tz in ['UTC', 'Asia/Tokyo', 'US/Eastern', 'Europe/Moscow']: - # US: 2014-03-09 - 2014-11-11 - # MOSCOW: 2014-10-26 / 2014-12-31 - tz_didx = date_range('2014-03-01', '2015-01-10', freq='H', tz=tz) - utc_didx = date_range('2014-03-01', '2015-01-10', freq='H') - compare_utc_to_local(tz_didx, utc_didx) - # local tz to UTC can be differ in hourly (or higher) freqs because - # of DST - compare_local_to_utc(tz_didx, utc_didx) - - tz_didx = date_range('2000-01-01', '2020-01-01', freq='D', tz=tz) - utc_didx = date_range('2000-01-01', '2020-01-01', freq='D') - compare_utc_to_local(tz_didx, utc_didx) - compare_local_to_utc(tz_didx, utc_didx) - - tz_didx = date_range('2000-01-01', '2100-01-01', freq='A', tz=tz) - utc_didx = date_range('2000-01-01', '2100-01-01', freq='A') - compare_utc_to_local(tz_didx, utc_didx) - compare_local_to_utc(tz_didx, utc_didx) - - # Check empty array - result = tslib.tz_convert(np.array([], dtype=np.int64), - tslib.maybe_get_tz('US/Eastern'), - tslib.maybe_get_tz('Asia/Tokyo')) - self.assert_numpy_array_equal(result, np.array([], dtype=np.int64)) - - # Check all-NaT array - result = tslib.tz_convert(np.array([tslib.iNaT], dtype=np.int64), - tslib.maybe_get_tz('US/Eastern'), - tslib.maybe_get_tz('Asia/Tokyo')) - self.assert_numpy_array_equal(result, np.array( - [tslib.iNaT], dtype=np.int64)) diff --git a/pandas/tests/tslibs/__init__.py b/pandas/tests/tslibs/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/tslibs/test_api.py b/pandas/tests/tslibs/test_api.py new file mode 100644 index 0000000000000..de937d1a4c526 --- /dev/null +++ b/pandas/tests/tslibs/test_api.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""Tests that the tslibs API is locked down""" + +from pandas._libs import tslibs + + +def test_namespace(): + + submodules = ['ccalendar', + 'conversion', + 'fields', + 'frequencies', + 'nattype', + 'np_datetime', + 'offsets', + 'parsing', + 'period', + 'resolution', + 'strptime', + 'timedeltas', + 'timestamps', + 'timezones'] + + api = ['NaT', + 'iNaT', + 'is_null_datetimelike', + 'OutOfBoundsDatetime', + 'Period', + 'IncompatibleFrequency', + 'Timedelta', + 'Timestamp', + 'delta_to_nanoseconds', + 'ints_to_pytimedelta', + 'localize_pydatetime', + 'normalize_date', + 'tz_convert_single'] + + expected = set(submodules + api) + names = [x for x in dir(tslibs) if not x.startswith('__')] + assert set(names) == expected diff --git a/pandas/tests/tslibs/test_array_to_datetime.py b/pandas/tests/tslibs/test_array_to_datetime.py new file mode 100644 index 0000000000000..f5b036dde2094 --- /dev/null +++ b/pandas/tests/tslibs/test_array_to_datetime.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +from datetime import date, datetime + +from dateutil.tz.tz import tzoffset +import numpy as np +import pytest +import pytz + +from pandas._libs import iNaT, tslib +from pandas.compat.numpy import np_array_datetime64_compat + +import pandas.util.testing as tm + + +@pytest.mark.parametrize("data,expected", [ + (["01-01-2013", "01-02-2013"], + ["2013-01-01T00:00:00.000000000-0000", + "2013-01-02T00:00:00.000000000-0000"]), + (["Mon Sep 16 2013", "Tue Sep 17 2013"], + ["2013-09-16T00:00:00.000000000-0000", + "2013-09-17T00:00:00.000000000-0000"]) +]) +def test_parsing_valid_dates(data, expected): + arr = np.array(data, dtype=object) + result, _ = tslib.array_to_datetime(arr) + + expected = np_array_datetime64_compat(expected, dtype="M8[ns]") + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize("dt_string, expected_tz", [ + ["01-01-2013 08:00:00+08:00", 480], + ["2013-01-01T08:00:00.000000000+0800", 480], + ["2012-12-31T16:00:00.000000000-0800", -480], + ["12-31-2012 23:00:00-01:00", -60] +]) +def test_parsing_timezone_offsets(dt_string, expected_tz): + # All of these datetime strings with offsets are equivalent + # to the same datetime after the timezone offset is added. + arr = np.array(["01-01-2013 00:00:00"], dtype=object) + expected, _ = tslib.array_to_datetime(arr) + + arr = np.array([dt_string], dtype=object) + result, result_tz = tslib.array_to_datetime(arr) + + tm.assert_numpy_array_equal(result, expected) + assert result_tz is pytz.FixedOffset(expected_tz) + + +def test_parsing_non_iso_timezone_offset(): + dt_string = "01-01-2013T00:00:00.000000000+0000" + arr = np.array([dt_string], dtype=object) + + result, result_tz = tslib.array_to_datetime(arr) + expected = np.array([np.datetime64("2013-01-01 00:00:00.000000000")]) + + tm.assert_numpy_array_equal(result, expected) + assert result_tz is pytz.FixedOffset(0) + + +def test_parsing_different_timezone_offsets(): + # see gh-17697 + data = ["2015-11-18 15:30:00+05:30", "2015-11-18 15:30:00+06:30"] + data = np.array(data, dtype=object) + + result, result_tz = tslib.array_to_datetime(data) + expected = np.array([datetime(2015, 11, 18, 15, 30, + tzinfo=tzoffset(None, 19800)), + datetime(2015, 11, 18, 15, 30, + tzinfo=tzoffset(None, 23400))], + dtype=object) + + tm.assert_numpy_array_equal(result, expected) + assert result_tz is None + + +@pytest.mark.parametrize("data", [ + ["-352.737091", "183.575577"], + ["1", "2", "3", "4", "5"] +]) +def test_number_looking_strings_not_into_datetime(data): + # see gh-4601 + # + # These strings don't look like datetimes, so + # they shouldn't be attempted to be converted. + arr = np.array(data, dtype=object) + result, _ = tslib.array_to_datetime(arr, errors="ignore") + + tm.assert_numpy_array_equal(result, arr) + + +@pytest.mark.parametrize("invalid_date", [ + date(1000, 1, 1), + datetime(1000, 1, 1), + "1000-01-01", + "Jan 1, 1000", + np.datetime64("1000-01-01")]) +@pytest.mark.parametrize("errors", ["coerce", "raise"]) +def test_coerce_outside_ns_bounds(invalid_date, errors): + arr = np.array([invalid_date], dtype="object") + kwargs = dict(values=arr, errors=errors) + + if errors == "raise": + msg = "Out of bounds nanosecond timestamp" + + with pytest.raises(ValueError, match=msg): + tslib.array_to_datetime(**kwargs) + else: # coerce. + result, _ = tslib.array_to_datetime(**kwargs) + expected = np.array([iNaT], dtype="M8[ns]") + + tm.assert_numpy_array_equal(result, expected) + + +def test_coerce_outside_ns_bounds_one_valid(): + arr = np.array(["1/1/1000", "1/1/2000"], dtype=object) + result, _ = tslib.array_to_datetime(arr, errors="coerce") + + expected = [iNaT, "2000-01-01T00:00:00.000000000-0000"] + expected = np_array_datetime64_compat(expected, dtype="M8[ns]") + + tm.assert_numpy_array_equal(result, expected) + + +@pytest.mark.parametrize("errors", ["ignore", "coerce"]) +def test_coerce_of_invalid_datetimes(errors): + arr = np.array(["01-01-2013", "not_a_date", "1"], dtype=object) + kwargs = dict(values=arr, errors=errors) + + if errors == "ignore": + # Without coercing, the presence of any invalid + # dates prevents any values from being converted. + result, _ = tslib.array_to_datetime(**kwargs) + tm.assert_numpy_array_equal(result, arr) + else: # coerce. + # With coercing, the invalid dates becomes iNaT + result, _ = tslib.array_to_datetime(arr, errors="coerce") + expected = ["2013-01-01T00:00:00.000000000-0000", + iNaT, + iNaT] + + tm.assert_numpy_array_equal( + result, + np_array_datetime64_compat(expected, dtype="M8[ns]")) + + +def test_to_datetime_barely_out_of_bounds(): + # see gh-19382, gh-19529 + # + # Close enough to bounds that dropping nanos + # would result in an in-bounds datetime. + arr = np.array(["2262-04-11 23:47:16.854775808"], dtype=object) + msg = "Out of bounds nanosecond timestamp: 2262-04-11 23:47:16" + + with pytest.raises(tslib.OutOfBoundsDatetime, match=msg): + tslib.array_to_datetime(arr) diff --git a/pandas/tests/tslibs/test_ccalendar.py b/pandas/tests/tslibs/test_ccalendar.py new file mode 100644 index 0000000000000..255558a80018b --- /dev/null +++ b/pandas/tests/tslibs/test_ccalendar.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import numpy as np +import pytest + +from pandas._libs.tslibs import ccalendar + + +@pytest.mark.parametrize("date_tuple,expected", [ + ((2001, 3, 1), 60), + ((2004, 3, 1), 61), + ((1907, 12, 31), 365), # End-of-year, non-leap year. + ((2004, 12, 31), 366), # End-of-year, leap year. +]) +def test_get_day_of_year_numeric(date_tuple, expected): + assert ccalendar.get_day_of_year(*date_tuple) == expected + + +def test_get_day_of_year_dt(): + dt = datetime.fromordinal(1 + np.random.randint(365 * 4000)) + result = ccalendar.get_day_of_year(dt.year, dt.month, dt.day) + + expected = (dt - dt.replace(month=1, day=1)).days + 1 + assert result == expected diff --git a/pandas/tests/tslibs/test_conversion.py b/pandas/tests/tslibs/test_conversion.py new file mode 100644 index 0000000000000..13398a69b4982 --- /dev/null +++ b/pandas/tests/tslibs/test_conversion.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pytest +from pytz import UTC + +from pandas._libs.tslib import iNaT +from pandas._libs.tslibs import conversion, timezones + +from pandas import date_range +import pandas.util.testing as tm + + +def _compare_utc_to_local(tz_didx): + def f(x): + return conversion.tz_convert_single(x, UTC, tz_didx.tz) + + result = conversion.tz_convert(tz_didx.asi8, UTC, tz_didx.tz) + expected = np.vectorize(f)(tz_didx.asi8) + + tm.assert_numpy_array_equal(result, expected) + + +def _compare_local_to_utc(tz_didx, utc_didx): + def f(x): + return conversion.tz_convert_single(x, tz_didx.tz, UTC) + + result = conversion.tz_convert(utc_didx.asi8, tz_didx.tz, UTC) + expected = np.vectorize(f)(utc_didx.asi8) + + tm.assert_numpy_array_equal(result, expected) + + +def test_tz_convert_single_matches_tz_convert_hourly(tz_aware_fixture): + tz = tz_aware_fixture + tz_didx = date_range("2014-03-01", "2015-01-10", freq="H", tz=tz) + utc_didx = date_range("2014-03-01", "2015-01-10", freq="H") + + _compare_utc_to_local(tz_didx) + _compare_local_to_utc(tz_didx, utc_didx) + + +@pytest.mark.parametrize("freq", ["D", "A"]) +def test_tz_convert_single_matches_tz_convert(tz_aware_fixture, freq): + tz = tz_aware_fixture + tz_didx = date_range("2000-01-01", "2020-01-01", freq=freq, tz=tz) + utc_didx = date_range("2000-01-01", "2020-01-01", freq=freq) + + _compare_utc_to_local(tz_didx) + _compare_local_to_utc(tz_didx, utc_didx) + + +@pytest.mark.parametrize("arr", [ + pytest.param(np.array([], dtype=np.int64), id="empty"), + pytest.param(np.array([iNaT], dtype=np.int64), id="all_nat")]) +def test_tz_convert_corner(arr): + result = conversion.tz_convert(arr, + timezones.maybe_get_tz("US/Eastern"), + timezones.maybe_get_tz("Asia/Tokyo")) + tm.assert_numpy_array_equal(result, arr) + + +@pytest.mark.parametrize("copy", [True, False]) +@pytest.mark.parametrize("dtype", ["M8[ns]", "M8[s]"]) +def test_length_zero_copy(dtype, copy): + arr = np.array([], dtype=dtype) + result = conversion.ensure_datetime64ns(arr, copy=copy) + assert result.base is (None if copy else arr) diff --git a/pandas/tests/tslibs/test_libfrequencies.py b/pandas/tests/tslibs/test_libfrequencies.py new file mode 100644 index 0000000000000..b9b1c72dbf2e1 --- /dev/null +++ b/pandas/tests/tslibs/test_libfrequencies.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas._libs.tslibs.frequencies import ( + INVALID_FREQ_ERR_MSG, _period_str_to_code, get_rule_month, is_subperiod, + is_superperiod) + +from pandas.tseries import offsets + + +@pytest.mark.parametrize("obj,expected", [ + ("W", "DEC"), + (offsets.Week(), "DEC"), + + ("D", "DEC"), + (offsets.Day(), "DEC"), + + ("Q", "DEC"), + (offsets.QuarterEnd(startingMonth=12), "DEC"), + + ("Q-JAN", "JAN"), + (offsets.QuarterEnd(startingMonth=1), "JAN"), + + ("A-DEC", "DEC"), + ("Y-DEC", "DEC"), + (offsets.YearEnd(), "DEC"), + + ("A-MAY", "MAY"), + ("Y-MAY", "MAY"), + (offsets.YearEnd(month=5), "MAY") +]) +def test_get_rule_month(obj, expected): + result = get_rule_month(obj) + assert result == expected + + +@pytest.mark.parametrize("obj,expected", [ + ("A", 1000), + ("A-DEC", 1000), + ("A-JAN", 1001), + + ("Y", 1000), + ("Y-DEC", 1000), + ("Y-JAN", 1001), + + ("Q", 2000), + ("Q-DEC", 2000), + ("Q-FEB", 2002), + + ("W", 4000), + ("W-SUN", 4000), + ("W-FRI", 4005), + + ("Min", 8000), + ("ms", 10000), + ("US", 11000), + ("NS", 12000) +]) +def test_period_str_to_code(obj, expected): + assert _period_str_to_code(obj) == expected + + +@pytest.mark.parametrize("p1,p2,expected", [ + # Input validation. + (offsets.MonthEnd(), None, False), + (offsets.YearEnd(), None, False), + (None, offsets.YearEnd(), False), + (None, offsets.MonthEnd(), False), + (None, None, False), + + (offsets.YearEnd(), offsets.MonthEnd(), True), + (offsets.Hour(), offsets.Minute(), True), + (offsets.Second(), offsets.Milli(), True), + (offsets.Milli(), offsets.Micro(), True), + (offsets.Micro(), offsets.Nano(), True) +]) +def test_super_sub_symmetry(p1, p2, expected): + assert is_superperiod(p1, p2) is expected + assert is_subperiod(p2, p1) is expected + + +@pytest.mark.parametrize("freq,expected,aliases", [ + ("D", 6000, ["DAY", "DLY", "DAILY"]), + ("M", 3000, ["MTH", "MONTH", "MONTHLY"]), + ("N", 12000, ["NANOSECOND", "NANOSECONDLY"]), + ("H", 7000, ["HR", "HOUR", "HRLY", "HOURLY"]), + ("T", 8000, ["minute", "MINUTE", "MINUTELY"]), + ("L", 10000, ["MILLISECOND", "MILLISECONDLY"]), + ("U", 11000, ["MICROSECOND", "MICROSECONDLY"]), + ("S", 9000, ["sec", "SEC", "SECOND", "SECONDLY"]), + ("B", 5000, ["BUS", "BUSINESS", "BUSINESSLY", "WEEKDAY"]), +]) +def test_assert_aliases_deprecated(freq, expected, aliases): + assert isinstance(aliases, list) + assert _period_str_to_code(freq) == expected + + for alias in aliases: + with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG): + _period_str_to_code(alias) diff --git a/pandas/tests/tslibs/test_liboffsets.py b/pandas/tests/tslibs/test_liboffsets.py new file mode 100644 index 0000000000000..cb699278595e7 --- /dev/null +++ b/pandas/tests/tslibs/test_liboffsets.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +""" +Tests for helper functions in the cython tslibs.offsets +""" +from datetime import datetime + +import pytest + +import pandas._libs.tslibs.offsets as liboffsets +from pandas._libs.tslibs.offsets import roll_qtrday + +from pandas import Timestamp + + +@pytest.fixture(params=["start", "end", "business_start", "business_end"]) +def day_opt(request): + return request.param + + +@pytest.mark.parametrize("dt,exp_week_day,exp_last_day", [ + (datetime(2017, 11, 30), 3, 30), # Business day. + (datetime(1993, 10, 31), 6, 29) # Non-business day. +]) +def test_get_last_bday(dt, exp_week_day, exp_last_day): + assert dt.weekday() == exp_week_day + assert liboffsets.get_lastbday(dt.year, dt.month) == exp_last_day + + +@pytest.mark.parametrize("dt,exp_week_day,exp_first_day", [ + (datetime(2017, 4, 1), 5, 3), # Non-weekday. + (datetime(1993, 10, 1), 4, 1) # Business day. +]) +def test_get_first_bday(dt, exp_week_day, exp_first_day): + assert dt.weekday() == exp_week_day + assert liboffsets.get_firstbday(dt.year, dt.month) == exp_first_day + + +@pytest.mark.parametrize("months,day_opt,expected", [ + (0, 15, datetime(2017, 11, 15)), + (0, None, datetime(2017, 11, 30)), + (1, "start", datetime(2017, 12, 1)), + (-145, "end", datetime(2005, 10, 31)), + (0, "business_end", datetime(2017, 11, 30)), + (0, "business_start", datetime(2017, 11, 1)) +]) +def test_shift_month_dt(months, day_opt, expected): + dt = datetime(2017, 11, 30) + assert liboffsets.shift_month(dt, months, day_opt=day_opt) == expected + + +@pytest.mark.parametrize("months,day_opt,expected", [ + (1, "start", Timestamp("1929-06-01")), + (-3, "end", Timestamp("1929-02-28")), + (25, None, Timestamp("1931-06-5")), + (-1, 31, Timestamp("1929-04-30")) +]) +def test_shift_month_ts(months, day_opt, expected): + ts = Timestamp("1929-05-05") + assert liboffsets.shift_month(ts, months, day_opt=day_opt) == expected + + +def test_shift_month_error(): + dt = datetime(2017, 11, 15) + day_opt = "this should raise" + + with pytest.raises(ValueError, match=day_opt): + liboffsets.shift_month(dt, 3, day_opt=day_opt) + + +@pytest.mark.parametrize("other,expected", [ + # Before March 1. + (datetime(2017, 2, 10), {2: 1, -7: -7, 0: 0}), + + # After March 1. + (Timestamp("2014-03-15", tz="US/Eastern"), {2: 2, -7: -6, 0: 1}) +]) +@pytest.mark.parametrize("n", [2, -7, 0]) +def test_roll_yearday(other, expected, n): + month = 3 + day_opt = "start" # `other` will be compared to March 1. + + assert liboffsets.roll_yearday(other, n, month, day_opt) == expected[n] + + +@pytest.mark.parametrize("other,expected", [ + # Before June 30. + (datetime(1999, 6, 29), {5: 4, -7: -7, 0: 0}), + + # After June 30. + (Timestamp(2072, 8, 24, 6, 17, 18), {5: 5, -7: -6, 0: 1}) +]) +@pytest.mark.parametrize("n", [5, -7, 0]) +def test_roll_yearday2(other, expected, n): + month = 6 + day_opt = "end" # `other` will be compared to June 30. + + assert liboffsets.roll_yearday(other, n, month, day_opt) == expected[n] + + +def test_get_day_of_month_error(): + # get_day_of_month is not directly exposed. + # We test it via roll_yearday. + dt = datetime(2017, 11, 15) + day_opt = "foo" + + with pytest.raises(ValueError, match=day_opt): + # To hit the raising case we need month == dt.month and n > 0. + liboffsets.roll_yearday(dt, n=3, month=11, day_opt=day_opt) + + +@pytest.mark.parametrize("month", [ + 3, # (other.month % 3) < (month % 3) + 5 # (other.month % 3) > (month % 3) +]) +@pytest.mark.parametrize("n", [4, -3]) +def test_roll_qtr_day_not_mod_unequal(day_opt, month, n): + expected = { + 3: { + -3: -2, + 4: 4 + }, + 5: { + -3: -3, + 4: 3 + } + } + + other = Timestamp(2072, 10, 1, 6, 17, 18) # Saturday. + assert roll_qtrday(other, n, month, day_opt, modby=3) == expected[month][n] + + +@pytest.mark.parametrize("other,month,exp_dict", [ + # Monday. + (datetime(1999, 5, 31), 2, { + -1: { + "start": 0, + "business_start": 0 + } + }), + + # Saturday. + (Timestamp(2072, 10, 1, 6, 17, 18), 4, { + 2: { + "end": 1, + "business_end": 1, + "business_start": 1 + } + }), + + # First business day. + (Timestamp(2072, 10, 3, 6, 17, 18), 4, { + 2: { + "end": 1, + "business_end": 1 + }, + -1: { + "start": 0 + } + }) +]) +@pytest.mark.parametrize("n", [2, -1]) +def test_roll_qtr_day_mod_equal(other, month, exp_dict, n, day_opt): + # All cases have (other.month % 3) == (month % 3). + expected = exp_dict.get(n, {}).get(day_opt, n) + assert roll_qtrday(other, n, month, day_opt, modby=3) == expected + + +@pytest.mark.parametrize("n,expected", [ + (42, {29: 42, 1: 42, 31: 41}), + (-4, {29: -4, 1: -3, 31: -4}) +]) +@pytest.mark.parametrize("compare", [29, 1, 31]) +def test_roll_convention(n, expected, compare): + assert liboffsets.roll_convention(29, n, compare) == expected[compare] diff --git a/pandas/tests/tslibs/test_normalize_date.py b/pandas/tests/tslibs/test_normalize_date.py new file mode 100644 index 0000000000000..6124121b97186 --- /dev/null +++ b/pandas/tests/tslibs/test_normalize_date.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Tests for functions from pandas._libs.tslibs""" + +from datetime import date, datetime + +import pytest + +from pandas._libs import tslibs + + +@pytest.mark.parametrize("value,expected", [ + (date(2012, 9, 7), datetime(2012, 9, 7)), + (datetime(2012, 9, 7, 12), datetime(2012, 9, 7)), + (datetime(2007, 10, 1, 1, 12, 5, 10), datetime(2007, 10, 1)) +]) +def test_normalize_date(value, expected): + result = tslibs.normalize_date(value) + assert result == expected diff --git a/pandas/tests/tslibs/test_parse_iso8601.py b/pandas/tests/tslibs/test_parse_iso8601.py new file mode 100644 index 0000000000000..d1b3dee948afe --- /dev/null +++ b/pandas/tests/tslibs/test_parse_iso8601.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import pytest + +from pandas._libs import tslib + + +@pytest.mark.parametrize("date_str, exp", [ + ("2011-01-02", datetime(2011, 1, 2)), + ("2011-1-2", datetime(2011, 1, 2)), + ("2011-01", datetime(2011, 1, 1)), + ("2011-1", datetime(2011, 1, 1)), + ("2011 01 02", datetime(2011, 1, 2)), + ("2011.01.02", datetime(2011, 1, 2)), + ("2011/01/02", datetime(2011, 1, 2)), + ("2011\\01\\02", datetime(2011, 1, 2)), + ("2013-01-01 05:30:00", datetime(2013, 1, 1, 5, 30)), + ("2013-1-1 5:30:00", datetime(2013, 1, 1, 5, 30))]) +def test_parsers_iso8601(date_str, exp): + # see gh-12060 + # + # Test only the ISO parser - flexibility to + # different separators and leading zero's. + actual = tslib._test_parse_iso8601(date_str) + assert actual == exp + + +@pytest.mark.parametrize("date_str", [ + "2011-01/02", + "2011=11=11", + "201401", + "201111", + "200101", + + # Mixed separated and unseparated. + "2005-0101", + "200501-01", + "20010101 12:3456", + "20010101 1234:56", + + # HHMMSS must have two digits in + # each component if unseparated. + "20010101 1", + "20010101 123", + "20010101 12345", + "20010101 12345Z", +]) +def test_parsers_iso8601_invalid(date_str): + msg = "Error parsing datetime string \"{s}\"".format(s=date_str) + + with pytest.raises(ValueError, match=msg): + tslib._test_parse_iso8601(date_str) + + +def test_parsers_iso8601_invalid_offset_invalid(): + date_str = "2001-01-01 12-34-56" + msg = ("Timezone hours offset out of range " + "in datetime string \"{s}\"".format(s=date_str)) + + with pytest.raises(ValueError, match=msg): + tslib._test_parse_iso8601(date_str) diff --git a/pandas/tests/tslibs/test_parsing.py b/pandas/tests/tslibs/test_parsing.py new file mode 100644 index 0000000000000..597ec6df7389f --- /dev/null +++ b/pandas/tests/tslibs/test_parsing.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +""" +Tests for Timestamp parsing, aimed at pandas/_libs/tslibs/parsing.pyx +""" +from datetime import datetime + +from dateutil.parser import parse +import numpy as np +import pytest + +from pandas._libs.tslibs import parsing +from pandas._libs.tslibs.parsing import parse_time_string +import pandas.util._test_decorators as td + +from pandas.util import testing as tm + + +def test_parse_time_string(): + (date, parsed, reso) = parse_time_string("4Q1984") + (date_lower, parsed_lower, reso_lower) = parse_time_string("4q1984") + + assert date == date_lower + assert reso == reso_lower + assert parsed == parsed_lower + + +@pytest.mark.parametrize("dashed,normal", [ + ("1988-Q2", "1988Q2"), + ("2Q-1988", "2Q1988") +]) +def test_parse_time_quarter_with_dash(dashed, normal): + # see gh-9688 + (date_dash, parsed_dash, reso_dash) = parse_time_string(dashed) + (date, parsed, reso) = parse_time_string(normal) + + assert date_dash == date + assert parsed_dash == parsed + assert reso_dash == reso + + +@pytest.mark.parametrize("dashed", [ + "-2Q1992", "2-Q1992", "4-4Q1992" +]) +def test_parse_time_quarter_with_dash_error(dashed): + msg = ("Unknown datetime string format, " + "unable to parse: {dashed}".format(dashed=dashed)) + + with pytest.raises(parsing.DateParseError, match=msg): + parse_time_string(dashed) + + +@pytest.mark.parametrize("date_string,expected", [ + ("123.1234", False), + ("-50000", False), + ("999", False), + ("m", False), + ("T", False), + + ("Mon Sep 16, 2013", True), + ("2012-01-01", True), + ("01/01/2012", True), + ("01012012", True), + ("0101", True), + ("1-1", True) +]) +def test_does_not_convert_mixed_integer(date_string, expected): + assert parsing._does_string_look_like_datetime(date_string) is expected + + +@pytest.mark.parametrize("date_str,kwargs,msg", [ + ("2013Q5", dict(), + ("Incorrect quarterly string is given, " + "quarter must be between 1 and 4: 2013Q5")), + + # see gh-5418 + ("2013Q1", dict(freq="INVLD-L-DEC-SAT"), + ("Unable to retrieve month information " + "from given freq: INVLD-L-DEC-SAT")) +]) +def test_parsers_quarterly_with_freq_error(date_str, kwargs, msg): + with pytest.raises(parsing.DateParseError, match=msg): + parsing.parse_time_string(date_str, **kwargs) + + +@pytest.mark.parametrize("date_str,freq,expected", [ + ("2013Q2", None, datetime(2013, 4, 1)), + ("2013Q2", "A-APR", datetime(2012, 8, 1)), + ("2013-Q2", "A-DEC", datetime(2013, 4, 1)) +]) +def test_parsers_quarterly_with_freq(date_str, freq, expected): + result, _, _ = parsing.parse_time_string(date_str, freq=freq) + assert result == expected + + +@pytest.mark.parametrize("date_str", [ + "2Q 2005", "2Q-200A", "2Q-200", + "22Q2005", "2Q200.", "6Q-20" +]) +def test_parsers_quarter_invalid(date_str): + if date_str == "6Q-20": + msg = ("Incorrect quarterly string is given, quarter " + "must be between 1 and 4: {date_str}".format(date_str=date_str)) + else: + msg = ("Unknown datetime string format, unable " + "to parse: {date_str}".format(date_str=date_str)) + + with pytest.raises(ValueError, match=msg): + parsing.parse_time_string(date_str) + + +@pytest.mark.parametrize("date_str,expected", [ + ("201101", datetime(2011, 1, 1, 0, 0)), + ("200005", datetime(2000, 5, 1, 0, 0)) +]) +def test_parsers_month_freq(date_str, expected): + result, _, _ = parsing.parse_time_string(date_str, freq="M") + assert result == expected + + +@td.skip_if_not_us_locale +@pytest.mark.parametrize("string,fmt", [ + ("20111230", "%Y%m%d"), + ("2011-12-30", "%Y-%m-%d"), + ("30-12-2011", "%d-%m-%Y"), + ("2011-12-30 00:00:00", "%Y-%m-%d %H:%M:%S"), + ("2011-12-30T00:00:00", "%Y-%m-%dT%H:%M:%S"), + ("2011-12-30 00:00:00.000000", "%Y-%m-%d %H:%M:%S.%f") +]) +def test_guess_datetime_format_with_parseable_formats(string, fmt): + result = parsing._guess_datetime_format(string) + assert result == fmt + + +@pytest.mark.parametrize("dayfirst,expected", [ + (True, "%d/%m/%Y"), + (False, "%m/%d/%Y") +]) +def test_guess_datetime_format_with_dayfirst(dayfirst, expected): + ambiguous_string = "01/01/2011" + result = parsing._guess_datetime_format(ambiguous_string, + dayfirst=dayfirst) + assert result == expected + + +@td.skip_if_has_locale +@pytest.mark.parametrize("string,fmt", [ + ("30/Dec/2011", "%d/%b/%Y"), + ("30/December/2011", "%d/%B/%Y"), + ("30/Dec/2011 00:00:00", "%d/%b/%Y %H:%M:%S") +]) +def test_guess_datetime_format_with_locale_specific_formats(string, fmt): + result = parsing._guess_datetime_format(string) + assert result == fmt + + +@pytest.mark.parametrize("invalid_dt", [ + "2013", "01/2013", "12:00:00", "1/1/1/1", + "this_is_not_a_datetime", "51a", 9, + datetime(2011, 1, 1) +]) +def test_guess_datetime_format_invalid_inputs(invalid_dt): + # A datetime string must include a year, month and a day for it to be + # guessable, in addition to being a string that looks like a datetime. + assert parsing._guess_datetime_format(invalid_dt) is None + + +@pytest.mark.parametrize("string,fmt", [ + ("2011-1-1", "%Y-%m-%d"), + ("1/1/2011", "%m/%d/%Y"), + ("30-1-2011", "%d-%m-%Y"), + ("2011-1-1 0:0:0", "%Y-%m-%d %H:%M:%S"), + ("2011-1-3T00:00:0", "%Y-%m-%dT%H:%M:%S"), + ("2011-1-1 00:00:00", "%Y-%m-%d %H:%M:%S") +]) +def test_guess_datetime_format_no_padding(string, fmt): + # see gh-11142 + result = parsing._guess_datetime_format(string) + assert result == fmt + + +def test_try_parse_dates(): + arr = np.array(["5/1/2000", "6/1/2000", "7/1/2000"], dtype=object) + result = parsing.try_parse_dates(arr, dayfirst=True) + + expected = np.array([parse(d, dayfirst=True) for d in arr]) + tm.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/tslibs/test_period_asfreq.py b/pandas/tests/tslibs/test_period_asfreq.py new file mode 100644 index 0000000000000..6a9522e705318 --- /dev/null +++ b/pandas/tests/tslibs/test_period_asfreq.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas._libs.tslibs.frequencies import get_freq +from pandas._libs.tslibs.period import period_asfreq, period_ordinal + + +@pytest.mark.parametrize("freq1,freq2,expected", [ + ("D", "H", 24), + ("D", "T", 1440), + ("D", "S", 86400), + ("D", "L", 86400000), + ("D", "U", 86400000000), + ("D", "N", 86400000000000), + + ("H", "T", 60), + ("H", "S", 3600), + ("H", "L", 3600000), + ("H", "U", 3600000000), + ("H", "N", 3600000000000), + + ("T", "S", 60), + ("T", "L", 60000), + ("T", "U", 60000000), + ("T", "N", 60000000000), + + ("S", "L", 1000), + ("S", "U", 1000000), + ("S", "N", 1000000000), + + ("L", "U", 1000), + ("L", "N", 1000000), + + ("U", "N", 1000) +]) +def test_intra_day_conversion_factors(freq1, freq2, expected): + assert period_asfreq(1, get_freq(freq1), + get_freq(freq2), False) == expected + + +@pytest.mark.parametrize("freq,expected", [ + ("A", 0), + ("M", 0), + ("W", 1), + ("D", 0), + ("B", 0) +]) +def test_period_ordinal_start_values(freq, expected): + # information for Jan. 1, 1970. + assert period_ordinal(1970, 1, 1, 0, 0, 0, + 0, 0, get_freq(freq)) == expected + + +@pytest.mark.parametrize("dt,expected", [ + ((1970, 1, 4, 0, 0, 0, 0, 0), 1), + ((1970, 1, 5, 0, 0, 0, 0, 0), 2), + ((2013, 10, 6, 0, 0, 0, 0, 0), 2284), + ((2013, 10, 7, 0, 0, 0, 0, 0), 2285) +]) +def test_period_ordinal_week(dt, expected): + args = dt + (get_freq("W"),) + assert period_ordinal(*args) == expected + + +@pytest.mark.parametrize("day,expected", [ + # Thursday (Oct. 3, 2013). + (3, 11415), + + # Friday (Oct. 4, 2013). + (4, 11416), + + # Saturday (Oct. 5, 2013). + (5, 11417), + + # Sunday (Oct. 6, 2013). + (6, 11417), + + # Monday (Oct. 7, 2013). + (7, 11417), + + # Tuesday (Oct. 8, 2013). + (8, 11418) +]) +def test_period_ordinal_business_day(day, expected): + args = (2013, 10, day, 0, 0, 0, 0, 0, get_freq("B")) + assert period_ordinal(*args) == expected diff --git a/pandas/tests/tslibs/test_timedeltas.py b/pandas/tests/tslibs/test_timedeltas.py new file mode 100644 index 0000000000000..fdc8eff80acad --- /dev/null +++ b/pandas/tests/tslibs/test_timedeltas.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import numpy as np +import pytest + +from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds + +import pandas as pd +from pandas import Timedelta + + +@pytest.mark.parametrize("obj,expected", [ + (np.timedelta64(14, "D"), 14 * 24 * 3600 * 1e9), + (Timedelta(minutes=-7), -7 * 60 * 1e9), + (Timedelta(minutes=-7).to_pytimedelta(), -7 * 60 * 1e9), + (pd.offsets.Nano(125), 125), + (1, 1), + (np.int64(2), 2), + (np.int32(3), 3) +]) +def test_delta_to_nanoseconds(obj, expected): + result = delta_to_nanoseconds(obj) + assert result == expected + + +def test_delta_to_nanoseconds_error(): + obj = np.array([123456789], dtype="m8[ns]") + + with pytest.raises(TypeError, match="<(class|type) 'numpy.ndarray'>"): + delta_to_nanoseconds(obj) diff --git a/pandas/tests/tslibs/test_timezones.py b/pandas/tests/tslibs/test_timezones.py new file mode 100644 index 0000000000000..0255865dbdf71 --- /dev/null +++ b/pandas/tests/tslibs/test_timezones.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +from datetime import datetime + +import dateutil.tz +import pytest +import pytz + +from pandas._libs.tslibs import conversion, timezones + +from pandas import Timestamp + + +@pytest.mark.parametrize("tz_name", list(pytz.common_timezones)) +def test_cache_keys_are_distinct_for_pytz_vs_dateutil(tz_name): + if tz_name == "UTC": + pytest.skip("UTC: special case in dateutil") + + tz_p = timezones.maybe_get_tz(tz_name) + tz_d = timezones.maybe_get_tz("dateutil/" + tz_name) + + if tz_d is None: + pytest.skip(tz_name + ": dateutil does not know about this one") + + assert timezones._p_tz_cache_key(tz_p) != timezones._p_tz_cache_key(tz_d) + + +def test_tzlocal_repr(): + # see gh-13583 + ts = Timestamp("2011-01-01", tz=dateutil.tz.tzlocal()) + assert ts.tz == dateutil.tz.tzlocal() + assert "tz='tzlocal()')" in repr(ts) + + +def test_tzlocal_maybe_get_tz(): + # see gh-13583 + tz = timezones.maybe_get_tz('tzlocal()') + assert tz == dateutil.tz.tzlocal() + + +def test_tzlocal_offset(): + # see gh-13583 + # + # Get offset using normal datetime for test. + ts = Timestamp("2011-01-01", tz=dateutil.tz.tzlocal()) + + offset = dateutil.tz.tzlocal().utcoffset(datetime(2011, 1, 1)) + offset = offset.total_seconds() * 1000000000 + + assert ts.value + offset == Timestamp("2011-01-01").value + + +@pytest.fixture(params=[ + (pytz.timezone("US/Eastern"), lambda tz, x: tz.localize(x)), + (dateutil.tz.gettz("US/Eastern"), lambda tz, x: x.replace(tzinfo=tz)) +]) +def infer_setup(request): + eastern, localize = request.param + + start_naive = datetime(2001, 1, 1) + end_naive = datetime(2009, 1, 1) + + start = localize(eastern, start_naive) + end = localize(eastern, end_naive) + + return eastern, localize, start, end, start_naive, end_naive + + +def test_infer_tz_compat(infer_setup): + eastern, _, start, end, start_naive, end_naive = infer_setup + + assert (timezones.infer_tzinfo(start, end) is + conversion.localize_pydatetime(start_naive, eastern).tzinfo) + assert (timezones.infer_tzinfo(start, None) is + conversion.localize_pydatetime(start_naive, eastern).tzinfo) + assert (timezones.infer_tzinfo(None, end) is + conversion.localize_pydatetime(end_naive, eastern).tzinfo) + + +def test_infer_tz_utc_localize(infer_setup): + _, _, start, end, start_naive, end_naive = infer_setup + utc = pytz.utc + + start = utc.localize(start_naive) + end = utc.localize(end_naive) + + assert timezones.infer_tzinfo(start, end) is utc + + +@pytest.mark.parametrize("ordered", [True, False]) +def test_infer_tz_mismatch(infer_setup, ordered): + eastern, _, _, _, start_naive, end_naive = infer_setup + msg = "Inputs must both have the same timezone" + + utc = pytz.utc + start = utc.localize(start_naive) + end = conversion.localize_pydatetime(end_naive, eastern) + + args = (start, end) if ordered else (end, start) + + with pytest.raises(AssertionError, match=msg): + timezones.infer_tzinfo(*args) diff --git a/pandas/tests/types/test_cast.py b/pandas/tests/types/test_cast.py deleted file mode 100644 index de6ef7af9d7f9..0000000000000 --- a/pandas/tests/types/test_cast.py +++ /dev/null @@ -1,320 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -These test the private routines in types/cast.py - -""" - -import pytest -from datetime import datetime, timedelta, date -import numpy as np - -from pandas import Timedelta, Timestamp, DatetimeIndex -from pandas.types.cast import (maybe_downcast_to_dtype, - maybe_convert_objects, - infer_dtype_from_scalar, - infer_dtype_from_array, - maybe_convert_string_to_object, - maybe_convert_scalar, - find_common_type) -from pandas.types.dtypes import (CategoricalDtype, - DatetimeTZDtype, PeriodDtype) -from pandas.util import testing as tm - - -class TestMaybeDowncast(tm.TestCase): - - def test_downcast_conv(self): - # test downcasting - - arr = np.array([8.5, 8.6, 8.7, 8.8, 8.9999999999995]) - result = maybe_downcast_to_dtype(arr, 'infer') - assert (np.array_equal(result, arr)) - - arr = np.array([8., 8., 8., 8., 8.9999999999995]) - result = maybe_downcast_to_dtype(arr, 'infer') - expected = np.array([8, 8, 8, 8, 9]) - assert (np.array_equal(result, expected)) - - arr = np.array([8., 8., 8., 8., 9.0000000000005]) - result = maybe_downcast_to_dtype(arr, 'infer') - expected = np.array([8, 8, 8, 8, 9]) - assert (np.array_equal(result, expected)) - - # conversions - - expected = np.array([1, 2]) - for dtype in [np.float64, object, np.int64]: - arr = np.array([1.0, 2.0], dtype=dtype) - result = maybe_downcast_to_dtype(arr, 'infer') - tm.assert_almost_equal(result, expected, check_dtype=False) - - for dtype in [np.float64, object]: - expected = np.array([1.0, 2.0, np.nan], dtype=dtype) - arr = np.array([1.0, 2.0, np.nan], dtype=dtype) - result = maybe_downcast_to_dtype(arr, 'infer') - tm.assert_almost_equal(result, expected) - - # empties - for dtype in [np.int32, np.float64, np.float32, np.bool_, - np.int64, object]: - arr = np.array([], dtype=dtype) - result = maybe_downcast_to_dtype(arr, 'int64') - tm.assert_almost_equal(result, np.array([], dtype=np.int64)) - assert result.dtype == np.int64 - - def test_datetimelikes_nan(self): - arr = np.array([1, 2, np.nan]) - exp = np.array([1, 2, np.datetime64('NaT')], dtype='datetime64[ns]') - res = maybe_downcast_to_dtype(arr, 'datetime64[ns]') - tm.assert_numpy_array_equal(res, exp) - - exp = np.array([1, 2, np.timedelta64('NaT')], dtype='timedelta64[ns]') - res = maybe_downcast_to_dtype(arr, 'timedelta64[ns]') - tm.assert_numpy_array_equal(res, exp) - - def test_datetime_with_timezone(self): - # GH 15426 - ts = Timestamp("2016-01-01 12:00:00", tz='US/Pacific') - exp = DatetimeIndex([ts, ts]) - res = maybe_downcast_to_dtype(exp, exp.dtype) - tm.assert_index_equal(res, exp) - - res = maybe_downcast_to_dtype(exp.asi8, exp.dtype) - tm.assert_index_equal(res, exp) - - -class TestInferDtype(object): - - def test_infer_dtype_from_scalar(self): - # Test that _infer_dtype_from_scalar is returning correct dtype for int - # and float. - - for dtypec in [np.uint8, np.int8, np.uint16, np.int16, np.uint32, - np.int32, np.uint64, np.int64]: - data = dtypec(12) - dtype, val = infer_dtype_from_scalar(data) - assert dtype == type(data) - - data = 12 - dtype, val = infer_dtype_from_scalar(data) - assert dtype == np.int64 - - for dtypec in [np.float16, np.float32, np.float64]: - data = dtypec(12) - dtype, val = infer_dtype_from_scalar(data) - assert dtype == dtypec - - data = np.float(12) - dtype, val = infer_dtype_from_scalar(data) - assert dtype == np.float64 - - for data in [True, False]: - dtype, val = infer_dtype_from_scalar(data) - assert dtype == np.bool_ - - for data in [np.complex64(1), np.complex128(1)]: - dtype, val = infer_dtype_from_scalar(data) - assert dtype == np.complex_ - - for data in [np.datetime64(1, 'ns'), Timestamp(1), - datetime(2000, 1, 1, 0, 0)]: - dtype, val = infer_dtype_from_scalar(data) - assert dtype == 'M8[ns]' - - for data in [np.timedelta64(1, 'ns'), Timedelta(1), - timedelta(1)]: - dtype, val = infer_dtype_from_scalar(data) - assert dtype == 'm8[ns]' - - for data in [date(2000, 1, 1), - Timestamp(1, tz='US/Eastern'), 'foo']: - dtype, val = infer_dtype_from_scalar(data) - assert dtype == np.object_ - - @pytest.mark.parametrize( - "arr, expected", - [('foo', np.object_), - (b'foo', np.object_), - (1, np.int_), - (1.5, np.float_), - ([1], np.int_), - (np.array([1]), np.int_), - ([np.nan, 1, ''], np.object_), - (np.array([[1.0, 2.0]]), np.float_), - (Timestamp('20160101'), np.object_), - (np.datetime64('2016-01-01'), np.dtype('= '1.7.0': - assert (s[8].value == np.datetime64('NaT').astype(np.int64)) - - -def test_is_scipy_sparse(spmatrix): # noqa: F811 - tm._skip_if_no_scipy() - assert is_scipy_sparse(spmatrix([[0, 1]])) - assert not is_scipy_sparse(np.array([1])) - - -def test_ensure_int32(): - values = np.arange(10, dtype=np.int32) - result = _ensure_int32(values) - assert (result.dtype == np.int32) - - values = np.arange(10, dtype=np.int64) - result = _ensure_int32(values) - assert (result.dtype == np.int32) - - -def test_ensure_categorical(): - values = np.arange(10, dtype=np.int32) - result = _ensure_categorical(values) - assert (result.dtype == 'category') - - values = Categorical(values) - result = _ensure_categorical(values) - tm.assert_categorical_equal(result, values) diff --git a/pandas/tests/types/test_io.py b/pandas/tests/types/test_io.py deleted file mode 100644 index b6c10394dd232..0000000000000 --- a/pandas/tests/types/test_io.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np -import pandas._libs.lib as lib -import pandas.util.testing as tm - -from pandas.compat import long, u - - -class TestParseSQL(tm.TestCase): - - def test_convert_sql_column_floats(self): - arr = np.array([1.5, None, 3, 4.2], dtype=object) - result = lib.convert_sql_column(arr) - expected = np.array([1.5, np.nan, 3, 4.2], dtype='f8') - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_strings(self): - arr = np.array(['1.5', None, '3', '4.2'], dtype=object) - result = lib.convert_sql_column(arr) - expected = np.array(['1.5', np.nan, '3', '4.2'], dtype=object) - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_unicode(self): - arr = np.array([u('1.5'), None, u('3'), u('4.2')], - dtype=object) - result = lib.convert_sql_column(arr) - expected = np.array([u('1.5'), np.nan, u('3'), u('4.2')], - dtype=object) - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_ints(self): - arr = np.array([1, 2, 3, 4], dtype='O') - arr2 = np.array([1, 2, 3, 4], dtype='i4').astype('O') - result = lib.convert_sql_column(arr) - result2 = lib.convert_sql_column(arr2) - expected = np.array([1, 2, 3, 4], dtype='i8') - self.assert_numpy_array_equal(result, expected) - self.assert_numpy_array_equal(result2, expected) - - arr = np.array([1, 2, 3, None, 4], dtype='O') - result = lib.convert_sql_column(arr) - expected = np.array([1, 2, 3, np.nan, 4], dtype='f8') - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_longs(self): - arr = np.array([long(1), long(2), long(3), long(4)], dtype='O') - result = lib.convert_sql_column(arr) - expected = np.array([1, 2, 3, 4], dtype='i8') - self.assert_numpy_array_equal(result, expected) - - arr = np.array([long(1), long(2), long(3), None, long(4)], dtype='O') - result = lib.convert_sql_column(arr) - expected = np.array([1, 2, 3, np.nan, 4], dtype='f8') - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_bools(self): - arr = np.array([True, False, True, False], dtype='O') - result = lib.convert_sql_column(arr) - expected = np.array([True, False, True, False], dtype=bool) - self.assert_numpy_array_equal(result, expected) - - arr = np.array([True, False, None, False], dtype='O') - result = lib.convert_sql_column(arr) - expected = np.array([True, False, np.nan, False], dtype=object) - self.assert_numpy_array_equal(result, expected) - - def test_convert_sql_column_decimals(self): - from decimal import Decimal - arr = np.array([Decimal('1.5'), None, Decimal('3'), Decimal('4.2')]) - result = lib.convert_sql_column(arr) - expected = np.array([1.5, np.nan, 3, 4.2], dtype='f8') - self.assert_numpy_array_equal(result, expected) - - def test_convert_downcast_int64(self): - from pandas.io.libparsers import na_values - - arr = np.array([1, 2, 7, 8, 10], dtype=np.int64) - expected = np.array([1, 2, 7, 8, 10], dtype=np.int8) - - # default argument - result = lib.downcast_int64(arr, na_values) - self.assert_numpy_array_equal(result, expected) - - result = lib.downcast_int64(arr, na_values, use_unsigned=False) - self.assert_numpy_array_equal(result, expected) - - expected = np.array([1, 2, 7, 8, 10], dtype=np.uint8) - result = lib.downcast_int64(arr, na_values, use_unsigned=True) - self.assert_numpy_array_equal(result, expected) - - # still cast to int8 despite use_unsigned=True - # because of the negative number as an element - arr = np.array([1, 2, -7, 8, 10], dtype=np.int64) - expected = np.array([1, 2, -7, 8, 10], dtype=np.int8) - result = lib.downcast_int64(arr, na_values, use_unsigned=True) - self.assert_numpy_array_equal(result, expected) - - arr = np.array([1, 2, 7, 8, 300], dtype=np.int64) - expected = np.array([1, 2, 7, 8, 300], dtype=np.int16) - result = lib.downcast_int64(arr, na_values) - self.assert_numpy_array_equal(result, expected) - - int8_na = na_values[np.int8] - int64_na = na_values[np.int64] - arr = np.array([int64_na, 2, 3, 10, 15], dtype=np.int64) - expected = np.array([int8_na, 2, 3, 10, 15], dtype=np.int8) - result = lib.downcast_int64(arr, na_values) - self.assert_numpy_array_equal(result, expected) diff --git a/pandas/tests/types/test_missing.py b/pandas/tests/types/test_missing.py deleted file mode 100644 index 2e35f5c1badbb..0000000000000 --- a/pandas/tests/types/test_missing.py +++ /dev/null @@ -1,303 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np -from datetime import datetime -from pandas.util import testing as tm - -import pandas as pd -from pandas.core import config as cf -from pandas.compat import u -from pandas._libs.tslib import iNaT -from pandas import (NaT, Float64Index, Series, - DatetimeIndex, TimedeltaIndex, date_range) -from pandas.types.dtypes import DatetimeTZDtype -from pandas.types.missing import (array_equivalent, isnull, notnull, - na_value_for_dtype) - - -def test_notnull(): - assert notnull(1.) - assert not notnull(None) - assert not notnull(np.NaN) - - with cf.option_context("mode.use_inf_as_null", False): - assert notnull(np.inf) - assert notnull(-np.inf) - - arr = np.array([1.5, np.inf, 3.5, -np.inf]) - result = notnull(arr) - assert result.all() - - with cf.option_context("mode.use_inf_as_null", True): - assert not notnull(np.inf) - assert not notnull(-np.inf) - - arr = np.array([1.5, np.inf, 3.5, -np.inf]) - result = notnull(arr) - assert result.sum() == 2 - - with cf.option_context("mode.use_inf_as_null", False): - for s in [tm.makeFloatSeries(), tm.makeStringSeries(), - tm.makeObjectSeries(), tm.makeTimeSeries(), - tm.makePeriodSeries()]: - assert (isinstance(isnull(s), Series)) - - -class TestIsNull(tm.TestCase): - - def test_0d_array(self): - self.assertTrue(isnull(np.array(np.nan))) - self.assertFalse(isnull(np.array(0.0))) - self.assertFalse(isnull(np.array(0))) - # test object dtype - self.assertTrue(isnull(np.array(np.nan, dtype=object))) - self.assertFalse(isnull(np.array(0.0, dtype=object))) - self.assertFalse(isnull(np.array(0, dtype=object))) - - def test_isnull(self): - self.assertFalse(isnull(1.)) - self.assertTrue(isnull(None)) - self.assertTrue(isnull(np.NaN)) - self.assertTrue(float('nan')) - self.assertFalse(isnull(np.inf)) - self.assertFalse(isnull(-np.inf)) - - # series - for s in [tm.makeFloatSeries(), tm.makeStringSeries(), - tm.makeObjectSeries(), tm.makeTimeSeries(), - tm.makePeriodSeries()]: - self.assertIsInstance(isnull(s), Series) - - # frame - for df in [tm.makeTimeDataFrame(), tm.makePeriodFrame(), - tm.makeMixedDataFrame()]: - result = isnull(df) - expected = df.apply(isnull) - tm.assert_frame_equal(result, expected) - - # panel - for p in [tm.makePanel(), tm.makePeriodPanel(), - tm.add_nans(tm.makePanel())]: - result = isnull(p) - expected = p.apply(isnull) - tm.assert_panel_equal(result, expected) - - # panel 4d - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - for p in [tm.makePanel4D(), tm.add_nans_panel4d(tm.makePanel4D())]: - result = isnull(p) - expected = p.apply(isnull) - tm.assert_panel4d_equal(result, expected) - - def test_isnull_lists(self): - result = isnull([[False]]) - exp = np.array([[False]]) - tm.assert_numpy_array_equal(result, exp) - - result = isnull([[1], [2]]) - exp = np.array([[False], [False]]) - tm.assert_numpy_array_equal(result, exp) - - # list of strings / unicode - result = isnull(['foo', 'bar']) - exp = np.array([False, False]) - tm.assert_numpy_array_equal(result, exp) - - result = isnull([u('foo'), u('bar')]) - exp = np.array([False, False]) - tm.assert_numpy_array_equal(result, exp) - - def test_isnull_nat(self): - result = isnull([NaT]) - exp = np.array([True]) - tm.assert_numpy_array_equal(result, exp) - - result = isnull(np.array([NaT], dtype=object)) - exp = np.array([True]) - tm.assert_numpy_array_equal(result, exp) - - def test_isnull_numpy_nat(self): - arr = np.array([NaT, np.datetime64('NaT'), np.timedelta64('NaT'), - np.datetime64('NaT', 's')]) - result = isnull(arr) - expected = np.array([True] * 4) - tm.assert_numpy_array_equal(result, expected) - - def test_isnull_datetime(self): - self.assertFalse(isnull(datetime.now())) - self.assertTrue(notnull(datetime.now())) - - idx = date_range('1/1/1990', periods=20) - exp = np.ones(len(idx), dtype=bool) - tm.assert_numpy_array_equal(notnull(idx), exp) - - idx = np.asarray(idx) - idx[0] = iNaT - idx = DatetimeIndex(idx) - mask = isnull(idx) - self.assertTrue(mask[0]) - exp = np.array([True] + [False] * (len(idx) - 1), dtype=bool) - self.assert_numpy_array_equal(mask, exp) - - # GH 9129 - pidx = idx.to_period(freq='M') - mask = isnull(pidx) - self.assertTrue(mask[0]) - exp = np.array([True] + [False] * (len(idx) - 1), dtype=bool) - self.assert_numpy_array_equal(mask, exp) - - mask = isnull(pidx[1:]) - exp = np.zeros(len(mask), dtype=bool) - self.assert_numpy_array_equal(mask, exp) - - def test_datetime_other_units(self): - idx = pd.DatetimeIndex(['2011-01-01', 'NaT', '2011-01-02']) - exp = np.array([False, True, False]) - tm.assert_numpy_array_equal(isnull(idx), exp) - tm.assert_numpy_array_equal(notnull(idx), ~exp) - tm.assert_numpy_array_equal(isnull(idx.values), exp) - tm.assert_numpy_array_equal(notnull(idx.values), ~exp) - - for dtype in ['datetime64[D]', 'datetime64[h]', 'datetime64[m]', - 'datetime64[s]', 'datetime64[ms]', 'datetime64[us]', - 'datetime64[ns]']: - values = idx.values.astype(dtype) - - exp = np.array([False, True, False]) - tm.assert_numpy_array_equal(isnull(values), exp) - tm.assert_numpy_array_equal(notnull(values), ~exp) - - exp = pd.Series([False, True, False]) - s = pd.Series(values) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - s = pd.Series(values, dtype=object) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - - def test_timedelta_other_units(self): - idx = pd.TimedeltaIndex(['1 days', 'NaT', '2 days']) - exp = np.array([False, True, False]) - tm.assert_numpy_array_equal(isnull(idx), exp) - tm.assert_numpy_array_equal(notnull(idx), ~exp) - tm.assert_numpy_array_equal(isnull(idx.values), exp) - tm.assert_numpy_array_equal(notnull(idx.values), ~exp) - - for dtype in ['timedelta64[D]', 'timedelta64[h]', 'timedelta64[m]', - 'timedelta64[s]', 'timedelta64[ms]', 'timedelta64[us]', - 'timedelta64[ns]']: - values = idx.values.astype(dtype) - - exp = np.array([False, True, False]) - tm.assert_numpy_array_equal(isnull(values), exp) - tm.assert_numpy_array_equal(notnull(values), ~exp) - - exp = pd.Series([False, True, False]) - s = pd.Series(values) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - s = pd.Series(values, dtype=object) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - - def test_period(self): - idx = pd.PeriodIndex(['2011-01', 'NaT', '2012-01'], freq='M') - exp = np.array([False, True, False]) - tm.assert_numpy_array_equal(isnull(idx), exp) - tm.assert_numpy_array_equal(notnull(idx), ~exp) - - exp = pd.Series([False, True, False]) - s = pd.Series(idx) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - s = pd.Series(idx, dtype=object) - tm.assert_series_equal(isnull(s), exp) - tm.assert_series_equal(notnull(s), ~exp) - - -def test_array_equivalent(): - assert array_equivalent(np.array([np.nan, np.nan]), - np.array([np.nan, np.nan])) - assert array_equivalent(np.array([np.nan, 1, np.nan]), - np.array([np.nan, 1, np.nan])) - assert array_equivalent(np.array([np.nan, None], dtype='object'), - np.array([np.nan, None], dtype='object')) - assert array_equivalent(np.array([np.nan, 1 + 1j], dtype='complex'), - np.array([np.nan, 1 + 1j], dtype='complex')) - assert not array_equivalent( - np.array([np.nan, 1 + 1j], dtype='complex'), np.array( - [np.nan, 1 + 2j], dtype='complex')) - assert not array_equivalent( - np.array([np.nan, 1, np.nan]), np.array([np.nan, 2, np.nan])) - assert not array_equivalent( - np.array(['a', 'b', 'c', 'd']), np.array(['e', 'e'])) - assert array_equivalent(Float64Index([0, np.nan]), - Float64Index([0, np.nan])) - assert not array_equivalent( - Float64Index([0, np.nan]), Float64Index([1, np.nan])) - assert array_equivalent(DatetimeIndex([0, np.nan]), - DatetimeIndex([0, np.nan])) - assert not array_equivalent( - DatetimeIndex([0, np.nan]), DatetimeIndex([1, np.nan])) - assert array_equivalent(TimedeltaIndex([0, np.nan]), - TimedeltaIndex([0, np.nan])) - assert not array_equivalent( - TimedeltaIndex([0, np.nan]), TimedeltaIndex([1, np.nan])) - assert array_equivalent(DatetimeIndex([0, np.nan], tz='US/Eastern'), - DatetimeIndex([0, np.nan], tz='US/Eastern')) - assert not array_equivalent( - DatetimeIndex([0, np.nan], tz='US/Eastern'), DatetimeIndex( - [1, np.nan], tz='US/Eastern')) - assert not array_equivalent( - DatetimeIndex([0, np.nan]), DatetimeIndex( - [0, np.nan], tz='US/Eastern')) - assert not array_equivalent( - DatetimeIndex([0, np.nan], tz='CET'), DatetimeIndex( - [0, np.nan], tz='US/Eastern')) - assert not array_equivalent( - DatetimeIndex([0, np.nan]), TimedeltaIndex([0, np.nan])) - - -def test_array_equivalent_compat(): - # see gh-13388 - m = np.array([(1, 2), (3, 4)], dtype=[('a', int), ('b', float)]) - n = np.array([(1, 2), (3, 4)], dtype=[('a', int), ('b', float)]) - assert (array_equivalent(m, n, strict_nan=True)) - assert (array_equivalent(m, n, strict_nan=False)) - - m = np.array([(1, 2), (3, 4)], dtype=[('a', int), ('b', float)]) - n = np.array([(1, 2), (4, 3)], dtype=[('a', int), ('b', float)]) - assert (not array_equivalent(m, n, strict_nan=True)) - assert (not array_equivalent(m, n, strict_nan=False)) - - m = np.array([(1, 2), (3, 4)], dtype=[('a', int), ('b', float)]) - n = np.array([(1, 2), (3, 4)], dtype=[('b', int), ('a', float)]) - assert (not array_equivalent(m, n, strict_nan=True)) - assert (not array_equivalent(m, n, strict_nan=False)) - - -def test_array_equivalent_str(): - for dtype in ['O', 'S', 'U']: - assert array_equivalent(np.array(['A', 'B'], dtype=dtype), - np.array(['A', 'B'], dtype=dtype)) - assert not array_equivalent(np.array(['A', 'B'], dtype=dtype), - np.array(['A', 'X'], dtype=dtype)) - - -def test_na_value_for_dtype(): - for dtype in [np.dtype('M8[ns]'), np.dtype('m8[ns]'), - DatetimeTZDtype('datetime64[ns, US/Eastern]')]: - assert na_value_for_dtype(dtype) is NaT - - for dtype in ['u1', 'u2', 'u4', 'u8', - 'i1', 'i2', 'i4', 'i8']: - assert na_value_for_dtype(np.dtype(dtype)) == 0 - - for dtype in ['bool']: - assert na_value_for_dtype(np.dtype(dtype)) is False - - for dtype in ['f2', 'f4', 'f8']: - assert np.isnan(na_value_for_dtype(np.dtype(dtype))) - - for dtype in ['O']: - assert np.isnan(na_value_for_dtype(np.dtype(dtype))) diff --git a/pandas/tests/util/__init__.py b/pandas/tests/util/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/util/conftest.py b/pandas/tests/util/conftest.py new file mode 100644 index 0000000000000..5eff49ab774b5 --- /dev/null +++ b/pandas/tests/util/conftest.py @@ -0,0 +1,26 @@ +import pytest + + +@pytest.fixture(params=[True, False]) +def check_dtype(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def check_exact(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def check_index_type(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def check_less_precise(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def check_categorical(request): + return request.param diff --git a/pandas/tests/util/test_assert_almost_equal.py b/pandas/tests/util/test_assert_almost_equal.py new file mode 100644 index 0000000000000..afee9c008295f --- /dev/null +++ b/pandas/tests/util/test_assert_almost_equal.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pytest + +from pandas import DataFrame, Index, Series, Timestamp +from pandas.util.testing import assert_almost_equal + + +def _assert_almost_equal_both(a, b, **kwargs): + """ + Check that two objects are approximately equal. + + This check is performed commutatively. + + Parameters + ---------- + a : object + The first object to compare. + b : object + The second object to compare. + kwargs : dict + The arguments passed to `assert_almost_equal`. + """ + assert_almost_equal(a, b, **kwargs) + assert_almost_equal(b, a, **kwargs) + + +def _assert_not_almost_equal(a, b, **kwargs): + """ + Check that two objects are not approximately equal. + + Parameters + ---------- + a : object + The first object to compare. + b : object + The second object to compare. + kwargs : dict + The arguments passed to `assert_almost_equal`. + """ + try: + assert_almost_equal(a, b, **kwargs) + msg = ("{a} and {b} were approximately equal " + "when they shouldn't have been").format(a=a, b=b) + pytest.fail(msg=msg) + except AssertionError: + pass + + +def _assert_not_almost_equal_both(a, b, **kwargs): + """ + Check that two objects are not approximately equal. + + This check is performed commutatively. + + Parameters + ---------- + a : object + The first object to compare. + b : object + The second object to compare. + kwargs : dict + The arguments passed to `tm.assert_almost_equal`. + """ + _assert_not_almost_equal(a, b, **kwargs) + _assert_not_almost_equal(b, a, **kwargs) + + +@pytest.mark.parametrize("a,b", [ + (1.1, 1.1), (1.1, 1.100001), (np.int16(1), 1.000001), + (np.float64(1.1), 1.1), (np.uint32(5), 5), +]) +def test_assert_almost_equal_numbers(a, b): + _assert_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + (1.1, 1), (1.1, True), (1, 2), (1.0001, np.int16(1)), +]) +def test_assert_not_almost_equal_numbers(a, b): + _assert_not_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + (0, 0), (0, 0.0), (0, np.float64(0)), (0.000001, 0), +]) +def test_assert_almost_equal_numbers_with_zeros(a, b): + _assert_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + (0.001, 0), (1, 0), +]) +def test_assert_not_almost_equal_numbers_with_zeros(a, b): + _assert_not_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + (1, "abc"), (1, [1, ]), (1, object()), +]) +def test_assert_not_almost_equal_numbers_with_mixed(a, b): + _assert_not_almost_equal_both(a, b) + + +@pytest.mark.parametrize( + "left_dtype", ["M8[ns]", "m8[ns]", "float64", "int64", "object"]) +@pytest.mark.parametrize( + "right_dtype", ["M8[ns]", "m8[ns]", "float64", "int64", "object"]) +def test_assert_almost_equal_edge_case_ndarrays(left_dtype, right_dtype): + # Empty compare. + _assert_almost_equal_both(np.array([], dtype=left_dtype), + np.array([], dtype=right_dtype), + check_dtype=False) + + +def test_assert_almost_equal_dicts(): + _assert_almost_equal_both({"a": 1, "b": 2}, {"a": 1, "b": 2}) + + +@pytest.mark.parametrize("a,b", [ + ({"a": 1, "b": 2}, {"a": 1, "b": 3}), + ({"a": 1, "b": 2}, {"a": 1, "b": 2, "c": 3}), + ({"a": 1}, 1), ({"a": 1}, "abc"), ({"a": 1}, [1, ]), +]) +def test_assert_not_almost_equal_dicts(a, b): + _assert_not_almost_equal_both(a, b) + + +@pytest.mark.parametrize("val", [1, 2]) +def test_assert_almost_equal_dict_like_object(val): + dict_val = 1 + real_dict = dict(a=val) + + class DictLikeObj(object): + def keys(self): + return "a", + + def __getitem__(self, item): + if item == "a": + return dict_val + + func = (_assert_almost_equal_both if val == dict_val + else _assert_not_almost_equal_both) + func(real_dict, DictLikeObj(), check_dtype=False) + + +def test_assert_almost_equal_strings(): + _assert_almost_equal_both("abc", "abc") + + +@pytest.mark.parametrize("a,b", [ + ("abc", "abcd"), ("abc", "abd"), ("abc", 1), ("abc", [1, ]), +]) +def test_assert_not_almost_equal_strings(a, b): + _assert_not_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + ([1, 2, 3], [1, 2, 3]), (np.array([1, 2, 3]), np.array([1, 2, 3])), +]) +def test_assert_almost_equal_iterables(a, b): + _assert_almost_equal_both(a, b) + + +@pytest.mark.parametrize("a,b", [ + # Class is different. + (np.array([1, 2, 3]), [1, 2, 3]), + + # Dtype is different. + (np.array([1, 2, 3]), np.array([1., 2., 3.])), + + # Can't compare generators. + (iter([1, 2, 3]), [1, 2, 3]), ([1, 2, 3], [1, 2, 4]), + ([1, 2, 3], [1, 2, 3, 4]), ([1, 2, 3], 1), +]) +def test_assert_not_almost_equal_iterables(a, b): + _assert_not_almost_equal(a, b) + + +def test_assert_almost_equal_null(): + _assert_almost_equal_both(None, None) + + +@pytest.mark.parametrize("a,b", [ + (None, np.NaN), (None, 0), (np.NaN, 0), +]) +def test_assert_not_almost_equal_null(a, b): + _assert_not_almost_equal(a, b) + + +@pytest.mark.parametrize("a,b", [ + (np.inf, np.inf), (np.inf, float("inf")), + (np.array([np.inf, np.nan, -np.inf]), + np.array([np.inf, np.nan, -np.inf])), + (np.array([np.inf, None, -np.inf], dtype=np.object_), + np.array([np.inf, np.nan, -np.inf], dtype=np.object_)), +]) +def test_assert_almost_equal_inf(a, b): + _assert_almost_equal_both(a, b) + + +def test_assert_not_almost_equal_inf(): + _assert_not_almost_equal_both(np.inf, 0) + + +@pytest.mark.parametrize("a,b", [ + (Index([1., 1.1]), Index([1., 1.100001])), + (Series([1., 1.1]), Series([1., 1.100001])), + (np.array([1.1, 2.000001]), np.array([1.1, 2.0])), + (DataFrame({"a": [1., 1.1]}), DataFrame({"a": [1., 1.100001]})) +]) +def test_assert_almost_equal_pandas(a, b): + _assert_almost_equal_both(a, b) + + +def test_assert_almost_equal_object(): + a = [Timestamp("2011-01-01"), Timestamp("2011-01-01")] + b = [Timestamp("2011-01-01"), Timestamp("2011-01-01")] + _assert_almost_equal_both(a, b) + + +def test_assert_almost_equal_value_mismatch(): + msg = "expected 2\\.00000 but got 1\\.00000, with decimal 5" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(1, 2) + + +@pytest.mark.parametrize("a,b,klass1,klass2", [ + (np.array([1]), 1, "ndarray", "int"), + (1, np.array([1]), "int", "ndarray"), +]) +def test_assert_almost_equal_class_mismatch(a, b, klass1, klass2): + msg = """numpy array are different + +numpy array classes are different +\\[left\\]: {klass1} +\\[right\\]: {klass2}""".format(klass1=klass1, klass2=klass2) + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(a, b) + + +def test_assert_almost_equal_value_mismatch1(): + msg = """numpy array are different + +numpy array values are different \\(66\\.66667 %\\) +\\[left\\]: \\[nan, 2\\.0, 3\\.0\\] +\\[right\\]: \\[1\\.0, nan, 3\\.0\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([np.nan, 2, 3]), + np.array([1, np.nan, 3])) + + +def test_assert_almost_equal_value_mismatch2(): + msg = """numpy array are different + +numpy array values are different \\(50\\.0 %\\) +\\[left\\]: \\[1, 2\\] +\\[right\\]: \\[1, 3\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([1, 2]), np.array([1, 3])) + + +def test_assert_almost_equal_value_mismatch3(): + msg = """numpy array are different + +numpy array values are different \\(16\\.66667 %\\) +\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\], \\[5, 6\\]\\] +\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\], \\[5, 6\\]\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([[1, 2], [3, 4], [5, 6]]), + np.array([[1, 3], [3, 4], [5, 6]])) + + +def test_assert_almost_equal_value_mismatch4(): + msg = """numpy array are different + +numpy array values are different \\(25\\.0 %\\) +\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\]\\] +\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\]\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([[1, 2], [3, 4]]), + np.array([[1, 3], [3, 4]])) + + +def test_assert_almost_equal_shape_mismatch_override(): + msg = """Index are different + +Index shapes are different +\\[left\\]: \\(2L*,\\) +\\[right\\]: \\(3L*,\\)""" + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([1, 2]), + np.array([3, 4, 5]), + obj="Index") + + +def test_assert_almost_equal_unicode(): + # see gh-20503 + msg = """numpy array are different + +numpy array values are different \\(33\\.33333 %\\) +\\[left\\]: \\[á, à, ä\\] +\\[right\\]: \\[á, à, å\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(np.array([u"á", u"à", u"ä"]), + np.array([u"á", u"à", u"å"])) + + +def test_assert_almost_equal_timestamp(): + a = np.array([Timestamp("2011-01-01"), Timestamp("2011-01-01")]) + b = np.array([Timestamp("2011-01-01"), Timestamp("2011-01-02")]) + + msg = """numpy array are different + +numpy array values are different \\(50\\.0 %\\) +\\[left\\]: \\[2011-01-01 00:00:00, 2011-01-01 00:00:00\\] +\\[right\\]: \\[2011-01-01 00:00:00, 2011-01-02 00:00:00\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal(a, b) + + +def test_assert_almost_equal_iterable_length_mismatch(): + msg = """Iterable are different + +Iterable length are different +\\[left\\]: 2 +\\[right\\]: 3""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal([1, 2], [3, 4, 5]) + + +def test_assert_almost_equal_iterable_values_mismatch(): + msg = """Iterable are different + +Iterable values are different \\(50\\.0 %\\) +\\[left\\]: \\[1, 2\\] +\\[right\\]: \\[1, 3\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_almost_equal([1, 2], [1, 3]) diff --git a/pandas/tests/util/test_assert_categorical_equal.py b/pandas/tests/util/test_assert_categorical_equal.py new file mode 100644 index 0000000000000..04c8301027039 --- /dev/null +++ b/pandas/tests/util/test_assert_categorical_equal.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas import Categorical +from pandas.util.testing import assert_categorical_equal + + +@pytest.mark.parametrize("c", [ + Categorical([1, 2, 3, 4]), + Categorical([1, 2, 3, 4], categories=[1, 2, 3, 4, 5]), +]) +def test_categorical_equal(c): + assert_categorical_equal(c, c) + + +@pytest.mark.parametrize("check_category_order", [True, False]) +def test_categorical_equal_order_mismatch(check_category_order): + c1 = Categorical([1, 2, 3, 4], categories=[1, 2, 3, 4]) + c2 = Categorical([1, 2, 3, 4], categories=[4, 3, 2, 1]) + kwargs = dict(check_category_order=check_category_order) + + if check_category_order: + msg = """Categorical\\.categories are different + +Categorical\\.categories values are different \\(100\\.0 %\\) +\\[left\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[4, 3, 2, 1\\], dtype='int64'\\)""" + with pytest.raises(AssertionError, match=msg): + assert_categorical_equal(c1, c2, **kwargs) + else: + assert_categorical_equal(c1, c2, **kwargs) + + +def test_categorical_equal_categories_mismatch(): + msg = """Categorical\\.categories are different + +Categorical\\.categories values are different \\(25\\.0 %\\) +\\[left\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[1, 2, 3, 5\\], dtype='int64'\\)""" + + c1 = Categorical([1, 2, 3, 4]) + c2 = Categorical([1, 2, 3, 5]) + + with pytest.raises(AssertionError, match=msg): + assert_categorical_equal(c1, c2) + + +def test_categorical_equal_codes_mismatch(): + categories = [1, 2, 3, 4] + msg = """Categorical\\.codes are different + +Categorical\\.codes values are different \\(50\\.0 %\\) +\\[left\\]: \\[0, 1, 3, 2\\] +\\[right\\]: \\[0, 1, 2, 3\\]""" + + c1 = Categorical([1, 2, 4, 3], categories=categories) + c2 = Categorical([1, 2, 3, 4], categories=categories) + + with pytest.raises(AssertionError, match=msg): + assert_categorical_equal(c1, c2) + + +def test_categorical_equal_ordered_mismatch(): + data = [1, 2, 3, 4] + msg = """Categorical are different + +Attribute "ordered" are different +\\[left\\]: False +\\[right\\]: True""" + + c1 = Categorical(data, ordered=False) + c2 = Categorical(data, ordered=True) + + with pytest.raises(AssertionError, match=msg): + assert_categorical_equal(c1, c2) + + +@pytest.mark.parametrize("obj", ["index", "foo", "pandas"]) +def test_categorical_equal_object_override(obj): + data = [1, 2, 3, 4] + msg = """{obj} are different + +Attribute "ordered" are different +\\[left\\]: False +\\[right\\]: True""".format(obj=obj) + + c1 = Categorical(data, ordered=False) + c2 = Categorical(data, ordered=True) + + with pytest.raises(AssertionError, match=msg): + assert_categorical_equal(c1, c2, obj=obj) diff --git a/pandas/tests/util/test_assert_extension_array_equal.py b/pandas/tests/util/test_assert_extension_array_equal.py new file mode 100644 index 0000000000000..3149078a56783 --- /dev/null +++ b/pandas/tests/util/test_assert_extension_array_equal.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pytest + +from pandas.core.arrays.sparse import SparseArray +from pandas.util.testing import assert_extension_array_equal + + +@pytest.mark.parametrize("kwargs", [ + dict(), # Default is check_exact=False + dict(check_exact=False), dict(check_exact=True) +]) +def test_assert_extension_array_equal_not_exact(kwargs): + # see gh-23709 + arr1 = SparseArray([-0.17387645482451206, 0.3414148016424936]) + arr2 = SparseArray([-0.17387645482451206, 0.3414148016424937]) + + if kwargs.get("check_exact", False): + msg = """\ +ExtensionArray are different + +ExtensionArray values are different \\(50\\.0 %\\) +\\[left\\]: \\[-0\\.17387645482.*, 0\\.341414801642.*\\] +\\[right\\]: \\[-0\\.17387645482.*, 0\\.341414801642.*\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_extension_array_equal(arr1, arr2, **kwargs) + else: + assert_extension_array_equal(arr1, arr2, **kwargs) + + +@pytest.mark.parametrize("check_less_precise", [ + True, False, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 +]) +def test_assert_extension_array_equal_less_precise(check_less_precise): + arr1 = SparseArray([0.5, 0.123456]) + arr2 = SparseArray([0.5, 0.123457]) + + kwargs = dict(check_less_precise=check_less_precise) + + if check_less_precise is False or check_less_precise >= 5: + msg = """\ +ExtensionArray are different + +ExtensionArray values are different \\(50\\.0 %\\) +\\[left\\]: \\[0\\.5, 0\\.123456\\] +\\[right\\]: \\[0\\.5, 0\\.123457\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_extension_array_equal(arr1, arr2, **kwargs) + else: + assert_extension_array_equal(arr1, arr2, **kwargs) + + +def test_assert_extension_array_equal_dtype_mismatch(check_dtype): + end = 5 + kwargs = dict(check_dtype=check_dtype) + + arr1 = SparseArray(np.arange(end, dtype="int64")) + arr2 = SparseArray(np.arange(end, dtype="int32")) + + if check_dtype: + msg = """\ +ExtensionArray are different + +Attribute "dtype" are different +\\[left\\]: Sparse\\[int64, 0\\] +\\[right\\]: Sparse\\[int32, 0\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_extension_array_equal(arr1, arr2, **kwargs) + else: + assert_extension_array_equal(arr1, arr2, **kwargs) + + +def test_assert_extension_array_equal_missing_values(): + arr1 = SparseArray([np.nan, 1, 2, np.nan]) + arr2 = SparseArray([np.nan, 1, 2, 3]) + + msg = """\ +ExtensionArray NA mask are different + +ExtensionArray NA mask values are different \\(25\\.0 %\\) +\\[left\\]: \\[True, False, False, True\\] +\\[right\\]: \\[True, False, False, False\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_extension_array_equal(arr1, arr2) + + +@pytest.mark.parametrize("side", ["left", "right"]) +def test_assert_extension_array_equal_non_extension_array(side): + numpy_array = np.arange(5) + extension_array = SparseArray(numpy_array) + + msg = "{side} is not an ExtensionArray".format(side=side) + args = ((numpy_array, extension_array) if side == "left" + else (extension_array, numpy_array)) + + with pytest.raises(AssertionError, match=msg): + assert_extension_array_equal(*args) diff --git a/pandas/tests/util/test_assert_frame_equal.py b/pandas/tests/util/test_assert_frame_equal.py new file mode 100644 index 0000000000000..1a941c0f0c265 --- /dev/null +++ b/pandas/tests/util/test_assert_frame_equal.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas import DataFrame +from pandas.util.testing import assert_frame_equal + + +@pytest.fixture(params=[True, False]) +def by_blocks(request): + return request.param + + +def _assert_frame_equal_both(a, b, **kwargs): + """ + Check that two DataFrame equal. + + This check is performed commutatively. + + Parameters + ---------- + a : DataFrame + The first DataFrame to compare. + b : DataFrame + The second DataFrame to compare. + kwargs : dict + The arguments passed to `assert_frame_equal`. + """ + assert_frame_equal(a, b, **kwargs) + assert_frame_equal(b, a, **kwargs) + + +def _assert_not_frame_equal(a, b, **kwargs): + """ + Check that two DataFrame are not equal. + + Parameters + ---------- + a : DataFrame + The first DataFrame to compare. + b : DataFrame + The second DataFrame to compare. + kwargs : dict + The arguments passed to `assert_frame_equal`. + """ + try: + assert_frame_equal(a, b, **kwargs) + msg = "The two DataFrames were equal when they shouldn't have been" + + pytest.fail(msg=msg) + except AssertionError: + pass + + +def _assert_not_frame_equal_both(a, b, **kwargs): + """ + Check that two DataFrame are not equal. + + This check is performed commutatively. + + Parameters + ---------- + a : DataFrame + The first DataFrame to compare. + b : DataFrame + The second DataFrame to compare. + kwargs : dict + The arguments passed to `assert_frame_equal`. + """ + _assert_not_frame_equal(a, b, **kwargs) + _assert_not_frame_equal(b, a, **kwargs) + + +@pytest.mark.parametrize("check_like", [True, False]) +def test_frame_equal_row_order_mismatch(check_like): + df1 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, + index=["a", "b", "c"]) + df2 = DataFrame({"A": [3, 2, 1], "B": [6, 5, 4]}, + index=["c", "b", "a"]) + + if not check_like: # Do not ignore row-column orderings. + msg = "DataFrame.index are different" + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2, check_like=check_like) + else: + _assert_frame_equal_both(df1, df2, check_like=check_like) + + +@pytest.mark.parametrize("df1,df2", [ + (DataFrame({"A": [1, 2, 3]}), DataFrame({"A": [1, 2, 3, 4]})), + (DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}), DataFrame({"A": [1, 2, 3]})), +]) +def test_frame_equal_shape_mismatch(df1, df2): + msg = "DataFrame are different" + + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2) + + +@pytest.mark.parametrize("df1,df2,msg", [ + # Index + (DataFrame.from_records({"a": [1, 2], + "c": ["l1", "l2"]}, index=["a"]), + DataFrame.from_records({"a": [1.0, 2.0], + "c": ["l1", "l2"]}, index=["a"]), + "DataFrame\\.index are different"), + + # MultiIndex + (DataFrame.from_records({"a": [1, 2], "b": [2.1, 1.5], + "c": ["l1", "l2"]}, index=["a", "b"]), + DataFrame.from_records({"a": [1.0, 2.0], "b": [2.1, 1.5], + "c": ["l1", "l2"]}, index=["a", "b"]), + "MultiIndex level \\[0\\] are different") +]) +def test_frame_equal_index_dtype_mismatch(df1, df2, msg, check_index_type): + kwargs = dict(check_index_type=check_index_type) + + if check_index_type: + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2, **kwargs) + else: + assert_frame_equal(df1, df2, **kwargs) + + +def test_empty_dtypes(check_dtype): + columns = ["col1", "col2"] + df1 = DataFrame(columns=columns) + df2 = DataFrame(columns=columns) + + kwargs = dict(check_dtype=check_dtype) + df1["col1"] = df1["col1"].astype("int64") + + if check_dtype: + msg = "Attributes are different" + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2, **kwargs) + else: + assert_frame_equal(df1, df2, **kwargs) + + +def test_frame_equal_index_mismatch(): + msg = """DataFrame\\.index are different + +DataFrame\\.index values are different \\(33\\.33333 %\\) +\\[left\\]: Index\\(\\[u?'a', u?'b', u?'c'\\], dtype='object'\\) +\\[right\\]: Index\\(\\[u?'a', u?'b', u?'d'\\], dtype='object'\\)""" + + df1 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, + index=["a", "b", "c"]) + df2 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, + index=["a", "b", "d"]) + + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2) + + +def test_frame_equal_columns_mismatch(): + msg = """DataFrame\\.columns are different + +DataFrame\\.columns values are different \\(50\\.0 %\\) +\\[left\\]: Index\\(\\[u?'A', u?'B'\\], dtype='object'\\) +\\[right\\]: Index\\(\\[u?'A', u?'b'\\], dtype='object'\\)""" + + df1 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}, + index=["a", "b", "c"]) + df2 = DataFrame({"A": [1, 2, 3], "b": [4, 5, 6]}, + index=["a", "b", "c"]) + + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2) + + +def test_frame_equal_block_mismatch(by_blocks): + msg = """DataFrame\\.iloc\\[:, 1\\] are different + +DataFrame\\.iloc\\[:, 1\\] values are different \\(33\\.33333 %\\) +\\[left\\]: \\[4, 5, 6\\] +\\[right\\]: \\[4, 5, 7\\]""" + + df1 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + df2 = DataFrame({"A": [1, 2, 3], "B": [4, 5, 7]}) + + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2, by_blocks=by_blocks) + + +@pytest.mark.parametrize("df1,df2,msg", [ + (DataFrame({"A": [u"á", u"à", u"ä"], "E": [u"é", u"è", u"ë"]}), + DataFrame({"A": [u"á", u"à", u"ä"], "E": [u"é", u"è", u"e̊"]}), + """DataFrame\\.iloc\\[:, 1\\] are different + +DataFrame\\.iloc\\[:, 1\\] values are different \\(33\\.33333 %\\) +\\[left\\]: \\[é, è, ë\\] +\\[right\\]: \\[é, è, e̊\\]"""), + (DataFrame({"A": [u"á", u"à", u"ä"], "E": [u"é", u"è", u"ë"]}), + DataFrame({"A": ["a", "a", "a"], "E": ["e", "e", "e"]}), + """DataFrame\\.iloc\\[:, 0\\] are different + +DataFrame\\.iloc\\[:, 0\\] values are different \\(100\\.0 %\\) +\\[left\\]: \\[á, à, ä\\] +\\[right\\]: \\[a, a, a\\]"""), +]) +def test_frame_equal_unicode(df1, df2, msg, by_blocks): + # see gh-20503 + # + # Test ensures that `assert_frame_equals` raises the right exception + # when comparing DataFrames containing differing unicode objects. + with pytest.raises(AssertionError, match=msg): + assert_frame_equal(df1, df2, by_blocks=by_blocks) diff --git a/pandas/tests/util/test_assert_index_equal.py b/pandas/tests/util/test_assert_index_equal.py new file mode 100644 index 0000000000000..b5409bf7cd2bf --- /dev/null +++ b/pandas/tests/util/test_assert_index_equal.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pytest + +from pandas import Categorical, Index, MultiIndex, NaT +from pandas.util.testing import assert_index_equal + + +def test_index_equal_levels_mismatch(): + msg = """Index are different + +Index levels are different +\\[left\\]: 1, Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) +\\[right\\]: 2, MultiIndex\\(levels=\\[\\[u?'A', u?'B'\\], \\[1, 2, 3, 4\\]\\], + codes=\\[\\[0, 0, 1, 1\\], \\[0, 1, 2, 3\\]\\]\\)""" + + idx1 = Index([1, 2, 3]) + idx2 = MultiIndex.from_tuples([("A", 1), ("A", 2), + ("B", 3), ("B", 4)]) + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, exact=False) + + +def test_index_equal_values_mismatch(check_exact): + msg = """MultiIndex level \\[1\\] are different + +MultiIndex level \\[1\\] values are different \\(25\\.0 %\\) +\\[left\\]: Int64Index\\(\\[2, 2, 3, 4\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" + + idx1 = MultiIndex.from_tuples([("A", 2), ("A", 2), + ("B", 3), ("B", 4)]) + idx2 = MultiIndex.from_tuples([("A", 1), ("A", 2), + ("B", 3), ("B", 4)]) + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, check_exact=check_exact) + + +def test_index_equal_length_mismatch(check_exact): + msg = """Index are different + +Index length are different +\\[left\\]: 3, Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) +\\[right\\]: 4, Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" + + idx1 = Index([1, 2, 3]) + idx2 = Index([1, 2, 3, 4]) + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, check_exact=check_exact) + + +def test_index_equal_class_mismatch(check_exact): + msg = """Index are different + +Index classes are different +\\[left\\]: Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) +\\[right\\]: Float64Index\\(\\[1\\.0, 2\\.0, 3\\.0\\], dtype='float64'\\)""" + + idx1 = Index([1, 2, 3]) + idx2 = Index([1, 2, 3.0]) + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, exact=True, check_exact=check_exact) + + +def test_index_equal_values_close(check_exact): + idx1 = Index([1, 2, 3.]) + idx2 = Index([1, 2, 3.0000000001]) + + if check_exact: + msg = """Index are different + +Index values are different \\(33\\.33333 %\\) +\\[left\\]: Float64Index\\(\\[1.0, 2.0, 3.0], dtype='float64'\\) +\\[right\\]: Float64Index\\(\\[1.0, 2.0, 3.0000000001\\], dtype='float64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, check_exact=check_exact) + else: + assert_index_equal(idx1, idx2, check_exact=check_exact) + + +def test_index_equal_values_less_close(check_exact, check_less_precise): + idx1 = Index([1, 2, 3.]) + idx2 = Index([1, 2, 3.0001]) + kwargs = dict(check_exact=check_exact, + check_less_precise=check_less_precise) + + if check_exact or not check_less_precise: + msg = """Index are different + +Index values are different \\(33\\.33333 %\\) +\\[left\\]: Float64Index\\(\\[1.0, 2.0, 3.0], dtype='float64'\\) +\\[right\\]: Float64Index\\(\\[1.0, 2.0, 3.0001\\], dtype='float64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, **kwargs) + else: + assert_index_equal(idx1, idx2, **kwargs) + + +def test_index_equal_values_too_far(check_exact, check_less_precise): + idx1 = Index([1, 2, 3]) + idx2 = Index([1, 2, 4]) + kwargs = dict(check_exact=check_exact, + check_less_precise=check_less_precise) + + msg = """Index are different + +Index values are different \\(33\\.33333 %\\) +\\[left\\]: Int64Index\\(\\[1, 2, 3\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[1, 2, 4\\], dtype='int64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, **kwargs) + + +def test_index_equal_level_values_mismatch(check_exact, check_less_precise): + idx1 = MultiIndex.from_tuples([("A", 2), ("A", 2), + ("B", 3), ("B", 4)]) + idx2 = MultiIndex.from_tuples([("A", 1), ("A", 2), + ("B", 3), ("B", 4)]) + kwargs = dict(check_exact=check_exact, + check_less_precise=check_less_precise) + + msg = """MultiIndex level \\[1\\] are different + +MultiIndex level \\[1\\] values are different \\(25\\.0 %\\) +\\[left\\]: Int64Index\\(\\[2, 2, 3, 4\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, **kwargs) + + +@pytest.mark.parametrize("name1,name2", [ + (None, "x"), ("x", "x"), (np.nan, np.nan), (NaT, NaT), (np.nan, NaT) +]) +def test_index_equal_names(name1, name2): + msg = """Index are different + +Attribute "names" are different +\\[left\\]: \\[{name1}\\] +\\[right\\]: \\[{name2}\\]""" + + idx1 = Index([1, 2, 3], name=name1) + idx2 = Index([1, 2, 3], name=name2) + + if name1 == name2 or name1 is name2: + assert_index_equal(idx1, idx2) + else: + name1 = "u?'x'" if name1 == "x" else name1 + name2 = "u?'x'" if name2 == "x" else name2 + msg = msg.format(name1=name1, name2=name2) + + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2) + + +def test_index_equal_category_mismatch(check_categorical): + msg = """Index are different + +Attribute "dtype" are different +\\[left\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\) +\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], \ +ordered=False\\)""" + + idx1 = Index(Categorical(["a", "b"])) + idx2 = Index(Categorical(["a", "b"], categories=["a", "b", "c"])) + + if check_categorical: + with pytest.raises(AssertionError, match=msg): + assert_index_equal(idx1, idx2, check_categorical=check_categorical) + else: + assert_index_equal(idx1, idx2, check_categorical=check_categorical) diff --git a/pandas/tests/util/test_assert_interval_array_equal.py b/pandas/tests/util/test_assert_interval_array_equal.py new file mode 100644 index 0000000000000..c81a27f9b3f19 --- /dev/null +++ b/pandas/tests/util/test_assert_interval_array_equal.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas import interval_range +from pandas.util.testing import assert_interval_array_equal + + +@pytest.mark.parametrize("kwargs", [ + dict(start=0, periods=4), + dict(start=1, periods=5), + dict(start=5, end=10, closed="left"), +]) +def test_interval_array_equal(kwargs): + arr = interval_range(**kwargs).values + assert_interval_array_equal(arr, arr) + + +def test_interval_array_equal_closed_mismatch(): + kwargs = dict(start=0, periods=5) + arr1 = interval_range(closed="left", **kwargs).values + arr2 = interval_range(closed="right", **kwargs).values + + msg = """\ +IntervalArray are different + +Attribute "closed" are different +\\[left\\]: left +\\[right\\]: right""" + + with pytest.raises(AssertionError, match=msg): + assert_interval_array_equal(arr1, arr2) + + +def test_interval_array_equal_periods_mismatch(): + kwargs = dict(start=0) + arr1 = interval_range(periods=5, **kwargs).values + arr2 = interval_range(periods=6, **kwargs).values + + msg = """\ +IntervalArray.left are different + +IntervalArray.left length are different +\\[left\\]: 5, Int64Index\\(\\[0, 1, 2, 3, 4\\], dtype='int64'\\) +\\[right\\]: 6, Int64Index\\(\\[0, 1, 2, 3, 4, 5\\], dtype='int64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_interval_array_equal(arr1, arr2) + + +def test_interval_array_equal_end_mismatch(): + kwargs = dict(start=0, periods=5) + arr1 = interval_range(end=10, **kwargs).values + arr2 = interval_range(end=20, **kwargs).values + + msg = """\ +IntervalArray.left are different + +IntervalArray.left values are different \\(80.0 %\\) +\\[left\\]: Int64Index\\(\\[0, 2, 4, 6, 8\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[0, 4, 8, 12, 16\\], dtype='int64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_interval_array_equal(arr1, arr2) + + +def test_interval_array_equal_start_mismatch(): + kwargs = dict(periods=4) + arr1 = interval_range(start=0, **kwargs).values + arr2 = interval_range(start=1, **kwargs).values + + msg = """\ +IntervalArray.left are different + +IntervalArray.left values are different \\(100.0 %\\) +\\[left\\]: Int64Index\\(\\[0, 1, 2, 3\\], dtype='int64'\\) +\\[right\\]: Int64Index\\(\\[1, 2, 3, 4\\], dtype='int64'\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_interval_array_equal(arr1, arr2) diff --git a/pandas/tests/util/test_assert_numpy_array_equal.py b/pandas/tests/util/test_assert_numpy_array_equal.py new file mode 100644 index 0000000000000..99037fcf96194 --- /dev/null +++ b/pandas/tests/util/test_assert_numpy_array_equal.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pytest + +from pandas import Timestamp +from pandas.util.testing import assert_numpy_array_equal + + +def test_assert_numpy_array_equal_shape_mismatch(): + msg = """numpy array are different + +numpy array shapes are different +\\[left\\]: \\(2L*,\\) +\\[right\\]: \\(3L*,\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([1, 2]), np.array([3, 4, 5])) + + +def test_assert_numpy_array_equal_bad_type(): + expected = "Expected type" + + with pytest.raises(AssertionError, match=expected): + assert_numpy_array_equal(1, 2) + + +@pytest.mark.parametrize("a,b,klass1,klass2", [ + (np.array([1]), 1, "ndarray", "int"), + (1, np.array([1]), "int", "ndarray"), +]) +def test_assert_numpy_array_equal_class_mismatch(a, b, klass1, klass2): + msg = """numpy array are different + +numpy array classes are different +\\[left\\]: {klass1} +\\[right\\]: {klass2}""".format(klass1=klass1, klass2=klass2) + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(a, b) + + +def test_assert_numpy_array_equal_value_mismatch1(): + msg = """numpy array are different + +numpy array values are different \\(66\\.66667 %\\) +\\[left\\]: \\[nan, 2\\.0, 3\\.0\\] +\\[right\\]: \\[1\\.0, nan, 3\\.0\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([np.nan, 2, 3]), + np.array([1, np.nan, 3])) + + +def test_assert_numpy_array_equal_value_mismatch2(): + msg = """numpy array are different + +numpy array values are different \\(50\\.0 %\\) +\\[left\\]: \\[1, 2\\] +\\[right\\]: \\[1, 3\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([1, 2]), np.array([1, 3])) + + +def test_assert_numpy_array_equal_value_mismatch3(): + msg = """numpy array are different + +numpy array values are different \\(16\\.66667 %\\) +\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\], \\[5, 6\\]\\] +\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\], \\[5, 6\\]\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([[1, 2], [3, 4], [5, 6]]), + np.array([[1, 3], [3, 4], [5, 6]])) + + +def test_assert_numpy_array_equal_value_mismatch4(): + msg = """numpy array are different + +numpy array values are different \\(50\\.0 %\\) +\\[left\\]: \\[1\\.1, 2\\.000001\\] +\\[right\\]: \\[1\\.1, 2.0\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([1.1, 2.000001]), + np.array([1.1, 2.0])) + + +def test_assert_numpy_array_equal_value_mismatch5(): + msg = """numpy array are different + +numpy array values are different \\(16\\.66667 %\\) +\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\], \\[5, 6\\]\\] +\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\], \\[5, 6\\]\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([[1, 2], [3, 4], [5, 6]]), + np.array([[1, 3], [3, 4], [5, 6]])) + + +def test_assert_numpy_array_equal_value_mismatch6(): + msg = """numpy array are different + +numpy array values are different \\(25\\.0 %\\) +\\[left\\]: \\[\\[1, 2\\], \\[3, 4\\]\\] +\\[right\\]: \\[\\[1, 3\\], \\[3, 4\\]\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([[1, 2], [3, 4]]), + np.array([[1, 3], [3, 4]])) + + +def test_assert_numpy_array_equal_shape_mismatch_override(): + msg = """Index are different + +Index shapes are different +\\[left\\]: \\(2L*,\\) +\\[right\\]: \\(3L*,\\)""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([1, 2]), + np.array([3, 4, 5]), + obj="Index") + + +def test_numpy_array_equal_unicode(): + # see gh-20503 + # + # Test ensures that `assert_numpy_array_equals` raises the right + # exception when comparing np.arrays containing differing unicode objects. + msg = """numpy array are different + +numpy array values are different \\(33\\.33333 %\\) +\\[left\\]: \\[á, à, ä\\] +\\[right\\]: \\[á, à, å\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(np.array([u"á", u"à", u"ä"]), + np.array([u"á", u"à", u"å"])) + + +def test_numpy_array_equal_object(): + a = np.array([Timestamp("2011-01-01"), Timestamp("2011-01-01")]) + b = np.array([Timestamp("2011-01-01"), Timestamp("2011-01-02")]) + + msg = """numpy array are different + +numpy array values are different \\(50\\.0 %\\) +\\[left\\]: \\[2011-01-01 00:00:00, 2011-01-01 00:00:00\\] +\\[right\\]: \\[2011-01-01 00:00:00, 2011-01-02 00:00:00\\]""" + + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(a, b) + + +@pytest.mark.parametrize("other_type", ["same", "copy"]) +@pytest.mark.parametrize("check_same", ["same", "copy"]) +def test_numpy_array_equal_copy_flag(other_type, check_same): + a = np.array([1, 2, 3]) + msg = None + + if other_type == "same": + other = a.view() + else: + other = a.copy() + + if check_same != other_type: + msg = (r"array\(\[1, 2, 3\]\) is not array\(\[1, 2, 3\]\)" + if check_same == "same" + else r"array\(\[1, 2, 3\]\) is array\(\[1, 2, 3\]\)") + + if msg is not None: + with pytest.raises(AssertionError, match=msg): + assert_numpy_array_equal(a, other, check_same=check_same) + else: + assert_numpy_array_equal(a, other, check_same=check_same) diff --git a/pandas/tests/util/test_assert_series_equal.py b/pandas/tests/util/test_assert_series_equal.py new file mode 100644 index 0000000000000..537a0e01ff85f --- /dev/null +++ b/pandas/tests/util/test_assert_series_equal.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- + +import pytest + +from pandas import Categorical, DataFrame, Series +from pandas.util.testing import assert_series_equal + + +def _assert_series_equal_both(a, b, **kwargs): + """ + Check that two Series equal. + + This check is performed commutatively. + + Parameters + ---------- + a : Series + The first Series to compare. + b : Series + The second Series to compare. + kwargs : dict + The arguments passed to `assert_series_equal`. + """ + assert_series_equal(a, b, **kwargs) + assert_series_equal(b, a, **kwargs) + + +def _assert_not_series_equal(a, b, **kwargs): + """ + Check that two Series are not equal. + + Parameters + ---------- + a : Series + The first Series to compare. + b : Series + The second Series to compare. + kwargs : dict + The arguments passed to `assert_series_equal`. + """ + try: + assert_series_equal(a, b, **kwargs) + msg = "The two Series were equal when they shouldn't have been" + + pytest.fail(msg=msg) + except AssertionError: + pass + + +def _assert_not_series_equal_both(a, b, **kwargs): + """ + Check that two Series are not equal. + + This check is performed commutatively. + + Parameters + ---------- + a : Series + The first Series to compare. + b : Series + The second Series to compare. + kwargs : dict + The arguments passed to `assert_series_equal`. + """ + _assert_not_series_equal(a, b, **kwargs) + _assert_not_series_equal(b, a, **kwargs) + + +@pytest.mark.parametrize("data", [ + range(3), list("abc"), list(u"áàä"), +]) +def test_series_equal(data): + _assert_series_equal_both(Series(data), Series(data)) + + +@pytest.mark.parametrize("data1,data2", [ + (range(3), range(1, 4)), + (list("abc"), list("xyz")), + (list(u"áàä"), list(u"éèë")), + (list(u"áàä"), list(b"aaa")), + (range(3), range(4)), +]) +def test_series_not_equal_value_mismatch(data1, data2): + _assert_not_series_equal_both(Series(data1), Series(data2)) + + +@pytest.mark.parametrize("kwargs", [ + dict(dtype="float64"), # dtype mismatch + dict(index=[1, 2, 4]), # index mismatch + dict(name="foo"), # name mismatch +]) +def test_series_not_equal_metadata_mismatch(kwargs): + data = range(3) + s1 = Series(data) + + s2 = Series(data, **kwargs) + _assert_not_series_equal_both(s1, s2) + + +@pytest.mark.parametrize("data1,data2", [(0.12345, 0.12346), (0.1235, 0.1236)]) +@pytest.mark.parametrize("dtype", ["float32", "float64"]) +@pytest.mark.parametrize("check_less_precise", [False, True, 0, 1, 2, 3, 10]) +def test_less_precise(data1, data2, dtype, check_less_precise): + s1 = Series([data1], dtype=dtype) + s2 = Series([data2], dtype=dtype) + + kwargs = dict(check_less_precise=check_less_precise) + + if ((check_less_precise is False or check_less_precise == 10) or + ((check_less_precise is True or check_less_precise >= 3) and + abs(data1 - data2) >= 0.0001)): + msg = "Series values are different" + with pytest.raises(AssertionError, match=msg): + assert_series_equal(s1, s2, **kwargs) + else: + _assert_series_equal_both(s1, s2, **kwargs) + + +@pytest.mark.parametrize("s1,s2,msg", [ + # Index + (Series(["l1", "l2"], index=[1, 2]), + Series(["l1", "l2"], index=[1., 2.]), + "Series\\.index are different"), + + # MultiIndex + (DataFrame.from_records({"a": [1, 2], "b": [2.1, 1.5], + "c": ["l1", "l2"]}, index=["a", "b"]).c, + DataFrame.from_records({"a": [1., 2.], "b": [2.1, 1.5], + "c": ["l1", "l2"]}, index=["a", "b"]).c, + "MultiIndex level \\[0\\] are different") +]) +def test_series_equal_index_dtype(s1, s2, msg, check_index_type): + kwargs = dict(check_index_type=check_index_type) + + if check_index_type: + with pytest.raises(AssertionError, match=msg): + assert_series_equal(s1, s2, **kwargs) + else: + assert_series_equal(s1, s2, **kwargs) + + +def test_series_equal_length_mismatch(check_less_precise): + msg = """Series are different + +Series length are different +\\[left\\]: 3, RangeIndex\\(start=0, stop=3, step=1\\) +\\[right\\]: 4, RangeIndex\\(start=0, stop=4, step=1\\)""" + + s1 = Series([1, 2, 3]) + s2 = Series([1, 2, 3, 4]) + + with pytest.raises(AssertionError, match=msg): + assert_series_equal(s1, s2, check_less_precise=check_less_precise) + + +def test_series_equal_values_mismatch(check_less_precise): + msg = """Series are different + +Series values are different \\(33\\.33333 %\\) +\\[left\\]: \\[1, 2, 3\\] +\\[right\\]: \\[1, 2, 4\\]""" + + s1 = Series([1, 2, 3]) + s2 = Series([1, 2, 4]) + + with pytest.raises(AssertionError, match=msg): + assert_series_equal(s1, s2, check_less_precise=check_less_precise) + + +def test_series_equal_categorical_mismatch(check_categorical): + msg = """Attributes are different + +Attribute "dtype" are different +\\[left\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b'\\], ordered=False\\) +\\[right\\]: CategoricalDtype\\(categories=\\[u?'a', u?'b', u?'c'\\], \ +ordered=False\\)""" + + s1 = Series(Categorical(["a", "b"])) + s2 = Series(Categorical(["a", "b"], categories=list("abc"))) + + if check_categorical: + with pytest.raises(AssertionError, match=msg): + assert_series_equal(s1, s2, check_categorical=check_categorical) + else: + _assert_series_equal_both(s1, s2, check_categorical=check_categorical) diff --git a/pandas/tests/util/test_deprecate.py b/pandas/tests/util/test_deprecate.py new file mode 100644 index 0000000000000..7fa7989eff690 --- /dev/null +++ b/pandas/tests/util/test_deprecate.py @@ -0,0 +1,63 @@ +from textwrap import dedent + +import pytest + +from pandas.util._decorators import deprecate + +import pandas.util.testing as tm + + +def new_func(): + """ + This is the summary. The deprecate directive goes next. + + This is the extended summary. The deprecate directive goes before this. + """ + return 'new_func called' + + +def new_func_no_docstring(): + return 'new_func_no_docstring called' + + +def new_func_wrong_docstring(): + """Summary should be in the next line.""" + return 'new_func_wrong_docstring called' + + +def new_func_with_deprecation(): + """ + This is the summary. The deprecate directive goes next. + + .. deprecated:: 1.0 + Use new_func instead. + + This is the extended summary. The deprecate directive goes before this. + """ + pass + + +def test_deprecate_ok(): + depr_func = deprecate('depr_func', new_func, '1.0', + msg='Use new_func instead.') + + with tm.assert_produces_warning(FutureWarning): + result = depr_func() + + assert result == 'new_func called' + assert depr_func.__doc__ == dedent(new_func_with_deprecation.__doc__) + + +def test_deprecate_no_docstring(): + depr_func = deprecate('depr_func', new_func_no_docstring, '1.0', + msg='Use new_func instead.') + with tm.assert_produces_warning(FutureWarning): + result = depr_func() + assert result == 'new_func_no_docstring called' + + +def test_deprecate_wrong_docstring(): + with pytest.raises(AssertionError, match='deprecate needs a correctly ' + 'formatted docstring'): + deprecate('depr_func', new_func_wrong_docstring, '1.0', + msg='Use new_func instead.') diff --git a/pandas/tests/util/test_deprecate_kwarg.py b/pandas/tests/util/test_deprecate_kwarg.py new file mode 100644 index 0000000000000..7287df9db8a62 --- /dev/null +++ b/pandas/tests/util/test_deprecate_kwarg.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import pytest + +from pandas.util._decorators import deprecate_kwarg + +import pandas.util.testing as tm + + +@deprecate_kwarg("old", "new") +def _f1(new=False): + return new + + +_f2_mappings = {"yes": True, "no": False} + + +@deprecate_kwarg("old", "new", _f2_mappings) +def _f2(new=False): + return new + + +def _f3_mapping(x): + return x + 1 + + +@deprecate_kwarg("old", "new", _f3_mapping) +def _f3(new=0): + return new + + +@pytest.mark.parametrize("key,klass", [ + ("old", FutureWarning), + ("new", None) +]) +def test_deprecate_kwarg(key, klass): + x = 78 + + with tm.assert_produces_warning(klass): + assert _f1(**{key: x}) == x + + +@pytest.mark.parametrize("key", list(_f2_mappings.keys())) +def test_dict_deprecate_kwarg(key): + with tm.assert_produces_warning(FutureWarning): + assert _f2(old=key) == _f2_mappings[key] + + +@pytest.mark.parametrize("key", ["bogus", 12345, -1.23]) +def test_missing_deprecate_kwarg(key): + with tm.assert_produces_warning(FutureWarning): + assert _f2(old=key) == key + + +@pytest.mark.parametrize("x", [1, -1.4, 0]) +def test_callable_deprecate_kwarg(x): + with tm.assert_produces_warning(FutureWarning): + assert _f3(old=x) == _f3_mapping(x) + + +def test_callable_deprecate_kwarg_fail(): + msg = "((can only|cannot) concatenate)|(must be str)|(Can't convert)" + + with pytest.raises(TypeError, match=msg): + _f3(old="hello") + + +def test_bad_deprecate_kwarg(): + msg = "mapping from old to new argument values must be dict or callable!" + + with pytest.raises(TypeError, match=msg): + @deprecate_kwarg("old", "new", 0) + def f4(new=None): + return new + + +@deprecate_kwarg("old", None) +def _f4(old=True, unchanged=True): + return old, unchanged + + +@pytest.mark.parametrize("key", ["old", "unchanged"]) +def test_deprecate_keyword(key): + x = 9 + + if key == "old": + klass = FutureWarning + expected = (x, True) + else: + klass = None + expected = (True, x) + + with tm.assert_produces_warning(klass): + assert _f4(**{key: x}) == expected diff --git a/pandas/tests/util/test_hashing.py b/pandas/tests/util/test_hashing.py new file mode 100644 index 0000000000000..c80b4483c0482 --- /dev/null +++ b/pandas/tests/util/test_hashing.py @@ -0,0 +1,327 @@ +import datetime + +import numpy as np +import pytest + +import pandas as pd +from pandas import DataFrame, Index, MultiIndex, Series +from pandas.core.util.hashing import _hash_scalar, hash_tuple, hash_tuples +from pandas.util import hash_array, hash_pandas_object +import pandas.util.testing as tm + + +@pytest.fixture(params=[ + Series([1, 2, 3] * 3, dtype="int32"), + Series([None, 2.5, 3.5] * 3, dtype="float32"), + Series(["a", "b", "c"] * 3, dtype="category"), + Series(["d", "e", "f"] * 3), + Series([True, False, True] * 3), + Series(pd.date_range("20130101", periods=9)), + Series(pd.date_range("20130101", periods=9, tz="US/Eastern")), + Series(pd.timedelta_range("2000", periods=9))]) +def series(request): + return request.param + + +@pytest.fixture(params=[True, False]) +def index(request): + return request.param + + +def _check_equal(obj, **kwargs): + """ + Check that hashing an objects produces the same value each time. + + Parameters + ---------- + obj : object + The object to hash. + kwargs : kwargs + Keyword arguments to pass to the hashing function. + """ + a = hash_pandas_object(obj, **kwargs) + b = hash_pandas_object(obj, **kwargs) + tm.assert_series_equal(a, b) + + +def _check_not_equal_with_index(obj): + """ + Check the hash of an object with and without its index is not the same. + + Parameters + ---------- + obj : object + The object to hash. + """ + if not isinstance(obj, Index): + a = hash_pandas_object(obj, index=True) + b = hash_pandas_object(obj, index=False) + + if len(obj): + assert not (a == b).all() + + +def test_consistency(): + # Check that our hash doesn't change because of a mistake + # in the actual code; this is the ground truth. + result = hash_pandas_object(Index(["foo", "bar", "baz"])) + expected = Series(np.array([3600424527151052760, 1374399572096150070, + 477881037637427054], dtype="uint64"), + index=["foo", "bar", "baz"]) + tm.assert_series_equal(result, expected) + + +def test_hash_array(series): + arr = series.values + tm.assert_numpy_array_equal(hash_array(arr), hash_array(arr)) + + +@pytest.mark.parametrize("arr2", [ + np.array([3, 4, "All"]), + np.array([3, 4, "All"], dtype=object), +]) +def test_hash_array_mixed(arr2): + result1 = hash_array(np.array(["3", "4", "All"])) + result2 = hash_array(arr2) + + tm.assert_numpy_array_equal(result1, result2) + + +@pytest.mark.parametrize("val", [5, "foo", pd.Timestamp("20130101")]) +def test_hash_array_errors(val): + msg = "must pass a ndarray-like" + with pytest.raises(TypeError, match=msg): + hash_array(val) + + +def test_hash_tuples(): + tuples = [(1, "one"), (1, "two"), (2, "one")] + result = hash_tuples(tuples) + + expected = hash_pandas_object(MultiIndex.from_tuples(tuples)).values + tm.assert_numpy_array_equal(result, expected) + + result = hash_tuples(tuples[0]) + assert result == expected[0] + + +@pytest.mark.parametrize("tup", [ + (1, "one"), (1, np.nan), (1.0, pd.NaT, "A"), + ("A", pd.Timestamp("2012-01-01"))]) +def test_hash_tuple(tup): + # Test equivalence between + # hash_tuples and hash_tuple. + result = hash_tuple(tup) + expected = hash_tuples([tup])[0] + + assert result == expected + + +@pytest.mark.parametrize("val", [ + 1, 1.4, "A", b"A", u"A", pd.Timestamp("2012-01-01"), + pd.Timestamp("2012-01-01", tz="Europe/Brussels"), + datetime.datetime(2012, 1, 1), + pd.Timestamp("2012-01-01", tz="EST").to_pydatetime(), + pd.Timedelta("1 days"), datetime.timedelta(1), + pd.Period("2012-01-01", freq="D"), pd.Interval(0, 1), + np.nan, pd.NaT, None]) +def test_hash_scalar(val): + result = _hash_scalar(val) + expected = hash_array(np.array([val], dtype=object), categorize=True) + + assert result[0] == expected[0] + + +@pytest.mark.parametrize("val", [5, "foo", pd.Timestamp("20130101")]) +def test_hash_tuples_err(val): + msg = "must be convertible to a list-of-tuples" + with pytest.raises(TypeError, match=msg): + hash_tuples(val) + + +def test_multiindex_unique(): + mi = MultiIndex.from_tuples([(118, 472), (236, 118), + (51, 204), (102, 51)]) + assert mi.is_unique is True + + result = hash_pandas_object(mi) + assert result.is_unique is True + + +def test_multiindex_objects(): + mi = MultiIndex(levels=[["b", "d", "a"], [1, 2, 3]], + codes=[[0, 1, 0, 2], [2, 0, 0, 1]], + names=["col1", "col2"]) + recons = mi._sort_levels_monotonic() + + # These are equal. + assert mi.equals(recons) + assert Index(mi.values).equals(Index(recons.values)) + + # _hashed_values and hash_pandas_object(..., index=False) equivalency. + expected = hash_pandas_object(mi, index=False).values + result = mi._hashed_values + + tm.assert_numpy_array_equal(result, expected) + + expected = hash_pandas_object(recons, index=False).values + result = recons._hashed_values + + tm.assert_numpy_array_equal(result, expected) + + expected = mi._hashed_values + result = recons._hashed_values + + # Values should match, but in different order. + tm.assert_numpy_array_equal(np.sort(result), np.sort(expected)) + + +@pytest.mark.parametrize("obj", [ + Series([1, 2, 3]), + Series([1.0, 1.5, 3.2]), + Series([1.0, 1.5, np.nan]), + Series([1.0, 1.5, 3.2], index=[1.5, 1.1, 3.3]), + Series(["a", "b", "c"]), + Series(["a", np.nan, "c"]), + Series(["a", None, "c"]), + Series([True, False, True]), + Series(), + Index([1, 2, 3]), + Index([True, False, True]), + DataFrame({"x": ["a", "b", "c"], "y": [1, 2, 3]}), + DataFrame(), + tm.makeMissingDataframe(), + tm.makeMixedDataFrame(), + tm.makeTimeDataFrame(), + tm.makeTimeSeries(), + tm.makeTimedeltaIndex(), + tm.makePeriodIndex(), + Series(tm.makePeriodIndex()), + Series(pd.date_range("20130101", periods=3, tz="US/Eastern")), + MultiIndex.from_product([range(5), ["foo", "bar", "baz"], + pd.date_range("20130101", periods=2)]), + MultiIndex.from_product([pd.CategoricalIndex(list("aabc")), range(3)]) +]) +def test_hash_pandas_object(obj, index): + _check_equal(obj, index=index) + _check_not_equal_with_index(obj) + + +def test_hash_pandas_object2(series, index): + _check_equal(series, index=index) + _check_not_equal_with_index(series) + + +@pytest.mark.parametrize("obj", [ + Series([], dtype="float64"), Series([], dtype="object"), Index([])]) +def test_hash_pandas_empty_object(obj, index): + # These are by-definition the same with + # or without the index as the data is empty. + _check_equal(obj, index=index) + + +@pytest.mark.parametrize("s1", [ + Series(["a", "b", "c", "d"]), + Series([1000, 2000, 3000, 4000]), + Series(pd.date_range(0, periods=4))]) +@pytest.mark.parametrize("categorize", [True, False]) +def test_categorical_consistency(s1, categorize): + # see gh-15143 + # + # Check that categoricals hash consistent with their values, + # not codes. This should work for categoricals of any dtype. + s2 = s1.astype("category").cat.set_categories(s1) + s3 = s2.cat.set_categories(list(reversed(s1))) + + # These should all hash identically. + h1 = hash_pandas_object(s1, categorize=categorize) + h2 = hash_pandas_object(s2, categorize=categorize) + h3 = hash_pandas_object(s3, categorize=categorize) + + tm.assert_series_equal(h1, h2) + tm.assert_series_equal(h1, h3) + + +def test_categorical_with_nan_consistency(): + c = pd.Categorical.from_codes( + [-1, 0, 1, 2, 3, 4], + categories=pd.date_range("2012-01-01", periods=5, name="B")) + expected = hash_array(c, categorize=False) + + c = pd.Categorical.from_codes( + [-1, 0], + categories=[pd.Timestamp("2012-01-01")]) + result = hash_array(c, categorize=False) + + assert result[0] in expected + assert result[1] in expected + + +@pytest.mark.parametrize("obj", [pd.Timestamp("20130101")]) +def test_pandas_errors(obj): + msg = "Unexpected type for hashing" + with pytest.raises(TypeError, match=msg): + hash_pandas_object(obj) + + +def test_hash_keys(): + # Using different hash keys, should have + # different hashes for the same data. + # + # This only matters for object dtypes. + obj = Series(list("abc")) + + a = hash_pandas_object(obj, hash_key="9876543210123456") + b = hash_pandas_object(obj, hash_key="9876543210123465") + + assert (a != b).all() + + +def test_invalid_key(): + # This only matters for object dtypes. + msg = "key should be a 16-byte string encoded" + + with pytest.raises(ValueError, match=msg): + hash_pandas_object(Series(list("abc")), hash_key="foo") + + +def test_already_encoded(index): + # If already encoded, then ok. + obj = Series(list("abc")).str.encode("utf8") + _check_equal(obj, index=index) + + +def test_alternate_encoding(index): + obj = Series(list("abc")) + _check_equal(obj, index=index, encoding="ascii") + + +@pytest.mark.parametrize("l_exp", range(8)) +@pytest.mark.parametrize("l_add", [0, 1]) +def test_same_len_hash_collisions(l_exp, l_add): + length = 2**(l_exp + 8) + l_add + s = tm.rands_array(length, 2) + + result = hash_array(s, "utf8") + assert not result[0] == result[1] + + +def test_hash_collisions(): + # Hash collisions are bad. + # + # https://github.com/pandas-dev/pandas/issues/14711#issuecomment-264885726 + hashes = ["Ingrid-9Z9fKIZmkO7i7Cn51Li34pJm44fgX6DYGBNj3VPlOH50m7HnBlPxfIwFMrcNJNMP6PSgLmwWnInciMWrCSAlLEvt7JkJl4IxiMrVbXSa8ZQoVaq5xoQPjltuJEfwdNlO6jo8qRRHvD8sBEBMQASrRa6TsdaPTPCBo3nwIBpE7YzzmyH0vMBhjQZLx1aCT7faSEx7PgFxQhHdKFWROcysamgy9iVj8DO2Fmwg1NNl93rIAqC3mdqfrCxrzfvIY8aJdzin2cHVzy3QUJxZgHvtUtOLxoqnUHsYbNTeq0xcLXpTZEZCxD4PGubIuCNf32c33M7HFsnjWSEjE2yVdWKhmSVodyF8hFYVmhYnMCztQnJrt3O8ZvVRXd5IKwlLexiSp4h888w7SzAIcKgc3g5XQJf6MlSMftDXm9lIsE1mJNiJEv6uY6pgvC3fUPhatlR5JPpVAHNSbSEE73MBzJrhCAbOLXQumyOXigZuPoME7QgJcBalliQol7YZ9", # noqa + "Tim-b9MddTxOWW2AT1Py6vtVbZwGAmYCjbp89p8mxsiFoVX4FyDOF3wFiAkyQTUgwg9sVqVYOZo09Dh1AzhFHbgij52ylF0SEwgzjzHH8TGY8Lypart4p4onnDoDvVMBa0kdthVGKl6K0BDVGzyOXPXKpmnMF1H6rJzqHJ0HywfwS4XYpVwlAkoeNsiicHkJUFdUAhG229INzvIAiJuAHeJDUoyO4DCBqtoZ5TDend6TK7Y914yHlfH3g1WZu5LksKv68VQHJriWFYusW5e6ZZ6dKaMjTwEGuRgdT66iU5nqWTHRH8WSzpXoCFwGcTOwyuqPSe0fTe21DVtJn1FKj9F9nEnR9xOvJUO7E0piCIF4Ad9yAIDY4DBimpsTfKXCu1vdHpKYerzbndfuFe5AhfMduLYZJi5iAw8qKSwR5h86ttXV0Mc0QmXz8dsRvDgxjXSmupPxBggdlqUlC828hXiTPD7am0yETBV0F3bEtvPiNJfremszcV8NcqAoARMe"] # noqa + + # These should be different. + result1 = hash_array(np.asarray(hashes[0:1], dtype=object), "utf8") + expected1 = np.array([14963968704024874985], dtype=np.uint64) + tm.assert_numpy_array_equal(result1, expected1) + + result2 = hash_array(np.asarray(hashes[1:2], dtype=object), "utf8") + expected2 = np.array([16428432627716348016], dtype=np.uint64) + tm.assert_numpy_array_equal(result2, expected2) + + result = hash_array(np.asarray(hashes, dtype=object), "utf8") + tm.assert_numpy_array_equal(result, np.concatenate([expected1, + expected2], axis=0)) diff --git a/pandas/tests/util/test_locale.py b/pandas/tests/util/test_locale.py new file mode 100644 index 0000000000000..b848b22994e7a --- /dev/null +++ b/pandas/tests/util/test_locale.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +import codecs +import locale +import os + +import pytest + +from pandas.compat import is_platform_windows + +import pandas.core.common as com +import pandas.util.testing as tm + +_all_locales = tm.get_locales() or [] +_current_locale = locale.getlocale() + +# Don't run any of these tests if we are on Windows or have no locales. +pytestmark = pytest.mark.skipif(is_platform_windows() or not _all_locales, + reason="Need non-Windows and locales") + +_skip_if_only_one_locale = pytest.mark.skipif( + len(_all_locales) <= 1, reason="Need multiple locales for meaningful test") + + +def test_can_set_locale_valid_set(): + # Can set the default locale. + assert tm.can_set_locale("") + + +def test_can_set_locale_invalid_set(): + # Cannot set an invalid locale. + assert not tm.can_set_locale("non-existent_locale") + + +def test_can_set_locale_invalid_get(monkeypatch): + # see gh-22129 + # + # In some cases, an invalid locale can be set, + # but a subsequent getlocale() raises a ValueError. + + def mock_get_locale(): + raise ValueError() + + with monkeypatch.context() as m: + m.setattr(locale, "getlocale", mock_get_locale) + assert not tm.can_set_locale("") + + +def test_get_locales_at_least_one(): + # see gh-9744 + assert len(_all_locales) > 0 + + +@_skip_if_only_one_locale +def test_get_locales_prefix(): + first_locale = _all_locales[0] + assert len(tm.get_locales(prefix=first_locale[:2])) > 0 + + +@_skip_if_only_one_locale +def test_set_locale(): + if com._all_none(_current_locale): + # Not sure why, but on some Travis runs with pytest, + # getlocale() returned (None, None). + pytest.skip("Current locale is not set.") + + locale_override = os.environ.get("LOCALE_OVERRIDE", None) + + if locale_override is None: + lang, enc = "it_CH", "UTF-8" + elif locale_override == "C": + lang, enc = "en_US", "ascii" + else: + lang, enc = locale_override.split(".") + + enc = codecs.lookup(enc).name + new_locale = lang, enc + + if not tm.can_set_locale(new_locale): + msg = "unsupported locale setting" + + with pytest.raises(locale.Error, match=msg): + with tm.set_locale(new_locale): + pass + else: + with tm.set_locale(new_locale) as normalized_locale: + new_lang, new_enc = normalized_locale.split(".") + new_enc = codecs.lookup(enc).name + + normalized_locale = new_lang, new_enc + assert normalized_locale == new_locale + + # Once we exit the "with" statement, locale should be back to what it was. + current_locale = locale.getlocale() + assert current_locale == _current_locale diff --git a/pandas/tests/util/test_move.py b/pandas/tests/util/test_move.py new file mode 100644 index 0000000000000..ef98f2032e6ca --- /dev/null +++ b/pandas/tests/util/test_move.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +import sys +from uuid import uuid4 + +import pytest + +from pandas.compat import PY3, intern +from pandas.util._move import BadMove, move_into_mutable_buffer, stolenbuf + + +def test_cannot_create_instance_of_stolen_buffer(): + # Stolen buffers need to be created through the smart constructor + # "move_into_mutable_buffer," which has a bunch of checks in it. + + msg = "cannot create 'pandas.util._move.stolenbuf' instances" + with pytest.raises(TypeError, match=msg): + stolenbuf() + + +def test_more_than_one_ref(): + # Test case for when we try to use "move_into_mutable_buffer" + # when the object being moved has other references. + + b = b"testing" + + with pytest.raises(BadMove, match="testing") as e: + def handle_success(type_, value, tb): + assert value.args[0] is b + return type(e).handle_success(e, type_, value, tb) # super + + e.handle_success = handle_success + move_into_mutable_buffer(b) + + +def test_exactly_one_ref(): + # Test case for when the object being moved has exactly one reference. + + b = b"testing" + + # We need to pass an expression on the stack to ensure that there are + # not extra references hanging around. We cannot rewrite this test as + # buf = b[:-3] + # as_stolen_buf = move_into_mutable_buffer(buf) + # because then we would have more than one reference to buf. + as_stolen_buf = move_into_mutable_buffer(b[:-3]) + + # Materialize as byte-array to show that it is mutable. + assert bytearray(as_stolen_buf) == b"test" + + +@pytest.mark.skipif(PY3, reason="bytes objects cannot be interned in PY3") +def test_interned(): + salt = uuid4().hex + + def make_string(): + # We need to actually create a new string so that it has refcount + # one. We use a uuid so that we know the string could not already + # be in the intern table. + return "".join(("testing: ", salt)) + + # This should work, the string has one reference on the stack. + move_into_mutable_buffer(make_string()) + refcount = [None] # nonlocal + + def ref_capture(ob): + # Subtract two because those are the references owned by this frame: + # 1. The local variables of this stack frame. + # 2. The python data stack of this stack frame. + refcount[0] = sys.getrefcount(ob) - 2 + return ob + + with pytest.raises(BadMove, match="testing"): + # If we intern the string, it will still have one reference. Now, + # it is in the intern table, so if other people intern the same + # string while the mutable buffer holds the first string they will + # be the same instance. + move_into_mutable_buffer(ref_capture(intern(make_string()))) # noqa + + assert refcount[0] == 1 diff --git a/pandas/tests/util/test_safe_import.py b/pandas/tests/util/test_safe_import.py new file mode 100644 index 0000000000000..a9c52ef788390 --- /dev/null +++ b/pandas/tests/util/test_safe_import.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import sys +import types + +import pytest + +import pandas.util._test_decorators as td + + +@pytest.mark.parametrize("name", ["foo", "hello123"]) +def test_safe_import_non_existent(name): + assert not td.safe_import(name) + + +def test_safe_import_exists(): + assert td.safe_import("pandas") + + +@pytest.mark.parametrize("min_version,valid", [ + ("0.0.0", True), + ("99.99.99", False) +]) +def test_safe_import_versions(min_version, valid): + result = td.safe_import("pandas", min_version=min_version) + result = result if valid else not result + assert result + + +@pytest.mark.parametrize("min_version,valid", [ + (None, False), + ("1.0", True), + ("2.0", False) +]) +def test_safe_import_dummy(monkeypatch, min_version, valid): + mod_name = "hello123" + + mod = types.ModuleType(mod_name) + mod.__version__ = "1.5" + + if min_version is not None: + monkeypatch.setitem(sys.modules, mod_name, mod) + + result = td.safe_import(mod_name, min_version=min_version) + result = result if valid else not result + assert result diff --git a/pandas/tests/util/test_util.py b/pandas/tests/util/test_util.py new file mode 100644 index 0000000000000..e40784fd5467c --- /dev/null +++ b/pandas/tests/util/test_util.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +import os +import sys + +import pytest + +import pandas.compat as compat +from pandas.compat import raise_with_traceback +from pandas.util._decorators import deprecate_kwarg, make_signature +from pandas.util._validators import validate_kwargs + +import pandas.util.testing as tm + + +def test_rands(): + r = tm.rands(10) + assert len(r) == 10 + + +def test_rands_array_1d(): + arr = tm.rands_array(5, size=10) + assert arr.shape == (10,) + assert len(arr[0]) == 5 + + +def test_rands_array_2d(): + arr = tm.rands_array(7, size=(10, 10)) + assert arr.shape == (10, 10) + assert len(arr[1, 1]) == 7 + + +def test_numpy_err_state_is_default(): + expected = {"over": "warn", "divide": "warn", + "invalid": "warn", "under": "ignore"} + import numpy as np + + # The error state should be unchanged after that import. + assert np.geterr() == expected + + +@pytest.mark.parametrize("func,expected", [ + # Case where the func does not have default kwargs. + (validate_kwargs, (["fname", "kwargs", "compat_args"], + ["fname", "kwargs", "compat_args"])), + + # Case where the func does have default kwargs. + (deprecate_kwarg, (["old_arg_name", "new_arg_name", + "mapping=None", "stacklevel=2"], + ["old_arg_name", "new_arg_name", + "mapping", "stacklevel"])) +]) +def test_make_signature(func, expected): + # see gh-17608 + assert make_signature(func) == expected + + +def test_raise_with_traceback(): + with pytest.raises(LookupError, match="error_text"): + try: + raise ValueError("THIS IS AN ERROR") + except ValueError: + e = LookupError("error_text") + raise_with_traceback(e) + + with pytest.raises(LookupError, match="error_text"): + try: + raise ValueError("This is another error") + except ValueError: + e = LookupError("error_text") + _, _, traceback = sys.exc_info() + raise_with_traceback(e, traceback) + + +def test_convert_rows_list_to_csv_str(): + rows_list = ["aaa", "bbb", "ccc"] + ret = tm.convert_rows_list_to_csv_str(rows_list) + + if compat.is_platform_windows(): + expected = "aaa\r\nbbb\r\nccc\r\n" + else: + expected = "aaa\nbbb\nccc\n" + + assert ret == expected + + +def test_create_temp_directory(): + with tm.ensure_clean_dir() as path: + assert os.path.exists(path) + assert os.path.isdir(path) + assert not os.path.exists(path) + + +def test_assert_raises_regex_deprecated(): + # see gh-23592 + + with tm.assert_produces_warning(FutureWarning): + msg = "Not equal!" + + with tm.assert_raises_regex(AssertionError, msg): + assert 1 == 2, msg + + +@pytest.mark.parametrize('strict_data_files', [True, False]) +def test_datapath_missing(datapath): + with pytest.raises(ValueError, match="Could not find file"): + datapath("not_a_file") + + +def test_datapath(datapath): + args = ("data", "iris.csv") + + result = datapath(*args) + expected = os.path.join(os.path.dirname(os.path.dirname(__file__)), *args) + + assert result == expected + + +def test_rng_context(): + import numpy as np + + expected0 = 1.764052345967664 + expected1 = 1.6243453636632417 + + with tm.RNGContext(0): + with tm.RNGContext(1): + assert np.random.randn() == expected1 + assert np.random.randn() == expected0 diff --git a/pandas/tests/util/test_validate_args.py b/pandas/tests/util/test_validate_args.py new file mode 100644 index 0000000000000..ca71b0c9d2522 --- /dev/null +++ b/pandas/tests/util/test_validate_args.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from collections import OrderedDict + +import pytest + +from pandas.util._validators import validate_args + +_fname = "func" + + +def test_bad_min_fname_arg_count(): + msg = "'max_fname_arg_count' must be non-negative" + + with pytest.raises(ValueError, match=msg): + validate_args(_fname, (None,), -1, "foo") + + +def test_bad_arg_length_max_value_single(): + args = (None, None) + compat_args = ("foo",) + + min_fname_arg_count = 0 + max_length = len(compat_args) + min_fname_arg_count + actual_length = len(args) + min_fname_arg_count + msg = (r"{fname}\(\) takes at most {max_length} " + r"argument \({actual_length} given\)" + .format(fname=_fname, max_length=max_length, + actual_length=actual_length)) + + with pytest.raises(TypeError, match=msg): + validate_args(_fname, args, min_fname_arg_count, compat_args) + + +def test_bad_arg_length_max_value_multiple(): + args = (None, None) + compat_args = dict(foo=None) + + min_fname_arg_count = 2 + max_length = len(compat_args) + min_fname_arg_count + actual_length = len(args) + min_fname_arg_count + msg = (r"{fname}\(\) takes at most {max_length} " + r"arguments \({actual_length} given\)" + .format(fname=_fname, max_length=max_length, + actual_length=actual_length)) + + with pytest.raises(TypeError, match=msg): + validate_args(_fname, args, min_fname_arg_count, compat_args) + + +@pytest.mark.parametrize("i", range(1, 3)) +def test_not_all_defaults(i): + bad_arg = "foo" + msg = ("the '{arg}' parameter is not supported " + r"in the pandas implementation of {func}\(\)". + format(arg=bad_arg, func=_fname)) + + compat_args = OrderedDict() + compat_args["foo"] = 2 + compat_args["bar"] = -1 + compat_args["baz"] = 3 + + arg_vals = (1, -1, 3) + + with pytest.raises(ValueError, match=msg): + validate_args(_fname, arg_vals[:i], 2, compat_args) + + +def test_validation(): + # No exceptions should be raised. + validate_args(_fname, (None,), 2, dict(out=None)) + + compat_args = OrderedDict() + compat_args["axis"] = 1 + compat_args["out"] = None + + validate_args(_fname, (1, None), 2, compat_args) diff --git a/pandas/tests/util/test_validate_args_and_kwargs.py b/pandas/tests/util/test_validate_args_and_kwargs.py new file mode 100644 index 0000000000000..c3c0b3dedc085 --- /dev/null +++ b/pandas/tests/util/test_validate_args_and_kwargs.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +from collections import OrderedDict + +import pytest + +from pandas.util._validators import validate_args_and_kwargs + +_fname = "func" + + +def test_invalid_total_length_max_length_one(): + compat_args = ("foo",) + kwargs = {"foo": "FOO"} + args = ("FoO", "BaZ") + + min_fname_arg_count = 0 + max_length = len(compat_args) + min_fname_arg_count + actual_length = len(kwargs) + len(args) + min_fname_arg_count + + msg = (r"{fname}\(\) takes at most {max_length} " + r"argument \({actual_length} given\)" + .format(fname=_fname, max_length=max_length, + actual_length=actual_length)) + + with pytest.raises(TypeError, match=msg): + validate_args_and_kwargs(_fname, args, kwargs, + min_fname_arg_count, + compat_args) + + +def test_invalid_total_length_max_length_multiple(): + compat_args = ("foo", "bar", "baz") + kwargs = {"foo": "FOO", "bar": "BAR"} + args = ("FoO", "BaZ") + + min_fname_arg_count = 2 + max_length = len(compat_args) + min_fname_arg_count + actual_length = len(kwargs) + len(args) + min_fname_arg_count + + msg = (r"{fname}\(\) takes at most {max_length} " + r"arguments \({actual_length} given\)" + .format(fname=_fname, max_length=max_length, + actual_length=actual_length)) + + with pytest.raises(TypeError, match=msg): + validate_args_and_kwargs(_fname, args, kwargs, + min_fname_arg_count, + compat_args) + + +@pytest.mark.parametrize("args,kwargs", [ + ((), {"foo": -5, "bar": 2}), + ((-5, 2), {}) +]) +def test_missing_args_or_kwargs(args, kwargs): + bad_arg = "bar" + min_fname_arg_count = 2 + + compat_args = OrderedDict() + compat_args["foo"] = -5 + compat_args[bad_arg] = 1 + + msg = (r"the '{arg}' parameter is not supported " + r"in the pandas implementation of {func}\(\)". + format(arg=bad_arg, func=_fname)) + + with pytest.raises(ValueError, match=msg): + validate_args_and_kwargs(_fname, args, kwargs, + min_fname_arg_count, compat_args) + + +def test_duplicate_argument(): + min_fname_arg_count = 2 + + compat_args = OrderedDict() + compat_args["foo"] = None + compat_args["bar"] = None + compat_args["baz"] = None + + kwargs = {"foo": None, "bar": None} + args = (None,) # duplicate value for "foo" + + msg = (r"{fname}\(\) got multiple values for keyword " + r"argument '{arg}'".format(fname=_fname, arg="foo")) + + with pytest.raises(TypeError, match=msg): + validate_args_and_kwargs(_fname, args, kwargs, + min_fname_arg_count, + compat_args) + + +def test_validation(): + # No exceptions should be raised. + compat_args = OrderedDict() + compat_args["foo"] = 1 + compat_args["bar"] = None + compat_args["baz"] = -2 + kwargs = {"baz": -2} + + args = (1, None) + min_fname_arg_count = 2 + + validate_args_and_kwargs(_fname, args, kwargs, + min_fname_arg_count, + compat_args) diff --git a/pandas/tests/util/test_validate_kwargs.py b/pandas/tests/util/test_validate_kwargs.py new file mode 100644 index 0000000000000..f36818ddfc9a8 --- /dev/null +++ b/pandas/tests/util/test_validate_kwargs.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +from collections import OrderedDict + +import pytest + +from pandas.util._validators import validate_bool_kwarg, validate_kwargs + +_fname = "func" + + +def test_bad_kwarg(): + good_arg = "f" + bad_arg = good_arg + "o" + + compat_args = OrderedDict() + compat_args[good_arg] = "foo" + compat_args[bad_arg + "o"] = "bar" + kwargs = {good_arg: "foo", bad_arg: "bar"} + + msg = (r"{fname}\(\) got an unexpected " + r"keyword argument '{arg}'".format(fname=_fname, arg=bad_arg)) + + with pytest.raises(TypeError, match=msg): + validate_kwargs(_fname, kwargs, compat_args) + + +@pytest.mark.parametrize("i", range(1, 3)) +def test_not_all_none(i): + bad_arg = "foo" + msg = (r"the '{arg}' parameter is not supported " + r"in the pandas implementation of {func}\(\)". + format(arg=bad_arg, func=_fname)) + + compat_args = OrderedDict() + compat_args["foo"] = 1 + compat_args["bar"] = "s" + compat_args["baz"] = None + + kwarg_keys = ("foo", "bar", "baz") + kwarg_vals = (2, "s", None) + + kwargs = dict(zip(kwarg_keys[:i], kwarg_vals[:i])) + + with pytest.raises(ValueError, match=msg): + validate_kwargs(_fname, kwargs, compat_args) + + +def test_validation(): + # No exceptions should be raised. + compat_args = OrderedDict() + compat_args["f"] = None + compat_args["b"] = 1 + compat_args["ba"] = "s" + + kwargs = dict(f=None, b=1) + validate_kwargs(_fname, kwargs, compat_args) + + +@pytest.mark.parametrize("name", ["inplace", "copy"]) +@pytest.mark.parametrize("value", [1, "True", [1, 2, 3], 5.0]) +def test_validate_bool_kwarg_fail(name, value): + msg = ("For argument \"%s\" expected type bool, received type %s" % + (name, type(value).__name__)) + + with pytest.raises(ValueError, match=msg): + validate_bool_kwarg(value, name) + + +@pytest.mark.parametrize("name", ["inplace", "copy"]) +@pytest.mark.parametrize("value", [True, False, None]) +def test_validate_bool_kwarg(name, value): + assert validate_bool_kwarg(value, name) == value diff --git a/pandas/tools/tile.py b/pandas/tools/tile.py deleted file mode 100644 index 4a3d452228e01..0000000000000 --- a/pandas/tools/tile.py +++ /dev/null @@ -1,390 +0,0 @@ -""" -Quantilization functions and related stuff -""" - -from pandas.types.missing import isnull -from pandas.types.common import (is_float, is_integer, - is_scalar, _ensure_int64) - -from pandas.core.api import Series -from pandas.core.categorical import Categorical -import pandas.core.algorithms as algos -import pandas.core.nanops as nanops -from pandas.compat import zip -from pandas import to_timedelta, to_datetime -from pandas.types.common import is_datetime64_dtype, is_timedelta64_dtype -from pandas._libs.lib import infer_dtype - -import numpy as np - - -def cut(x, bins, right=True, labels=None, retbins=False, precision=3, - include_lowest=False): - """ - Return indices of half-open bins to which each value of `x` belongs. - - Parameters - ---------- - x : array-like - Input array to be binned. It has to be 1-dimensional. - bins : int or sequence of scalars - If `bins` is an int, it defines the number of equal-width bins in the - range of `x`. However, in this case, the range of `x` is extended - by .1% on each side to include the min or max values of `x`. If - `bins` is a sequence it defines the bin edges allowing for - non-uniform bin width. No extension of the range of `x` is done in - this case. - right : bool, optional - Indicates whether the bins include the rightmost edge or not. If - right == True (the default), then the bins [1,2,3,4] indicate - (1,2], (2,3], (3,4]. - labels : array or boolean, default None - Used as labels for the resulting bins. Must be of the same length as - the resulting bins. If False, return only integer indicators of the - bins. - retbins : bool, optional - Whether to return the bins or not. Can be useful if bins is given - as a scalar. - precision : int - The precision at which to store and display the bins labels - include_lowest : bool - Whether the first interval should be left-inclusive or not. - - Returns - ------- - out : Categorical or Series or array of integers if labels is False - The return type (Categorical or Series) depends on the input: a Series - of type category if input is a Series else Categorical. Bins are - represented as categories when categorical data is returned. - bins : ndarray of floats - Returned only if `retbins` is True. - - Notes - ----- - The `cut` function can be useful for going from a continuous variable to - a categorical variable. For example, `cut` could convert ages to groups - of age ranges. - - Any NA values will be NA in the result. Out of bounds values will be NA in - the resulting Categorical object - - - Examples - -------- - >>> pd.cut(np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1]), 3, retbins=True) - ([(0.191, 3.367], (0.191, 3.367], (0.191, 3.367], (3.367, 6.533], - (6.533, 9.7], (0.191, 3.367]] - Categories (3, object): [(0.191, 3.367] < (3.367, 6.533] < (6.533, 9.7]], - array([ 0.1905 , 3.36666667, 6.53333333, 9.7 ])) - >>> pd.cut(np.array([.2, 1.4, 2.5, 6.2, 9.7, 2.1]), 3, - labels=["good","medium","bad"]) - [good, good, good, medium, bad, good] - Categories (3, object): [good < medium < bad] - >>> pd.cut(np.ones(5), 4, labels=False) - array([1, 1, 1, 1, 1], dtype=int64) - """ - # NOTE: this binning code is changed a bit from histogram for var(x) == 0 - - # for handling the cut for datetime and timedelta objects - x_is_series, series_index, name, x = _preprocess_for_cut(x) - x, dtype = _coerce_to_type(x) - - if not np.iterable(bins): - if is_scalar(bins) and bins < 1: - raise ValueError("`bins` should be a positive integer.") - - sz = x.size - - if sz == 0: - raise ValueError('Cannot cut empty array') - # handle empty arrays. Can't determine range, so use 0-1. - # rng = (0, 1) - else: - rng = (nanops.nanmin(x), nanops.nanmax(x)) - mn, mx = [mi + 0.0 for mi in rng] - - if mn == mx: # adjust end points before binning - mn -= .001 * abs(mn) if mn != 0 else .001 - mx += .001 * abs(mx) if mx != 0 else .001 - bins = np.linspace(mn, mx, bins + 1, endpoint=True) - else: # adjust end points after binning - bins = np.linspace(mn, mx, bins + 1, endpoint=True) - adj = (mx - mn) * 0.001 # 0.1% of the range - if right: - bins[0] -= adj - else: - bins[-1] += adj - - else: - bins = np.asarray(bins) - bins = _convert_bin_to_numeric_type(bins) - if (np.diff(bins) < 0).any(): - raise ValueError('bins must increase monotonically.') - - fac, bins = _bins_to_cuts(x, bins, right=right, labels=labels, - precision=precision, - include_lowest=include_lowest, dtype=dtype) - - return _postprocess_for_cut(fac, bins, retbins, x_is_series, - series_index, name) - - -def qcut(x, q, labels=None, retbins=False, precision=3, duplicates='raise'): - """ - Quantile-based discretization function. Discretize variable into - equal-sized buckets based on rank or based on sample quantiles. For example - 1000 values for 10 quantiles would produce a Categorical object indicating - quantile membership for each data point. - - Parameters - ---------- - x : ndarray or Series - q : integer or array of quantiles - Number of quantiles. 10 for deciles, 4 for quartiles, etc. Alternately - array of quantiles, e.g. [0, .25, .5, .75, 1.] for quartiles - labels : array or boolean, default None - Used as labels for the resulting bins. Must be of the same length as - the resulting bins. If False, return only integer indicators of the - bins. - retbins : bool, optional - Whether to return the bins or not. Can be useful if bins is given - as a scalar. - precision : int - The precision at which to store and display the bins labels - duplicates : {default 'raise', 'drop'}, optional - If bin edges are not unique, raise ValueError or drop non-uniques. - - .. versionadded:: 0.20.0 - - Returns - ------- - out : Categorical or Series or array of integers if labels is False - The return type (Categorical or Series) depends on the input: a Series - of type category if input is a Series else Categorical. Bins are - represented as categories when categorical data is returned. - bins : ndarray of floats - Returned only if `retbins` is True. - - Notes - ----- - Out of bounds values will be NA in the resulting Categorical object - - Examples - -------- - >>> pd.qcut(range(5), 4) - [[0, 1], [0, 1], (1, 2], (2, 3], (3, 4]] - Categories (4, object): [[0, 1] < (1, 2] < (2, 3] < (3, 4]] - >>> pd.qcut(range(5), 3, labels=["good","medium","bad"]) - [good, good, medium, bad, bad] - Categories (3, object): [good < medium < bad] - >>> pd.qcut(range(5), 4, labels=False) - array([0, 0, 1, 2, 3], dtype=int64) - """ - x_is_series, series_index, name, x = _preprocess_for_cut(x) - - x, dtype = _coerce_to_type(x) - - if is_integer(q): - quantiles = np.linspace(0, 1, q + 1) - else: - quantiles = q - bins = algos.quantile(x, quantiles) - fac, bins = _bins_to_cuts(x, bins, labels=labels, - precision=precision, include_lowest=True, - dtype=dtype, duplicates=duplicates) - - return _postprocess_for_cut(fac, bins, retbins, x_is_series, - series_index, name) - - -def _bins_to_cuts(x, bins, right=True, labels=None, - precision=3, include_lowest=False, - dtype=None, duplicates='raise'): - - if duplicates not in ['raise', 'drop']: - raise ValueError("invalid value for 'duplicates' parameter, " - "valid options are: raise, drop") - - unique_bins = algos.unique(bins) - if len(unique_bins) < len(bins) and len(bins) != 2: - if duplicates == 'raise': - raise ValueError("Bin edges must be unique: {}.\nYou " - "can drop duplicate edges by setting " - "the 'duplicates' kwarg".format(repr(bins))) - else: - bins = unique_bins - - side = 'left' if right else 'right' - ids = _ensure_int64(bins.searchsorted(x, side=side)) - - if include_lowest: - ids[x == bins[0]] = 1 - - na_mask = isnull(x) | (ids == len(bins)) | (ids == 0) - has_nas = na_mask.any() - - if labels is not False: - if labels is None: - increases = 0 - while True: - try: - levels = _format_levels(bins, precision, right=right, - include_lowest=include_lowest, - dtype=dtype) - except ValueError: - increases += 1 - precision += 1 - if increases >= 20: - raise - else: - break - - else: - if len(labels) != len(bins) - 1: - raise ValueError('Bin labels must be one fewer than ' - 'the number of bin edges') - levels = labels - - levels = np.asarray(levels, dtype=object) - np.putmask(ids, na_mask, 0) - fac = Categorical(ids - 1, levels, ordered=True, fastpath=True) - else: - fac = ids - 1 - if has_nas: - fac = fac.astype(np.float64) - np.putmask(fac, na_mask, np.nan) - - return fac, bins - - -def _format_levels(bins, prec, right=True, - include_lowest=False, dtype=None): - fmt = lambda v: _format_label(v, precision=prec, dtype=dtype) - if right: - levels = [] - for a, b in zip(bins, bins[1:]): - fa, fb = fmt(a), fmt(b) - - if a != b and fa == fb: - raise ValueError('precision too low') - - formatted = '(%s, %s]' % (fa, fb) - - levels.append(formatted) - - if include_lowest: - levels[0] = '[' + levels[0][1:] - else: - levels = ['[%s, %s)' % (fmt(a), fmt(b)) - for a, b in zip(bins, bins[1:])] - return levels - - -def _format_label(x, precision=3, dtype=None): - fmt_str = '%%.%dg' % precision - - if is_datetime64_dtype(dtype): - return to_datetime(x, unit='ns') - if is_timedelta64_dtype(dtype): - return to_timedelta(x, unit='ns') - if np.isinf(x): - return str(x) - elif is_float(x): - frac, whole = np.modf(x) - sgn = '-' if x < 0 else '' - whole = abs(whole) - if frac != 0.0: - val = fmt_str % frac - - # rounded up or down - if '.' not in val: - if x < 0: - return '%d' % (-whole - 1) - else: - return '%d' % (whole + 1) - - if 'e' in val: - return _trim_zeros(fmt_str % x) - else: - val = _trim_zeros(val) - if '.' in val: - return sgn + '.'.join(('%d' % whole, val.split('.')[1])) - else: # pragma: no cover - return sgn + '.'.join(('%d' % whole, val)) - else: - return sgn + '%0.f' % whole - else: - return str(x) - - -def _trim_zeros(x): - while len(x) > 1 and x[-1] == '0': - x = x[:-1] - if len(x) > 1 and x[-1] == '.': - x = x[:-1] - return x - - -def _coerce_to_type(x): - """ - if the passed data is of datetime/timedelta type, - this method converts it to integer so that cut method can - handle it - """ - dtype = None - - if is_timedelta64_dtype(x): - x = to_timedelta(x).view(np.int64) - dtype = np.timedelta64 - elif is_datetime64_dtype(x): - x = to_datetime(x).view(np.int64) - dtype = np.datetime64 - - return x, dtype - - -def _convert_bin_to_numeric_type(x): - """ - if the passed bin is of datetime/timedelta type, - this method converts it to integer - """ - dtype = infer_dtype(x) - if dtype == 'timedelta' or dtype == 'timedelta64': - x = to_timedelta(x).view(np.int64) - elif dtype == 'datetime' or dtype == 'datetime64': - x = to_datetime(x).view(np.int64) - return x - - -def _preprocess_for_cut(x): - """ - handles preprocessing for cut where we convert passed - input to array, strip the index information and store it - seperately - """ - x_is_series = isinstance(x, Series) - series_index = None - name = None - - if x_is_series: - series_index = x.index - name = x.name - - x = np.asarray(x) - - return x_is_series, series_index, name, x - - -def _postprocess_for_cut(fac, bins, retbins, x_is_series, series_index, name): - """ - handles post processing for the cut method where - we combine the index information if the originally passed - datatype was a series - """ - if x_is_series: - fac = Series(fac, index=series_index, name=name) - - if not retbins: - return fac - - return fac, bins diff --git a/pandas/tseries/api.py b/pandas/tseries/api.py index a00ccf99e1b96..2094791ecdc60 100644 --- a/pandas/tseries/api.py +++ b/pandas/tseries/api.py @@ -1,14 +1,8 @@ """ - +Timeseries API """ # flake8: noqa -from pandas.tseries.index import DatetimeIndex, date_range, bdate_range from pandas.tseries.frequencies import infer_freq -from pandas.tseries.tdi import Timedelta, TimedeltaIndex, timedelta_range -from pandas.tseries.period import Period, PeriodIndex, period_range, pnow -from pandas.tseries.resample import TimeGrouper -from pandas.tseries.timedeltas import to_timedelta -from pandas._libs.lib import NaT import pandas.tseries.offsets as offsets diff --git a/pandas/tseries/base.py b/pandas/tseries/base.py deleted file mode 100644 index ae40c2f66a590..0000000000000 --- a/pandas/tseries/base.py +++ /dev/null @@ -1,867 +0,0 @@ -""" -Base and utility classes for tseries type pandas objects. -""" -import warnings - -from datetime import datetime, timedelta - -from pandas import compat -from pandas.compat.numpy import function as nv - -import numpy as np -from pandas.types.common import (is_integer, is_float, - is_bool_dtype, _ensure_int64, - is_scalar, is_dtype_equal, - is_list_like) -from pandas.types.generic import (ABCIndex, ABCSeries, - ABCPeriodIndex, ABCIndexClass) -from pandas.types.missing import isnull -from pandas.core import common as com, algorithms -from pandas.core.algorithms import checked_add_with_arr -from pandas.core.common import AbstractMethodError - -import pandas.formats.printing as printing -from pandas._libs import (tslib as libts, lib, - Timedelta, Timestamp, iNaT, NaT) -from pandas._libs.period import Period - -from pandas.core.index import Index -from pandas.indexes.base import _index_shared_docs -from pandas.util.decorators import Appender, cache_readonly -import pandas.types.concat as _concat -import pandas.tseries.frequencies as frequencies - - -class DatelikeOps(object): - """ common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex """ - - def strftime(self, date_format): - return np.asarray(self.format(date_format=date_format), - dtype=compat.text_type) - strftime.__doc__ = """ - Return an array of formatted strings specified by date_format, which - supports the same string format as the python standard library. Details - of the string format can be found in `python string format doc <{0}>`__ - - .. versionadded:: 0.17.0 - - Parameters - ---------- - date_format : str - date format string (e.g. "%Y-%m-%d") - - Returns - ------- - ndarray of formatted strings - """.format("https://docs.python.org/2/library/datetime.html" - "#strftime-and-strptime-behavior") - - -class TimelikeOps(object): - """ common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex """ - - _round_doc = ( - """ - %s the index to the specified freq - - Parameters - ---------- - freq : freq string/object - - Returns - ------- - index of same type - - Raises - ------ - ValueError if the freq cannot be converted - """) - - def _round(self, freq, rounder): - - from pandas.tseries.frequencies import to_offset - unit = to_offset(freq).nanos - # round the local times - values = _ensure_datetimelike_to_i8(self) - if unit < 1000 and unit % 1000 != 0: - # for nano rounding, work with the last 6 digits separately - # due to float precision - buff = 1000000 - result = (buff * (values // buff) + unit * - (rounder((values % buff) / float(unit))).astype('i8')) - elif unit >= 1000 and unit % 1000 != 0: - msg = 'Precision will be lost using frequency: {}' - warnings.warn(msg.format(freq)) - result = (unit * rounder(values / float(unit)).astype('i8')) - else: - result = (unit * rounder(values / float(unit)).astype('i8')) - result = self._maybe_mask_results(result, fill_value=NaT) - - attribs = self._get_attributes_dict() - if 'freq' in attribs: - attribs['freq'] = None - if 'tz' in attribs: - attribs['tz'] = None - return self._ensure_localized( - self._shallow_copy(result, **attribs)) - - @Appender(_round_doc % "round") - def round(self, freq, *args, **kwargs): - return self._round(freq, np.round) - - @Appender(_round_doc % "floor") - def floor(self, freq): - return self._round(freq, np.floor) - - @Appender(_round_doc % "ceil") - def ceil(self, freq): - return self._round(freq, np.ceil) - - -class DatetimeIndexOpsMixin(object): - """ common ops mixin to support a unified inteface datetimelike Index """ - - def equals(self, other): - """ - Determines if two Index objects contain the same elements. - """ - if self.is_(other): - return True - - if not isinstance(other, ABCIndexClass): - return False - elif not isinstance(other, type(self)): - try: - other = type(self)(other) - except: - return False - - if not is_dtype_equal(self.dtype, other.dtype): - # have different timezone - return False - - # ToDo: Remove this when PeriodDtype is added - elif isinstance(self, ABCPeriodIndex): - if not isinstance(other, ABCPeriodIndex): - return False - if self.freq != other.freq: - return False - - return np.array_equal(self.asi8, other.asi8) - - def __iter__(self): - return (self._box_func(v) for v in self.asi8) - - @staticmethod - def _join_i8_wrapper(joinf, dtype, with_indexers=True): - """ create the join wrapper methods """ - - @staticmethod - def wrapper(left, right): - if isinstance(left, (np.ndarray, ABCIndex, ABCSeries)): - left = left.view('i8') - if isinstance(right, (np.ndarray, ABCIndex, ABCSeries)): - right = right.view('i8') - results = joinf(left, right) - if with_indexers: - join_index, left_indexer, right_indexer = results - join_index = join_index.view(dtype) - return join_index, left_indexer, right_indexer - return results - - return wrapper - - def _evaluate_compare(self, other, op): - """ - We have been called because a comparison between - 8 aware arrays. numpy >= 1.11 will - now warn about NaT comparisons - """ - - # coerce to a similar object - if not isinstance(other, type(self)): - if not is_list_like(other): - # scalar - other = [other] - elif is_scalar(lib.item_from_zerodim(other)): - # ndarray scalar - other = [other.item()] - other = type(self)(other) - - # compare - result = op(self.asi8, other.asi8) - - # technically we could support bool dtyped Index - # for now just return the indexing array directly - mask = (self._isnan) | (other._isnan) - if is_bool_dtype(result): - result[mask] = False - return result - try: - result[mask] = iNaT - return Index(result) - except TypeError: - return result - - def _ensure_localized(self, result): - """ - ensure that we are re-localized - - This is for compat as we can then call this on all datetimelike - indexes generally (ignored for Period/Timedelta) - - Parameters - ---------- - result : DatetimeIndex / i8 ndarray - - Returns - ------- - localized DTI - """ - - # reconvert to local tz - if getattr(self, 'tz', None) is not None: - if not isinstance(result, ABCIndexClass): - result = self._simple_new(result) - result = result.tz_localize(self.tz) - return result - - @property - def _box_func(self): - """ - box function to get object from internal representation - """ - raise AbstractMethodError(self) - - def _box_values(self, values): - """ - apply box func to passed values - """ - return lib.map_infer(values, self._box_func) - - def _format_with_header(self, header, **kwargs): - return header + list(self._format_native_types(**kwargs)) - - def __contains__(self, key): - try: - res = self.get_loc(key) - return is_scalar(res) or type(res) == slice or np.any(res) - except (KeyError, TypeError, ValueError): - return False - - def __getitem__(self, key): - """ - This getitem defers to the underlying array, which by-definition can - only handle list-likes, slices, and integer scalars - """ - - is_int = is_integer(key) - if is_scalar(key) and not is_int: - raise ValueError - - getitem = self._data.__getitem__ - if is_int: - val = getitem(key) - return self._box_func(val) - else: - if com.is_bool_indexer(key): - key = np.asarray(key) - if key.all(): - key = slice(0, None, None) - else: - key = lib.maybe_booleans_to_slice(key.view(np.uint8)) - - attribs = self._get_attributes_dict() - - is_period = isinstance(self, ABCPeriodIndex) - if is_period: - freq = self.freq - else: - freq = None - if isinstance(key, slice): - if self.freq is not None and key.step is not None: - freq = key.step * self.freq - else: - freq = self.freq - - attribs['freq'] = freq - - result = getitem(key) - if result.ndim > 1: - # To support MPL which performs slicing with 2 dim - # even though it only has 1 dim by definition - if is_period: - return self._simple_new(result, **attribs) - return result - - return self._simple_new(result, **attribs) - - @property - def freqstr(self): - """ - Return the frequency object as a string if its set, otherwise None - """ - if self.freq is None: - return None - return self.freq.freqstr - - @cache_readonly - def inferred_freq(self): - """ - Trys to return a string representing a frequency guess, - generated by infer_freq. Returns None if it can't autodetect the - frequency. - """ - try: - return frequencies.infer_freq(self) - except ValueError: - return None - - def _nat_new(self, box=True): - """ - Return Index or ndarray filled with NaT which has the same - length as the caller. - - Parameters - ---------- - box : boolean, default True - - If True returns a Index as the same as caller. - - If False returns ndarray of np.int64. - """ - result = np.zeros(len(self), dtype=np.int64) - result.fill(iNaT) - if not box: - return result - - attribs = self._get_attributes_dict() - if not isinstance(self, ABCPeriodIndex): - attribs['freq'] = None - return self._simple_new(result, **attribs) - - # Try to run function on index first, and then on elements of index - # Especially important for group-by functionality - def map(self, f): - try: - result = f(self) - - # Try to use this result if we can - if isinstance(result, np.ndarray): - self._shallow_copy(result) - - if not isinstance(result, Index): - raise TypeError('The map function must return an Index object') - return result - except Exception: - return self.asobject.map(f) - - def sort_values(self, return_indexer=False, ascending=True): - """ - Return sorted copy of Index - """ - if return_indexer: - _as = self.argsort() - if not ascending: - _as = _as[::-1] - sorted_index = self.take(_as) - return sorted_index, _as - else: - sorted_values = np.sort(self._values) - attribs = self._get_attributes_dict() - freq = attribs['freq'] - - if freq is not None and not isinstance(self, ABCPeriodIndex): - if freq.n > 0 and not ascending: - freq = freq * -1 - elif freq.n < 0 and ascending: - freq = freq * -1 - attribs['freq'] = freq - - if not ascending: - sorted_values = sorted_values[::-1] - - return self._simple_new(sorted_values, **attribs) - - @Appender(_index_shared_docs['take']) - def take(self, indices, axis=0, allow_fill=True, - fill_value=None, **kwargs): - nv.validate_take(tuple(), kwargs) - indices = _ensure_int64(indices) - - maybe_slice = lib.maybe_indices_to_slice(indices, len(self)) - if isinstance(maybe_slice, slice): - return self[maybe_slice] - - taken = self._assert_take_fillable(self.asi8, indices, - allow_fill=allow_fill, - fill_value=fill_value, - na_value=iNaT) - - # keep freq in PeriodIndex, reset otherwise - freq = self.freq if isinstance(self, ABCPeriodIndex) else None - return self._shallow_copy(taken, freq=freq) - - def get_duplicates(self): - values = Index.get_duplicates(self) - return self._simple_new(values) - - _can_hold_na = True - - _na_value = NaT - """The expected NA value to use with this index.""" - - @cache_readonly - def _isnan(self): - """ return if each value is nan""" - return (self.asi8 == iNaT) - - @property - def asobject(self): - """ - return object Index which contains boxed values - - *this is an internal non-public method* - """ - from pandas.core.index import Index - return Index(self._box_values(self.asi8), name=self.name, dtype=object) - - def _convert_tolerance(self, tolerance): - try: - return Timedelta(tolerance).to_timedelta64() - except ValueError: - raise ValueError('tolerance argument for %s must be convertible ' - 'to Timedelta: %r' - % (type(self).__name__, tolerance)) - - def _maybe_mask_results(self, result, fill_value=None, convert=None): - """ - Parameters - ---------- - result : a ndarray - convert : string/dtype or None - - Returns - ------- - result : ndarray with values replace by the fill_value - - mask the result if needed, convert to the provided dtype if its not - None - - This is an internal routine - """ - - if self.hasnans: - if convert: - result = result.astype(convert) - if fill_value is None: - fill_value = np.nan - result[self._isnan] = fill_value - return result - - def tolist(self): - """ - return a list of the underlying data - """ - return list(self.asobject) - - def min(self, axis=None, *args, **kwargs): - """ - Return the minimum value of the Index or minimum along - an axis. - - See also - -------- - numpy.ndarray.min - """ - nv.validate_min(args, kwargs) - - try: - i8 = self.asi8 - - # quick check - if len(i8) and self.is_monotonic: - if i8[0] != iNaT: - return self._box_func(i8[0]) - - if self.hasnans: - min_stamp = self[~self._isnan].asi8.min() - else: - min_stamp = i8.min() - return self._box_func(min_stamp) - except ValueError: - return self._na_value - - def argmin(self, axis=None, *args, **kwargs): - """ - Returns the indices of the minimum values along an axis. - See `numpy.ndarray.argmin` for more information on the - `axis` parameter. - - See also - -------- - numpy.ndarray.argmin - """ - nv.validate_argmin(args, kwargs) - - i8 = self.asi8 - if self.hasnans: - mask = self._isnan - if mask.all(): - return -1 - i8 = i8.copy() - i8[mask] = np.iinfo('int64').max - return i8.argmin() - - def max(self, axis=None, *args, **kwargs): - """ - Return the maximum value of the Index or maximum along - an axis. - - See also - -------- - numpy.ndarray.max - """ - nv.validate_max(args, kwargs) - - try: - i8 = self.asi8 - - # quick check - if len(i8) and self.is_monotonic: - if i8[-1] != iNaT: - return self._box_func(i8[-1]) - - if self.hasnans: - max_stamp = self[~self._isnan].asi8.max() - else: - max_stamp = i8.max() - return self._box_func(max_stamp) - except ValueError: - return self._na_value - - def argmax(self, axis=None, *args, **kwargs): - """ - Returns the indices of the maximum values along an axis. - See `numpy.ndarray.argmax` for more information on the - `axis` parameter. - - See also - -------- - numpy.ndarray.argmax - """ - nv.validate_argmax(args, kwargs) - - i8 = self.asi8 - if self.hasnans: - mask = self._isnan - if mask.all(): - return -1 - i8 = i8.copy() - i8[mask] = 0 - return i8.argmax() - - @property - def _formatter_func(self): - raise AbstractMethodError(self) - - def _format_attrs(self): - """ - Return a list of tuples of the (attr,formatted_value) - """ - attrs = super(DatetimeIndexOpsMixin, self)._format_attrs() - for attrib in self._attributes: - if attrib == 'freq': - freq = self.freqstr - if freq is not None: - freq = "'%s'" % freq - attrs.append(('freq', freq)) - return attrs - - @cache_readonly - def _resolution(self): - return frequencies.Resolution.get_reso_from_freq(self.freqstr) - - @cache_readonly - def resolution(self): - """ - Returns day, hour, minute, second, millisecond or microsecond - """ - return frequencies.Resolution.get_str(self._resolution) - - def _convert_scalar_indexer(self, key, kind=None): - """ - we don't allow integer or float indexing on datetime-like when using - loc - - Parameters - ---------- - key : label of the slice bound - kind : {'ix', 'loc', 'getitem', 'iloc'} or None - """ - - assert kind in ['ix', 'loc', 'getitem', 'iloc', None] - - # we don't allow integer/float indexing for loc - # we don't allow float indexing for ix/getitem - if is_scalar(key): - is_int = is_integer(key) - is_flt = is_float(key) - if kind in ['loc'] and (is_int or is_flt): - self._invalid_indexer('index', key) - elif kind in ['ix', 'getitem'] and is_flt: - self._invalid_indexer('index', key) - - return (super(DatetimeIndexOpsMixin, self) - ._convert_scalar_indexer(key, kind=kind)) - - def _add_datelike(self, other): - raise AbstractMethodError(self) - - def _sub_datelike(self, other): - raise AbstractMethodError(self) - - def _sub_period(self, other): - return NotImplemented - - @classmethod - def _add_datetimelike_methods(cls): - """ - add in the datetimelike methods (as we may have to override the - superclass) - """ - - def __add__(self, other): - from pandas.core.index import Index - from pandas.tseries.tdi import TimedeltaIndex - from pandas.tseries.offsets import DateOffset - if isinstance(other, TimedeltaIndex): - return self._add_delta(other) - elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): - if hasattr(other, '_add_delta'): - return other._add_delta(self) - raise TypeError("cannot add TimedeltaIndex and {typ}" - .format(typ=type(other))) - elif isinstance(other, Index): - raise TypeError("cannot add {typ1} and {typ2}" - .format(typ1=type(self).__name__, - typ2=type(other).__name__)) - elif isinstance(other, (DateOffset, timedelta, np.timedelta64, - Timedelta)): - return self._add_delta(other) - elif is_integer(other): - return self.shift(other) - elif isinstance(other, (Timestamp, datetime)): - return self._add_datelike(other) - else: # pragma: no cover - return NotImplemented - cls.__add__ = __add__ - cls.__radd__ = __add__ - - def __sub__(self, other): - from pandas.core.index import Index - from pandas.tseries.index import DatetimeIndex - from pandas.tseries.tdi import TimedeltaIndex - from pandas.tseries.offsets import DateOffset - if isinstance(other, TimedeltaIndex): - return self._add_delta(-other) - elif isinstance(self, TimedeltaIndex) and isinstance(other, Index): - if not isinstance(other, TimedeltaIndex): - raise TypeError("cannot subtract TimedeltaIndex and {typ}" - .format(typ=type(other).__name__)) - return self._add_delta(-other) - elif isinstance(other, DatetimeIndex): - return self._sub_datelike(other) - elif isinstance(other, Index): - raise TypeError("cannot subtract {typ1} and {typ2}" - .format(typ1=type(self).__name__, - typ2=type(other).__name__)) - elif isinstance(other, (DateOffset, timedelta, np.timedelta64, - Timedelta)): - return self._add_delta(-other) - elif is_integer(other): - return self.shift(-other) - elif isinstance(other, (Timestamp, datetime)): - return self._sub_datelike(other) - elif isinstance(other, Period): - return self._sub_period(other) - else: # pragma: no cover - return NotImplemented - cls.__sub__ = __sub__ - - def __rsub__(self, other): - return -(self - other) - cls.__rsub__ = __rsub__ - - cls.__iadd__ = __add__ - cls.__isub__ = __sub__ - - def _add_delta(self, other): - return NotImplemented - - def _add_delta_td(self, other): - # add a delta of a timedeltalike - # return the i8 result view - - inc = libts._delta_to_nanoseconds(other) - new_values = checked_add_with_arr(self.asi8, inc, - arr_mask=self._isnan).view('i8') - if self.hasnans: - new_values[self._isnan] = iNaT - return new_values.view('i8') - - def _add_delta_tdi(self, other): - # add a delta of a TimedeltaIndex - # return the i8 result view - - # delta operation - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = checked_add_with_arr(self_i8, other_i8, - arr_mask=self._isnan, - b_mask=other._isnan) - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = iNaT - return new_values.view(self.dtype) - - def isin(self, values): - """ - Compute boolean array of whether each index value is found in the - passed set of values - - Parameters - ---------- - values : set or sequence of values - - Returns - ------- - is_contained : ndarray (boolean dtype) - """ - if not isinstance(values, type(self)): - try: - values = type(self)(values) - except ValueError: - return self.asobject.isin(values) - - return algorithms.isin(self.asi8, values.asi8) - - def shift(self, n, freq=None): - """ - Specialized shift which produces a DatetimeIndex - - Parameters - ---------- - n : int - Periods to shift by - freq : DateOffset or timedelta-like, optional - - Returns - ------- - shifted : DatetimeIndex - """ - if freq is not None and freq != self.freq: - if isinstance(freq, compat.string_types): - freq = frequencies.to_offset(freq) - offset = n * freq - result = self + offset - - if hasattr(self, 'tz'): - result.tz = self.tz - - return result - - if n == 0: - # immutable so OK - return self - - if self.freq is None: - raise ValueError("Cannot shift with no freq") - - start = self[0] + n * self.freq - end = self[-1] + n * self.freq - attribs = self._get_attributes_dict() - attribs['start'] = start - attribs['end'] = end - return type(self)(**attribs) - - def repeat(self, repeats, *args, **kwargs): - """ - Analogous to ndarray.repeat - """ - nv.validate_repeat(args, kwargs) - if isinstance(self, ABCPeriodIndex): - freq = self.freq - else: - freq = None - return self._shallow_copy(self.asi8.repeat(repeats), - freq=freq) - - @Appender(_index_shared_docs['where']) - def where(self, cond, other=None): - other = _ensure_datetimelike_to_i8(other) - values = _ensure_datetimelike_to_i8(self) - result = np.where(cond, values, other).astype('i8') - - result = self._ensure_localized(result) - return self._shallow_copy(result, - **self._get_attributes_dict()) - - def summary(self, name=None): - """ - return a summarized representation - """ - formatter = self._formatter_func - if len(self) > 0: - index_summary = ', %s to %s' % (formatter(self[0]), - formatter(self[-1])) - else: - index_summary = '' - - if name is None: - name = type(self).__name__ - result = '%s: %s entries%s' % (printing.pprint_thing(name), - len(self), index_summary) - if self.freq: - result += '\nFreq: %s' % self.freqstr - - # display as values, not quoted - result = result.replace("'", "") - return result - - def _append_same_dtype(self, to_concat, name): - """ - Concatenate to_concat which has the same class - """ - attribs = self._get_attributes_dict() - attribs['name'] = name - - if not isinstance(self, ABCPeriodIndex): - # reset freq - attribs['freq'] = None - - if getattr(self, 'tz', None) is not None: - return _concat._concat_datetimetz(to_concat, name) - else: - new_data = np.concatenate([c.asi8 for c in to_concat]) - return self._simple_new(new_data, **attribs) - - -def _ensure_datetimelike_to_i8(other): - """ helper for coercing an input scalar or array to i8 """ - if lib.isscalar(other) and isnull(other): - other = iNaT - elif isinstance(other, ABCIndexClass): - # convert tz if needed - if getattr(other, 'tz', None) is not None: - other = other.tz_localize(None).asi8 - else: - other = other.asi8 - else: - try: - other = np.array(other, copy=False).view('i8') - except TypeError: - # period array cannot be coerces to int - other = Index(other).asi8 - return other diff --git a/pandas/tseries/common.py b/pandas/tseries/common.py deleted file mode 100644 index 7940efc7e1b59..0000000000000 --- a/pandas/tseries/common.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -datetimelike delegation -""" - -import numpy as np - -from pandas.types.common import (_NS_DTYPE, _TD_DTYPE, - is_period_arraylike, - is_datetime_arraylike, is_integer_dtype, - is_datetime64_dtype, is_datetime64tz_dtype, - is_timedelta64_dtype, is_categorical_dtype, - is_list_like) - -from pandas.core.base import PandasDelegate, NoNewAttributesMixin -from pandas.tseries.index import DatetimeIndex -from pandas._libs.period import IncompatibleFrequency # flake8: noqa -from pandas.tseries.period import PeriodIndex -from pandas.tseries.tdi import TimedeltaIndex -from pandas.core.algorithms import take_1d - - -def is_datetimelike(data): - """ - return a boolean if we can be successfully converted to a datetimelike - """ - try: - maybe_to_datetimelike(data) - return True - except (Exception): - pass - return False - - -def maybe_to_datetimelike(data, copy=False): - """ - return a DelegatedClass of a Series that is datetimelike - (e.g. datetime64[ns],timedelta64[ns] dtype or a Series of Periods) - raise TypeError if this is not possible. - - Parameters - ---------- - data : Series - copy : boolean, default False - copy the input data - - Returns - ------- - DelegatedClass - - """ - from pandas import Series - - if not isinstance(data, Series): - raise TypeError("cannot convert an object of type {0} to a " - "datetimelike index".format(type(data))) - - index = data.index - name = data.name - orig = data if is_categorical_dtype(data) else None - if orig is not None: - data = orig.values.categories - - if is_datetime64_dtype(data.dtype): - return DatetimeProperties(DatetimeIndex(data, copy=copy, freq='infer'), - index, name=name, orig=orig) - elif is_datetime64tz_dtype(data.dtype): - return DatetimeProperties(DatetimeIndex(data, copy=copy, freq='infer', - ambiguous='infer'), - index, data.name, orig=orig) - elif is_timedelta64_dtype(data.dtype): - return TimedeltaProperties(TimedeltaIndex(data, copy=copy, - freq='infer'), index, - name=name, orig=orig) - else: - if is_period_arraylike(data): - return PeriodProperties(PeriodIndex(data, copy=copy), index, - name=name, orig=orig) - if is_datetime_arraylike(data): - return DatetimeProperties(DatetimeIndex(data, copy=copy, - freq='infer'), index, - name=name, orig=orig) - - raise TypeError("cannot convert an object of type {0} to a " - "datetimelike index".format(type(data))) - - -class Properties(PandasDelegate, NoNewAttributesMixin): - - def __init__(self, values, index, name, orig=None): - self.values = values - self.index = index - self.name = name - self.orig = orig - self._freeze() - - def _delegate_property_get(self, name): - from pandas import Series - - result = getattr(self.values, name) - - # maybe need to upcast (ints) - if isinstance(result, np.ndarray): - if is_integer_dtype(result): - result = result.astype('int64') - elif not is_list_like(result): - return result - - result = np.asarray(result) - - # blow up if we operate on categories - if self.orig is not None: - result = take_1d(result, self.orig.cat.codes) - - # return the result as a Series, which is by definition a copy - result = Series(result, index=self.index, name=self.name) - - # setting this object will show a SettingWithCopyWarning/Error - result.is_copy = ("modifications to a property of a datetimelike " - "object are not supported and are discarded. " - "Change values on the original.") - - return result - - def _delegate_property_set(self, name, value, *args, **kwargs): - raise ValueError("modifications to a property of a datetimelike " - "object are not supported. Change values on the " - "original.") - - def _delegate_method(self, name, *args, **kwargs): - from pandas import Series - - method = getattr(self.values, name) - result = method(*args, **kwargs) - - if not is_list_like(result): - return result - - result = Series(result, index=self.index, name=self.name) - - # setting this object will show a SettingWithCopyWarning/Error - result.is_copy = ("modifications to a method of a datetimelike object " - "are not supported and are discarded. Change " - "values on the original.") - - return result - - -class DatetimeProperties(Properties): - """ - Accessor object for datetimelike properties of the Series values. - - Examples - -------- - >>> s.dt.hour - >>> s.dt.second - >>> s.dt.quarter - - Returns a Series indexed like the original Series. - Raises TypeError if the Series does not contain datetimelike values. - """ - - def to_pydatetime(self): - return self.values.to_pydatetime() - -DatetimeProperties._add_delegate_accessors( - delegate=DatetimeIndex, - accessors=DatetimeIndex._datetimelike_ops, - typ='property') -DatetimeProperties._add_delegate_accessors( - delegate=DatetimeIndex, - accessors=DatetimeIndex._datetimelike_methods, - typ='method') - - -class TimedeltaProperties(Properties): - """ - Accessor object for datetimelike properties of the Series values. - - Examples - -------- - >>> s.dt.hours - >>> s.dt.seconds - - Returns a Series indexed like the original Series. - Raises TypeError if the Series does not contain datetimelike values. - """ - - def to_pytimedelta(self): - return self.values.to_pytimedelta() - - @property - def components(self): - """ - Return a dataframe of the components (days, hours, minutes, - seconds, milliseconds, microseconds, nanoseconds) of the Timedeltas. - - Returns - ------- - a DataFrame - - """ - return self.values.components.set_index(self.index) - -TimedeltaProperties._add_delegate_accessors( - delegate=TimedeltaIndex, - accessors=TimedeltaIndex._datetimelike_ops, - typ='property') -TimedeltaProperties._add_delegate_accessors( - delegate=TimedeltaIndex, - accessors=TimedeltaIndex._datetimelike_methods, - typ='method') - - -class PeriodProperties(Properties): - """ - Accessor object for datetimelike properties of the Series values. - - Examples - -------- - >>> s.dt.hour - >>> s.dt.second - >>> s.dt.quarter - - Returns a Series indexed like the original Series. - Raises TypeError if the Series does not contain datetimelike values. - """ - -PeriodProperties._add_delegate_accessors( - delegate=PeriodIndex, - accessors=PeriodIndex._datetimelike_ops, - typ='property') -PeriodProperties._add_delegate_accessors( - delegate=PeriodIndex, - accessors=PeriodIndex._datetimelike_methods, - typ='method') - - -class CombinedDatetimelikeProperties(DatetimeProperties, TimedeltaProperties): - # This class is never instantiated, and exists solely for the benefit of - # the Series.dt class property. For Series objects, .dt will always be one - # of the more specific classes above. - __doc__ = DatetimeProperties.__doc__ diff --git a/pandas/tseries/converter.py b/pandas/tseries/converter.py index bc768a8bc5b58..05dd7cea1bd2f 100644 --- a/pandas/tseries/converter.py +++ b/pandas/tseries/converter.py @@ -1,1032 +1,16 @@ -from datetime import datetime, timedelta -import datetime as pydt -import numpy as np +# flake8: noqa +import warnings -from dateutil.relativedelta import relativedelta - -import matplotlib.units as units -import matplotlib.dates as dates - -from matplotlib.ticker import Formatter, AutoLocator, Locator -from matplotlib.transforms import nonsingular - - -from pandas.types.common import (is_float, is_integer, - is_integer_dtype, - is_float_dtype, - is_datetime64_ns_dtype, - is_period_arraylike, - ) - -from pandas.compat import lrange -import pandas.compat as compat -import pandas._libs.lib as lib -import pandas.core.common as com -from pandas.core.index import Index - -from pandas.core.series import Series -from pandas.tseries.index import date_range -import pandas.tseries.tools as tools -import pandas.tseries.frequencies as frequencies -from pandas.tseries.frequencies import FreqGroup -from pandas.tseries.period import Period, PeriodIndex - -# constants -HOURS_PER_DAY = 24. -MIN_PER_HOUR = 60. -SEC_PER_MIN = 60. - -SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR -SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY - -MUSEC_PER_DAY = 1e6 * SEC_PER_DAY - - -def _mpl_le_2_0_0(): - try: - import matplotlib - return matplotlib.compare_versions('2.0.0', matplotlib.__version__) - except ImportError: - return False +from pandas.plotting._converter import ( + DatetimeConverter, MilliSecondLocator, PandasAutoDateFormatter, + PandasAutoDateLocator, PeriodConverter, TimeConverter, TimeFormatter, + TimeSeries_DateFormatter, TimeSeries_DateLocator, get_datevalue, + get_finder, time2num) def register(): - units.registry[lib.Timestamp] = DatetimeConverter() - units.registry[Period] = PeriodConverter() - units.registry[pydt.datetime] = DatetimeConverter() - units.registry[pydt.date] = DatetimeConverter() - units.registry[pydt.time] = TimeConverter() - units.registry[np.datetime64] = DatetimeConverter() - - -def _to_ordinalf(tm): - tot_sec = (tm.hour * 3600 + tm.minute * 60 + tm.second + - float(tm.microsecond / 1e6)) - return tot_sec - - -def time2num(d): - if isinstance(d, compat.string_types): - parsed = tools.to_datetime(d) - if not isinstance(parsed, datetime): - raise ValueError('Could not parse time %s' % d) - return _to_ordinalf(parsed.time()) - if isinstance(d, pydt.time): - return _to_ordinalf(d) - return d - - -class TimeConverter(units.ConversionInterface): - - @staticmethod - def convert(value, unit, axis): - valid_types = (str, pydt.time) - if (isinstance(value, valid_types) or is_integer(value) or - is_float(value)): - return time2num(value) - if isinstance(value, Index): - return value.map(time2num) - if isinstance(value, (list, tuple, np.ndarray, Index)): - return [time2num(x) for x in value] - return value - - @staticmethod - def axisinfo(unit, axis): - if unit != 'time': - return None - - majloc = AutoLocator() - majfmt = TimeFormatter(majloc) - return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='time') - - @staticmethod - def default_units(x, axis): - return 'time' - - -# time formatter -class TimeFormatter(Formatter): - - def __init__(self, locs): - self.locs = locs - - def __call__(self, x, pos=0): - fmt = '%H:%M:%S' - s = int(x) - ms = int((x - s) * 1e3) - us = int((x - s) * 1e6 - ms) - m, s = divmod(s, 60) - h, m = divmod(m, 60) - _, h = divmod(h, 24) - if us != 0: - fmt += '.%6f' - elif ms != 0: - fmt += '.%3f' - - return pydt.time(h, m, s, us).strftime(fmt) - - -# Period Conversion - - -class PeriodConverter(dates.DateConverter): - - @staticmethod - def convert(values, units, axis): - if not hasattr(axis, 'freq'): - raise TypeError('Axis must have `freq` set to convert to Periods') - valid_types = (compat.string_types, datetime, - Period, pydt.date, pydt.time) - if (isinstance(values, valid_types) or is_integer(values) or - is_float(values)): - return get_datevalue(values, axis.freq) - if isinstance(values, PeriodIndex): - return values.asfreq(axis.freq)._values - if isinstance(values, Index): - return values.map(lambda x: get_datevalue(x, axis.freq)) - if is_period_arraylike(values): - return PeriodIndex(values, freq=axis.freq)._values - if isinstance(values, (list, tuple, np.ndarray, Index)): - return [get_datevalue(x, axis.freq) for x in values] - return values - - -def get_datevalue(date, freq): - if isinstance(date, Period): - return date.asfreq(freq).ordinal - elif isinstance(date, (compat.string_types, datetime, - pydt.date, pydt.time)): - return Period(date, freq).ordinal - elif (is_integer(date) or is_float(date) or - (isinstance(date, (np.ndarray, Index)) and (date.size == 1))): - return date - elif date is None: - return None - raise ValueError("Unrecognizable date '%s'" % date) - - -def _dt_to_float_ordinal(dt): - """ - Convert :mod:`datetime` to the Gregorian date as UTC float days, - preserving hours, minutes, seconds and microseconds. Return value - is a :func:`float`. - """ - if (isinstance(dt, (np.ndarray, Index, Series) - ) and is_datetime64_ns_dtype(dt)): - base = dates.epoch2num(dt.asi8 / 1.0E9) - else: - base = dates.date2num(dt) - return base - - -# Datetime Conversion -class DatetimeConverter(dates.DateConverter): - - @staticmethod - def convert(values, unit, axis): - def try_parse(values): - try: - return _dt_to_float_ordinal(tools.to_datetime(values)) - except Exception: - return values - - if isinstance(values, (datetime, pydt.date)): - return _dt_to_float_ordinal(values) - elif isinstance(values, np.datetime64): - return _dt_to_float_ordinal(lib.Timestamp(values)) - elif isinstance(values, pydt.time): - return dates.date2num(values) - elif (is_integer(values) or is_float(values)): - return values - elif isinstance(values, compat.string_types): - return try_parse(values) - elif isinstance(values, (list, tuple, np.ndarray, Index)): - if isinstance(values, Index): - values = values.values - if not isinstance(values, np.ndarray): - values = com._asarray_tuplesafe(values) - - if is_integer_dtype(values) or is_float_dtype(values): - return values - - try: - values = tools.to_datetime(values) - if isinstance(values, Index): - values = _dt_to_float_ordinal(values) - else: - values = [_dt_to_float_ordinal(x) for x in values] - except Exception: - values = _dt_to_float_ordinal(values) - - return values - - @staticmethod - def axisinfo(unit, axis): - """ - Return the :class:`~matplotlib.units.AxisInfo` for *unit*. - - *unit* is a tzinfo instance or None. - The *axis* argument is required but not used. - """ - tz = unit - - majloc = PandasAutoDateLocator(tz=tz) - majfmt = PandasAutoDateFormatter(majloc, tz=tz) - datemin = pydt.date(2000, 1, 1) - datemax = pydt.date(2010, 1, 1) - - return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', - default_limits=(datemin, datemax)) - - -class PandasAutoDateFormatter(dates.AutoDateFormatter): - - def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'): - dates.AutoDateFormatter.__init__(self, locator, tz, defaultfmt) - # matplotlib.dates._UTC has no _utcoffset called by pandas - if self._tz is dates.UTC: - self._tz._utcoffset = self._tz.utcoffset(None) - - # For mpl > 2.0 the format strings are controlled via rcparams - # so do not mess with them. For mpl < 2.0 change the second - # break point and add a musec break point - if _mpl_le_2_0_0(): - self.scaled[1. / SEC_PER_DAY] = '%H:%M:%S' - self.scaled[1. / MUSEC_PER_DAY] = '%H:%M:%S.%f' - - -class PandasAutoDateLocator(dates.AutoDateLocator): - - def get_locator(self, dmin, dmax): - 'Pick the best locator based on a distance.' - delta = relativedelta(dmax, dmin) - - num_days = (delta.years * 12.0 + delta.months) * 31.0 + delta.days - num_sec = (delta.hours * 60.0 + delta.minutes) * 60.0 + delta.seconds - tot_sec = num_days * 86400. + num_sec - - if abs(tot_sec) < self.minticks: - self._freq = -1 - locator = MilliSecondLocator(self.tz) - locator.set_axis(self.axis) - - locator.set_view_interval(*self.axis.get_view_interval()) - locator.set_data_interval(*self.axis.get_data_interval()) - return locator - - return dates.AutoDateLocator.get_locator(self, dmin, dmax) - - def _get_unit(self): - return MilliSecondLocator.get_unit_generic(self._freq) - - -class MilliSecondLocator(dates.DateLocator): - - UNIT = 1. / (24 * 3600 * 1000) - - def __init__(self, tz): - dates.DateLocator.__init__(self, tz) - self._interval = 1. - - def _get_unit(self): - return self.get_unit_generic(-1) - - @staticmethod - def get_unit_generic(freq): - unit = dates.RRuleLocator.get_unit_generic(freq) - if unit < 0: - return MilliSecondLocator.UNIT - return unit - - def __call__(self): - # if no data have been set, this will tank with a ValueError - try: - dmin, dmax = self.viewlim_to_dt() - except ValueError: - return [] - - if dmin > dmax: - dmax, dmin = dmin, dmax - # We need to cap at the endpoints of valid datetime - - # TODO(wesm) unused? - # delta = relativedelta(dmax, dmin) - # try: - # start = dmin - delta - # except ValueError: - # start = _from_ordinal(1.0) - - # try: - # stop = dmax + delta - # except ValueError: - # # The magic number! - # stop = _from_ordinal(3652059.9999999) - - nmax, nmin = dates.date2num((dmax, dmin)) - - num = (nmax - nmin) * 86400 * 1000 - max_millis_ticks = 6 - for interval in [1, 10, 50, 100, 200, 500]: - if num <= interval * (max_millis_ticks - 1): - self._interval = interval - break - else: - # We went through the whole loop without breaking, default to 1 - self._interval = 1000. - - estimate = (nmax - nmin) / (self._get_unit() * self._get_interval()) - - if estimate > self.MAXTICKS * 2: - raise RuntimeError(('MillisecondLocator estimated to generate %d ' - 'ticks from %s to %s: exceeds Locator.MAXTICKS' - '* 2 (%d) ') % - (estimate, dmin, dmax, self.MAXTICKS * 2)) - - freq = '%dL' % self._get_interval() - tz = self.tz.tzname(None) - st = _from_ordinal(dates.date2num(dmin)) # strip tz - ed = _from_ordinal(dates.date2num(dmax)) - all_dates = date_range(start=st, end=ed, freq=freq, tz=tz).asobject - - try: - if len(all_dates) > 0: - locs = self.raise_if_exceeds(dates.date2num(all_dates)) - return locs - except Exception: # pragma: no cover - pass - - lims = dates.date2num([dmin, dmax]) - return lims - - def _get_interval(self): - return self._interval - - def autoscale(self): - """ - Set the view limits to include the data range. - """ - dmin, dmax = self.datalim_to_dt() - if dmin > dmax: - dmax, dmin = dmin, dmax - - # We need to cap at the endpoints of valid datetime - - # TODO(wesm): unused? - - # delta = relativedelta(dmax, dmin) - # try: - # start = dmin - delta - # except ValueError: - # start = _from_ordinal(1.0) - - # try: - # stop = dmax + delta - # except ValueError: - # # The magic number! - # stop = _from_ordinal(3652059.9999999) - - dmin, dmax = self.datalim_to_dt() - - vmin = dates.date2num(dmin) - vmax = dates.date2num(dmax) - - return self.nonsingular(vmin, vmax) - - -def _from_ordinal(x, tz=None): - ix = int(x) - dt = datetime.fromordinal(ix) - remainder = float(x) - ix - hour, remainder = divmod(24 * remainder, 1) - minute, remainder = divmod(60 * remainder, 1) - second, remainder = divmod(60 * remainder, 1) - microsecond = int(1e6 * remainder) - if microsecond < 10: - microsecond = 0 # compensate for rounding errors - dt = datetime(dt.year, dt.month, dt.day, int(hour), int(minute), - int(second), microsecond) - if tz is not None: - dt = dt.astimezone(tz) - - if microsecond > 999990: # compensate for rounding errors - dt += timedelta(microseconds=1e6 - microsecond) - - return dt - -# Fixed frequency dynamic tick locators and formatters - -# ------------------------------------------------------------------------- -# --- Locators --- -# ------------------------------------------------------------------------- - - -def _get_default_annual_spacing(nyears): - """ - Returns a default spacing between consecutive ticks for annual data. - """ - if nyears < 11: - (min_spacing, maj_spacing) = (1, 1) - elif nyears < 20: - (min_spacing, maj_spacing) = (1, 2) - elif nyears < 50: - (min_spacing, maj_spacing) = (1, 5) - elif nyears < 100: - (min_spacing, maj_spacing) = (5, 10) - elif nyears < 200: - (min_spacing, maj_spacing) = (5, 25) - elif nyears < 600: - (min_spacing, maj_spacing) = (10, 50) - else: - factor = nyears // 1000 + 1 - (min_spacing, maj_spacing) = (factor * 20, factor * 100) - return (min_spacing, maj_spacing) - - -def period_break(dates, period): - """ - Returns the indices where the given period changes. - - Parameters - ---------- - dates : PeriodIndex - Array of intervals to monitor. - period : string - Name of the period to monitor. - """ - current = getattr(dates, period) - previous = getattr(dates - 1, period) - return np.nonzero(current - previous)[0] - - -def has_level_label(label_flags, vmin): - """ - Returns true if the ``label_flags`` indicate there is at least one label - for this level. - - if the minimum view limit is not an exact integer, then the first tick - label won't be shown, so we must adjust for that. - """ - if label_flags.size == 0 or (label_flags.size == 1 and - label_flags[0] == 0 and - vmin % 1 > 0.0): - return False - else: - return True - - -def _daily_finder(vmin, vmax, freq): - periodsperday = -1 - - if freq >= FreqGroup.FR_HR: - if freq == FreqGroup.FR_NS: - periodsperday = 24 * 60 * 60 * 1000000000 - elif freq == FreqGroup.FR_US: - periodsperday = 24 * 60 * 60 * 1000000 - elif freq == FreqGroup.FR_MS: - periodsperday = 24 * 60 * 60 * 1000 - elif freq == FreqGroup.FR_SEC: - periodsperday = 24 * 60 * 60 - elif freq == FreqGroup.FR_MIN: - periodsperday = 24 * 60 - elif freq == FreqGroup.FR_HR: - periodsperday = 24 - else: # pragma: no cover - raise ValueError("unexpected frequency: %s" % freq) - periodsperyear = 365 * periodsperday - periodspermonth = 28 * periodsperday - - elif freq == FreqGroup.FR_BUS: - periodsperyear = 261 - periodspermonth = 19 - elif freq == FreqGroup.FR_DAY: - periodsperyear = 365 - periodspermonth = 28 - elif frequencies.get_freq_group(freq) == FreqGroup.FR_WK: - periodsperyear = 52 - periodspermonth = 3 - else: # pragma: no cover - raise ValueError("unexpected frequency") - - # save this for later usage - vmin_orig = vmin - - (vmin, vmax) = (Period(ordinal=int(vmin), freq=freq), - Period(ordinal=int(vmax), freq=freq)) - span = vmax.ordinal - vmin.ordinal + 1 - dates_ = PeriodIndex(start=vmin, end=vmax, freq=freq) - # Initialize the output - info = np.zeros(span, - dtype=[('val', np.int64), ('maj', bool), - ('min', bool), ('fmt', '|S20')]) - info['val'][:] = dates_._values - info['fmt'][:] = '' - info['maj'][[0, -1]] = True - # .. and set some shortcuts - info_maj = info['maj'] - info_min = info['min'] - info_fmt = info['fmt'] - - def first_label(label_flags): - if (label_flags[0] == 0) and (label_flags.size > 1) and \ - ((vmin_orig % 1) > 0.0): - return label_flags[1] - else: - return label_flags[0] - - # Case 1. Less than a month - if span <= periodspermonth: - day_start = period_break(dates_, 'day') - month_start = period_break(dates_, 'month') - - def _hour_finder(label_interval, force_year_start): - _hour = dates_.hour - _prev_hour = (dates_ - 1).hour - hour_start = (_hour - _prev_hour) != 0 - info_maj[day_start] = True - info_min[hour_start & (_hour % label_interval == 0)] = True - year_start = period_break(dates_, 'year') - info_fmt[hour_start & (_hour % label_interval == 0)] = '%H:%M' - info_fmt[day_start] = '%H:%M\n%d-%b' - info_fmt[year_start] = '%H:%M\n%d-%b\n%Y' - if force_year_start and not has_level_label(year_start, vmin_orig): - info_fmt[first_label(day_start)] = '%H:%M\n%d-%b\n%Y' - - def _minute_finder(label_interval): - hour_start = period_break(dates_, 'hour') - _minute = dates_.minute - _prev_minute = (dates_ - 1).minute - minute_start = (_minute - _prev_minute) != 0 - info_maj[hour_start] = True - info_min[minute_start & (_minute % label_interval == 0)] = True - year_start = period_break(dates_, 'year') - info_fmt = info['fmt'] - info_fmt[minute_start & (_minute % label_interval == 0)] = '%H:%M' - info_fmt[day_start] = '%H:%M\n%d-%b' - info_fmt[year_start] = '%H:%M\n%d-%b\n%Y' - - def _second_finder(label_interval): - minute_start = period_break(dates_, 'minute') - _second = dates_.second - _prev_second = (dates_ - 1).second - second_start = (_second - _prev_second) != 0 - info['maj'][minute_start] = True - info['min'][second_start & (_second % label_interval == 0)] = True - year_start = period_break(dates_, 'year') - info_fmt = info['fmt'] - info_fmt[second_start & (_second % - label_interval == 0)] = '%H:%M:%S' - info_fmt[day_start] = '%H:%M:%S\n%d-%b' - info_fmt[year_start] = '%H:%M:%S\n%d-%b\n%Y' - - if span < periodsperday / 12000.0: - _second_finder(1) - elif span < periodsperday / 6000.0: - _second_finder(2) - elif span < periodsperday / 2400.0: - _second_finder(5) - elif span < periodsperday / 1200.0: - _second_finder(10) - elif span < periodsperday / 800.0: - _second_finder(15) - elif span < periodsperday / 400.0: - _second_finder(30) - elif span < periodsperday / 150.0: - _minute_finder(1) - elif span < periodsperday / 70.0: - _minute_finder(2) - elif span < periodsperday / 24.0: - _minute_finder(5) - elif span < periodsperday / 12.0: - _minute_finder(15) - elif span < periodsperday / 6.0: - _minute_finder(30) - elif span < periodsperday / 2.5: - _hour_finder(1, False) - elif span < periodsperday / 1.5: - _hour_finder(2, False) - elif span < periodsperday * 1.25: - _hour_finder(3, False) - elif span < periodsperday * 2.5: - _hour_finder(6, True) - elif span < periodsperday * 4: - _hour_finder(12, True) - else: - info_maj[month_start] = True - info_min[day_start] = True - year_start = period_break(dates_, 'year') - info_fmt = info['fmt'] - info_fmt[day_start] = '%d' - info_fmt[month_start] = '%d\n%b' - info_fmt[year_start] = '%d\n%b\n%Y' - if not has_level_label(year_start, vmin_orig): - if not has_level_label(month_start, vmin_orig): - info_fmt[first_label(day_start)] = '%d\n%b\n%Y' - else: - info_fmt[first_label(month_start)] = '%d\n%b\n%Y' - - # Case 2. Less than three months - elif span <= periodsperyear // 4: - month_start = period_break(dates_, 'month') - info_maj[month_start] = True - if freq < FreqGroup.FR_HR: - info['min'] = True - else: - day_start = period_break(dates_, 'day') - info['min'][day_start] = True - week_start = period_break(dates_, 'week') - year_start = period_break(dates_, 'year') - info_fmt[week_start] = '%d' - info_fmt[month_start] = '\n\n%b' - info_fmt[year_start] = '\n\n%b\n%Y' - if not has_level_label(year_start, vmin_orig): - if not has_level_label(month_start, vmin_orig): - info_fmt[first_label(week_start)] = '\n\n%b\n%Y' - else: - info_fmt[first_label(month_start)] = '\n\n%b\n%Y' - # Case 3. Less than 14 months ............... - elif span <= 1.15 * periodsperyear: - year_start = period_break(dates_, 'year') - month_start = period_break(dates_, 'month') - week_start = period_break(dates_, 'week') - info_maj[month_start] = True - info_min[week_start] = True - info_min[year_start] = False - info_min[month_start] = False - info_fmt[month_start] = '%b' - info_fmt[year_start] = '%b\n%Y' - if not has_level_label(year_start, vmin_orig): - info_fmt[first_label(month_start)] = '%b\n%Y' - # Case 4. Less than 2.5 years ............... - elif span <= 2.5 * periodsperyear: - year_start = period_break(dates_, 'year') - quarter_start = period_break(dates_, 'quarter') - month_start = period_break(dates_, 'month') - info_maj[quarter_start] = True - info_min[month_start] = True - info_fmt[quarter_start] = '%b' - info_fmt[year_start] = '%b\n%Y' - # Case 4. Less than 4 years ................. - elif span <= 4 * periodsperyear: - year_start = period_break(dates_, 'year') - month_start = period_break(dates_, 'month') - info_maj[year_start] = True - info_min[month_start] = True - info_min[year_start] = False - - month_break = dates_[month_start].month - jan_or_jul = month_start[(month_break == 1) | (month_break == 7)] - info_fmt[jan_or_jul] = '%b' - info_fmt[year_start] = '%b\n%Y' - # Case 5. Less than 11 years ................ - elif span <= 11 * periodsperyear: - year_start = period_break(dates_, 'year') - quarter_start = period_break(dates_, 'quarter') - info_maj[year_start] = True - info_min[quarter_start] = True - info_min[year_start] = False - info_fmt[year_start] = '%Y' - # Case 6. More than 12 years ................ - else: - year_start = period_break(dates_, 'year') - year_break = dates_[year_start].year - nyears = span / periodsperyear - (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears) - major_idx = year_start[(year_break % maj_anndef == 0)] - info_maj[major_idx] = True - minor_idx = year_start[(year_break % min_anndef == 0)] - info_min[minor_idx] = True - info_fmt[major_idx] = '%Y' - - return info - - -def _monthly_finder(vmin, vmax, freq): - periodsperyear = 12 - - vmin_orig = vmin - (vmin, vmax) = (int(vmin), int(vmax)) - span = vmax - vmin + 1 - - # Initialize the output - info = np.zeros(span, - dtype=[('val', int), ('maj', bool), ('min', bool), - ('fmt', '|S8')]) - info['val'] = np.arange(vmin, vmax + 1) - dates_ = info['val'] - info['fmt'] = '' - year_start = (dates_ % 12 == 0).nonzero()[0] - info_maj = info['maj'] - info_fmt = info['fmt'] - - if span <= 1.15 * periodsperyear: - info_maj[year_start] = True - info['min'] = True - - info_fmt[:] = '%b' - info_fmt[year_start] = '%b\n%Y' - - if not has_level_label(year_start, vmin_orig): - if dates_.size > 1: - idx = 1 - else: - idx = 0 - info_fmt[idx] = '%b\n%Y' - - elif span <= 2.5 * periodsperyear: - quarter_start = (dates_ % 3 == 0).nonzero() - info_maj[year_start] = True - # TODO: Check the following : is it really info['fmt'] ? - info['fmt'][quarter_start] = True - info['min'] = True - - info_fmt[quarter_start] = '%b' - info_fmt[year_start] = '%b\n%Y' - - elif span <= 4 * periodsperyear: - info_maj[year_start] = True - info['min'] = True - - jan_or_jul = (dates_ % 12 == 0) | (dates_ % 12 == 6) - info_fmt[jan_or_jul] = '%b' - info_fmt[year_start] = '%b\n%Y' - - elif span <= 11 * periodsperyear: - quarter_start = (dates_ % 3 == 0).nonzero() - info_maj[year_start] = True - info['min'][quarter_start] = True - - info_fmt[year_start] = '%Y' - - else: - nyears = span / periodsperyear - (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears) - years = dates_[year_start] // 12 + 1 - major_idx = year_start[(years % maj_anndef == 0)] - info_maj[major_idx] = True - info['min'][year_start[(years % min_anndef == 0)]] = True - - info_fmt[major_idx] = '%Y' - - return info - - -def _quarterly_finder(vmin, vmax, freq): - periodsperyear = 4 - vmin_orig = vmin - (vmin, vmax) = (int(vmin), int(vmax)) - span = vmax - vmin + 1 - - info = np.zeros(span, - dtype=[('val', int), ('maj', bool), ('min', bool), - ('fmt', '|S8')]) - info['val'] = np.arange(vmin, vmax + 1) - info['fmt'] = '' - dates_ = info['val'] - info_maj = info['maj'] - info_fmt = info['fmt'] - year_start = (dates_ % 4 == 0).nonzero()[0] - - if span <= 3.5 * periodsperyear: - info_maj[year_start] = True - info['min'] = True - - info_fmt[:] = 'Q%q' - info_fmt[year_start] = 'Q%q\n%F' - if not has_level_label(year_start, vmin_orig): - if dates_.size > 1: - idx = 1 - else: - idx = 0 - info_fmt[idx] = 'Q%q\n%F' - - elif span <= 11 * periodsperyear: - info_maj[year_start] = True - info['min'] = True - info_fmt[year_start] = '%F' - - else: - years = dates_[year_start] // 4 + 1 - nyears = span / periodsperyear - (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears) - major_idx = year_start[(years % maj_anndef == 0)] - info_maj[major_idx] = True - info['min'][year_start[(years % min_anndef == 0)]] = True - info_fmt[major_idx] = '%F' - - return info - - -def _annual_finder(vmin, vmax, freq): - (vmin, vmax) = (int(vmin), int(vmax + 1)) - span = vmax - vmin + 1 - - info = np.zeros(span, - dtype=[('val', int), ('maj', bool), ('min', bool), - ('fmt', '|S8')]) - info['val'] = np.arange(vmin, vmax + 1) - info['fmt'] = '' - dates_ = info['val'] - - (min_anndef, maj_anndef) = _get_default_annual_spacing(span) - major_idx = dates_ % maj_anndef == 0 - info['maj'][major_idx] = True - info['min'][(dates_ % min_anndef == 0)] = True - info['fmt'][major_idx] = '%Y' - - return info - - -def get_finder(freq): - if isinstance(freq, compat.string_types): - freq = frequencies.get_freq(freq) - fgroup = frequencies.get_freq_group(freq) - - if fgroup == FreqGroup.FR_ANN: - return _annual_finder - elif fgroup == FreqGroup.FR_QTR: - return _quarterly_finder - elif freq == FreqGroup.FR_MTH: - return _monthly_finder - elif ((freq >= FreqGroup.FR_BUS) or fgroup == FreqGroup.FR_WK): - return _daily_finder - else: # pragma: no cover - errmsg = "Unsupported frequency: %s" % (freq) - raise NotImplementedError(errmsg) - - -class TimeSeries_DateLocator(Locator): - """ - Locates the ticks along an axis controlled by a :class:`Series`. - - Parameters - ---------- - freq : {var} - Valid frequency specifier. - minor_locator : {False, True}, optional - Whether the locator is for minor ticks (True) or not. - dynamic_mode : {True, False}, optional - Whether the locator should work in dynamic mode. - base : {int}, optional - quarter : {int}, optional - month : {int}, optional - day : {int}, optional - """ - - def __init__(self, freq, minor_locator=False, dynamic_mode=True, - base=1, quarter=1, month=1, day=1, plot_obj=None): - if isinstance(freq, compat.string_types): - freq = frequencies.get_freq(freq) - self.freq = freq - self.base = base - (self.quarter, self.month, self.day) = (quarter, month, day) - self.isminor = minor_locator - self.isdynamic = dynamic_mode - self.offset = 0 - self.plot_obj = plot_obj - self.finder = get_finder(freq) - - def _get_default_locs(self, vmin, vmax): - "Returns the default locations of ticks." - - if self.plot_obj.date_axis_info is None: - self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq) - - locator = self.plot_obj.date_axis_info - - if self.isminor: - return np.compress(locator['min'], locator['val']) - return np.compress(locator['maj'], locator['val']) - - def __call__(self): - 'Return the locations of the ticks.' - # axis calls Locator.set_axis inside set_m_formatter - vi = tuple(self.axis.get_view_interval()) - if vi != self.plot_obj.view_interval: - self.plot_obj.date_axis_info = None - self.plot_obj.view_interval = vi - vmin, vmax = vi - if vmax < vmin: - vmin, vmax = vmax, vmin - if self.isdynamic: - locs = self._get_default_locs(vmin, vmax) - else: # pragma: no cover - base = self.base - (d, m) = divmod(vmin, base) - vmin = (d + 1) * base - locs = lrange(vmin, vmax + 1, base) - return locs - - def autoscale(self): - """ - Sets the view limits to the nearest multiples of base that contain the - data. - """ - # requires matplotlib >= 0.98.0 - (vmin, vmax) = self.axis.get_data_interval() - - locs = self._get_default_locs(vmin, vmax) - (vmin, vmax) = locs[[0, -1]] - if vmin == vmax: - vmin -= 1 - vmax += 1 - return nonsingular(vmin, vmax) - -# ------------------------------------------------------------------------- -# --- Formatter --- -# ------------------------------------------------------------------------- - - -class TimeSeries_DateFormatter(Formatter): - """ - Formats the ticks along an axis controlled by a :class:`PeriodIndex`. - - Parameters - ---------- - freq : {int, string} - Valid frequency specifier. - minor_locator : {False, True} - Whether the current formatter should apply to minor ticks (True) or - major ticks (False). - dynamic_mode : {True, False} - Whether the formatter works in dynamic mode or not. - """ - - def __init__(self, freq, minor_locator=False, dynamic_mode=True, - plot_obj=None): - if isinstance(freq, compat.string_types): - freq = frequencies.get_freq(freq) - self.format = None - self.freq = freq - self.locs = [] - self.formatdict = None - self.isminor = minor_locator - self.isdynamic = dynamic_mode - self.offset = 0 - self.plot_obj = plot_obj - self.finder = get_finder(freq) - - def _set_default_format(self, vmin, vmax): - "Returns the default ticks spacing." - - if self.plot_obj.date_axis_info is None: - self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq) - info = self.plot_obj.date_axis_info - - if self.isminor: - format = np.compress(info['min'] & np.logical_not(info['maj']), - info) - else: - format = np.compress(info['maj'], info) - self.formatdict = dict([(x, f) for (x, _, _, f) in format]) - return self.formatdict - - def set_locs(self, locs): - 'Sets the locations of the ticks' - # don't actually use the locs. This is just needed to work with - # matplotlib. Force to use vmin, vmax - self.locs = locs - - (vmin, vmax) = vi = tuple(self.axis.get_view_interval()) - if vi != self.plot_obj.view_interval: - self.plot_obj.date_axis_info = None - self.plot_obj.view_interval = vi - if vmax < vmin: - (vmin, vmax) = (vmax, vmin) - self._set_default_format(vmin, vmax) - - def __call__(self, x, pos=0): - if self.formatdict is None: - return '' - else: - fmt = self.formatdict.pop(x, '') - return Period(ordinal=int(x), freq=self.freq).strftime(fmt) - - -class TimeSeries_TimedeltaFormatter(Formatter): - """ - Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`. - """ - - @staticmethod - def format_timedelta_ticks(x, pos, n_decimals): - """ - Convert seconds to 'D days HH:MM:SS.F' - """ - s, ns = divmod(x, 1e9) - m, s = divmod(s, 60) - h, m = divmod(m, 60) - d, h = divmod(h, 24) - decimals = int(ns * 10**(n_decimals - 9)) - s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s)) - if n_decimals > 0: - s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals) - if d != 0: - s = '{:d} days '.format(int(d)) + s - return s - - def __call__(self, x, pos=0): - (vmin, vmax) = tuple(self.axis.get_view_interval()) - n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin)))) - if n_decimals > 9: - n_decimals = 9 - return self.format_timedelta_ticks(x, pos, n_decimals) + from pandas.plotting._converter import register as register_ + msg = ("'pandas.tseries.converter.register' has been moved and renamed to " + "'pandas.plotting.register_matplotlib_converters'. ") + warnings.warn(msg, FutureWarning, stacklevel=2) + register_() diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 8013947babc5a..1b782b430a1a7 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -1,443 +1,52 @@ +# -*- coding: utf-8 -*- from datetime import timedelta -from pandas.compat import long, zip -from pandas import compat import re -import warnings import numpy as np - -from pandas.types.generic import ABCSeries -from pandas.types.common import (is_integer, - is_period_arraylike, - is_timedelta64_dtype, - is_datetime64_dtype) - -import pandas.core.algorithms as algos -from pandas.core.algorithms import unique -from pandas.tseries.offsets import DateOffset -from pandas.util.decorators import cache_readonly, deprecate_kwarg -import pandas.tseries.offsets as offsets - -from pandas._libs import lib, tslib -from pandas._libs.tslib import Timedelta from pytz import AmbiguousTimeError +from pandas._libs.algos import unique_deltas +from pandas._libs.tslibs import Timedelta, Timestamp +from pandas._libs.tslibs.ccalendar import MONTH_ALIASES, int_to_weekday +from pandas._libs.tslibs.conversion import tz_convert +from pandas._libs.tslibs.fields import build_field_sarray +import pandas._libs.tslibs.frequencies as libfreqs +from pandas._libs.tslibs.offsets import _offset_to_period_map +import pandas._libs.tslibs.resolution as libresolution +from pandas._libs.tslibs.resolution import Resolution +from pandas._libs.tslibs.timezones import UTC +import pandas.compat as compat +from pandas.compat import zip +from pandas.util._decorators import cache_readonly + +from pandas.core.dtypes.common import ( + is_datetime64_dtype, is_period_arraylike, is_timedelta64_dtype) +from pandas.core.dtypes.generic import ABCSeries -class FreqGroup(object): - FR_ANN = 1000 - FR_QTR = 2000 - FR_MTH = 3000 - FR_WK = 4000 - FR_BUS = 5000 - FR_DAY = 6000 - FR_HR = 7000 - FR_MIN = 8000 - FR_SEC = 9000 - FR_MS = 10000 - FR_US = 11000 - FR_NS = 12000 - - -RESO_NS = 0 -RESO_US = 1 -RESO_MS = 2 -RESO_SEC = 3 -RESO_MIN = 4 -RESO_HR = 5 -RESO_DAY = 6 - - -class Resolution(object): - - RESO_US = RESO_US - RESO_MS = RESO_MS - RESO_SEC = RESO_SEC - RESO_MIN = RESO_MIN - RESO_HR = RESO_HR - RESO_DAY = RESO_DAY - - _reso_str_map = { - RESO_NS: 'nanosecond', - RESO_US: 'microsecond', - RESO_MS: 'millisecond', - RESO_SEC: 'second', - RESO_MIN: 'minute', - RESO_HR: 'hour', - RESO_DAY: 'day' - } - - # factor to multiply a value by to convert it to the next finer grained - # resolution - _reso_mult_map = { - RESO_NS: None, - RESO_US: 1000, - RESO_MS: 1000, - RESO_SEC: 1000, - RESO_MIN: 60, - RESO_HR: 60, - RESO_DAY: 24 - } - - _reso_str_bump_map = { - 'D': 'H', - 'H': 'T', - 'T': 'S', - 'S': 'L', - 'L': 'U', - 'U': 'N', - 'N': None - } - - _str_reso_map = dict([(v, k) for k, v in compat.iteritems(_reso_str_map)]) - - _reso_freq_map = { - 'year': 'A', - 'quarter': 'Q', - 'month': 'M', - 'day': 'D', - 'hour': 'H', - 'minute': 'T', - 'second': 'S', - 'millisecond': 'L', - 'microsecond': 'U', - 'nanosecond': 'N'} - - _freq_reso_map = dict([(v, k) - for k, v in compat.iteritems(_reso_freq_map)]) - - @classmethod - def get_str(cls, reso): - """ - Return resolution str against resolution code. - - Example - ------- - >>> Resolution.get_str(Resolution.RESO_SEC) - 'second' - """ - return cls._reso_str_map.get(reso, 'day') - - @classmethod - def get_reso(cls, resostr): - """ - Return resolution str against resolution code. - - Example - ------- - >>> Resolution.get_reso('second') - 2 - - >>> Resolution.get_reso('second') == Resolution.RESO_SEC - True - """ - return cls._str_reso_map.get(resostr, cls.RESO_DAY) - - @classmethod - def get_freq_group(cls, resostr): - """ - Return frequency str against resolution str. - - Example - ------- - >>> f.Resolution.get_freq_group('day') - 4000 - """ - return get_freq_group(cls.get_freq(resostr)) - - @classmethod - def get_freq(cls, resostr): - """ - Return frequency str against resolution str. - - Example - ------- - >>> f.Resolution.get_freq('day') - 'D' - """ - return cls._reso_freq_map[resostr] - - @classmethod - def get_str_from_freq(cls, freq): - """ - Return resolution str against frequency str. - - Example - ------- - >>> Resolution.get_str_from_freq('H') - 'hour' - """ - return cls._freq_reso_map.get(freq, 'day') - - @classmethod - def get_reso_from_freq(cls, freq): - """ - Return resolution code against frequency str. - - Example - ------- - >>> Resolution.get_reso_from_freq('H') - 4 - - >>> Resolution.get_reso_from_freq('H') == Resolution.RESO_HR - True - """ - return cls.get_reso(cls.get_str_from_freq(freq)) - - @classmethod - def get_stride_from_decimal(cls, value, freq): - """ - Convert freq with decimal stride into a higher freq with integer stride - - Parameters - ---------- - value : integer or float - freq : string - Frequency string - - Raises - ------ - ValueError - If the float cannot be converted to an integer at any resolution. - - Example - ------- - >>> Resolution.get_stride_from_decimal(1.5, 'T') - (90, 'S') - - >>> Resolution.get_stride_from_decimal(1.04, 'H') - (3744, 'S') - - >>> Resolution.get_stride_from_decimal(1, 'D') - (1, 'D') - """ - - if np.isclose(value % 1, 0): - return int(value), freq - else: - start_reso = cls.get_reso_from_freq(freq) - if start_reso == 0: - raise ValueError( - "Could not convert to integer offset at any resolution" - ) - - next_value = cls._reso_mult_map[start_reso] * value - next_name = cls._reso_str_bump_map[freq] - return cls.get_stride_from_decimal(next_value, next_name) - - -def get_to_timestamp_base(base): - """ - Return frequency code group used for base of to_timestamp against - frequency code. - - Example - ------- - # Return day freq code against longer freq than day - >>> get_to_timestamp_base(get_freq_code('D')[0]) - 6000 - >>> get_to_timestamp_base(get_freq_code('W')[0]) - 6000 - >>> get_to_timestamp_base(get_freq_code('M')[0]) - 6000 - - # Return second freq code against hour between second - >>> get_to_timestamp_base(get_freq_code('H')[0]) - 9000 - >>> get_to_timestamp_base(get_freq_code('S')[0]) - 9000 - """ - if base < FreqGroup.FR_BUS: - return FreqGroup.FR_DAY - if FreqGroup.FR_HR <= base <= FreqGroup.FR_SEC: - return FreqGroup.FR_SEC - return base - - -def get_freq_group(freq): - """ - Return frequency code group of given frequency str or offset. - - Example - ------- - >>> get_freq_group('W-MON') - 4000 - - >>> get_freq_group('W-FRI') - 4000 - """ - if isinstance(freq, offsets.DateOffset): - freq = freq.rule_code - - if isinstance(freq, compat.string_types): - base, mult = get_freq_code(freq) - freq = base - elif isinstance(freq, int): - pass - else: - raise ValueError('input must be str, offset or int') - return (freq // 1000) * 1000 - - -def get_freq(freq): - """ - Return frequency code of given frequency str. - If input is not string, return input as it is. - - Example - ------- - >>> get_freq('A') - 1000 - - >>> get_freq('3A') - 1000 - """ - if isinstance(freq, compat.string_types): - base, mult = get_freq_code(freq) - freq = base - return freq - - -def get_freq_code(freqstr): - """ - Return freq str or tuple to freq code and stride (mult) - - Parameters - ---------- - freqstr : str or tuple - - Returns - ------- - return : tuple of base frequency code and stride (mult) - - Example - ------- - >>> get_freq_code('3D') - (6000, 3) - - >>> get_freq_code('D') - (6000, 1) - - >>> get_freq_code(('D', 3)) - (6000, 3) - """ - if isinstance(freqstr, DateOffset): - freqstr = (freqstr.rule_code, freqstr.n) - - if isinstance(freqstr, tuple): - if (is_integer(freqstr[0]) and - is_integer(freqstr[1])): - # e.g., freqstr = (2000, 1) - return freqstr - else: - # e.g., freqstr = ('T', 5) - try: - code = _period_str_to_code(freqstr[0]) - stride = freqstr[1] - except: - if is_integer(freqstr[1]): - raise - code = _period_str_to_code(freqstr[1]) - stride = freqstr[0] - return code, stride - - if is_integer(freqstr): - return (freqstr, 1) - - base, stride = _base_and_stride(freqstr) - code = _period_str_to_code(base) - - return code, stride - +from pandas.core.algorithms import unique -def _get_freq_str(base, mult=1): - code = _reverse_period_code_map.get(base) - if mult == 1: - return code - return str(mult) + code +from pandas.tseries.offsets import ( + DateOffset, Day, Hour, Micro, Milli, Minute, Nano, Second, prefix_mapping) +_ONE_MICRO = 1000 +_ONE_MILLI = (_ONE_MICRO * 1000) +_ONE_SECOND = (_ONE_MILLI * 1000) +_ONE_MINUTE = (60 * _ONE_SECOND) +_ONE_HOUR = (60 * _ONE_MINUTE) +_ONE_DAY = (24 * _ONE_HOUR) # --------------------------------------------------------------------- # Offset names ("time rules") and related functions - -from pandas.tseries.offsets import (Nano, Micro, Milli, Second, # noqa - Minute, Hour, - Day, BDay, CDay, Week, MonthBegin, - MonthEnd, BMonthBegin, BMonthEnd, - QuarterBegin, QuarterEnd, BQuarterBegin, - BQuarterEnd, YearBegin, YearEnd, - BYearBegin, BYearEnd, prefix_mapping) -try: - cday = CDay() -except NotImplementedError: - cday = None - #: cache of previously seen offsets _offset_map = {} -_offset_to_period_map = { - 'WEEKDAY': 'D', - 'EOM': 'M', - 'BM': 'M', - 'BQS': 'Q', - 'QS': 'Q', - 'BQ': 'Q', - 'BA': 'A', - 'AS': 'A', - 'BAS': 'A', - 'MS': 'M', - 'D': 'D', - 'C': 'C', - 'B': 'B', - 'T': 'T', - 'S': 'S', - 'L': 'L', - 'U': 'U', - 'N': 'N', - 'H': 'H', - 'Q': 'Q', - 'A': 'A', - 'W': 'W', - 'M': 'M' -} - -need_suffix = ['QS', 'BQ', 'BQS', 'AS', 'BA', 'BAS'] -for __prefix in need_suffix: - for _m in tslib._MONTHS: - _offset_to_period_map['%s-%s' % (__prefix, _m)] = \ - _offset_to_period_map[__prefix] -for __prefix in ['A', 'Q']: - for _m in tslib._MONTHS: - _alias = '%s-%s' % (__prefix, _m) - _offset_to_period_map[_alias] = _alias - -_days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] -for _d in _days: - _offset_to_period_map['W-%s' % _d] = 'W-%s' % _d - def get_period_alias(offset_str): """ alias to closest period strings BQ->Q etc""" return _offset_to_period_map.get(offset_str, None) -_lite_rule_alias = { - 'W': 'W-SUN', - 'Q': 'Q-DEC', - - 'A': 'A-DEC', # YearEnd(month=12), - 'AS': 'AS-JAN', # YearBegin(month=1), - 'BA': 'BA-DEC', # BYearEnd(month=12), - 'BAS': 'BAS-JAN', # BYearBegin(month=1), - - 'Min': 'T', - 'min': 'T', - 'ms': 'L', - 'us': 'U', - 'ns': 'N' -} - - _name_to_offset_map = {'days': Day(1), 'hours': Hour(1), 'minutes': Minute(1), @@ -447,10 +56,6 @@ def get_period_alias(offset_str): 'nanoseconds': Nano(1)} -_INVALID_FREQ_ERROR = "Invalid frequency: {0}" - - -@deprecate_kwarg(old_arg_name='freqstr', new_arg_name='freq') def to_offset(freq): """ Return DateOffset object from string or tuple representation @@ -462,8 +67,8 @@ def to_offset(freq): Returns ------- - delta : DateOffset - None if freq is None + DateOffset + None if freq is None. Raises ------ @@ -472,7 +77,7 @@ def to_offset(freq): See Also -------- - pandas.DateOffset + DateOffset Examples -------- @@ -505,7 +110,7 @@ def to_offset(freq): stride = freq[1] if isinstance(stride, compat.string_types): name, stride = stride, name - name, _ = _base_and_stride(name) + name, _ = libfreqs._base_and_stride(name) delta = get_offset(name) * stride elif isinstance(freq, timedelta): @@ -522,13 +127,13 @@ def to_offset(freq): else: delta = delta + offset except Exception: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) else: delta = None stride_sign = None try: - splitted = re.split(opattern, freq) + splitted = re.split(libfreqs.opattern, freq) if splitted[-1] != '' and not splitted[-1].isspace(): # the last element must be blank raise ValueError('last element must be blank') @@ -536,7 +141,7 @@ def to_offset(freq): splitted[2::4]): if sep != '' and not sep.isspace(): raise ValueError('separator must be spaces') - prefix = _lite_rule_alias.get(name) or name + prefix = libfreqs._lite_rule_alias.get(name) or name if stride_sign is None: stride_sign = -1 if stride.startswith('-') else 1 if not stride: @@ -553,55 +158,14 @@ def to_offset(freq): else: delta = delta + offset except Exception: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) if delta is None: - raise ValueError(_INVALID_FREQ_ERROR.format(freq)) + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(freq)) return delta -# hack to handle WOM-1MON -opattern = re.compile( - r'([\-]?\d*|[\-]?\d*\.\d*)\s*([A-Za-z]+([\-][\dA-Za-z\-]+)?)' -) - - -def _base_and_stride(freqstr): - """ - Return base freq and stride info from string representation - - Examples - -------- - _freq_and_stride('5Min') -> 'Min', 5 - """ - groups = opattern.match(freqstr) - - if not groups: - raise ValueError("Could not evaluate %s" % freqstr) - - stride = groups.group(1) - - if len(stride): - stride = int(stride) - else: - stride = 1 - - base = groups.group(2) - - return (base, stride) - - -def get_base_alias(freqstr): - """ - Returns the base frequency alias, e.g., '5D' -> 'D' - """ - return _base_and_stride(freqstr)[0] - - -_dont_uppercase = set(('MS', 'ms')) - - def get_offset(name): """ Return DateOffset object associated with rule name @@ -610,12 +174,12 @@ def get_offset(name): -------- get_offset('EOM') --> BMonthEnd(1) """ - if name not in _dont_uppercase: + if name not in libfreqs._dont_uppercase: name = name.upper() - name = _lite_rule_alias.get(name, name) - name = _lite_rule_alias.get(name.lower(), name) + name = libfreqs._lite_rule_alias.get(name, name) + name = libfreqs._lite_rule_alias.get(name.lower(), name) else: - name = _lite_rule_alias.get(name, name) + name = libfreqs._lite_rule_alias.get(name, name) if name not in _offset_map: try: @@ -626,124 +190,16 @@ def get_offset(name): offset = klass._from_name(*split[1:]) except (ValueError, TypeError, KeyError): # bad prefix or suffix - raise ValueError(_INVALID_FREQ_ERROR.format(name)) + raise ValueError(libfreqs.INVALID_FREQ_ERR_MSG.format(name)) # cache _offset_map[name] = offset - # do not return cache because it's mutable - return _offset_map[name].copy() - - -getOffset = get_offset - - -def get_offset_name(offset): - """ - Return rule name associated with a DateOffset object - - Examples - -------- - get_offset_name(BMonthEnd(1)) --> 'EOM' - """ - - msg = "get_offset_name(offset) is deprecated. Use offset.freqstr instead" - warnings.warn(msg, FutureWarning, stacklevel=2) - return offset.freqstr - -def get_standard_freq(freq): - """ - Return the standardized frequency string - """ - - msg = ("get_standard_freq is deprecated. Use to_offset(freq).rule_code " - "instead.") - warnings.warn(msg, FutureWarning, stacklevel=2) - return to_offset(freq).rule_code + return _offset_map[name] # --------------------------------------------------------------------- # Period codes -# period frequency constants corresponding to scikits timeseries -# originals -_period_code_map = { - # Annual freqs with various fiscal year ends. - # eg, 2005 for A-FEB runs Mar 1, 2004 to Feb 28, 2005 - "A-DEC": 1000, # Annual - December year end - "A-JAN": 1001, # Annual - January year end - "A-FEB": 1002, # Annual - February year end - "A-MAR": 1003, # Annual - March year end - "A-APR": 1004, # Annual - April year end - "A-MAY": 1005, # Annual - May year end - "A-JUN": 1006, # Annual - June year end - "A-JUL": 1007, # Annual - July year end - "A-AUG": 1008, # Annual - August year end - "A-SEP": 1009, # Annual - September year end - "A-OCT": 1010, # Annual - October year end - "A-NOV": 1011, # Annual - November year end - - # Quarterly frequencies with various fiscal year ends. - # eg, Q42005 for Q-OCT runs Aug 1, 2005 to Oct 31, 2005 - "Q-DEC": 2000, # Quarterly - December year end - "Q-JAN": 2001, # Quarterly - January year end - "Q-FEB": 2002, # Quarterly - February year end - "Q-MAR": 2003, # Quarterly - March year end - "Q-APR": 2004, # Quarterly - April year end - "Q-MAY": 2005, # Quarterly - May year end - "Q-JUN": 2006, # Quarterly - June year end - "Q-JUL": 2007, # Quarterly - July year end - "Q-AUG": 2008, # Quarterly - August year end - "Q-SEP": 2009, # Quarterly - September year end - "Q-OCT": 2010, # Quarterly - October year end - "Q-NOV": 2011, # Quarterly - November year end - - "M": 3000, # Monthly - - "W-SUN": 4000, # Weekly - Sunday end of week - "W-MON": 4001, # Weekly - Monday end of week - "W-TUE": 4002, # Weekly - Tuesday end of week - "W-WED": 4003, # Weekly - Wednesday end of week - "W-THU": 4004, # Weekly - Thursday end of week - "W-FRI": 4005, # Weekly - Friday end of week - "W-SAT": 4006, # Weekly - Saturday end of week - - "B": 5000, # Business days - "D": 6000, # Daily - "H": 7000, # Hourly - "T": 8000, # Minutely - "S": 9000, # Secondly - "L": 10000, # Millisecondly - "U": 11000, # Microsecondly - "N": 12000, # Nanosecondly -} - -_reverse_period_code_map = {} -for _k, _v in compat.iteritems(_period_code_map): - _reverse_period_code_map[_v] = _k - -# Additional aliases -_period_code_map.update({ - "Q": 2000, # Quarterly - December year end (default quarterly) - "A": 1000, # Annual - "W": 4000, # Weekly - "C": 5000, # Custom Business Day -}) - - -def _period_str_to_code(freqstr): - freqstr = _lite_rule_alias.get(freqstr, freqstr) - - if freqstr not in _dont_uppercase: - lower = freqstr.lower() - freqstr = _lite_rule_alias.get(lower, freqstr) - - if freqstr not in _dont_uppercase: - freqstr = freqstr.upper() - try: - return _period_code_map[freqstr] - except KeyError: - raise ValueError(_INVALID_FREQ_ERROR.format(freqstr)) - def infer_freq(index, warn=True): """ @@ -758,7 +214,7 @@ def infer_freq(index, warn=True): Returns ------- - freq : string or None + str or None None if no discernible frequency TypeError if the index is not datetime-like ValueError if there are less than three values. @@ -770,21 +226,22 @@ def infer_freq(index, warn=True): if not (is_datetime64_dtype(values) or is_timedelta64_dtype(values) or values.dtype == object): - raise TypeError("cannot infer freq from a non-convertible " - "dtype on a Series of {0}".format(index.dtype)) + raise TypeError("cannot infer freq from a non-convertible dtype " + "on a Series of {dtype}".format(dtype=index.dtype)) index = values if is_period_arraylike(index): raise TypeError("PeriodIndex given. Check the `freq` attribute " "instead of using infer_freq.") - elif isinstance(index, pd.TimedeltaIndex): + elif is_timedelta64_dtype(index): + # Allow TimedeltaIndex and TimedeltaArray inferer = _TimedeltaFrequencyInferer(index, warn=warn) return inferer.get_freq() if isinstance(index, pd.Index) and not isinstance(index, pd.DatetimeIndex): if isinstance(index, (pd.Int64Index, pd.Float64Index)): raise TypeError("cannot infer freq from a non-convertible index " - "type {0}".format(type(index))) + "type {type}".format(type=type(index))) index = index.values if not isinstance(index, pd.DatetimeIndex): @@ -797,14 +254,6 @@ def infer_freq(index, warn=True): return inferer.get_freq() -_ONE_MICRO = long(1000) -_ONE_MILLI = _ONE_MICRO * 1000 -_ONE_SECOND = _ONE_MILLI * 1000 -_ONE_MINUTE = 60 * _ONE_SECOND -_ONE_HOUR = 60 * _ONE_MINUTE -_ONE_DAY = 24 * _ONE_HOUR - - class _FrequencyInferer(object): """ Not sure if I can avoid the state machine here @@ -812,29 +261,29 @@ class _FrequencyInferer(object): def __init__(self, index, warn=True): self.index = index - self.values = np.asarray(index).view('i8') + self.values = index.asi8 # This moves the values, which are implicitly in UTC, to the # the timezone so they are in local time if hasattr(index, 'tz'): if index.tz is not None: - self.values = tslib.tz_convert(self.values, 'UTC', index.tz) + self.values = tz_convert(self.values, UTC, index.tz) self.warn = warn if len(index) < 3: raise ValueError('Need at least 3 dates to infer frequency') - self.is_monotonic = (self.index.is_monotonic_increasing or - self.index.is_monotonic_decreasing) + self.is_monotonic = (self.index._is_monotonic_increasing or + self.index._is_monotonic_decreasing) @cache_readonly def deltas(self): - return tslib.unique_deltas(self.values) + return unique_deltas(self.values) @cache_readonly def deltas_asi8(self): - return tslib.unique_deltas(self.index.asi8) + return unique_deltas(self.index.asi8) @cache_readonly def is_unique(self): @@ -845,40 +294,49 @@ def is_unique_asi8(self): return len(self.deltas_asi8) == 1 def get_freq(self): - if not self.is_monotonic or not self.index.is_unique: + """ + Find the appropriate frequency string to describe the inferred + frequency of self.values + + Returns + ------- + str or None + """ + if not self.is_monotonic or not self.index._is_unique: return None delta = self.deltas[0] if _is_multiple(delta, _ONE_DAY): return self._infer_daily_rule() + + # Business hourly, maybe. 17: one day / 65: one weekend + if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]): + return 'BH' + # Possibly intraday frequency. Here we use the + # original .asi8 values as the modified values + # will not work around DST transitions. See #8772 + elif not self.is_unique_asi8: + return None + + delta = self.deltas_asi8[0] + if _is_multiple(delta, _ONE_HOUR): + # Hours + return _maybe_add_count('H', delta / _ONE_HOUR) + elif _is_multiple(delta, _ONE_MINUTE): + # Minutes + return _maybe_add_count('T', delta / _ONE_MINUTE) + elif _is_multiple(delta, _ONE_SECOND): + # Seconds + return _maybe_add_count('S', delta / _ONE_SECOND) + elif _is_multiple(delta, _ONE_MILLI): + # Milliseconds + return _maybe_add_count('L', delta / _ONE_MILLI) + elif _is_multiple(delta, _ONE_MICRO): + # Microseconds + return _maybe_add_count('U', delta / _ONE_MICRO) else: - # Business hourly, maybe. 17: one day / 65: one weekend - if self.hour_deltas in ([1, 17], [1, 65], [1, 17, 65]): - return 'BH' - # Possibly intraday frequency. Here we use the - # original .asi8 values as the modified values - # will not work around DST transitions. See #8772 - elif not self.is_unique_asi8: - return None - delta = self.deltas_asi8[0] - if _is_multiple(delta, _ONE_HOUR): - # Hours - return _maybe_add_count('H', delta / _ONE_HOUR) - elif _is_multiple(delta, _ONE_MINUTE): - # Minutes - return _maybe_add_count('T', delta / _ONE_MINUTE) - elif _is_multiple(delta, _ONE_SECOND): - # Seconds - return _maybe_add_count('S', delta / _ONE_SECOND) - elif _is_multiple(delta, _ONE_MILLI): - # Milliseconds - return _maybe_add_count('L', delta / _ONE_MILLI) - elif _is_multiple(delta, _ONE_MICRO): - # Microseconds - return _maybe_add_count('U', delta / _ONE_MICRO) - else: - # Nanoseconds - return _maybe_add_count('N', delta) + # Nanoseconds + return _maybe_add_count('N', delta) @cache_readonly def day_deltas(self): @@ -890,76 +348,41 @@ def hour_deltas(self): @cache_readonly def fields(self): - return tslib.build_field_sarray(self.values) + return build_field_sarray(self.values) @cache_readonly def rep_stamp(self): - return lib.Timestamp(self.values[0]) + return Timestamp(self.values[0]) def month_position_check(self): - # TODO: cythonize this, very slow - calendar_end = True - business_end = True - calendar_start = True - business_start = True - - years = self.fields['Y'] - months = self.fields['M'] - days = self.fields['D'] - weekdays = self.index.dayofweek - - from calendar import monthrange - for y, m, d, wd in zip(years, months, days, weekdays): - - if calendar_start: - calendar_start &= d == 1 - if business_start: - business_start &= d == 1 or (d <= 3 and wd == 0) - - if calendar_end or business_end: - _, daysinmonth = monthrange(y, m) - cal = d == daysinmonth - if calendar_end: - calendar_end &= cal - if business_end: - business_end &= cal or (daysinmonth - d < 3 and wd == 4) - elif not calendar_start and not business_start: - break - - if calendar_end: - return 'ce' - elif business_end: - return 'be' - elif calendar_start: - return 'cs' - elif business_start: - return 'bs' - else: - return None + return libresolution.month_position_check(self.fields, + self.index.dayofweek) @cache_readonly def mdiffs(self): nmonths = self.fields['Y'] * 12 + self.fields['M'] - return tslib.unique_deltas(nmonths.astype('i8')) + return unique_deltas(nmonths.astype('i8')) @cache_readonly def ydiffs(self): - return tslib.unique_deltas(self.fields['Y'].astype('i8')) + return unique_deltas(self.fields['Y'].astype('i8')) def _infer_daily_rule(self): annual_rule = self._get_annual_rule() if annual_rule: nyears = self.ydiffs[0] - month = _month_aliases[self.rep_stamp.month] - return _maybe_add_count('%s-%s' % (annual_rule, month), nyears) + month = MONTH_ALIASES[self.rep_stamp.month] + alias = '{prefix}-{month}'.format(prefix=annual_rule, month=month) + return _maybe_add_count(alias, nyears) quarterly_rule = self._get_quarterly_rule() if quarterly_rule: nquarters = self.mdiffs[0] / 3 mod_dict = {0: 12, 2: 11, 1: 10} - month = _month_aliases[mod_dict[self.rep_stamp.month % 3]] - return _maybe_add_count('%s-%s' % (quarterly_rule, month), - nquarters) + month = MONTH_ALIASES[mod_dict[self.rep_stamp.month % 3]] + alias = '{prefix}-{month}'.format(prefix=quarterly_rule, + month=month) + return _maybe_add_count(alias, nquarters) monthly_rule = self._get_monthly_rule() if monthly_rule: @@ -969,13 +392,13 @@ def _infer_daily_rule(self): days = self.deltas[0] / _ONE_DAY if days % 7 == 0: # Weekly - alias = _weekday_rule_aliases[self.rep_stamp.weekday()] - return _maybe_add_count('W-%s' % alias, days / 7) + day = int_to_weekday[self.rep_stamp.weekday()] + return _maybe_add_count( + 'W-{day}'.format(day=day), days / 7) else: return _maybe_add_count('D', days) - # Business daily. Maybe - if self.day_deltas == [1, 3]: + if self._is_business_daily(): return 'B' wom_rule = self._get_wom_rule() @@ -986,7 +409,7 @@ def _get_annual_rule(self): if len(self.ydiffs) > 1: return None - if len(algos.unique(self.fields['M'])) > 1: + if len(unique(self.fields['M'])) > 1: return None pos_check = self.month_position_check() @@ -1011,6 +434,19 @@ def _get_monthly_rule(self): return {'cs': 'MS', 'bs': 'BMS', 'ce': 'M', 'be': 'BM'}.get(pos_check) + def _is_business_daily(self): + # quick check: cannot be business daily + if self.day_deltas != [1, 3]: + return False + + # probably business daily, but need to confirm + first_weekday = self.index[0].weekday() + shifts = np.diff(self.index.asi8) + shifts = np.floor_divide(shifts, _ONE_DAY) + weekdays = np.mod(first_weekday + np.cumsum(shifts), 7) + return np.all(((weekdays == 0) & (shifts == 3)) | + ((weekdays > 0) & (weekdays <= 4) & (shifts == 1))) + def _get_wom_rule(self): # wdiffs = unique(np.diff(self.index.week)) # We also need -47, -49, -48 to catch index spanning year boundary @@ -1029,9 +465,9 @@ def _get_wom_rule(self): # get which week week = week_of_months[0] + 1 - wd = _weekday_rule_aliases[weekdays[0]] + wd = int_to_weekday[weekdays[0]] - return 'WOM-%d%s' % (week, wd) + return 'WOM-{week}{weekday}'.format(week=week, weekday=wd) class _TimedeltaFrequencyInferer(_FrequencyInferer): @@ -1041,184 +477,21 @@ def _infer_daily_rule(self): days = self.deltas[0] / _ONE_DAY if days % 7 == 0: # Weekly - alias = _weekday_rule_aliases[self.rep_stamp.weekday()] - return _maybe_add_count('W-%s' % alias, days / 7) + wd = int_to_weekday[self.rep_stamp.weekday()] + alias = 'W-{weekday}'.format(weekday=wd) + return _maybe_add_count(alias, days / 7) else: return _maybe_add_count('D', days) +def _is_multiple(us, mult): + return us % mult == 0 + + def _maybe_add_count(base, count): if count != 1: - return '%d%s' % (count, base) + assert count == int(count) + count = int(count) + return '{count}{base}'.format(count=count, base=base) else: return base - - -def _maybe_coerce_freq(code): - """ we might need to coerce a code to a rule_code - and uppercase it - - Parameters - ---------- - source : string - Frequency converting from - - Returns - ------- - string code - """ - - assert code is not None - if isinstance(code, offsets.DateOffset): - code = code.rule_code - return code.upper() - - -def is_subperiod(source, target): - """ - Returns True if downsampling is possible between source and target - frequencies - - Parameters - ---------- - source : string - Frequency converting from - target : string - Frequency converting to - - Returns - ------- - is_subperiod : boolean - """ - - if target is None or source is None: - return False - source = _maybe_coerce_freq(source) - target = _maybe_coerce_freq(target) - - if _is_annual(target): - if _is_quarterly(source): - return _quarter_months_conform(_get_rule_month(source), - _get_rule_month(target)) - return source in ['D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_quarterly(target): - return source in ['D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_monthly(target): - return source in ['D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_weekly(target): - return source in [target, 'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif target == 'B': - return source in ['B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif target == 'C': - return source in ['C', 'H', 'T', 'S', 'L', 'U', 'N'] - elif target == 'D': - return source in ['D', 'H', 'T', 'S', 'L', 'U', 'N'] - elif target == 'H': - return source in ['H', 'T', 'S', 'L', 'U', 'N'] - elif target == 'T': - return source in ['T', 'S', 'L', 'U', 'N'] - elif target == 'S': - return source in ['S', 'L', 'U', 'N'] - elif target == 'L': - return source in ['L', 'U', 'N'] - elif target == 'U': - return source in ['U', 'N'] - elif target == 'N': - return source in ['N'] - - -def is_superperiod(source, target): - """ - Returns True if upsampling is possible between source and target - frequencies - - Parameters - ---------- - source : string - Frequency converting from - target : string - Frequency converting to - - Returns - ------- - is_superperiod : boolean - """ - if target is None or source is None: - return False - source = _maybe_coerce_freq(source) - target = _maybe_coerce_freq(target) - - if _is_annual(source): - if _is_annual(target): - return _get_rule_month(source) == _get_rule_month(target) - - if _is_quarterly(target): - smonth = _get_rule_month(source) - tmonth = _get_rule_month(target) - return _quarter_months_conform(smonth, tmonth) - return target in ['D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_quarterly(source): - return target in ['D', 'C', 'B', 'M', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_monthly(source): - return target in ['D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif _is_weekly(source): - return target in [source, 'D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif source == 'B': - return target in ['D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif source == 'C': - return target in ['D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif source == 'D': - return target in ['D', 'C', 'B', 'H', 'T', 'S', 'L', 'U', 'N'] - elif source == 'H': - return target in ['H', 'T', 'S', 'L', 'U', 'N'] - elif source == 'T': - return target in ['T', 'S', 'L', 'U', 'N'] - elif source == 'S': - return target in ['S', 'L', 'U', 'N'] - elif source == 'L': - return target in ['L', 'U', 'N'] - elif source == 'U': - return target in ['U', 'N'] - elif source == 'N': - return target in ['N'] - - -_get_rule_month = tslib._get_rule_month - - -def _is_annual(rule): - rule = rule.upper() - return rule == 'A' or rule.startswith('A-') - - -def _quarter_months_conform(source, target): - snum = _month_numbers[source] - tnum = _month_numbers[target] - return snum % 3 == tnum % 3 - - -def _is_quarterly(rule): - rule = rule.upper() - return rule == 'Q' or rule.startswith('Q-') or rule.startswith('BQ') - - -def _is_monthly(rule): - rule = rule.upper() - return rule == 'M' or rule == 'BM' - - -def _is_weekly(rule): - rule = rule.upper() - return rule == 'W' or rule.startswith('W-') - - -DAYS = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] - -MONTHS = tslib._MONTHS -_month_numbers = tslib._MONTH_NUMBERS -_month_aliases = tslib._MONTH_ALIASES -_weekday_rule_aliases = dict((k, v) for k, v in enumerate(DAYS)) - - -def _is_multiple(us, mult): - return us % mult == 0 diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py index 9acb52ebe0e9f..4016114919f5b 100644 --- a/pandas/tseries/holiday.py +++ b/pandas/tseries/holiday.py @@ -1,12 +1,16 @@ +from datetime import datetime, timedelta import warnings -from pandas import DateOffset, DatetimeIndex, Series, Timestamp -from pandas.compat import add_metaclass -from datetime import datetime, timedelta -from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU # noqa -from pandas.tseries.offsets import Easter, Day +from dateutil.relativedelta import FR, MO, SA, SU, TH, TU, WE # noqa import numpy as np +from pandas.compat import add_metaclass +from pandas.errors import PerformanceWarning + +from pandas import DateOffset, Series, Timestamp, date_range + +from pandas.tseries.offsets import Day, Easter + def next_monday(dt): """ @@ -133,7 +137,7 @@ def __init__(self, name, year=None, month=None, day=None, offset=None, Name of the holiday , defaults to class name offset : array of pandas.tseries.offsets or class from pandas.tseries.offsets - computes offset from date + computes offset from date observance: function computes when holiday is given a pandas Timestamp days_of_week: @@ -143,12 +147,11 @@ class from pandas.tseries.offsets Examples -------- >>> from pandas.tseries.holiday import Holiday, nearest_workday - >>> from pandas import DateOffset >>> from dateutil.relativedelta import MO >>> USMemorialDay = Holiday('MemorialDay', month=5, day=24, - offset=DateOffset(weekday=MO(1))) + offset=pd.DateOffset(weekday=MO(1))) >>> USLaborDay = Holiday('Labor Day', month=9, day=1, - offset=DateOffset(weekday=MO(1))) + offset=pd.DateOffset(weekday=MO(1))) >>> July3rd = Holiday('July 3rd', month=7, day=3,) >>> NewYears = Holiday('New Years Day', month=1, day=1, observance=nearest_workday), @@ -174,16 +177,16 @@ class from pandas.tseries.offsets def __repr__(self): info = '' if self.year is not None: - info += 'year=%s, ' % self.year - info += 'month=%s, day=%s, ' % (self.month, self.day) + info += 'year={year}, '.format(year=self.year) + info += 'month={mon}, day={day}, '.format(mon=self.month, day=self.day) if self.offset is not None: - info += 'offset=%s' % self.offset + info += 'offset={offset}'.format(offset=self.offset) if self.observance is not None: - info += 'observance=%s' % self.observance + info += 'observance={obs}'.format(obs=self.observance) - repr = 'Holiday: %s (%s)' % (self.name, info) + repr = 'Holiday: {name} ({info})'.format(name=self.name, info=info) return repr def dates(self, start_date, end_date, return_name=False): @@ -251,9 +254,9 @@ def _reference_dates(self, start_date, end_date): reference_end_date = Timestamp( datetime(end_date.year + 1, self.month, self.day)) # Don't process unnecessary holidays - dates = DatetimeIndex(start=reference_start_date, - end=reference_end_date, - freq=year_offset, tz=start_date.tz) + dates = date_range(start=reference_start_date, + end=reference_end_date, + freq=year_offset, tz=start_date.tz) return dates @@ -282,7 +285,8 @@ def _apply_rule(self, dates): # if we are adding a non-vectorized value # ignore the PerformanceWarnings: - with warnings.catch_warnings(record=True): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PerformanceWarning) dates += offset return dates @@ -293,7 +297,7 @@ def _apply_rule(self, dates): def register(cls): try: name = cls.name - except: + except AttributeError: name = cls.__name__ holiday_calendars[name] = cls @@ -374,8 +378,8 @@ def holidays(self, start=None, end=None, return_name=False): DatetimeIndex of holidays """ if self.rules is None: - raise Exception('Holiday Calendar %s does not have any ' - 'rules specified' % self.name) + raise Exception('Holiday Calendar {name} does not have any ' + 'rules specified'.format(name=self.name)) if start is None: start = AbstractHolidayCalendar.start_date @@ -425,21 +429,21 @@ def merge_class(base, other): """ try: other = other.rules - except: + except AttributeError: pass if not isinstance(other, list): other = [other] - other_holidays = dict((holiday.name, holiday) for holiday in other) + other_holidays = {holiday.name: holiday for holiday in other} try: base = base.rules - except: + except AttributeError: pass if not isinstance(base, list): base = [base] - base_holidays = dict([(holiday.name, holiday) for holiday in base]) + base_holidays = {holiday.name: holiday for holiday in base} other_holidays.update(base_holidays) return list(other_holidays.values()) diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py deleted file mode 100644 index 9123131a6dccf..0000000000000 --- a/pandas/tseries/index.py +++ /dev/null @@ -1,2181 +0,0 @@ -# pylint: disable=E1101 -from __future__ import division -import operator -import warnings -from datetime import time, datetime -from datetime import timedelta -import numpy as np -from pandas.core.base import _shared_docs - -from pandas.types.common import (_NS_DTYPE, _INT64_DTYPE, - is_object_dtype, is_datetime64_dtype, - is_datetimetz, is_dtype_equal, - is_integer, is_float, - is_integer_dtype, - is_datetime64_ns_dtype, - is_period_dtype, - is_bool_dtype, - is_string_dtype, - is_list_like, - is_scalar, - pandas_dtype, - _ensure_int64) -from pandas.types.generic import ABCSeries -from pandas.types.dtypes import DatetimeTZDtype -from pandas.types.missing import isnull - -import pandas.types.concat as _concat -from pandas.core.common import (_values_from_object, _maybe_box, - PerformanceWarning) - -from pandas.core.index import Index, Int64Index, Float64Index -from pandas.indexes.base import _index_shared_docs -import pandas.compat as compat -from pandas.tseries.frequencies import ( - to_offset, get_period_alias, - Resolution) -from pandas.tseries.base import DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin -from pandas.tseries.offsets import DateOffset, generate_range, Tick, CDay -from pandas.tseries.tools import parse_time_string, normalize_date, to_time -from pandas.tseries.timedeltas import to_timedelta -from pandas.util.decorators import (Appender, cache_readonly, - deprecate_kwarg, Substitution) -import pandas.core.common as com -import pandas.tseries.offsets as offsets -import pandas.tseries.tools as tools - -from pandas._libs import (lib, index as libindex, tslib as libts, - algos as libalgos, join as libjoin, - Timestamp, period as libperiod) - - -def _utc(): - import pytz - return pytz.utc - -# -------- some conversion wrapper functions - - -def _field_accessor(name, field, docstring=None): - def f(self): - values = self.asi8 - if self.tz is not None: - utc = _utc() - if self.tz is not utc: - values = self._local_timestamps() - - if field in self._bool_ops: - if field in ['is_month_start', 'is_month_end', - 'is_quarter_start', 'is_quarter_end', - 'is_year_start', 'is_year_end']: - month_kw = (self.freq.kwds.get('startingMonth', - self.freq.kwds.get('month', 12)) - if self.freq else 12) - - result = libts.get_start_end_field(values, field, self.freqstr, - month_kw) - else: - result = libts.get_date_field(values, field) - - # these return a boolean by-definition - return result - - if field in self._object_ops: - result = libts.get_date_name_field(values, field) - result = self._maybe_mask_results(result) - - else: - result = libts.get_date_field(values, field) - result = self._maybe_mask_results(result, convert='float64') - - return Index(result, name=self.name) - - f.__name__ = name - f.__doc__ = docstring - return property(f) - - -def _dt_index_cmp(opname, nat_result=False): - """ - Wrap comparison operations to convert datetime-like to datetime64 - """ - - def wrapper(self, other): - func = getattr(super(DatetimeIndex, self), opname) - if (isinstance(other, datetime) or - isinstance(other, compat.string_types)): - other = _to_m8(other, tz=self.tz) - result = func(other) - if isnull(other): - result.fill(nat_result) - else: - if isinstance(other, list): - other = DatetimeIndex(other) - elif not isinstance(other, (np.ndarray, Index, ABCSeries)): - other = _ensure_datetime64(other) - result = func(np.asarray(other)) - result = _values_from_object(result) - - if isinstance(other, Index): - o_mask = other.values.view('i8') == libts.iNaT - else: - o_mask = other.view('i8') == libts.iNaT - - if o_mask.any(): - result[o_mask] = nat_result - - if self.hasnans: - result[self._isnan] = nat_result - - # support of bool dtype indexers - if is_bool_dtype(result): - return result - return Index(result) - - return wrapper - - -def _ensure_datetime64(other): - if isinstance(other, np.datetime64): - return other - raise TypeError('%s type object %s' % (type(other), str(other))) - - -_midnight = time(0, 0) - - -def _new_DatetimeIndex(cls, d): - """ This is called upon unpickling, rather than the default which doesn't - have arguments and breaks __new__ """ - - # data are already in UTC - # so need to localize - tz = d.pop('tz', None) - - result = cls.__new__(cls, verify_integrity=False, **d) - if tz is not None: - result = result.tz_localize('UTC').tz_convert(tz) - return result - - -class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, - Int64Index): - """ - Immutable ndarray of datetime64 data, represented internally as int64, and - which can be boxed to Timestamp objects that are subclasses of datetime and - carry metadata such as frequency information. - - Parameters - ---------- - data : array-like (1-dimensional), optional - Optional datetime-like data to construct index with - copy : bool - Make a copy of input ndarray - freq : string or pandas offset object, optional - One of pandas date offset strings or corresponding objects - start : starting value, datetime-like, optional - If data is None, start is used as the start point in generating regular - timestamp data. - periods : int, optional, > 0 - Number of periods to generate, if generating index. Takes precedence - over end argument - end : end time, datetime-like, optional - If periods is none, generated index will extend to first conforming - time on or just past end argument - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - tz : pytz.timezone or dateutil.tz.tzfile - ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' - - 'infer' will attempt to infer fall dst-transition hours based on - order - - bool-ndarray where True signifies a DST time, False signifies a - non-DST time (note that this flag is only applicable for ambiguous - times) - - 'NaT' will return NaT where there are ambiguous times - - 'raise' will raise an AmbiguousTimeError if there are ambiguous times - infer_dst : boolean, default False (DEPRECATED) - Attempt to infer fall dst-transition hours based on order - name : object - Name to be stored in the index - - Notes - ----- - - To learn more about the frequency strings, please see `this link - `__. - """ - - _typ = 'datetimeindex' - _join_precedence = 10 - - def _join_i8_wrapper(joinf, **kwargs): - return DatetimeIndexOpsMixin._join_i8_wrapper(joinf, dtype='M8[ns]', - **kwargs) - - _inner_indexer = _join_i8_wrapper(libjoin.inner_join_indexer_int64) - _outer_indexer = _join_i8_wrapper(libjoin.outer_join_indexer_int64) - _left_indexer = _join_i8_wrapper(libjoin.left_join_indexer_int64) - _left_indexer_unique = _join_i8_wrapper( - libjoin.left_join_indexer_unique_int64, with_indexers=False) - _arrmap = None - - __eq__ = _dt_index_cmp('__eq__') - __ne__ = _dt_index_cmp('__ne__', nat_result=True) - __lt__ = _dt_index_cmp('__lt__') - __gt__ = _dt_index_cmp('__gt__') - __le__ = _dt_index_cmp('__le__') - __ge__ = _dt_index_cmp('__ge__') - - _engine_type = libindex.DatetimeEngine - - tz = None - offset = None - _comparables = ['name', 'freqstr', 'tz'] - _attributes = ['name', 'freq', 'tz'] - - # define my properties & methods for delegation - _bool_ops = ['is_month_start', 'is_month_end', - 'is_quarter_start', 'is_quarter_end', 'is_year_start', - 'is_year_end', 'is_leap_year'] - _object_ops = ['weekday_name', 'freq', 'tz'] - _field_ops = ['year', 'month', 'day', 'hour', 'minute', 'second', - 'weekofyear', 'week', 'weekday', 'dayofweek', - 'dayofyear', 'quarter', 'days_in_month', - 'daysinmonth', 'microsecond', - 'nanosecond'] - _other_ops = ['date', 'time'] - _datetimelike_ops = _field_ops + _object_ops + _bool_ops + _other_ops - _datetimelike_methods = ['to_period', 'tz_localize', - 'tz_convert', - 'normalize', 'strftime', 'round', 'floor', - 'ceil'] - - _is_numeric_dtype = False - _infer_as_myclass = True - - @deprecate_kwarg(old_arg_name='infer_dst', new_arg_name='ambiguous', - mapping={True: 'infer', False: 'raise'}) - def __new__(cls, data=None, - freq=None, start=None, end=None, periods=None, - copy=False, name=None, tz=None, - verify_integrity=True, normalize=False, - closed=None, ambiguous='raise', dtype=None, **kwargs): - - # This allows to later ensure that the 'copy' parameter is honored: - if isinstance(data, Index): - ref_to_data = data._data - else: - ref_to_data = data - - if name is None and hasattr(data, 'name'): - name = data.name - - dayfirst = kwargs.pop('dayfirst', None) - yearfirst = kwargs.pop('yearfirst', None) - - freq_infer = False - if not isinstance(freq, DateOffset): - - # if a passed freq is None, don't infer automatically - if freq != 'infer': - freq = to_offset(freq) - else: - freq_infer = True - freq = None - - if periods is not None: - if is_float(periods): - periods = int(periods) - elif not is_integer(periods): - raise ValueError('Periods must be a number, got %s' % - str(periods)) - - if data is None and freq is None: - raise ValueError("Must provide freq argument if no data is " - "supplied") - - # if dtype has an embeded tz, capture it - if dtype is not None: - try: - dtype = DatetimeTZDtype.construct_from_string(dtype) - dtz = getattr(dtype, 'tz', None) - if dtz is not None: - if tz is not None and str(tz) != str(dtz): - raise ValueError("cannot supply both a tz and a dtype" - " with a tz") - tz = dtz - except TypeError: - pass - - if data is None: - return cls._generate(start, end, periods, name, freq, - tz=tz, normalize=normalize, closed=closed, - ambiguous=ambiguous) - - if not isinstance(data, (np.ndarray, Index, ABCSeries)): - if is_scalar(data): - raise ValueError('DatetimeIndex() must be called with a ' - 'collection of some kind, %s was passed' - % repr(data)) - # other iterable of some kind - if not isinstance(data, (list, tuple)): - data = list(data) - data = np.asarray(data, dtype='O') - elif isinstance(data, ABCSeries): - data = data._values - - # data must be Index or np.ndarray here - if not (is_datetime64_dtype(data) or is_datetimetz(data) or - is_integer_dtype(data)): - data = tools.to_datetime(data, dayfirst=dayfirst, - yearfirst=yearfirst) - - if issubclass(data.dtype.type, np.datetime64) or is_datetimetz(data): - - if isinstance(data, DatetimeIndex): - if tz is None: - tz = data.tz - elif data.tz is None: - data = data.tz_localize(tz, ambiguous=ambiguous) - else: - # the tz's must match - if str(tz) != str(data.tz): - msg = ('data is already tz-aware {0}, unable to ' - 'set specified tz: {1}') - raise TypeError(msg.format(data.tz, tz)) - - subarr = data.values - - if freq is None: - freq = data.offset - verify_integrity = False - else: - if data.dtype != _NS_DTYPE: - subarr = libts.cast_to_nanoseconds(data) - else: - subarr = data - else: - # must be integer dtype otherwise - if isinstance(data, Int64Index): - raise TypeError('cannot convert Int64Index->DatetimeIndex') - if data.dtype != _INT64_DTYPE: - data = data.astype(np.int64) - subarr = data.view(_NS_DTYPE) - - if isinstance(subarr, DatetimeIndex): - if tz is None: - tz = subarr.tz - else: - if tz is not None: - tz = libts.maybe_get_tz(tz) - - if (not isinstance(data, DatetimeIndex) or - getattr(data, 'tz', None) is None): - # Convert tz-naive to UTC - ints = subarr.view('i8') - subarr = libts.tz_localize_to_utc(ints, tz, - ambiguous=ambiguous) - subarr = subarr.view(_NS_DTYPE) - - subarr = cls._simple_new(subarr, name=name, freq=freq, tz=tz) - if dtype is not None: - if not is_dtype_equal(subarr.dtype, dtype): - # dtype must be coerced to DatetimeTZDtype above - if subarr.tz is not None: - raise ValueError("cannot localize from non-UTC data") - - if verify_integrity and len(subarr) > 0: - if freq is not None and not freq_infer: - inferred = subarr.inferred_freq - if inferred != freq.freqstr: - on_freq = cls._generate(subarr[0], None, len(subarr), None, - freq, tz=tz, ambiguous=ambiguous) - if not np.array_equal(subarr.asi8, on_freq.asi8): - raise ValueError('Inferred frequency {0} from passed ' - 'dates does not conform to passed ' - 'frequency {1}' - .format(inferred, freq.freqstr)) - - if freq_infer: - inferred = subarr.inferred_freq - if inferred: - subarr.offset = to_offset(inferred) - - return subarr._deepcopy_if_needed(ref_to_data, copy) - - @classmethod - def _generate(cls, start, end, periods, name, offset, - tz=None, normalize=False, ambiguous='raise', closed=None): - if com._count_not_none(start, end, periods) != 2: - raise ValueError('Must specify two of start, end, or periods') - - _normalized = True - - if start is not None: - start = Timestamp(start) - - if end is not None: - end = Timestamp(end) - - left_closed = False - right_closed = False - - if start is None and end is None: - if closed is not None: - raise ValueError("Closed has to be None if not both of start" - "and end are defined") - - if closed is None: - left_closed = True - right_closed = True - elif closed == "left": - left_closed = True - elif closed == "right": - right_closed = True - else: - raise ValueError("Closed has to be either 'left', 'right' or None") - - try: - inferred_tz = tools._infer_tzinfo(start, end) - except: - raise TypeError('Start and end cannot both be tz-aware with ' - 'different timezones') - - inferred_tz = libts.maybe_get_tz(inferred_tz) - - # these may need to be localized - tz = libts.maybe_get_tz(tz) - if tz is not None: - date = start or end - if date.tzinfo is not None and hasattr(tz, 'localize'): - tz = tz.localize(date.replace(tzinfo=None)).tzinfo - - if tz is not None and inferred_tz is not None: - if not libts.get_timezone(inferred_tz) == libts.get_timezone(tz): - raise AssertionError("Inferred time zone not equal to passed " - "time zone") - - elif inferred_tz is not None: - tz = inferred_tz - - if start is not None: - if normalize: - start = normalize_date(start) - _normalized = True - else: - _normalized = _normalized and start.time() == _midnight - - if end is not None: - if normalize: - end = normalize_date(end) - _normalized = True - else: - _normalized = _normalized and end.time() == _midnight - - if hasattr(offset, 'delta') and offset != offsets.Day(): - if inferred_tz is None and tz is not None: - # naive dates - if start is not None and start.tz is None: - start = start.tz_localize(tz, ambiguous=False) - - if end is not None and end.tz is None: - end = end.tz_localize(tz, ambiguous=False) - - if start and end: - if start.tz is None and end.tz is not None: - start = start.tz_localize(end.tz, ambiguous=False) - - if end.tz is None and start.tz is not None: - end = end.tz_localize(start.tz, ambiguous=False) - - if _use_cached_range(offset, _normalized, start, end): - index = cls._cached_range(start, end, periods=periods, - offset=offset, name=name) - else: - index = _generate_regular_range(start, end, periods, offset) - - else: - - if tz is not None: - # naive dates - if start is not None and start.tz is not None: - start = start.replace(tzinfo=None) - - if end is not None and end.tz is not None: - end = end.replace(tzinfo=None) - - if start and end: - if start.tz is None and end.tz is not None: - end = end.replace(tzinfo=None) - - if end.tz is None and start.tz is not None: - start = start.replace(tzinfo=None) - - if _use_cached_range(offset, _normalized, start, end): - index = cls._cached_range(start, end, periods=periods, - offset=offset, name=name) - else: - index = _generate_regular_range(start, end, periods, offset) - - if tz is not None and getattr(index, 'tz', None) is None: - index = libts.tz_localize_to_utc(_ensure_int64(index), tz, - ambiguous=ambiguous) - index = index.view(_NS_DTYPE) - - # index is localized datetime64 array -> have to convert - # start/end as well to compare - if start is not None: - start = start.tz_localize(tz).asm8 - if end is not None: - end = end.tz_localize(tz).asm8 - - if not left_closed and len(index) and index[0] == start: - index = index[1:] - if not right_closed and len(index) and index[-1] == end: - index = index[:-1] - index = cls._simple_new(index, name=name, freq=offset, tz=tz) - return index - - @property - def _box_func(self): - return lambda x: Timestamp(x, freq=self.offset, tz=self.tz) - - def _convert_for_op(self, value): - """ Convert value to be insertable to ndarray """ - if self._has_same_tz(value): - return _to_m8(value) - raise ValueError('Passed item and index have different timezone') - - def _local_timestamps(self): - utc = _utc() - - if self.is_monotonic: - return libts.tz_convert(self.asi8, utc, self.tz) - else: - values = self.asi8 - indexer = values.argsort() - result = libts.tz_convert(values.take(indexer), utc, self.tz) - - n = len(indexer) - reverse = np.empty(n, dtype=np.int_) - reverse.put(indexer, np.arange(n)) - return result.take(reverse) - - @classmethod - def _simple_new(cls, values, name=None, freq=None, tz=None, - dtype=None, **kwargs): - """ - we require the we have a dtype compat for the values - if we are passed a non-dtype compat, then coerce using the constructor - """ - - if getattr(values, 'dtype', None) is None: - # empty, but with dtype compat - if values is None: - values = np.empty(0, dtype=_NS_DTYPE) - return cls(values, name=name, freq=freq, tz=tz, - dtype=dtype, **kwargs) - values = np.array(values, copy=False) - - if is_object_dtype(values): - return cls(values, name=name, freq=freq, tz=tz, - dtype=dtype, **kwargs).values - elif not is_datetime64_dtype(values): - values = _ensure_int64(values).view(_NS_DTYPE) - - result = object.__new__(cls) - result._data = values - result.name = name - result.offset = freq - result.tz = libts.maybe_get_tz(tz) - result._reset_identity() - return result - - @property - def tzinfo(self): - """ - Alias for tz attribute - """ - return self.tz - - @cache_readonly - def _timezone(self): - """ Comparable timezone both for pytz / dateutil""" - return libts.get_timezone(self.tzinfo) - - def _has_same_tz(self, other): - zzone = self._timezone - - # vzone sholdn't be None if value is non-datetime like - if isinstance(other, np.datetime64): - # convert to Timestamp as np.datetime64 doesn't have tz attr - other = Timestamp(other) - vzone = libts.get_timezone(getattr(other, 'tzinfo', '__no_tz__')) - return zzone == vzone - - @classmethod - def _cached_range(cls, start=None, end=None, periods=None, offset=None, - name=None): - if start is None and end is None: - # I somewhat believe this should never be raised externally and - # therefore should be a `PandasError` but whatever... - raise TypeError('Must specify either start or end.') - if start is not None: - start = Timestamp(start) - if end is not None: - end = Timestamp(end) - if (start is None or end is None) and periods is None: - raise TypeError( - 'Must either specify period or provide both start and end.') - - if offset is None: - # This can't happen with external-facing code, therefore - # PandasError - raise TypeError('Must provide offset.') - - drc = _daterange_cache - if offset not in _daterange_cache: - xdr = generate_range(offset=offset, start=_CACHE_START, - end=_CACHE_END) - - arr = tools.to_datetime(list(xdr), box=False) - - cachedRange = DatetimeIndex._simple_new(arr) - cachedRange.offset = offset - cachedRange.tz = None - cachedRange.name = None - drc[offset] = cachedRange - else: - cachedRange = drc[offset] - - if start is None: - if not isinstance(end, Timestamp): - raise AssertionError('end must be an instance of Timestamp') - - end = offset.rollback(end) - - endLoc = cachedRange.get_loc(end) + 1 - startLoc = endLoc - periods - elif end is None: - if not isinstance(start, Timestamp): - raise AssertionError('start must be an instance of Timestamp') - - start = offset.rollforward(start) - - startLoc = cachedRange.get_loc(start) - endLoc = startLoc + periods - else: - if not offset.onOffset(start): - start = offset.rollforward(start) - - if not offset.onOffset(end): - end = offset.rollback(end) - - startLoc = cachedRange.get_loc(start) - endLoc = cachedRange.get_loc(end) + 1 - - indexSlice = cachedRange[startLoc:endLoc] - indexSlice.name = name - indexSlice.offset = offset - - return indexSlice - - def _mpl_repr(self): - # how to represent ourselves to matplotlib - return libts.ints_to_pydatetime(self.asi8, self.tz) - - @cache_readonly - def _is_dates_only(self): - from pandas.formats.format import _is_dates_only - return _is_dates_only(self.values) - - @property - def _formatter_func(self): - from pandas.formats.format import _get_format_datetime64 - formatter = _get_format_datetime64(is_dates_only=self._is_dates_only) - return lambda x: "'%s'" % formatter(x, tz=self.tz) - - def __reduce__(self): - - # we use a special reudce here because we need - # to simply set the .tz (and not reinterpret it) - - d = dict(data=self._data) - d.update(self._get_attributes_dict()) - return _new_DatetimeIndex, (self.__class__, d), None - - def __setstate__(self, state): - """Necessary for making this object picklable""" - if isinstance(state, dict): - super(DatetimeIndex, self).__setstate__(state) - - elif isinstance(state, tuple): - - # < 0.15 compat - if len(state) == 2: - nd_state, own_state = state - data = np.empty(nd_state[1], dtype=nd_state[2]) - np.ndarray.__setstate__(data, nd_state) - - self.name = own_state[0] - self.offset = own_state[1] - self.tz = own_state[2] - - # provide numpy < 1.7 compat - if nd_state[2] == 'M8[us]': - new_state = np.ndarray.__reduce__(data.astype('M8[ns]')) - np.ndarray.__setstate__(data, new_state[2]) - - else: # pragma: no cover - data = np.empty(state) - np.ndarray.__setstate__(data, state) - - self._data = data - self._reset_identity() - - else: - raise Exception("invalid pickle state") - _unpickle_compat = __setstate__ - - def _add_datelike(self, other): - # adding a timedeltaindex to a datetimelike - if other is libts.NaT: - return self._nat_new(box=True) - raise TypeError("cannot add a datelike to a DatetimeIndex") - - def _sub_datelike(self, other): - # subtract a datetime from myself, yielding a TimedeltaIndex - from pandas import TimedeltaIndex - if isinstance(other, DatetimeIndex): - # require tz compat - if not self._has_same_tz(other): - raise TypeError("DatetimeIndex subtraction must have the same " - "timezones or no timezones") - result = self._sub_datelike_dti(other) - elif isinstance(other, (libts.Timestamp, datetime)): - other = Timestamp(other) - if other is libts.NaT: - result = self._nat_new(box=False) - # require tz compat - elif not self._has_same_tz(other): - raise TypeError("Timestamp subtraction must have the same " - "timezones or no timezones") - else: - i8 = self.asi8 - result = i8 - other.value - result = self._maybe_mask_results(result, - fill_value=libts.iNaT) - else: - raise TypeError("cannot subtract DatetimeIndex and {typ}" - .format(typ=type(other).__name__)) - return TimedeltaIndex(result, name=self.name, copy=False) - - def _sub_datelike_dti(self, other): - """subtraction of two DatetimeIndexes""" - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = self_i8 - other_i8 - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = libts.iNaT - return new_values.view('i8') - - def _maybe_update_attributes(self, attrs): - """ Update Index attributes (e.g. freq) depending on op """ - freq = attrs.get('freq', None) - if freq is not None: - # no need to infer if freq is None - attrs['freq'] = 'infer' - return attrs - - def _add_delta(self, delta): - from pandas import TimedeltaIndex - name = self.name - - if isinstance(delta, (Tick, timedelta, np.timedelta64)): - new_values = self._add_delta_td(delta) - elif isinstance(delta, TimedeltaIndex): - new_values = self._add_delta_tdi(delta) - # update name when delta is Index - name = com._maybe_match_name(self, delta) - elif isinstance(delta, DateOffset): - new_values = self._add_offset(delta).asi8 - else: - new_values = self.astype('O') + delta - - tz = 'UTC' if self.tz is not None else None - result = DatetimeIndex(new_values, tz=tz, name=name, freq='infer') - utc = _utc() - if self.tz is not None and self.tz is not utc: - result = result.tz_convert(self.tz) - return result - - def _add_offset(self, offset): - try: - if self.tz is not None: - values = self.tz_localize(None) - else: - values = self - result = offset.apply_index(values) - if self.tz is not None: - result = result.tz_localize(self.tz) - return result - - except NotImplementedError: - warnings.warn("Non-vectorized DateOffset being applied to Series " - "or DatetimeIndex", PerformanceWarning) - return self.astype('O') + offset - - def _format_native_types(self, na_rep='NaT', date_format=None, **kwargs): - from pandas.formats.format import _get_format_datetime64_from_values - format = _get_format_datetime64_from_values(self, date_format) - - return libts.format_array_from_datetime(self.asi8, - tz=self.tz, - format=format, - na_rep=na_rep) - - def to_datetime(self, dayfirst=False): - return self.copy() - - @Appender(_index_shared_docs['astype']) - def astype(self, dtype, copy=True): - dtype = pandas_dtype(dtype) - if is_object_dtype(dtype): - return self.asobject - elif is_integer_dtype(dtype): - return Index(self.values.astype('i8', copy=copy), name=self.name, - dtype='i8') - elif is_datetime64_ns_dtype(dtype): - if self.tz is not None: - return self.tz_convert('UTC').tz_localize(None) - elif copy is True: - return self.copy() - return self - elif is_string_dtype(dtype): - return Index(self.format(), name=self.name, dtype=object) - elif is_period_dtype(dtype): - return self.to_period(freq=dtype.freq) - raise ValueError('Cannot cast DatetimeIndex to dtype %s' % dtype) - - def _get_time_micros(self): - utc = _utc() - values = self.asi8 - if self.tz is not None and self.tz is not utc: - values = self._local_timestamps() - return libts.get_time_micros(values) - - def to_series(self, keep_tz=False): - """ - Create a Series with both index and values equal to the index keys - useful with map for returning an indexer based on an index - - Parameters - ---------- - keep_tz : optional, defaults False. - return the data keeping the timezone. - - If keep_tz is True: - - If the timezone is not set, the resulting - Series will have a datetime64[ns] dtype. - - Otherwise the Series will have an datetime64[ns, tz] dtype; the - tz will be preserved. - - If keep_tz is False: - - Series will have a datetime64[ns] dtype. TZ aware - objects will have the tz removed. - - Returns - ------- - Series - """ - from pandas import Series - return Series(self._to_embed(keep_tz), index=self, name=self.name) - - def _to_embed(self, keep_tz=False): - """ - return an array repr of this object, potentially casting to object - - This is for internal compat - """ - if keep_tz and self.tz is not None: - - # preserve the tz & copy - return self.copy(deep=True) - - return self.values.copy() - - def to_pydatetime(self): - """ - Return DatetimeIndex as object ndarray of datetime.datetime objects - - Returns - ------- - datetimes : ndarray - """ - return libts.ints_to_pydatetime(self.asi8, tz=self.tz) - - def to_period(self, freq=None): - """ - Cast to PeriodIndex at a particular frequency - """ - from pandas.tseries.period import PeriodIndex - - if freq is None: - freq = self.freqstr or self.inferred_freq - - if freq is None: - msg = ("You must pass a freq argument as " - "current index has none.") - raise ValueError(msg) - - freq = get_period_alias(freq) - - return PeriodIndex(self.values, name=self.name, freq=freq, tz=self.tz) - - def snap(self, freq='S'): - """ - Snap time stamps to nearest occurring frequency - - """ - # Superdumb, punting on any optimizing - freq = to_offset(freq) - - snapped = np.empty(len(self), dtype=_NS_DTYPE) - - for i, v in enumerate(self): - s = v - if not freq.onOffset(s): - t0 = freq.rollback(s) - t1 = freq.rollforward(s) - if abs(s - t0) < abs(t1 - s): - s = t0 - else: - s = t1 - snapped[i] = s - - # we know it conforms; skip check - return DatetimeIndex(snapped, freq=freq, verify_integrity=False) - - def union(self, other): - """ - Specialized union for DatetimeIndex objects. If combine - overlapping ranges with the same DateOffset, will be much - faster than Index.union - - Parameters - ---------- - other : DatetimeIndex or array-like - - Returns - ------- - y : Index or DatetimeIndex - """ - self._assert_can_do_setop(other) - if not isinstance(other, DatetimeIndex): - try: - other = DatetimeIndex(other) - except TypeError: - pass - - this, other = self._maybe_utc_convert(other) - - if this._can_fast_union(other): - return this._fast_union(other) - else: - result = Index.union(this, other) - if isinstance(result, DatetimeIndex): - result.tz = this.tz - if (result.freq is None and - (this.freq is not None or other.freq is not None)): - result.offset = to_offset(result.inferred_freq) - return result - - def to_perioddelta(self, freq): - """ - Calcuates TimedeltaIndex of difference between index - values and index converted to PeriodIndex at specified - freq. Used for vectorized offsets - - .. versionadded:: 0.17.0 - - Parameters - ---------- - freq : Period frequency - - Returns - ------- - y : TimedeltaIndex - """ - return to_timedelta(self.asi8 - self.to_period(freq) - .to_timestamp().asi8) - - def union_many(self, others): - """ - A bit of a hack to accelerate unioning a collection of indexes - """ - this = self - - for other in others: - if not isinstance(this, DatetimeIndex): - this = Index.union(this, other) - continue - - if not isinstance(other, DatetimeIndex): - try: - other = DatetimeIndex(other) - except TypeError: - pass - - this, other = this._maybe_utc_convert(other) - - if this._can_fast_union(other): - this = this._fast_union(other) - else: - tz = this.tz - this = Index.union(this, other) - if isinstance(this, DatetimeIndex): - this.tz = tz - - if this.freq is None: - this.offset = to_offset(this.inferred_freq) - return this - - def join(self, other, how='left', level=None, return_indexers=False, - sort=False): - """ - See Index.join - """ - if (not isinstance(other, DatetimeIndex) and len(other) > 0 and - other.inferred_type not in ('floating', 'mixed-integer', - 'mixed-integer-float', 'mixed')): - try: - other = DatetimeIndex(other) - except (TypeError, ValueError): - pass - - this, other = self._maybe_utc_convert(other) - return Index.join(this, other, how=how, level=level, - return_indexers=return_indexers, sort=sort) - - def _maybe_utc_convert(self, other): - this = self - if isinstance(other, DatetimeIndex): - if self.tz is not None: - if other.tz is None: - raise TypeError('Cannot join tz-naive with tz-aware ' - 'DatetimeIndex') - elif other.tz is not None: - raise TypeError('Cannot join tz-naive with tz-aware ' - 'DatetimeIndex') - - if self.tz != other.tz: - this = self.tz_convert('UTC') - other = other.tz_convert('UTC') - return this, other - - def _wrap_joined_index(self, joined, other): - name = self.name if self.name == other.name else None - if (isinstance(other, DatetimeIndex) and - self.offset == other.offset and - self._can_fast_union(other)): - joined = self._shallow_copy(joined) - joined.name = name - return joined - else: - tz = getattr(other, 'tz', None) - return self._simple_new(joined, name, tz=tz) - - def _can_fast_union(self, other): - if not isinstance(other, DatetimeIndex): - return False - - offset = self.offset - - if offset is None or offset != other.offset: - return False - - if not self.is_monotonic or not other.is_monotonic: - return False - - if len(self) == 0 or len(other) == 0: - return True - - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - right_start = right[0] - left_end = left[-1] - - # Only need to "adjoin", not overlap - try: - return (right_start == left_end + offset) or right_start in left - except (ValueError): - - # if we are comparing an offset that does not propagate timezones - # this will raise - return False - - def _fast_union(self, other): - if len(other) == 0: - return self.view(type(self)) - - if len(self) == 0: - return other.view(type(self)) - - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - left_start, left_end = left[0], left[-1] - right_end = right[-1] - - if not self.offset._should_cache(): - # concatenate dates - if left_end < right_end: - loc = right.searchsorted(left_end, side='right') - right_chunk = right.values[loc:] - dates = _concat._concat_compat((left.values, right_chunk)) - return self._shallow_copy(dates) - else: - return left - else: - return type(self)(start=left_start, - end=max(left_end, right_end), - freq=left.offset) - - def __iter__(self): - """ - Return an iterator over the boxed values - - Returns - ------- - Timestamps : ndarray - """ - - # convert in chunks of 10k for efficiency - data = self.asi8 - l = len(self) - chunksize = 10000 - chunks = int(l / chunksize) + 1 - for i in range(chunks): - start_i = i * chunksize - end_i = min((i + 1) * chunksize, l) - converted = libts.ints_to_pydatetime(data[start_i:end_i], - tz=self.tz, freq=self.freq, - box=True) - for v in converted: - yield v - - def _wrap_union_result(self, other, result): - name = self.name if self.name == other.name else None - if self.tz != other.tz: - raise ValueError('Passed item and index have different timezone') - return self._simple_new(result, name=name, freq=None, tz=self.tz) - - def intersection(self, other): - """ - Specialized intersection for DatetimeIndex objects. May be much faster - than Index.intersection - - Parameters - ---------- - other : DatetimeIndex or array-like - - Returns - ------- - y : Index or DatetimeIndex - """ - self._assert_can_do_setop(other) - if not isinstance(other, DatetimeIndex): - try: - other = DatetimeIndex(other) - except (TypeError, ValueError): - pass - result = Index.intersection(self, other) - if isinstance(result, DatetimeIndex): - if result.freq is None: - result.offset = to_offset(result.inferred_freq) - return result - - elif (other.offset is None or self.offset is None or - other.offset != self.offset or - not other.offset.isAnchored() or - (not self.is_monotonic or not other.is_monotonic)): - result = Index.intersection(self, other) - result = self._shallow_copy(result._values, name=result.name, - tz=result.tz, freq=None) - if result.freq is None: - result.offset = to_offset(result.inferred_freq) - return result - - if len(self) == 0: - return self - if len(other) == 0: - return other - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - end = min(left[-1], right[-1]) - start = right[0] - - if end < start: - return type(self)(data=[]) - else: - lslice = slice(*left.slice_locs(start, end)) - left_chunk = left.values[lslice] - return self._shallow_copy(left_chunk) - - def _parsed_string_to_bounds(self, reso, parsed): - """ - Calculate datetime bounds for parsed time string and its resolution. - - Parameters - ---------- - reso : Resolution - Resolution provided by parsed string. - parsed : datetime - Datetime from parsed string. - - Returns - ------- - lower, upper: pd.Timestamp - - """ - if reso == 'year': - return (Timestamp(datetime(parsed.year, 1, 1), tz=self.tz), - Timestamp(datetime(parsed.year, 12, 31, 23, - 59, 59, 999999), tz=self.tz)) - elif reso == 'month': - d = libts.monthrange(parsed.year, parsed.month)[1] - return (Timestamp(datetime(parsed.year, parsed.month, 1), - tz=self.tz), - Timestamp(datetime(parsed.year, parsed.month, d, 23, - 59, 59, 999999), tz=self.tz)) - elif reso == 'quarter': - qe = (((parsed.month - 1) + 2) % 12) + 1 # two months ahead - d = libts.monthrange(parsed.year, qe)[1] # at end of month - return (Timestamp(datetime(parsed.year, parsed.month, 1), - tz=self.tz), - Timestamp(datetime(parsed.year, qe, d, 23, 59, - 59, 999999), tz=self.tz)) - elif reso == 'day': - st = datetime(parsed.year, parsed.month, parsed.day) - return (Timestamp(st, tz=self.tz), - Timestamp(Timestamp(st + offsets.Day(), - tz=self.tz).value - 1)) - elif reso == 'hour': - st = datetime(parsed.year, parsed.month, parsed.day, - hour=parsed.hour) - return (Timestamp(st, tz=self.tz), - Timestamp(Timestamp(st + offsets.Hour(), - tz=self.tz).value - 1)) - elif reso == 'minute': - st = datetime(parsed.year, parsed.month, parsed.day, - hour=parsed.hour, minute=parsed.minute) - return (Timestamp(st, tz=self.tz), - Timestamp(Timestamp(st + offsets.Minute(), - tz=self.tz).value - 1)) - elif reso == 'second': - st = datetime(parsed.year, parsed.month, parsed.day, - hour=parsed.hour, minute=parsed.minute, - second=parsed.second) - return (Timestamp(st, tz=self.tz), - Timestamp(Timestamp(st + offsets.Second(), - tz=self.tz).value - 1)) - elif reso == 'microsecond': - st = datetime(parsed.year, parsed.month, parsed.day, - parsed.hour, parsed.minute, parsed.second, - parsed.microsecond) - return (Timestamp(st, tz=self.tz), Timestamp(st, tz=self.tz)) - else: - raise KeyError - - def _partial_date_slice(self, reso, parsed, use_lhs=True, use_rhs=True): - is_monotonic = self.is_monotonic - if (is_monotonic and reso in ['day', 'hour', 'minute', 'second'] and - self._resolution >= Resolution.get_reso(reso)): - # These resolution/monotonicity validations came from GH3931, - # GH3452 and GH2369. - - # See also GH14826 - raise KeyError - - if reso == 'microsecond': - # _partial_date_slice doesn't allow microsecond resolution, but - # _parsed_string_to_bounds allows it. - raise KeyError - - t1, t2 = self._parsed_string_to_bounds(reso, parsed) - stamps = self.asi8 - - if is_monotonic: - - # we are out of range - if (len(stamps) and ((use_lhs and t1.value < stamps[0] and - t2.value < stamps[0]) or - ((use_rhs and t1.value > stamps[-1] and - t2.value > stamps[-1])))): - raise KeyError - - # a monotonic (sorted) series can be sliced - left = stamps.searchsorted( - t1.value, side='left') if use_lhs else None - right = stamps.searchsorted( - t2.value, side='right') if use_rhs else None - - return slice(left, right) - - lhs_mask = (stamps >= t1.value) if use_lhs else True - rhs_mask = (stamps <= t2.value) if use_rhs else True - - # try to find a the dates - return (lhs_mask & rhs_mask).nonzero()[0] - - def _maybe_promote(self, other): - if other.inferred_type == 'date': - other = DatetimeIndex(other) - return self, other - - def get_value(self, series, key): - """ - Fast lookup of value from 1-dimensional ndarray. Only use this if you - know what you're doing - """ - - if isinstance(key, datetime): - - # needed to localize naive datetimes - if self.tz is not None: - key = Timestamp(key, tz=self.tz) - - return self.get_value_maybe_box(series, key) - - if isinstance(key, time): - locs = self.indexer_at_time(key) - return series.take(locs) - - try: - return _maybe_box(self, Index.get_value(self, series, key), - series, key) - except KeyError: - try: - loc = self._get_string_slice(key) - return series[loc] - except (TypeError, ValueError, KeyError): - pass - - try: - return self.get_value_maybe_box(series, key) - except (TypeError, ValueError, KeyError): - raise KeyError(key) - - def get_value_maybe_box(self, series, key): - # needed to localize naive datetimes - if self.tz is not None: - key = Timestamp(key, tz=self.tz) - elif not isinstance(key, Timestamp): - key = Timestamp(key) - values = self._engine.get_value(_values_from_object(series), - key, tz=self.tz) - return _maybe_box(self, values, series, key) - - def get_loc(self, key, method=None, tolerance=None): - """ - Get integer location for requested label - - Returns - ------- - loc : int - """ - - if tolerance is not None: - # try converting tolerance now, so errors don't get swallowed by - # the try/except clauses below - tolerance = self._convert_tolerance(tolerance) - - if isinstance(key, datetime): - # needed to localize naive datetimes - key = Timestamp(key, tz=self.tz) - return Index.get_loc(self, key, method, tolerance) - - if isinstance(key, time): - if method is not None: - raise NotImplementedError('cannot yet lookup inexact labels ' - 'when key is a time object') - return self.indexer_at_time(key) - - try: - return Index.get_loc(self, key, method, tolerance) - except (KeyError, ValueError, TypeError): - try: - return self._get_string_slice(key) - except (TypeError, KeyError, ValueError): - pass - - try: - stamp = Timestamp(key, tz=self.tz) - return Index.get_loc(self, stamp, method, tolerance) - except (KeyError, ValueError): - raise KeyError(key) - - def _maybe_cast_slice_bound(self, label, side, kind): - """ - If label is a string, cast it to datetime according to resolution. - - Parameters - ---------- - label : object - side : {'left', 'right'} - kind : {'ix', 'loc', 'getitem'} - - Returns - ------- - label : object - - Notes - ----- - Value of `side` parameter should be validated in caller. - - """ - assert kind in ['ix', 'loc', 'getitem', None] - - if is_float(label) or isinstance(label, time) or is_integer(label): - self._invalid_indexer('slice', label) - - if isinstance(label, compat.string_types): - freq = getattr(self, 'freqstr', - getattr(self, 'inferred_freq', None)) - _, parsed, reso = parse_time_string(label, freq) - lower, upper = self._parsed_string_to_bounds(reso, parsed) - # lower, upper form the half-open interval: - # [parsed, parsed + 1 freq) - # because label may be passed to searchsorted - # the bounds need swapped if index is reverse sorted and has a - # length (is_monotonic_decreasing gives True for empty index) - if self.is_monotonic_decreasing and len(self): - return upper if side == 'left' else lower - return lower if side == 'left' else upper - else: - return label - - def _get_string_slice(self, key, use_lhs=True, use_rhs=True): - freq = getattr(self, 'freqstr', - getattr(self, 'inferred_freq', None)) - _, parsed, reso = parse_time_string(key, freq) - loc = self._partial_date_slice(reso, parsed, use_lhs=use_lhs, - use_rhs=use_rhs) - return loc - - def slice_indexer(self, start=None, end=None, step=None, kind=None): - """ - Return indexer for specified label slice. - Index.slice_indexer, customized to handle time slicing. - - In addition to functionality provided by Index.slice_indexer, does the - following: - - - if both `start` and `end` are instances of `datetime.time`, it - invokes `indexer_between_time` - - if `start` and `end` are both either string or None perform - value-based selection in non-monotonic cases. - - """ - # For historical reasons DatetimeIndex supports slices between two - # instances of datetime.time as if it were applying a slice mask to - # an array of (self.hour, self.minute, self.seconds, self.microsecond). - if isinstance(start, time) and isinstance(end, time): - if step is not None and step != 1: - raise ValueError('Must have step size of 1 with time slices') - return self.indexer_between_time(start, end) - - if isinstance(start, time) or isinstance(end, time): - raise KeyError('Cannot mix time and non-time slice keys') - - try: - return Index.slice_indexer(self, start, end, step, kind=kind) - except KeyError: - # For historical reasons DatetimeIndex by default supports - # value-based partial (aka string) slices on non-monotonic arrays, - # let's try that. - if ((start is None or isinstance(start, compat.string_types)) and - (end is None or isinstance(end, compat.string_types))): - mask = True - if start is not None: - start_casted = self._maybe_cast_slice_bound( - start, 'left', kind) - mask = start_casted <= self - - if end is not None: - end_casted = self._maybe_cast_slice_bound( - end, 'right', kind) - mask = (self <= end_casted) & mask - - indexer = mask.nonzero()[0][::step] - if len(indexer) == len(self): - return slice(None) - else: - return indexer - else: - raise - - # alias to offset - def _get_freq(self): - return self.offset - - def _set_freq(self, value): - self.offset = value - freq = property(fget=_get_freq, fset=_set_freq, - doc="get/set the frequency of the Index") - - year = _field_accessor('year', 'Y', "The year of the datetime") - month = _field_accessor('month', 'M', - "The month as January=1, December=12") - day = _field_accessor('day', 'D', "The days of the datetime") - hour = _field_accessor('hour', 'h', "The hours of the datetime") - minute = _field_accessor('minute', 'm', "The minutes of the datetime") - second = _field_accessor('second', 's', "The seconds of the datetime") - microsecond = _field_accessor('microsecond', 'us', - "The microseconds of the datetime") - nanosecond = _field_accessor('nanosecond', 'ns', - "The nanoseconds of the datetime") - weekofyear = _field_accessor('weekofyear', 'woy', - "The week ordinal of the year") - week = weekofyear - dayofweek = _field_accessor('dayofweek', 'dow', - "The day of the week with Monday=0, Sunday=6") - weekday = dayofweek - - weekday_name = _field_accessor( - 'weekday_name', - 'weekday_name', - "The name of day in a week (ex: Friday)\n\n.. versionadded:: 0.18.1") - - dayofyear = _field_accessor('dayofyear', 'doy', - "The ordinal day of the year") - quarter = _field_accessor('quarter', 'q', "The quarter of the date") - days_in_month = _field_accessor( - 'days_in_month', - 'dim', - "The number of days in the month\n\n.. versionadded:: 0.16.0") - daysinmonth = days_in_month - is_month_start = _field_accessor( - 'is_month_start', - 'is_month_start', - "Logical indicating if first day of month (defined by frequency)") - is_month_end = _field_accessor( - 'is_month_end', - 'is_month_end', - "Logical indicating if last day of month (defined by frequency)") - is_quarter_start = _field_accessor( - 'is_quarter_start', - 'is_quarter_start', - "Logical indicating if first day of quarter (defined by frequency)") - is_quarter_end = _field_accessor( - 'is_quarter_end', - 'is_quarter_end', - "Logical indicating if last day of quarter (defined by frequency)") - is_year_start = _field_accessor( - 'is_year_start', - 'is_year_start', - "Logical indicating if first day of year (defined by frequency)") - is_year_end = _field_accessor( - 'is_year_end', - 'is_year_end', - "Logical indicating if last day of year (defined by frequency)") - is_leap_year = _field_accessor( - 'is_leap_year', - 'is_leap_year', - "Logical indicating if the date belongs to a leap year") - - @property - def time(self): - """ - Returns numpy array of datetime.time. The time part of the Timestamps. - """ - return self._maybe_mask_results(libalgos.arrmap_object( - self.asobject.values, - lambda x: np.nan if x is libts.NaT else x.time())) - - @property - def date(self): - """ - Returns numpy array of python datetime.date objects (namely, the date - part of Timestamps without timezone information). - """ - return self._maybe_mask_results(libalgos.arrmap_object( - self.asobject.values, lambda x: x.date())) - - def normalize(self): - """ - Return DatetimeIndex with times to midnight. Length is unaltered - - Returns - ------- - normalized : DatetimeIndex - """ - new_values = libts.date_normalize(self.asi8, self.tz) - return DatetimeIndex(new_values, freq='infer', name=self.name, - tz=self.tz) - - @Substitution(klass='DatetimeIndex') - @Appender(_shared_docs['searchsorted']) - @deprecate_kwarg(old_arg_name='key', new_arg_name='value') - def searchsorted(self, value, side='left', sorter=None): - if isinstance(value, (np.ndarray, Index)): - value = np.array(value, dtype=_NS_DTYPE, copy=False) - else: - value = _to_m8(value, tz=self.tz) - - return self.values.searchsorted(value, side=side) - - def is_type_compatible(self, typ): - return typ == self.inferred_type or typ == 'datetime' - - @property - def inferred_type(self): - # b/c datetime is represented as microseconds since the epoch, make - # sure we can't have ambiguous indexing - return 'datetime64' - - @cache_readonly - def dtype(self): - if self.tz is None: - return _NS_DTYPE - return DatetimeTZDtype('ns', self.tz) - - @property - def is_all_dates(self): - return True - - @cache_readonly - def is_normalized(self): - """ - Returns True if all of the dates are at midnight ("no time") - """ - return libts.dates_normalized(self.asi8, self.tz) - - @cache_readonly - def _resolution(self): - return libperiod.resolution(self.asi8, self.tz) - - def insert(self, loc, item): - """ - Make new Index inserting new item at location - - Parameters - ---------- - loc : int - item : object - if not either a Python datetime or a numpy integer-like, returned - Index dtype will be object rather than datetime. - - Returns - ------- - new_index : Index - """ - - freq = None - - if isinstance(item, (datetime, np.datetime64)): - self._assert_can_do_op(item) - if not self._has_same_tz(item): - raise ValueError( - 'Passed item and index have different timezone') - # check freq can be preserved on edge cases - if self.size and self.freq is not None: - if ((loc == 0 or loc == -len(self)) and - item + self.freq == self[0]): - freq = self.freq - elif (loc == len(self)) and item - self.freq == self[-1]: - freq = self.freq - item = _to_m8(item, tz=self.tz) - try: - new_dates = np.concatenate((self[:loc].asi8, [item.view(np.int64)], - self[loc:].asi8)) - if self.tz is not None: - new_dates = libts.tz_convert(new_dates, 'UTC', self.tz) - return DatetimeIndex(new_dates, name=self.name, freq=freq, - tz=self.tz) - - except (AttributeError, TypeError): - - # fall back to object index - if isinstance(item, compat.string_types): - return self.asobject.insert(loc, item) - raise TypeError( - "cannot insert DatetimeIndex with incompatible label") - - def delete(self, loc): - """ - Make a new DatetimeIndex with passed location(s) deleted. - - Parameters - ---------- - loc: int, slice or array of ints - Indicate which sub-arrays to remove. - - Returns - ------- - new_index : DatetimeIndex - """ - new_dates = np.delete(self.asi8, loc) - - freq = None - if is_integer(loc): - if loc in (0, -len(self), -1, len(self) - 1): - freq = self.freq - else: - if is_list_like(loc): - loc = lib.maybe_indices_to_slice( - _ensure_int64(np.array(loc)), len(self)) - if isinstance(loc, slice) and loc.step in (1, None): - if (loc.start in (0, None) or loc.stop in (len(self), None)): - freq = self.freq - - if self.tz is not None: - new_dates = libts.tz_convert(new_dates, 'UTC', self.tz) - return DatetimeIndex(new_dates, name=self.name, freq=freq, tz=self.tz) - - def tz_convert(self, tz): - """ - Convert tz-aware DatetimeIndex from one time zone to another (using - pytz/dateutil) - - Parameters - ---------- - tz : string, pytz.timezone, dateutil.tz.tzfile or None - Time zone for time. Corresponding timestamps would be converted to - time zone of the TimeSeries. - None will remove timezone holding UTC time. - - Returns - ------- - normalized : DatetimeIndex - - Raises - ------ - TypeError - If DatetimeIndex is tz-naive. - """ - tz = libts.maybe_get_tz(tz) - - if self.tz is None: - # tz naive, use tz_localize - raise TypeError('Cannot convert tz-naive timestamps, use ' - 'tz_localize to localize') - - # No conversion since timestamps are all UTC to begin with - return self._shallow_copy(tz=tz) - - @deprecate_kwarg(old_arg_name='infer_dst', new_arg_name='ambiguous', - mapping={True: 'infer', False: 'raise'}) - def tz_localize(self, tz, ambiguous='raise', errors='raise'): - """ - Localize tz-naive DatetimeIndex to given time zone (using - pytz/dateutil), or remove timezone from tz-aware DatetimeIndex - - Parameters - ---------- - tz : string, pytz.timezone, dateutil.tz.tzfile or None - Time zone for time. Corresponding timestamps would be converted to - time zone of the TimeSeries. - None will remove timezone holding local time. - ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' - - 'infer' will attempt to infer fall dst-transition hours based on - order - - bool-ndarray where True signifies a DST time, False signifies a - non-DST time (note that this flag is only applicable for - ambiguous times) - - 'NaT' will return NaT where there are ambiguous times - - 'raise' will raise an AmbiguousTimeError if there are ambiguous - times - errors : 'raise', 'coerce', default 'raise' - - 'raise' will raise a NonExistentTimeError if a timestamp is not - valid in the specified timezone (e.g. due to a transition from - or to DST time) - - 'coerce' will return NaT if the timestamp can not be converted - into the specified timezone - - .. versionadded:: 0.19.0 - - infer_dst : boolean, default False (DEPRECATED) - Attempt to infer fall dst-transition hours based on order - - Returns - ------- - localized : DatetimeIndex - - Raises - ------ - TypeError - If the DatetimeIndex is tz-aware and tz is not None. - """ - if self.tz is not None: - if tz is None: - new_dates = libts.tz_convert(self.asi8, 'UTC', self.tz) - else: - raise TypeError("Already tz-aware, use tz_convert to convert.") - else: - tz = libts.maybe_get_tz(tz) - # Convert to UTC - - new_dates = libts.tz_localize_to_utc(self.asi8, tz, - ambiguous=ambiguous, - errors=errors) - new_dates = new_dates.view(_NS_DTYPE) - return self._shallow_copy(new_dates, tz=tz) - - def indexer_at_time(self, time, asof=False): - """ - Select values at particular time of day (e.g. 9:30AM) - - Parameters - ---------- - time : datetime.time or string - - Returns - ------- - values_at_time : TimeSeries - """ - from dateutil.parser import parse - - if asof: - raise NotImplementedError("'asof' argument is not supported") - - if isinstance(time, compat.string_types): - time = parse(time).time() - - if time.tzinfo: - # TODO - raise NotImplementedError("argument 'time' with timezone info is " - "not supported") - - time_micros = self._get_time_micros() - micros = _time_to_micros(time) - return (micros == time_micros).nonzero()[0] - - def indexer_between_time(self, start_time, end_time, include_start=True, - include_end=True): - """ - Select values between particular times of day (e.g., 9:00-9:30AM). - - Return values of the index between two times. If start_time or - end_time are strings then tseres.tools.to_time is used to convert to - a time object. - - Parameters - ---------- - start_time, end_time : datetime.time, str - datetime.time or string in appropriate format ("%H:%M", "%H%M", - "%I:%M%p", "%I%M%p", "%H:%M:%S", "%H%M%S", "%I:%M:%S%p", - "%I%M%S%p") - include_start : boolean, default True - include_end : boolean, default True - - Returns - ------- - values_between_time : TimeSeries - """ - start_time = to_time(start_time) - end_time = to_time(end_time) - time_micros = self._get_time_micros() - start_micros = _time_to_micros(start_time) - end_micros = _time_to_micros(end_time) - - if include_start and include_end: - lop = rop = operator.le - elif include_start: - lop = operator.le - rop = operator.lt - elif include_end: - lop = operator.lt - rop = operator.le - else: - lop = rop = operator.lt - - if start_time <= end_time: - join_op = operator.and_ - else: - join_op = operator.or_ - - mask = join_op(lop(start_micros, time_micros), - rop(time_micros, end_micros)) - - return mask.nonzero()[0] - - def to_julian_date(self): - """ - Convert DatetimeIndex to Float64Index of Julian Dates. - 0 Julian date is noon January 1, 4713 BC. - http://en.wikipedia.org/wiki/Julian_day - """ - - # http://mysite.verizon.net/aesir_research/date/jdalg2.htm - year = np.asarray(self.year) - month = np.asarray(self.month) - day = np.asarray(self.day) - testarr = month < 3 - year[testarr] -= 1 - month[testarr] += 12 - return Float64Index(day + - np.fix((153 * month - 457) / 5) + - 365 * year + - np.floor(year / 4) - - np.floor(year / 100) + - np.floor(year / 400) + - 1721118.5 + - (self.hour + - self.minute / 60.0 + - self.second / 3600.0 + - self.microsecond / 3600.0 / 1e+6 + - self.nanosecond / 3600.0 / 1e+9 - ) / 24.0) - - -DatetimeIndex._add_numeric_methods_disabled() -DatetimeIndex._add_logical_methods_disabled() -DatetimeIndex._add_datetimelike_methods() - - -def _generate_regular_range(start, end, periods, offset): - if isinstance(offset, Tick): - stride = offset.nanos - if periods is None: - b = Timestamp(start).value - # cannot just use e = Timestamp(end) + 1 because arange breaks when - # stride is too large, see GH10887 - e = (b + (Timestamp(end).value - b) // stride * stride + - stride // 2 + 1) - # end.tz == start.tz by this point due to _generate implementation - tz = start.tz - elif start is not None: - b = Timestamp(start).value - e = b + np.int64(periods) * stride - tz = start.tz - elif end is not None: - e = Timestamp(end).value + stride - b = e - np.int64(periods) * stride - tz = end.tz - else: - raise ValueError("at least 'start' or 'end' should be specified " - "if a 'period' is given.") - - data = np.arange(b, e, stride, dtype=np.int64) - data = DatetimeIndex._simple_new(data, None, tz=tz) - else: - if isinstance(start, Timestamp): - start = start.to_pydatetime() - - if isinstance(end, Timestamp): - end = end.to_pydatetime() - - xdr = generate_range(start=start, end=end, - periods=periods, offset=offset) - - dates = list(xdr) - # utc = len(dates) > 0 and dates[0].tzinfo is not None - data = tools.to_datetime(dates) - - return data - - -def date_range(start=None, end=None, periods=None, freq='D', tz=None, - normalize=False, name=None, closed=None, **kwargs): - """ - Return a fixed frequency datetime index, with day (calendar) as the default - frequency - - Parameters - ---------- - start : string or datetime-like, default None - Left bound for generating dates - end : string or datetime-like, default None - Right bound for generating dates - periods : integer or None, default None - If None, must specify start and end - freq : string or DateOffset, default 'D' (calendar daily) - Frequency strings can have multiples, e.g. '5H' - tz : string or None - Time zone name for returning localized DatetimeIndex, for example - Asia/Hong_Kong - normalize : bool, default False - Normalize start/end dates to midnight before generating date range - name : str, default None - Name of the resulting index - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - - Notes - ----- - 2 of start, end, or periods must be specified - - To learn more about the frequency strings, please see `this link - `__. - - Returns - ------- - rng : DatetimeIndex - """ - return DatetimeIndex(start=start, end=end, periods=periods, - freq=freq, tz=tz, normalize=normalize, name=name, - closed=closed, **kwargs) - - -def bdate_range(start=None, end=None, periods=None, freq='B', tz=None, - normalize=True, name=None, closed=None, **kwargs): - """ - Return a fixed frequency datetime index, with business day as the default - frequency - - Parameters - ---------- - start : string or datetime-like, default None - Left bound for generating dates - end : string or datetime-like, default None - Right bound for generating dates - periods : integer or None, default None - If None, must specify start and end - freq : string or DateOffset, default 'B' (business daily) - Frequency strings can have multiples, e.g. '5H' - tz : string or None - Time zone name for returning localized DatetimeIndex, for example - Asia/Beijing - normalize : bool, default False - Normalize start/end dates to midnight before generating date range - name : str, default None - Name for the resulting index - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - - Notes - ----- - 2 of start, end, or periods must be specified - - To learn more about the frequency strings, please see `this link - `__. - - Returns - ------- - rng : DatetimeIndex - """ - - return DatetimeIndex(start=start, end=end, periods=periods, - freq=freq, tz=tz, normalize=normalize, name=name, - closed=closed, **kwargs) - - -def cdate_range(start=None, end=None, periods=None, freq='C', tz=None, - normalize=True, name=None, closed=None, **kwargs): - """ - **EXPERIMENTAL** Return a fixed frequency datetime index, with - CustomBusinessDay as the default frequency - - .. warning:: EXPERIMENTAL - - The CustomBusinessDay class is not officially supported and the API is - likely to change in future versions. Use this at your own risk. - - Parameters - ---------- - start : string or datetime-like, default None - Left bound for generating dates - end : string or datetime-like, default None - Right bound for generating dates - periods : integer or None, default None - If None, must specify start and end - freq : string or DateOffset, default 'C' (CustomBusinessDay) - Frequency strings can have multiples, e.g. '5H' - tz : string or None - Time zone name for returning localized DatetimeIndex, for example - Asia/Beijing - normalize : bool, default False - Normalize start/end dates to midnight before generating date range - name : str, default None - Name for the resulting index - weekmask : str, Default 'Mon Tue Wed Thu Fri' - weekmask of valid business days, passed to ``numpy.busdaycalendar`` - holidays : list - list/array of dates to exclude from the set of valid business days, - passed to ``numpy.busdaycalendar`` - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - - Notes - ----- - 2 of start, end, or periods must be specified - - To learn more about the frequency strings, please see `this link - `__. - - Returns - ------- - rng : DatetimeIndex - """ - - if freq == 'C': - holidays = kwargs.pop('holidays', []) - weekmask = kwargs.pop('weekmask', 'Mon Tue Wed Thu Fri') - freq = CDay(holidays=holidays, weekmask=weekmask) - return DatetimeIndex(start=start, end=end, periods=periods, freq=freq, - tz=tz, normalize=normalize, name=name, - closed=closed, **kwargs) - - -def _to_m8(key, tz=None): - """ - Timestamp-like => dt64 - """ - if not isinstance(key, Timestamp): - # this also converts strings - key = Timestamp(key, tz=tz) - - return np.int64(libts.pydt_to_i8(key)).view(_NS_DTYPE) - - -_CACHE_START = Timestamp(datetime(1950, 1, 1)) -_CACHE_END = Timestamp(datetime(2030, 1, 1)) - -_daterange_cache = {} - - -def _naive_in_cache_range(start, end): - if start is None or end is None: - return False - else: - if start.tzinfo is not None or end.tzinfo is not None: - return False - return _in_range(start, end, _CACHE_START, _CACHE_END) - - -def _in_range(start, end, rng_start, rng_end): - return start > rng_start and end < rng_end - - -def _use_cached_range(offset, _normalized, start, end): - return (offset._should_cache() and - not (offset._normalize_cache and not _normalized) and - _naive_in_cache_range(start, end)) - - -def _time_to_micros(time): - seconds = time.hour * 60 * 60 + 60 * time.minute + time.second - return 1000000 * seconds + time.microsecond diff --git a/pandas/tseries/interval.py b/pandas/tseries/interval.py deleted file mode 100644 index 22801318a1853..0000000000000 --- a/pandas/tseries/interval.py +++ /dev/null @@ -1,35 +0,0 @@ - -from pandas.core.index import Index - - -class Interval(object): - """ - Represents an interval of time defined by two timestamps - """ - - def __init__(self, start, end): - self.start = start - self.end = end - - -class PeriodInterval(object): - """ - Represents an interval of time defined by two Period objects (time - ordinals) - """ - - def __init__(self, start, end): - self.start = start - self.end = end - - -class IntervalIndex(Index): - """ - - """ - - def __new__(self, starts, ends): - pass - - def dtype(self): - return self.values.dtype diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 2b6a684fc39dd..f208ce37a3b14 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,19 +1,26 @@ +# -*- coding: utf-8 -*- from datetime import date, datetime, timedelta -from pandas.compat import range -from pandas import compat +import functools +import operator + +from dateutil.easter import easter import numpy as np -from pandas.types.generic import ABCSeries, ABCDatetimeIndex, ABCPeriod -from pandas.tseries.tools import to_datetime, normalize_date -from pandas.core.common import AbstractMethodError +from pandas._libs.tslibs import ( + NaT, OutOfBoundsDatetime, Timedelta, Timestamp, ccalendar, conversion, + delta_to_nanoseconds, frequencies as libfrequencies, normalize_date, + offsets as liboffsets, timezones) +from pandas._libs.tslibs.offsets import ( + ApplyTypeError, BaseOffset, _get_calendar, _is_normalized, _to_dt64, + apply_index_wraps, as_datetime, roll_yearday, shift_month) +import pandas.compat as compat +from pandas.compat import range +from pandas.errors import AbstractMethodError +from pandas.util._decorators import cache_readonly -# import after tools, dateutil check -from dateutil.relativedelta import relativedelta, weekday -from dateutil.easter import easter -from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta +from pandas.core.dtypes.generic import ABCPeriod -import functools -import operator +from pandas.core.tools.datetimes import to_datetime __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay', 'CBMonthEnd', 'CBMonthBegin', @@ -41,18 +48,11 @@ def as_timestamp(obj): return obj -def as_datetime(obj): - f = getattr(obj, 'to_pydatetime', None) - if f is not None: - obj = f() - return obj - - def apply_wraps(func): @functools.wraps(func) def wrapper(self, other): - if other is tslib.NaT: - return tslib.NaT + if other is NaT: + return NaT elif isinstance(other, (timedelta, Tick, DateOffset)): # timedelta path return func(self, other) @@ -69,7 +69,7 @@ def wrapper(self, other): result = func(self, other) if self._adjust_dst: - result = tslib._localize_pydatetime(result, tz) + result = conversion.localize_pydatetime(result, tz) result = Timestamp(result) if self.normalize: @@ -80,14 +80,14 @@ def wrapper(self, other): if not isinstance(self, Nano) and result.nanosecond != nano: if result.tz is not None: # convert to UTC - value = tslib.tz_convert_single( - result.value, 'UTC', result.tz) + value = conversion.tz_convert_single( + result.value, timezones.UTC, result.tz) else: value = result.value result = Timestamp(value + nano) if tz is not None and result.tzinfo is None: - result = tslib._localize_pydatetime(result, tz) + result = conversion.localize_pydatetime(result, tz) except OutOfBoundsDatetime: result = func(self, as_datetime(other)) @@ -97,42 +97,17 @@ def wrapper(self, other): result = normalize_date(result) if tz is not None and result.tzinfo is None: - result = tslib._localize_pydatetime(result, tz) - - return result - return wrapper - + result = conversion.localize_pydatetime(result, tz) -def apply_index_wraps(func): - @functools.wraps(func) - def wrapper(self, other): - result = func(self, other) - if self.normalize: - result = result.to_period('D').to_timestamp() return result return wrapper -def _is_normalized(dt): - if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 or - dt.microsecond != 0 or getattr(dt, 'nanosecond', 0) != 0): - return False - return True - # --------------------------------------------------------------------- # DateOffset -class ApplyTypeError(TypeError): - # sentinel class for catching the apply error to return NotImplemented - pass - - -class CacheableOffset(object): - _cacheable = True - - -class DateOffset(object): +class DateOffset(BaseOffset): """ Standard kind of date increment used for a date range. @@ -174,46 +149,73 @@ def __add__(date): date + BDay(0) == BDay.rollforward(date) Since 0 is a bit weird, we suggest avoiding its use. + + Parameters + ---------- + n : int, default 1 + The number of time periods the offset represents. + normalize : bool, default False + Whether to round the result of a DateOffset addition down to the + previous midnight. + **kwds + Temporal parameter that add to or replace the offset value. + + Parameters that **add** to the offset (like Timedelta): + + - years + - months + - weeks + - days + - hours + - minutes + - seconds + - microseconds + - nanoseconds + + Parameters that **replace** the offset value: + + - year + - month + - day + - weekday + - hour + - minute + - second + - microsecond + - nanosecond + + See Also + -------- + dateutil.relativedelta.relativedelta + + Examples + -------- + >>> ts = pd.Timestamp('2017-01-01 09:10:11') + >>> ts + DateOffset(months=3) + Timestamp('2017-04-01 09:10:11') + + >>> ts = pd.Timestamp('2017-01-01 09:10:11') + >>> ts + DateOffset(month=3) + Timestamp('2017-03-01 09:10:11') """ - _cacheable = False - _normalize_cache = True - _kwds_use_relativedelta = ( - 'years', 'months', 'weeks', 'days', - 'year', 'month', 'week', 'day', 'weekday', - 'hour', 'minute', 'second', 'microsecond' - ) + _params = cache_readonly(BaseOffset._params.fget) _use_relativedelta = False _adjust_dst = False + _attributes = frozenset(['n', 'normalize'] + + list(liboffsets.relativedelta_kwds)) # default for prior pickles normalize = False def __init__(self, n=1, normalize=False, **kwds): - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self._offset, self._use_relativedelta = self._determine_offset() - - def _determine_offset(self): - # timedelta is used for sub-daily plural offsets and all singular - # offsets relativedelta is used for plural offsets of daily length or - # more nanosecond(s) are handled by apply_wraps - kwds_no_nanos = dict( - (k, v) for k, v in self.kwds.items() - if k not in ('nanosecond', 'nanoseconds') - ) - use_relativedelta = False - - if len(kwds_no_nanos) > 0: - if any(k in self._kwds_use_relativedelta for k in kwds_no_nanos): - use_relativedelta = True - offset = relativedelta(**kwds_no_nanos) - else: - # sub-daily offset - use timedelta (tz-aware) - offset = timedelta(**kwds_no_nanos) - else: - offset = timedelta(1) - return offset, use_relativedelta + BaseOffset.__init__(self, n, normalize) + + off, use_rd = liboffsets._determine_offset(kwds) + object.__setattr__(self, "_offset", off) + object.__setattr__(self, "_use_relativedelta", use_rd) + for key in kwds: + val = kwds[key] + object.__setattr__(self, key, val) @apply_wraps def apply(self, other): @@ -235,7 +237,7 @@ def apply(self, other): if tzinfo is not None and self._use_relativedelta: # bring tz back from UTC calculation - other = tslib._localize_pydatetime(other, tzinfo) + other = conversion.localize_pydatetime(other, tzinfo) return as_timestamp(other) else: @@ -246,9 +248,7 @@ def apply_index(self, i): """ Vectorized apply of DateOffset to DatetimeIndex, raises NotImplentedError for offsets without a - vectorized implementation - - .. versionadded:: 0.17.0 + vectorized implementation. Parameters ---------- @@ -259,32 +259,38 @@ def apply_index(self, i): y : DatetimeIndex """ - if not type(self) is DateOffset: - raise NotImplementedError("DateOffset subclass %s " + if type(self) is not DateOffset: + raise NotImplementedError("DateOffset subclass {name} " "does not have a vectorized " - "implementation" - % (self.__class__.__name__,)) - relativedelta_fast = set(['years', 'months', 'weeks', - 'days', 'hours', 'minutes', - 'seconds', 'microseconds']) + "implementation".format( + name=self.__class__.__name__)) + kwds = self.kwds + relativedelta_fast = {'years', 'months', 'weeks', 'days', 'hours', + 'minutes', 'seconds', 'microseconds'} # relativedelta/_offset path only valid for base DateOffset if (self._use_relativedelta and - set(self.kwds).issubset(relativedelta_fast)): + set(kwds).issubset(relativedelta_fast)): - months = ((self.kwds.get('years', 0) * 12 + - self.kwds.get('months', 0)) * self.n) + months = ((kwds.get('years', 0) * 12 + + kwds.get('months', 0)) * self.n) if months: - shifted = tslib.shift_months(i.asi8, months) - i = i._shallow_copy(shifted) + shifted = liboffsets.shift_months(i.asi8, months) + i = type(i)(shifted, freq=i.freq, dtype=i.dtype) - weeks = (self.kwds.get('weeks', 0)) * self.n + weeks = (kwds.get('weeks', 0)) * self.n if weeks: - i = (i.to_period('W') + weeks).to_timestamp() + \ - i.to_perioddelta('W') - - timedelta_kwds = dict((k, v) for k, v in self.kwds.items() - if k in ['days', 'hours', 'minutes', - 'seconds', 'microseconds']) + # integer addition on PeriodIndex is deprecated, + # so we directly use _time_shift instead + asper = i.to_period('W') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + shifted = asper._time_shift(weeks) + i = shifted.to_timestamp() + i.to_perioddelta('W') + + timedelta_kwds = {k: v for k, v in kwds.items() + if k in ['days', 'hours', 'minutes', + 'seconds', 'microseconds']} if timedelta_kwds: delta = Timedelta(**timedelta_kwds) i = i + (self.n * delta) @@ -294,139 +300,51 @@ def apply_index(self, i): return i + (self._offset * self.n) else: # relativedelta with other keywords + kwd = set(kwds) - relativedelta_fast raise NotImplementedError("DateOffset with relativedelta " - "keyword(s) %s not able to be " - "applied vectorized" % - (set(self.kwds) - relativedelta_fast),) + "keyword(s) {kwd} not able to be " + "applied vectorized".format(kwd=kwd)) def isAnchored(self): + # TODO: Does this make sense for the general case? It would help + # if there were a canonical docstring for what isAnchored means. return (self.n == 1) - def copy(self): - return self.__class__(self.n, normalize=self.normalize, **self.kwds) - - def _should_cache(self): - return self.isAnchored() and self._cacheable - - def _params(self): - all_paras = dict(list(vars(self).items()) + list(self.kwds.items())) - if 'holidays' in all_paras and not all_paras['holidays']: - all_paras.pop('holidays') - exclude = ['kwds', 'name', 'normalize', 'calendar'] - attrs = [(k, v) for k, v in all_paras.items() - if (k not in exclude) and (k[0] != '_')] - attrs = sorted(set(attrs)) - params = tuple([str(self.__class__)] + attrs) - return params - - def __repr__(self): - className = getattr(self, '_outputName', type(self).__name__) - exclude = set(['n', 'inc', 'normalize']) + # TODO: Combine this with BusinessMixin version by defining a whitelisted + # set of attributes on each object rather than the existing behavior of + # iterating over internal ``__dict__`` + def _repr_attrs(self): + exclude = {'n', 'inc', 'normalize'} attrs = [] for attr in sorted(self.__dict__): - if ((attr == 'kwds' and len(self.kwds) == 0) or - attr.startswith('_')): + if attr.startswith('_') or attr == 'kwds': continue - elif attr == 'kwds': - kwds_new = {} - for key in self.kwds: - if not hasattr(self, key): - kwds_new[key] = self.kwds[key] - if len(kwds_new) > 0: - attrs.append('='.join((attr, repr(kwds_new)))) - else: - if attr not in exclude: - attrs.append('='.join((attr, repr(getattr(self, attr))))) - - if abs(self.n) != 1: - plural = 's' - else: - plural = '' - - n_str = "" - if self.n != 1: - n_str = "%s * " % self.n + elif attr not in exclude: + value = getattr(self, attr) + attrs.append('{attr}={value}'.format(attr=attr, value=value)) - out = '<%s' % n_str + className + plural + out = '' if attrs: out += ': ' + ', '.join(attrs) - out += '>' return out @property def name(self): return self.rule_code - def __eq__(self, other): - if other is None: - return False - - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if not isinstance(other, DateOffset): - return False - - return self._params() == other._params() - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash(self._params()) - - def __call__(self, other): - return self.apply(other) - - def __add__(self, other): - if isinstance(other, (ABCDatetimeIndex, ABCSeries)): - return other + self - elif isinstance(other, ABCPeriod): - return other + self - try: - return self.apply(other) - except ApplyTypeError: - return NotImplemented - - def __radd__(self, other): - return self.__add__(other) - - def __sub__(self, other): - if isinstance(other, datetime): - raise TypeError('Cannot subtract datetime from offset.') - elif type(other) == type(self): - return self.__class__(self.n - other.n, normalize=self.normalize, - **self.kwds) - else: # pragma: no cover - return NotImplemented - - def __rsub__(self, other): - if isinstance(other, (ABCDatetimeIndex, ABCSeries)): - return other - self - return self.__class__(-self.n, normalize=self.normalize, - **self.kwds) + other - - def __mul__(self, someInt): - return self.__class__(n=someInt * self.n, normalize=self.normalize, - **self.kwds) - - def __rmul__(self, someInt): - return self.__mul__(someInt) - - def __neg__(self): - return self.__class__(-self.n, normalize=self.normalize, **self.kwds) - def rollback(self, dt): - """Roll provided date backward to next offset only if not on offset""" + """ + Roll provided date backward to next offset only if not on offset. + """ dt = as_timestamp(dt) if not self.onOffset(dt): dt = dt - self.__class__(1, normalize=self.normalize, **self.kwds) return dt def rollforward(self, dt): - """Roll provided date forward to next offset only if not on offset""" + """ + Roll provided date forward to next offset only if not on offset. + """ dt = as_timestamp(dt) if not self.onOffset(dt): dt = dt + self.__class__(1, normalize=self.normalize, **self.kwds) @@ -446,43 +364,6 @@ def onOffset(self, dt): b = ((dt + self) - self) return a == b - # helpers for vectorized offsets - def _beg_apply_index(self, i, freq): - """Offsets index to beginning of Period frequency""" - - off = i.to_perioddelta('D') - - from pandas.tseries.frequencies import get_freq_code - base, mult = get_freq_code(freq) - base_period = i.to_period(base) - if self.n <= 0: - # when subtracting, dates on start roll to prior - roll = np.where(base_period.to_timestamp() == i - off, - self.n, self.n + 1) - else: - roll = self.n - - base = (base_period + roll).to_timestamp() - return base + off - - def _end_apply_index(self, i, freq): - """Offsets index to end of Period frequency""" - - off = i.to_perioddelta('D') - - from pandas.tseries.frequencies import get_freq_code - base, mult = get_freq_code(freq) - base_period = i.to_period(base) - if self.n > 0: - # when adding, dates on end roll to next - roll = np.where(base_period.to_timestamp(how='end') == i - off, - self.n, self.n - 1) - else: - roll = self.n - - base = (base_period + roll).to_timestamp(how='end') - return base + off - # way to get around weirdness with rule_code @property def _prefix(self): @@ -492,7 +373,7 @@ def _prefix(self): def rule_code(self): return self._prefix - @property + @cache_readonly def freqstr(self): try: code = self.rule_code @@ -500,51 +381,70 @@ def freqstr(self): return repr(self) if self.n != 1: - fstr = '%d%s' % (self.n, code) + fstr = '{n}{code}'.format(n=self.n, code=code) else: fstr = code + try: + if self._offset: + fstr += self._offset_str() + except AttributeError: + # TODO: standardize `_offset` vs `offset` naming convention + pass + return fstr + def _offset_str(self): + return '' + @property def nanos(self): - raise ValueError("{0} is a non-fixed frequency".format(self)) + raise ValueError("{name} is a non-fixed frequency".format(name=self)) class SingleConstructorOffset(DateOffset): - @classmethod def _from_name(cls, suffix=None): # default _from_name calls cls with no args if suffix: - raise ValueError("Bad freq suffix %s" % suffix) + raise ValueError("Bad freq suffix {suffix}".format(suffix=suffix)) return cls() -class BusinessMixin(object): - """ mixin to business types to provide related functions """ +class _CustomMixin(object): + """ + Mixin for classes that define and validate calendar, holidays, + and weekdays attributes. + """ + def __init__(self, weekmask, holidays, calendar): + calendar, holidays = _get_calendar(weekmask=weekmask, + holidays=holidays, + calendar=calendar) + # Custom offset instances are identified by the + # following two attributes. See DateOffset._params() + # holidays, weekmask - # TODO: Combine this with DateOffset by defining a whitelisted set of - # attributes on each object rather than the existing behavior of iterating - # over internal ``__dict__`` - def __repr__(self): - className = getattr(self, '_outputName', self.__class__.__name__) + object.__setattr__(self, "weekmask", weekmask) + object.__setattr__(self, "holidays", holidays) + object.__setattr__(self, "calendar", calendar) - if abs(self.n) != 1: - plural = 's' - else: - plural = '' - n_str = "" - if self.n != 1: - n_str = "%s * " % self.n +class BusinessMixin(object): + """ + Mixin to business types to provide related functions. + """ - out = '<%s' % n_str + className + plural + self._repr_attrs() + '>' - return out + @property + def offset(self): + """ + Alias for self._offset. + """ + # Alias for backward compat + return self._offset def _repr_attrs(self): if self.offset: - attrs = ['offset=%s' % repr(self.offset)] + attrs = ['offset={offset!r}'.format(offset=self.offset)] else: attrs = None out = '' @@ -552,62 +452,18 @@ def _repr_attrs(self): out += ': ' + ', '.join(attrs) return out - def __getstate__(self): - """Return a pickleable state""" - state = self.__dict__.copy() - - # we don't want to actually pickle the calendar object - # as its a np.busyday; we recreate on deserilization - if 'calendar' in state: - del state['calendar'] - try: - state['kwds'].pop('calendar') - except KeyError: - pass - - return state - - def __setstate__(self, state): - """Reconstruct an instance from a pickled state""" - self.__dict__ = state - if 'weekmask' in state and 'holidays' in state: - calendar, holidays = self.get_calendar(weekmask=self.weekmask, - holidays=self.holidays, - calendar=None) - self.kwds['calendar'] = self.calendar = calendar - self.kwds['holidays'] = self.holidays = holidays - self.kwds['weekmask'] = state['weekmask'] - class BusinessDay(BusinessMixin, SingleConstructorOffset): """ - DateOffset subclass representing possibly n business days + DateOffset subclass representing possibly n business days. """ _prefix = 'B' _adjust_dst = True + _attributes = frozenset(['n', 'normalize', 'offset']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self.offset = kwds.get('offset', timedelta(0)) - - @property - def freqstr(self): - try: - code = self.rule_code - except NotImplementedError: - return repr(self) - - if self.n != 1: - fstr = '%d%s' % (self.n, code) - else: - fstr = code - - if self.offset: - fstr += self._offset_str() - - return fstr + def __init__(self, n=1, normalize=False, offset=timedelta(0)): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "_offset", offset) def _offset_str(self): def get_str(td): @@ -640,35 +496,35 @@ def get_str(td): else: return '+' + repr(self.offset) - def isAnchored(self): - return (self.n == 1) - @apply_wraps def apply(self, other): if isinstance(other, datetime): n = self.n + wday = other.weekday() - if n == 0 and other.weekday() > 4: - n = 1 - - result = other - - # avoid slowness below - if abs(n) > 5: - k = n // 5 - result = result + timedelta(7 * k) - if n < 0 and result.weekday() > 4: - n += 1 - n -= 5 * k - if n == 0 and result.weekday() > 4: - n -= 1 + # avoid slowness below by operating on weeks first + weeks = n // 5 + if n <= 0 and wday > 4: + # roll forward + n += 1 - while n != 0: - k = n // abs(n) - result = result + timedelta(k) - if result.weekday() < 5: - n -= k + n -= 5 * weeks + + # n is always >= 0 at this point + if n == 0 and wday > 4: + # roll back + days = 4 - wday + elif wday > 4: + # roll forward + days = (7 - wday) + (n - 1) + elif wday + n <= 4: + # shift by n days without leaving the current week + days = n + else: + # shift by n days plus 2 to get past the weekend + days = n + 2 + result = other + timedelta(days=7 * weeks + days) if self.offset: result = result + self.offset return result @@ -685,13 +541,25 @@ def apply_index(self, i): time = i.to_perioddelta('D') # to_period rolls forward to next BDay; track and # reduce n where it does when rolling forward - shifted = (i.to_perioddelta('B') - time).asi8 != 0 + asper = i.to_period('B') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + if self.n > 0: + shifted = (i.to_perioddelta('B') - time).asi8 != 0 + + # Integer-array addition is deprecated, so we use + # _time_shift directly roll = np.where(shifted, self.n - 1, self.n) + shifted = asper._addsub_int_array(roll, operator.add) else: + # Integer addition is deprecated, so we use _time_shift directly roll = self.n + shifted = asper._time_shift(roll) - return (i.to_period('B') + roll).to_timestamp() + time + result = shifted.to_timestamp() + time + return result def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -701,32 +569,33 @@ def onOffset(self, dt): class BusinessHourMixin(BusinessMixin): - def __init__(self, **kwds): + def __init__(self, start='09:00', end='17:00', offset=timedelta(0)): # must be validated here to equality check - kwds['start'] = self._validate_time(kwds.get('start', '09:00')) - kwds['end'] = self._validate_time(kwds.get('end', '17:00')) - self.kwds = kwds - self.offset = kwds.get('offset', timedelta(0)) - self.start = kwds.get('start', '09:00') - self.end = kwds.get('end', '17:00') - - def _validate_time(self, t_input): - from datetime import time as dt_time - import time - if isinstance(t_input, compat.string_types): - try: - t = time.strptime(t_input, '%H:%M') - return dt_time(hour=t.tm_hour, minute=t.tm_min) - except ValueError: - raise ValueError("time data must match '%H:%M' format") - elif isinstance(t_input, dt_time): - if t_input.second != 0 or t_input.microsecond != 0: - raise ValueError( - "time data must be specified only with hour and minute") - return t_input + start = liboffsets._validate_business_time(start) + object.__setattr__(self, "start", start) + end = liboffsets._validate_business_time(end) + object.__setattr__(self, "end", end) + object.__setattr__(self, "_offset", offset) + + @cache_readonly + def next_bday(self): + """ + Used for moving to next business day. + """ + if self.n >= 0: + nb_offset = 1 else: - raise ValueError("time data must be string or datetime.time") + nb_offset = -1 + if self._prefix.startswith('C'): + # CustomBusinessHour + return CustomBusinessDay(n=nb_offset, + weekmask=self.weekmask, + holidays=self.holidays, + calendar=self.calendar) + else: + return BusinessDay(n=nb_offset) + @cache_readonly def _get_daytime_flag(self): if self.start == self.end: raise ValueError('start and end must not be the same') @@ -768,26 +637,28 @@ def _prev_opening_time(self, other): return datetime(other.year, other.month, other.day, self.start.hour, self.start.minute) + @cache_readonly def _get_business_hours_by_sec(self): """ Return business hours in a day by seconds. """ - if self._get_daytime_flag(): - # create dummy datetime to calcurate businesshours in a day + if self._get_daytime_flag: + # create dummy datetime to calculate businesshours in a day dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute) until = datetime(2014, 4, 1, self.end.hour, self.end.minute) - return tslib.tot_seconds(until - dtstart) + return (until - dtstart).total_seconds() else: - self.daytime = False dtstart = datetime(2014, 4, 1, self.start.hour, self.start.minute) until = datetime(2014, 4, 2, self.end.hour, self.end.minute) - return tslib.tot_seconds(until - dtstart) + return (until - dtstart).total_seconds() @apply_wraps def rollback(self, dt): - """Roll provided date backward to next offset only if not on offset""" + """ + Roll provided date backward to next offset only if not on offset. + """ if not self.onOffset(dt): - businesshours = self._get_business_hours_by_sec() + businesshours = self._get_business_hours_by_sec if self.n >= 0: dt = self._prev_opening_time( dt) + timedelta(seconds=businesshours) @@ -798,7 +669,9 @@ def rollback(self, dt): @apply_wraps def rollforward(self, dt): - """Roll provided date forward to next offset only if not on offset""" + """ + Roll provided date forward to next offset only if not on offset. + """ if not self.onOffset(dt): if self.n >= 0: return self._next_opening_time(dt) @@ -808,9 +681,8 @@ def rollforward(self, dt): @apply_wraps def apply(self, other): - # calcurate here because offset is not immutable - daytime = self._get_daytime_flag() - businesshours = self._get_business_hours_by_sec() + daytime = self._get_daytime_flag + businesshours = self._get_business_hours_by_sec bhdelta = timedelta(seconds=businesshours) if isinstance(other, datetime): @@ -857,7 +729,7 @@ def apply(self, other): if n >= 0: bday_edge = self._prev_opening_time(other) bday_edge = bday_edge + bhdelta - # calcurate remainder + # calculate remainder bday_remain = result - bday_edge result = self._next_opening_time(other) result += bday_remain @@ -878,6 +750,7 @@ def apply(self, other): return result else: + # TODO: Figure out the end of this sente raise ApplyTypeError( 'Only know how to combine business hour with ') @@ -890,12 +763,12 @@ def onOffset(self, dt): dt.minute, dt.second, dt.microsecond) # Valid BH can be on the different BusinessDay during midnight # Distinguish by the time spent from previous opening time - businesshours = self._get_business_hours_by_sec() + businesshours = self._get_business_hours_by_sec return self._onOffset(dt, businesshours) def _onOffset(self, dt, businesshours): """ - Slight speedups using calcurated values + Slight speedups using calculated values. """ # if self.normalize and not _is_normalized(dt): # return False @@ -905,7 +778,7 @@ def _onOffset(self, dt, businesshours): op = self._prev_opening_time(dt) else: op = self._next_opening_time(dt) - span = tslib.tot_seconds(dt - op) + span = (dt - op).total_seconds() if span <= businesshours: return True else: @@ -923,41 +796,28 @@ def _repr_attrs(self): class BusinessHour(BusinessHourMixin, SingleConstructorOffset): """ - DateOffset subclass representing possibly n business days - - .. versionadded: 0.16.1 + DateOffset subclass representing possibly n business days. + .. versionadded:: 0.16.1 """ _prefix = 'BH' _anchor = 0 + _attributes = frozenset(['n', 'normalize', 'start', 'end', 'offset']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = int(n) - self.normalize = normalize - super(BusinessHour, self).__init__(**kwds) - - # used for moving to next businessday - if self.n >= 0: - nb_offset = 1 - else: - nb_offset = -1 - self.next_bday = BusinessDay(n=nb_offset) + def __init__(self, n=1, normalize=False, start='09:00', + end='17:00', offset=timedelta(0)): + BaseOffset.__init__(self, n, normalize) + super(BusinessHour, self).__init__(start=start, end=end, offset=offset) -class CustomBusinessDay(BusinessDay): +class CustomBusinessDay(_CustomMixin, BusinessDay): """ - **EXPERIMENTAL** DateOffset subclass representing possibly n business days - excluding holidays - - .. warning:: EXPERIMENTAL - - This class is not officially supported and the API is likely to change - in future versions. Use this at your own risk. + DateOffset subclass representing possibly n custom business days, + excluding holidays. Parameters ---------- n : int, default 1 - offset : timedelta, default timedelta(0) normalize : bool, default False Normalize start/end dates to midnight before generating date range weekmask : str, Default 'Mon Tue Wed Thu Fri' @@ -966,56 +826,18 @@ class CustomBusinessDay(BusinessDay): list/array of dates to exclude from the set of valid business days, passed to ``numpy.busdaycalendar`` calendar : pd.HolidayCalendar or np.busdaycalendar + offset : timedelta, default timedelta(0) """ - _cacheable = False _prefix = 'C' + _attributes = frozenset(['n', 'normalize', + 'weekmask', 'holidays', 'calendar', 'offset']) def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', - holidays=None, calendar=None, **kwds): - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self.offset = kwds.get('offset', timedelta(0)) - calendar, holidays = self.get_calendar(weekmask=weekmask, - holidays=holidays, - calendar=calendar) - # CustomBusinessDay instances are identified by the - # following two attributes. See DateOffset._params() - # holidays, weekmask - - self.kwds['weekmask'] = self.weekmask = weekmask - self.kwds['holidays'] = self.holidays = holidays - self.kwds['calendar'] = self.calendar = calendar - - def get_calendar(self, weekmask, holidays, calendar): - """Generate busdaycalendar""" - if isinstance(calendar, np.busdaycalendar): - if not holidays: - holidays = tuple(calendar.holidays) - elif not isinstance(holidays, tuple): - holidays = tuple(holidays) - else: - # trust that calendar.holidays and holidays are - # consistent - pass - return calendar, holidays - - if holidays is None: - holidays = [] - try: - holidays = holidays + calendar.holidays().tolist() - except AttributeError: - pass - holidays = [self._to_dt64(dt, dtype='datetime64[D]') for dt in - holidays] - holidays = tuple(sorted(holidays)) + holidays=None, calendar=None, offset=timedelta(0)): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "_offset", offset) - kwargs = {'weekmask': weekmask} - if holidays: - kwargs['holidays'] = holidays - - busdaycalendar = np.busdaycalendar(**kwargs) - return busdaycalendar, holidays + _CustomMixin.__init__(self, weekmask, holidays, calendar) @apply_wraps def apply(self, other): @@ -1048,177 +870,265 @@ def apply(self, other): def apply_index(self, i): raise NotImplementedError - @staticmethod - def _to_dt64(dt, dtype='datetime64'): - # Currently - # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]') - # numpy.datetime64('2013-05-01T02:00:00.000000+0200') - # Thus astype is needed to cast datetime to datetime64[D] - if getattr(dt, 'tzinfo', None) is not None: - i8 = tslib.pydt_to_i8(dt) - dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo) - dt = Timestamp(dt) - dt = np.datetime64(dt) - if dt.dtype.name != dtype: - dt = dt.astype(dtype) - return dt - def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - day64 = self._to_dt64(dt, 'datetime64[D]') + day64 = _to_dt64(dt, 'datetime64[D]') return np.is_busday(day64, busdaycal=self.calendar) -class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset): +class CustomBusinessHour(_CustomMixin, BusinessHourMixin, + SingleConstructorOffset): """ - DateOffset subclass representing possibly n custom business days - - .. versionadded: 0.18.1 + DateOffset subclass representing possibly n custom business days. + .. versionadded:: 0.18.1 """ _prefix = 'CBH' _anchor = 0 + _attributes = frozenset(['n', 'normalize', + 'weekmask', 'holidays', 'calendar', + 'start', 'end', 'offset']) def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', - holidays=None, calendar=None, **kwds): - self.n = int(n) - self.normalize = normalize - super(CustomBusinessHour, self).__init__(**kwds) - # used for moving to next businessday - if self.n >= 0: - nb_offset = 1 - else: - nb_offset = -1 - self.next_bday = CustomBusinessDay(n=nb_offset, - weekmask=weekmask, - holidays=holidays, - calendar=calendar) + holidays=None, calendar=None, + start='09:00', end='17:00', offset=timedelta(0)): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "_offset", offset) - self.kwds['weekmask'] = self.next_bday.weekmask - self.kwds['holidays'] = self.next_bday.holidays - self.kwds['calendar'] = self.next_bday.calendar + _CustomMixin.__init__(self, weekmask, holidays, calendar) + BusinessHourMixin.__init__(self, start=start, end=end, offset=offset) + + +# --------------------------------------------------------------------- +# Month-Based Offset Classes class MonthOffset(SingleConstructorOffset): _adjust_dst = True + _attributes = frozenset(['n', 'normalize']) + + __init__ = BaseOffset.__init__ @property def name(self): if self.isAnchored: return self.rule_code else: - return "%s-%s" % (self.rule_code, _int_to_month[self.n]) + month = ccalendar.MONTH_ALIASES[self.n] + return "{code}-{month}".format(code=self.rule_code, + month=month) - -class MonthEnd(MonthOffset): - """DateOffset of one month end""" + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + return dt.day == self._get_offset_day(dt) @apply_wraps def apply(self, other): - n = self.n - _, days_in_month = tslib.monthrange(other.year, other.month) - if other.day != days_in_month: - other = other + relativedelta(months=-1, day=31) - if n <= 0: - n = n + 1 - other = other + relativedelta(months=n, day=31) - return other + compare_day = self._get_offset_day(other) + n = liboffsets.roll_convention(other.day, self.n, compare_day) + return shift_month(other, n, self._day_opt) @apply_index_wraps def apply_index(self, i): - shifted = tslib.shift_months(i.asi8, self.n, 'end') - return i._shallow_copy(shifted) + shifted = liboffsets.shift_months(i.asi8, self.n, self._day_opt) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(i)._simple_new(shifted, freq=i.freq, dtype=i.dtype) - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - days_in_month = tslib.monthrange(dt.year, dt.month)[1] - return dt.day == days_in_month +class MonthEnd(MonthOffset): + """ + DateOffset of one month end. + """ _prefix = 'M' + _day_opt = 'end' class MonthBegin(MonthOffset): - """DateOffset of one month at beginning""" + """ + DateOffset of one month at beginning. + """ + _prefix = 'MS' + _day_opt = 'start' - @apply_wraps - def apply(self, other): - n = self.n - if other.day > 1 and n <= 0: # then roll forward if n<=0 - n += 1 +class BusinessMonthEnd(MonthOffset): + """ + DateOffset increments between business EOM dates. + """ + _prefix = 'BM' + _day_opt = 'business_end' - return other + relativedelta(months=n, day=1) - @apply_index_wraps - def apply_index(self, i): - shifted = tslib.shift_months(i.asi8, self.n, 'start') - return i._shallow_copy(shifted) +class BusinessMonthBegin(MonthOffset): + """ + DateOffset of one business month at beginning. + """ + _prefix = 'BMS' + _day_opt = 'business_start' - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - return dt.day == 1 - _prefix = 'MS' +class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset): + """ + DateOffset subclass representing one custom business month, incrementing + between [BEGIN/END] of month dates. + Parameters + ---------- + n : int, default 1 + normalize : bool, default False + Normalize start/end dates to midnight before generating date range + weekmask : str, Default 'Mon Tue Wed Thu Fri' + weekmask of valid business days, passed to ``numpy.busdaycalendar`` + holidays : list + list/array of dates to exclude from the set of valid business days, + passed to ``numpy.busdaycalendar`` + calendar : pd.HolidayCalendar or np.busdaycalendar + offset : timedelta, default timedelta(0) + """ + _attributes = frozenset(['n', 'normalize', + 'weekmask', 'holidays', 'calendar', 'offset']) -class SemiMonthOffset(DateOffset): - _adjust_dst = True - _default_day_of_month = 15 - _min_day_of_month = 2 + onOffset = DateOffset.onOffset # override MonthOffset method + apply_index = DateOffset.apply_index # override MonthOffset method - def __init__(self, n=1, day_of_month=None, normalize=False, **kwds): - if day_of_month is None: - self.day_of_month = self._default_day_of_month - else: - self.day_of_month = int(day_of_month) - if not self._min_day_of_month <= self.day_of_month <= 27: - raise ValueError('day_of_month must be ' - '{}<=day_of_month<=27, got {}'.format( - self._min_day_of_month, self.day_of_month)) - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self.kwds['day_of_month'] = self.day_of_month + def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', + holidays=None, calendar=None, offset=timedelta(0)): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "_offset", offset) - @classmethod - def _from_name(cls, suffix=None): - return cls(day_of_month=suffix) + _CustomMixin.__init__(self, weekmask, holidays, calendar) - @property - def rule_code(self): - suffix = '-{}'.format(self.day_of_month) + @cache_readonly + def cbday_roll(self): + """ + Define default roll function to be called in apply method. + """ + cbday = CustomBusinessDay(n=self.n, normalize=False, **self.kwds) + + if self._prefix.endswith('S'): + # MonthBegin + roll_func = cbday.rollforward + else: + # MonthEnd + roll_func = cbday.rollback + return roll_func + + @cache_readonly + def m_offset(self): + if self._prefix.endswith('S'): + # MonthBegin + moff = MonthBegin(n=1, normalize=False) + else: + # MonthEnd + moff = MonthEnd(n=1, normalize=False) + return moff + + @cache_readonly + def month_roll(self): + """ + Define default roll function to be called in apply method. + """ + if self._prefix.endswith('S'): + # MonthBegin + roll_func = self.m_offset.rollback + else: + # MonthEnd + roll_func = self.m_offset.rollforward + return roll_func + + @apply_wraps + def apply(self, other): + # First move to month offset + cur_month_offset_date = self.month_roll(other) + + # Find this custom month offset + compare_date = self.cbday_roll(cur_month_offset_date) + n = liboffsets.roll_convention(other.day, self.n, compare_date.day) + + new = cur_month_offset_date + n * self.m_offset + result = self.cbday_roll(new) + return result + + +class CustomBusinessMonthEnd(_CustomBusinessMonth): + # TODO(py27): Replace condition with Subsitution after dropping Py27 + if _CustomBusinessMonth.__doc__: + __doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]', 'end') + _prefix = 'CBM' + + +class CustomBusinessMonthBegin(_CustomBusinessMonth): + # TODO(py27): Replace condition with Subsitution after dropping Py27 + if _CustomBusinessMonth.__doc__: + __doc__ = _CustomBusinessMonth.__doc__.replace('[BEGIN/END]', + 'beginning') + _prefix = 'CBMS' + + +# --------------------------------------------------------------------- +# Semi-Month Based Offset Classes + +class SemiMonthOffset(DateOffset): + _adjust_dst = True + _default_day_of_month = 15 + _min_day_of_month = 2 + _attributes = frozenset(['n', 'normalize', 'day_of_month']) + + def __init__(self, n=1, normalize=False, day_of_month=None): + BaseOffset.__init__(self, n, normalize) + + if day_of_month is None: + object.__setattr__(self, "day_of_month", + self._default_day_of_month) + else: + object.__setattr__(self, "day_of_month", int(day_of_month)) + if not self._min_day_of_month <= self.day_of_month <= 27: + msg = 'day_of_month must be {min}<=day_of_month<=27, got {day}' + raise ValueError(msg.format(min=self._min_day_of_month, + day=self.day_of_month)) + + @classmethod + def _from_name(cls, suffix=None): + return cls(day_of_month=suffix) + + @property + def rule_code(self): + suffix = '-{day_of_month}'.format(day_of_month=self.day_of_month) return self._prefix + suffix @apply_wraps def apply(self, other): - n = self.n - if not self.onOffset(other): - _, days_in_month = tslib.monthrange(other.year, other.month) - if 1 < other.day < self.day_of_month: - other += relativedelta(day=self.day_of_month) - if n > 0: - # rollforward so subtract 1 - n -= 1 - elif self.day_of_month < other.day < days_in_month: - other += relativedelta(day=self.day_of_month) - if n < 0: - # rollforward in the negative direction so add 1 - n += 1 - elif n == 0: - n = 1 + # shift `other` to self.day_of_month, incrementing `n` if necessary + n = liboffsets.roll_convention(other.day, self.n, self.day_of_month) + + days_in_month = ccalendar.get_days_in_month(other.year, other.month) + + # For SemiMonthBegin on other.day == 1 and + # SemiMonthEnd on other.day == days_in_month, + # shifting `other` to `self.day_of_month` _always_ requires + # incrementing/decrementing `n`, regardless of whether it is + # initially positive. + if type(self) is SemiMonthBegin and (self.n <= 0 and other.day == 1): + n -= 1 + elif type(self) is SemiMonthEnd and (self.n > 0 and + other.day == days_in_month): + n += 1 return self._apply(n, other) def _apply(self, n, other): - """Handle specific apply logic for child classes""" + """ + Handle specific apply logic for child classes. + """ raise AbstractMethodError(self) @apply_index_wraps def apply_index(self, i): # determine how many days away from the 1st of the month we are + dti = i days_from_start = i.to_perioddelta('M').asi8 delta = Timedelta(days=self.day_of_month - 1).value @@ -1235,7 +1145,16 @@ def apply_index(self, i): time = i.to_perioddelta('D') # apply the correct number of months - i = (i.to_period('M') + (roll // 2)).to_timestamp() + + # integer-array addition on PeriodIndex is deprecated, + # so we use _addsub_int_array directly + asper = i.to_period('M') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + + shifted = asper._addsub_int_array(roll // 2, operator.add) + i = type(dti)(shifted.to_timestamp()) # apply the correct day i = self._apply_index_days(i, roll) @@ -1243,7 +1162,8 @@ def apply_index(self, i): return i + time def _get_roll(self, i, before_day_of_month, after_day_of_month): - """Return an array with the correct n for each date in i. + """ + Return an array with the correct n for each date in i. The roll array is based on the fact that i gets rolled back to the first day of the month. @@ -1251,7 +1171,9 @@ def _get_roll(self, i, before_day_of_month, after_day_of_month): raise AbstractMethodError(self) def _apply_index_days(self, i, roll): - """Apply the correct day for each date in i""" + """ + Apply the correct day for each date in i. + """ raise AbstractMethodError(self) @@ -1264,9 +1186,9 @@ class SemiMonthEnd(SemiMonthOffset): Parameters ---------- - n: int + n : int normalize : bool, default False - day_of_month: int, {1, 3,...,27}, default 15 + day_of_month : int, {1, 3,...,27}, default 15 """ _prefix = 'SM' _min_day_of_month = 1 @@ -1274,25 +1196,13 @@ class SemiMonthEnd(SemiMonthOffset): def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - _, days_in_month = tslib.monthrange(dt.year, dt.month) + days_in_month = ccalendar.get_days_in_month(dt.year, dt.month) return dt.day in (self.day_of_month, days_in_month) def _apply(self, n, other): - # if other.day is not day_of_month move to day_of_month and update n - if other.day < self.day_of_month: - other += relativedelta(day=self.day_of_month) - if n > 0: - n -= 1 - elif other.day > self.day_of_month: - other += relativedelta(day=self.day_of_month) - if n == 0: - n = 1 - else: - n += 1 - months = n // 2 day = 31 if n % 2 else self.day_of_month - return other + relativedelta(months=months, day=day) + return shift_month(other, months, day) def _get_roll(self, i, before_day_of_month, after_day_of_month): n = self.n @@ -1310,7 +1220,20 @@ def _get_roll(self, i, before_day_of_month, after_day_of_month): return roll def _apply_index_days(self, i, roll): - i += (roll % 2) * Timedelta(days=self.day_of_month).value + """ + Add days portion of offset to DatetimeIndex i. + + Parameters + ---------- + i : DatetimeIndex + roll : ndarray[int64_t] + + Returns + ------- + result : DatetimeIndex + """ + nanos = (roll % 2) * Timedelta(days=self.day_of_month).value + i += nanos.astype('timedelta64[ns]') return i + Timedelta(days=-1) @@ -1323,9 +1246,9 @@ class SemiMonthBegin(SemiMonthOffset): Parameters ---------- - n: int + n : int normalize : bool, default False - day_of_month: int, {2, 3,...,27}, default 15 + day_of_month : int, {2, 3,...,27}, default 15 """ _prefix = 'SMS' @@ -1335,23 +1258,9 @@ def onOffset(self, dt): return dt.day in (1, self.day_of_month) def _apply(self, n, other): - # if other.day is not day_of_month move to day_of_month and update n - if other.day < self.day_of_month: - other += relativedelta(day=self.day_of_month) - if n == 0: - n = -1 - else: - n -= 1 - elif other.day > self.day_of_month: - other += relativedelta(day=self.day_of_month) - if n == 0: - n = 1 - elif n < 0: - n += 1 - months = n // 2 + n % 2 day = 1 if n % 2 else self.day_of_month - return other + relativedelta(months=months, day=day) + return shift_month(other, months, day) def _get_roll(self, i, before_day_of_month, after_day_of_month): n = self.n @@ -1369,197 +1278,28 @@ def _get_roll(self, i, before_day_of_month, after_day_of_month): return roll def _apply_index_days(self, i, roll): - return i + (roll % 2) * Timedelta(days=self.day_of_month - 1).value - - -class BusinessMonthEnd(MonthOffset): - """DateOffset increments between business EOM dates""" - - def isAnchored(self): - return (self.n == 1) - - @apply_wraps - def apply(self, other): - n = self.n - wkday, days_in_month = tslib.monthrange(other.year, other.month) - lastBDay = days_in_month - max(((wkday + days_in_month - 1) - % 7) - 4, 0) - - if n > 0 and not other.day >= lastBDay: - n = n - 1 - elif n <= 0 and other.day > lastBDay: - n = n + 1 - other = other + relativedelta(months=n, day=31) - - if other.weekday() > 4: - other = other - BDay() - return other - - _prefix = 'BM' - - -class BusinessMonthBegin(MonthOffset): - """DateOffset of one business month at beginning""" - - @apply_wraps - def apply(self, other): - n = self.n - wkday, _ = tslib.monthrange(other.year, other.month) - first = _get_firstbday(wkday) - - if other.day > first and n <= 0: - # as if rolled forward already - n += 1 - elif other.day < first and n > 0: - other = other + timedelta(days=first - other.day) - n -= 1 - - other = other + relativedelta(months=n) - wkday, _ = tslib.monthrange(other.year, other.month) - first = _get_firstbday(wkday) - result = datetime(other.year, other.month, first, - other.hour, other.minute, - other.second, other.microsecond) - return result - - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - first_weekday, _ = tslib.monthrange(dt.year, dt.month) - if first_weekday == 5: - return dt.day == 3 - elif first_weekday == 6: - return dt.day == 2 - else: - return dt.day == 1 - - _prefix = 'BMS' - - -class CustomBusinessMonthEnd(BusinessMixin, MonthOffset): - """ - **EXPERIMENTAL** DateOffset of one custom business month - - .. warning:: EXPERIMENTAL - - This class is not officially supported and the API is likely to change - in future versions. Use this at your own risk. - - Parameters - ---------- - n : int, default 1 - offset : timedelta, default timedelta(0) - normalize : bool, default False - Normalize start/end dates to midnight before generating date range - weekmask : str, Default 'Mon Tue Wed Thu Fri' - weekmask of valid business days, passed to ``numpy.busdaycalendar`` - holidays : list - list/array of dates to exclude from the set of valid business days, - passed to ``numpy.busdaycalendar`` - calendar : pd.HolidayCalendar or np.busdaycalendar - """ - - _cacheable = False - _prefix = 'CBM' - - def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', - holidays=None, calendar=None, **kwds): - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self.offset = kwds.get('offset', timedelta(0)) - self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, - weekmask=weekmask, holidays=holidays, - calendar=calendar, **kwds) - self.m_offset = MonthEnd(n=1, normalize=normalize, **kwds) - self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar - - @apply_wraps - def apply(self, other): - n = self.n - # First move to month offset - cur_mend = self.m_offset.rollforward(other) - # Find this custom month offset - cur_cmend = self.cbday.rollback(cur_mend) - - # handle zero case. arbitrarily rollforward - if n == 0 and other != cur_cmend: - n += 1 - - if other < cur_cmend and n >= 1: - n -= 1 - elif other > cur_cmend and n <= -1: - n += 1 - - new = cur_mend + n * self.m_offset - result = self.cbday.rollback(new) - return result - - -class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): - """ - **EXPERIMENTAL** DateOffset of one custom business month - - .. warning:: EXPERIMENTAL - - This class is not officially supported and the API is likely to change - in future versions. Use this at your own risk. - - Parameters - ---------- - n : int, default 1 - offset : timedelta, default timedelta(0) - normalize : bool, default False - Normalize start/end dates to midnight before generating date range - weekmask : str, Default 'Mon Tue Wed Thu Fri' - weekmask of valid business days, passed to ``numpy.busdaycalendar`` - holidays : list - list/array of dates to exclude from the set of valid business days, - passed to ``numpy.busdaycalendar`` - calendar : pd.HolidayCalendar or np.busdaycalendar - """ - - _cacheable = False - _prefix = 'CBMS' - - def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', - holidays=None, calendar=None, **kwds): - self.n = int(n) - self.normalize = normalize - self.kwds = kwds - self.offset = kwds.get('offset', timedelta(0)) - self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, - weekmask=weekmask, holidays=holidays, - calendar=calendar, **kwds) - self.m_offset = MonthBegin(n=1, normalize=normalize, **kwds) - self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar + """ + Add days portion of offset to DatetimeIndex i. - @apply_wraps - def apply(self, other): - n = self.n - dt_in = other - # First move to month offset - cur_mbegin = self.m_offset.rollback(dt_in) - # Find this custom month offset - cur_cmbegin = self.cbday.rollforward(cur_mbegin) + Parameters + ---------- + i : DatetimeIndex + roll : ndarray[int64_t] - # handle zero case. arbitrarily rollforward - if n == 0 and dt_in != cur_cmbegin: - n += 1 + Returns + ------- + result : DatetimeIndex + """ + nanos = (roll % 2) * Timedelta(days=self.day_of_month - 1).value + return i + nanos.astype('timedelta64[ns]') - if dt_in > cur_cmbegin and n <= -1: - n += 1 - elif dt_in < cur_cmbegin and n >= 1: - n -= 1 - - new = cur_mbegin + n * self.m_offset - result = self.cbday.rollforward(new) - return result +# --------------------------------------------------------------------- +# Week-Based Offset Classes class Week(DateOffset): """ - Weekly offset + Weekly offset. Parameters ---------- @@ -1567,70 +1307,102 @@ class Week(DateOffset): Always generate specific day of week. 0 for Monday """ _adjust_dst = True + _inc = timedelta(weeks=1) + _prefix = 'W' + _attributes = frozenset(['n', 'normalize', 'weekday']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.weekday = kwds.get('weekday', None) + def __init__(self, n=1, normalize=False, weekday=None): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "weekday", weekday) if self.weekday is not None: if self.weekday < 0 or self.weekday > 6: - raise ValueError('Day must be 0<=day<=6, got %d' % - self.weekday) - - self._inc = timedelta(weeks=1) - self.kwds = kwds + raise ValueError('Day must be 0<=day<=6, got {day}' + .format(day=self.weekday)) def isAnchored(self): return (self.n == 1 and self.weekday is not None) @apply_wraps def apply(self, other): - base = other if self.weekday is None: return other + self.n * self._inc - if self.n > 0: - k = self.n - otherDay = other.weekday() - if otherDay != self.weekday: - other = other + timedelta((self.weekday - otherDay) % 7) - k = k - 1 - other = other - for i in range(k): - other = other + self._inc - else: - k = self.n - otherDay = other.weekday() - if otherDay != self.weekday: - other = other + timedelta((self.weekday - otherDay) % 7) - for i in range(-k): - other = other - self._inc + k = self.n + otherDay = other.weekday() + if otherDay != self.weekday: + other = other + timedelta((self.weekday - otherDay) % 7) + if k > 0: + k -= 1 - other = datetime(other.year, other.month, other.day, - base.hour, base.minute, base.second, base.microsecond) - return other + return other + timedelta(weeks=k) @apply_index_wraps def apply_index(self, i): if self.weekday is None: - return ((i.to_period('W') + self.n).to_timestamp() + - i.to_perioddelta('W')) + # integer addition on PeriodIndex is deprecated, + # so we use _time_shift directly + asper = i.to_period('W') + if not isinstance(asper._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + asper = asper._data + + shifted = asper._time_shift(self.n) + return shifted.to_timestamp() + i.to_perioddelta('W') else: - return self._end_apply_index(i, self.freqstr) + return self._end_apply_index(i) + + def _end_apply_index(self, dtindex): + """ + Add self to the given DatetimeIndex, specialized for case where + self.weekday is non-null. + + Parameters + ---------- + dtindex : DatetimeIndex + + Returns + ------- + result : DatetimeIndex + """ + off = dtindex.to_perioddelta('D') + + base, mult = libfrequencies.get_freq_code(self.freqstr) + base_period = dtindex.to_period(base) + if not isinstance(base_period._data, np.ndarray): + # unwrap PeriodIndex --> PeriodArray + base_period = base_period._data + + if self.n > 0: + # when adding, dates on end roll to next + normed = dtindex - off + Timedelta(1, 'D') - Timedelta(1, 'ns') + roll = np.where(base_period.to_timestamp(how='end') == normed, + self.n, self.n - 1) + # integer-array addition on PeriodIndex is deprecated, + # so we use _addsub_int_array directly + shifted = base_period._addsub_int_array(roll, operator.add) + base = shifted.to_timestamp(how='end') + else: + # integer addition on PeriodIndex is deprecated, + # so we use _time_shift directly + roll = self.n + base = base_period._time_shift(roll).to_timestamp(how='end') + + return base + off + Timedelta(1, 'ns') - Timedelta(1, 'D') def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False + elif self.weekday is None: + return True return dt.weekday() == self.weekday - _prefix = 'W' - @property def rule_code(self): suffix = '' if self.weekday is not None: - suffix = '-%s' % (_int_to_weekday[self.weekday]) + weekday = ccalendar.int_to_weekday[self.weekday] + suffix = '-{weekday}'.format(weekday=weekday) return self._prefix + suffix @classmethod @@ -1638,43 +1410,44 @@ def _from_name(cls, suffix=None): if not suffix: weekday = None else: - weekday = _weekday_to_int[suffix] + weekday = ccalendar.weekday_to_int[suffix] return cls(weekday=weekday) -class WeekDay(object): - MON = 0 - TUE = 1 - WED = 2 - THU = 3 - FRI = 4 - SAT = 5 - SUN = 6 +class _WeekOfMonthMixin(object): + """ + Mixin for methods common to WeekOfMonth and LastWeekOfMonth. + """ + @apply_wraps + def apply(self, other): + compare_day = self._get_offset_day(other) + months = self.n + if months > 0 and compare_day > other.day: + months -= 1 + elif months <= 0 and compare_day < other.day: + months += 1 -_int_to_weekday = { - WeekDay.MON: 'MON', - WeekDay.TUE: 'TUE', - WeekDay.WED: 'WED', - WeekDay.THU: 'THU', - WeekDay.FRI: 'FRI', - WeekDay.SAT: 'SAT', - WeekDay.SUN: 'SUN' -} + shifted = shift_month(other, months, 'start') + to_day = self._get_offset_day(shifted) + return liboffsets.shift_day(shifted, to_day - shifted.day) -_weekday_to_int = dict((v, k) for k, v in _int_to_weekday.items()) + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + return dt.day == self._get_offset_day(dt) -class WeekOfMonth(DateOffset): +class WeekOfMonth(_WeekOfMonthMixin, DateOffset): """ - Describes monthly dates like "the Tuesday of the 2nd week of each month" + Describes monthly dates like "the Tuesday of the 2nd week of each month". Parameters ---------- n : int - week : {0, 1, 2, 3, ...} + week : {0, 1, 2, 3, ...}, default 0 0 is 1st week of month, 1 2nd week, etc. - weekday : {0, 1, ..., 6} + weekday : {0, 1, ..., 6}, default 0 0: Mondays 1: Tuesdays 2: Wednesdays @@ -1683,94 +1456,68 @@ class WeekOfMonth(DateOffset): 5: Saturdays 6: Sundays """ - + _prefix = 'WOM' _adjust_dst = True + _attributes = frozenset(['n', 'normalize', 'week', 'weekday']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.weekday = kwds['weekday'] - self.week = kwds['week'] - - if self.n == 0: - raise ValueError('N cannot be 0') + def __init__(self, n=1, normalize=False, week=0, weekday=0): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "weekday", weekday) + object.__setattr__(self, "week", week) if self.weekday < 0 or self.weekday > 6: - raise ValueError('Day must be 0<=day<=6, got %d' % - self.weekday) + raise ValueError('Day must be 0<=day<=6, got {day}' + .format(day=self.weekday)) if self.week < 0 or self.week > 3: - raise ValueError('Week must be 0<=day<=3, got %d' % - self.week) + raise ValueError('Week must be 0<=week<=3, got {week}' + .format(week=self.week)) - self.kwds = kwds - - @apply_wraps - def apply(self, other): - base = other - offsetOfMonth = self.getOffsetOfMonth(other) - - if offsetOfMonth > other: - if self.n > 0: - months = self.n - 1 - else: - months = self.n - elif offsetOfMonth == other: - months = self.n - else: - if self.n > 0: - months = self.n - else: - months = self.n + 1 - - other = self.getOffsetOfMonth( - other + relativedelta(months=months, day=1)) - other = datetime(other.year, other.month, other.day, base.hour, - base.minute, base.second, base.microsecond) - return other - - def getOffsetOfMonth(self, dt): - w = Week(weekday=self.weekday) - d = datetime(dt.year, dt.month, 1, tzinfo=dt.tzinfo) - d = w.rollforward(d) - - for i in range(self.week): - d = w.apply(d) + def _get_offset_day(self, other): + """ + Find the day in the same month as other that has the same + weekday as self.weekday and is the self.week'th such day in the month. - return d + Parameters + ---------- + other : datetime - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - d = datetime(dt.year, dt.month, dt.day, tzinfo=dt.tzinfo) - return d == self.getOffsetOfMonth(dt) + Returns + ------- + day : int + """ + mstart = datetime(other.year, other.month, 1) + wday = mstart.weekday() + shift_days = (self.weekday - wday) % 7 + return 1 + shift_days + self.week * 7 @property def rule_code(self): - return '%s-%d%s' % (self._prefix, self.week + 1, - _int_to_weekday.get(self.weekday, '')) - - _prefix = 'WOM' + weekday = ccalendar.int_to_weekday.get(self.weekday, '') + return '{prefix}-{week}{weekday}'.format(prefix=self._prefix, + week=self.week + 1, + weekday=weekday) @classmethod def _from_name(cls, suffix=None): if not suffix: - raise ValueError("Prefix %r requires a suffix." % (cls._prefix)) + raise ValueError("Prefix {prefix!r} requires a suffix." + .format(prefix=cls._prefix)) # TODO: handle n here... # only one digit weeks (1 --> week 0, 2 --> week 1, etc.) week = int(suffix[0]) - 1 - weekday = _weekday_to_int[suffix[1:]] + weekday = ccalendar.weekday_to_int[suffix[1:]] return cls(week=week, weekday=weekday) -class LastWeekOfMonth(DateOffset): +class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset): """ Describes monthly dates in last week of month like "the last Tuesday of - each month" + each month". Parameters ---------- - n : int - weekday : {0, 1, ..., 6} + n : int, default 1 + weekday : {0, 1, ..., 6}, default 0 0: Mondays 1: Tuesdays 2: Wednesdays @@ -1779,87 +1526,77 @@ class LastWeekOfMonth(DateOffset): 5: Saturdays 6: Sundays """ + _prefix = 'LWOM' + _adjust_dst = True + _attributes = frozenset(['n', 'normalize', 'weekday']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.weekday = kwds['weekday'] + def __init__(self, n=1, normalize=False, weekday=0): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "weekday", weekday) if self.n == 0: raise ValueError('N cannot be 0') if self.weekday < 0 or self.weekday > 6: - raise ValueError('Day must be 0<=day<=6, got %d' % - self.weekday) - - self.kwds = kwds - - @apply_wraps - def apply(self, other): - offsetOfMonth = self.getOffsetOfMonth(other) - - if offsetOfMonth > other: - if self.n > 0: - months = self.n - 1 - else: - months = self.n - elif offsetOfMonth == other: - months = self.n - else: - if self.n > 0: - months = self.n - else: - months = self.n + 1 + raise ValueError('Day must be 0<=day<=6, got {day}' + .format(day=self.weekday)) - return self.getOffsetOfMonth( - other + relativedelta(months=months, day=1)) + def _get_offset_day(self, other): + """ + Find the day in the same month as other that has the same + weekday as self.weekday and is the last such day in the month. - def getOffsetOfMonth(self, dt): - m = MonthEnd() - d = datetime(dt.year, dt.month, 1, dt.hour, dt.minute, - dt.second, dt.microsecond, tzinfo=dt.tzinfo) - eom = m.rollforward(d) - w = Week(weekday=self.weekday) - return w.rollback(eom) + Parameters + ---------- + other: datetime - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - return dt == self.getOffsetOfMonth(dt) + Returns + ------- + day: int + """ + dim = ccalendar.get_days_in_month(other.year, other.month) + mend = datetime(other.year, other.month, dim) + wday = mend.weekday() + shift_days = (wday - self.weekday) % 7 + return dim - shift_days @property def rule_code(self): - return '%s-%s' % (self._prefix, _int_to_weekday.get(self.weekday, '')) - - _prefix = 'LWOM' + weekday = ccalendar.int_to_weekday.get(self.weekday, '') + return '{prefix}-{weekday}'.format(prefix=self._prefix, + weekday=weekday) @classmethod def _from_name(cls, suffix=None): if not suffix: - raise ValueError("Prefix %r requires a suffix." % (cls._prefix)) + raise ValueError("Prefix {prefix!r} requires a suffix." + .format(prefix=cls._prefix)) # TODO: handle n here... - weekday = _weekday_to_int[suffix] + weekday = ccalendar.weekday_to_int[suffix] return cls(weekday=weekday) +# --------------------------------------------------------------------- +# Quarter-Based Offset Classes -class QuarterOffset(DateOffset): - """Quarter representation - doesn't call super""" - #: default month for __init__ +class QuarterOffset(DateOffset): + """ + Quarter representation - doesn't call super. + """ _default_startingMonth = None - #: default month in _from_name _from_name_startingMonth = None _adjust_dst = True + _attributes = frozenset(['n', 'normalize', 'startingMonth']) # TODO: Consider combining QuarterOffset and YearOffset __init__ at some - # point + # point. Also apply_index, onOffset, rule_code if + # startingMonth vs month attr names are resolved - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.startingMonth = kwds.get('startingMonth', - self._default_startingMonth) + def __init__(self, n=1, normalize=False, startingMonth=None): + BaseOffset.__init__(self, n, normalize) - self.kwds = kwds + if startingMonth is None: + startingMonth = self._default_startingMonth + object.__setattr__(self, "startingMonth", startingMonth) def isAnchored(self): return (self.n == 1 and self.startingMonth is not None) @@ -1868,7 +1605,7 @@ def isAnchored(self): def _from_name(cls, suffix=None): kwargs = {} if suffix: - kwargs['startingMonth'] = _month_to_int[suffix] + kwargs['startingMonth'] = ccalendar.MONTH_TO_CAL_NUM[suffix] else: if cls._from_name_startingMonth is not None: kwargs['startingMonth'] = cls._from_name_startingMonth @@ -1876,57 +1613,51 @@ def _from_name(cls, suffix=None): @property def rule_code(self): - return '%s-%s' % (self._prefix, _int_to_month[self.startingMonth]) + month = ccalendar.MONTH_ALIASES[self.startingMonth] + return '{prefix}-{month}'.format(prefix=self._prefix, month=month) + + @apply_wraps + def apply(self, other): + # months_since: find the calendar quarter containing other.month, + # e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep]. + # Then find the month in that quarter containing an onOffset date for + # self. `months_since` is the number of months to shift other.month + # to get to this on-offset month. + months_since = other.month % 3 - self.startingMonth % 3 + qtrs = liboffsets.roll_qtrday(other, self.n, self.startingMonth, + day_opt=self._day_opt, modby=3) + months = qtrs * 3 - months_since + return shift_month(other, months, self._day_opt) + + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + mod_month = (dt.month - self.startingMonth) % 3 + return mod_month == 0 and dt.day == self._get_offset_day(dt) + + @apply_index_wraps + def apply_index(self, dtindex): + shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, + self.startingMonth, self._day_opt) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(dtindex)._simple_new(shifted, freq=dtindex.freq, + dtype=dtindex.dtype) class BQuarterEnd(QuarterOffset): - """DateOffset increments between business Quarter dates + """ + DateOffset increments between business Quarter dates. + startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... """ _outputName = 'BusinessQuarterEnd' _default_startingMonth = 3 - # 'BQ' _from_name_startingMonth = 12 _prefix = 'BQ' - - @apply_wraps - def apply(self, other): - n = self.n - base = other - other = datetime(other.year, other.month, other.day, - other.hour, other.minute, other.second, - other.microsecond) - - wkday, days_in_month = tslib.monthrange(other.year, other.month) - lastBDay = days_in_month - max(((wkday + days_in_month - 1) - % 7) - 4, 0) - - monthsToGo = 3 - ((other.month - self.startingMonth) % 3) - if monthsToGo == 3: - monthsToGo = 0 - - if n > 0 and not (other.day >= lastBDay and monthsToGo == 0): - n = n - 1 - elif n <= 0 and other.day > lastBDay and monthsToGo == 0: - n = n + 1 - - other = other + relativedelta(months=monthsToGo + 3 * n, day=31) - other = tslib._localize_pydatetime(other, base.tzinfo) - if other.weekday() > 4: - other = other - BDay() - return other - - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - modMonth = (dt.month - self.startingMonth) % 3 - return BMonthEnd().onOffset(dt) and modMonth == 0 - - -_int_to_month = tslib._MONTH_ALIASES -_month_to_int = dict((v, k) for k, v in _int_to_month.items()) + _day_opt = 'business_end' # TODO: This is basically the same as BQuarterEnd @@ -1936,38 +1667,13 @@ class BQuarterBegin(QuarterOffset): _default_startingMonth = 3 _from_name_startingMonth = 1 _prefix = 'BQS' - - @apply_wraps - def apply(self, other): - n = self.n - wkday, _ = tslib.monthrange(other.year, other.month) - - first = _get_firstbday(wkday) - - monthsSince = (other.month - self.startingMonth) % 3 - - if n <= 0 and monthsSince != 0: # make sure to roll forward so negate - monthsSince = monthsSince - 3 - - # roll forward if on same month later than first bday - if n <= 0 and (monthsSince == 0 and other.day > first): - n = n + 1 - # pretend to roll back if on same month but before firstbday - elif n > 0 and (monthsSince == 0 and other.day < first): - n = n - 1 - - # get the first bday for result - other = other + relativedelta(months=3 * n - monthsSince) - wkday, _ = tslib.monthrange(other.year, other.month) - first = _get_firstbday(wkday) - result = datetime(other.year, other.month, first, - other.hour, other.minute, other.second, - other.microsecond) - return result + _day_opt = 'business_start' class QuarterEnd(QuarterOffset): - """DateOffset increments between business Quarter dates + """ + DateOffset increments between business Quarter dates. + startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ... @@ -1975,44 +1681,7 @@ class QuarterEnd(QuarterOffset): _outputName = 'QuarterEnd' _default_startingMonth = 3 _prefix = 'Q' - - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.startingMonth = kwds.get('startingMonth', 3) - - self.kwds = kwds - - def isAnchored(self): - return (self.n == 1 and self.startingMonth is not None) - - @apply_wraps - def apply(self, other): - n = self.n - other = datetime(other.year, other.month, other.day, - other.hour, other.minute, other.second, - other.microsecond) - wkday, days_in_month = tslib.monthrange(other.year, other.month) - - monthsToGo = 3 - ((other.month - self.startingMonth) % 3) - if monthsToGo == 3: - monthsToGo = 0 - - if n > 0 and not (other.day >= days_in_month and monthsToGo == 0): - n = n - 1 - - other = other + relativedelta(months=monthsToGo + 3 * n, day=31) - return other - - @apply_index_wraps - def apply_index(self, i): - return self._end_apply_index(i, self.freqstr) - - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - modMonth = (dt.month - self.startingMonth) % 3 - return MonthEnd().onOffset(dt) and modMonth == 0 + _day_opt = 'end' class QuarterBegin(QuarterOffset): @@ -2020,240 +1689,108 @@ class QuarterBegin(QuarterOffset): _default_startingMonth = 3 _from_name_startingMonth = 1 _prefix = 'QS' + _day_opt = 'start' - def isAnchored(self): - return (self.n == 1 and self.startingMonth is not None) - - @apply_wraps - def apply(self, other): - n = self.n - wkday, days_in_month = tslib.monthrange(other.year, other.month) - monthsSince = (other.month - self.startingMonth) % 3 +# --------------------------------------------------------------------- +# Year-Based Offset Classes - if n <= 0 and monthsSince != 0: - # make sure you roll forward, so negate - monthsSince = monthsSince - 3 +class YearOffset(DateOffset): + """ + DateOffset that just needs a month. + """ + _adjust_dst = True + _attributes = frozenset(['n', 'normalize', 'month']) - if n <= 0 and (monthsSince == 0 and other.day > 1): - # after start, so come back an extra period as if rolled forward - n = n + 1 + def _get_offset_day(self, other): + # override BaseOffset method to use self.month instead of other.month + # TODO: there may be a more performant way to do this + return liboffsets.get_day_of_month(other.replace(month=self.month), + self._day_opt) - other = other + relativedelta(months=3 * n - monthsSince, day=1) - return other + @apply_wraps + def apply(self, other): + years = roll_yearday(other, self.n, self.month, self._day_opt) + months = years * 12 + (self.month - other.month) + return shift_month(other, months, self._day_opt) @apply_index_wraps - def apply_index(self, i): - freq_month = 12 if self.startingMonth == 1 else self.startingMonth - 1 - # freq_month = self.startingMonth - freqstr = 'Q-%s' % (_int_to_month[freq_month],) - return self._beg_apply_index(i, freqstr) + def apply_index(self, dtindex): + shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, + self.month, self._day_opt, + modby=12) + # TODO: going through __new__ raises on call to _validate_frequency; + # are we passing incorrect freq? + return type(dtindex)._simple_new(shifted, freq=dtindex.freq, + dtype=dtindex.dtype) + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + return dt.month == self.month and dt.day == self._get_offset_day(dt) -class YearOffset(DateOffset): - """DateOffset that just needs a month""" - _adjust_dst = True + def __init__(self, n=1, normalize=False, month=None): + BaseOffset.__init__(self, n, normalize) - def __init__(self, n=1, normalize=False, **kwds): - self.month = kwds.get('month', self._default_month) + month = month if month is not None else self._default_month + object.__setattr__(self, "month", month) if self.month < 1 or self.month > 12: raise ValueError('Month must go from 1 to 12') - DateOffset.__init__(self, n=n, normalize=normalize, **kwds) - @classmethod def _from_name(cls, suffix=None): kwargs = {} if suffix: - kwargs['month'] = _month_to_int[suffix] + kwargs['month'] = ccalendar.MONTH_TO_CAL_NUM[suffix] return cls(**kwargs) @property def rule_code(self): - return '%s-%s' % (self._prefix, _int_to_month[self.month]) + month = ccalendar.MONTH_ALIASES[self.month] + return '{prefix}-{month}'.format(prefix=self._prefix, month=month) class BYearEnd(YearOffset): - """DateOffset increments between business EOM dates""" + """ + DateOffset increments between business EOM dates. + """ _outputName = 'BusinessYearEnd' _default_month = 12 _prefix = 'BA' - - @apply_wraps - def apply(self, other): - n = self.n - wkday, days_in_month = tslib.monthrange(other.year, self.month) - lastBDay = (days_in_month - - max(((wkday + days_in_month - 1) % 7) - 4, 0)) - - years = n - if n > 0: - if (other.month < self.month or - (other.month == self.month and other.day < lastBDay)): - years -= 1 - elif n <= 0: - if (other.month > self.month or - (other.month == self.month and other.day > lastBDay)): - years += 1 - - other = other + relativedelta(years=years) - - _, days_in_month = tslib.monthrange(other.year, self.month) - result = datetime(other.year, self.month, days_in_month, - other.hour, other.minute, other.second, - other.microsecond) - - if result.weekday() > 4: - result = result - BDay() - - return result + _day_opt = 'business_end' class BYearBegin(YearOffset): - """DateOffset increments between business year begin dates""" + """ + DateOffset increments between business year begin dates. + """ _outputName = 'BusinessYearBegin' _default_month = 1 _prefix = 'BAS' - - @apply_wraps - def apply(self, other): - n = self.n - wkday, days_in_month = tslib.monthrange(other.year, self.month) - - first = _get_firstbday(wkday) - - years = n - - if n > 0: # roll back first for positive n - if (other.month < self.month or - (other.month == self.month and other.day < first)): - years -= 1 - elif n <= 0: # roll forward - if (other.month > self.month or - (other.month == self.month and other.day > first)): - years += 1 - - # set first bday for result - other = other + relativedelta(years=years) - wkday, days_in_month = tslib.monthrange(other.year, self.month) - first = _get_firstbday(wkday) - return datetime(other.year, self.month, first, other.hour, - other.minute, other.second, other.microsecond) + _day_opt = 'business_start' class YearEnd(YearOffset): - """DateOffset increments between calendar year ends""" + """ + DateOffset increments between calendar year ends. + """ _default_month = 12 _prefix = 'A' - - @apply_wraps - def apply(self, other): - def _increment(date): - if date.month == self.month: - _, days_in_month = tslib.monthrange(date.year, self.month) - if date.day != days_in_month: - year = date.year - else: - year = date.year + 1 - elif date.month < self.month: - year = date.year - else: - year = date.year + 1 - _, days_in_month = tslib.monthrange(year, self.month) - return datetime(year, self.month, days_in_month, - date.hour, date.minute, date.second, - date.microsecond) - - def _decrement(date): - year = date.year if date.month > self.month else date.year - 1 - _, days_in_month = tslib.monthrange(year, self.month) - return datetime(year, self.month, days_in_month, - date.hour, date.minute, date.second, - date.microsecond) - - def _rollf(date): - if date.month != self.month or\ - date.day < tslib.monthrange(date.year, date.month)[1]: - date = _increment(date) - return date - - n = self.n - result = other - if n > 0: - while n > 0: - result = _increment(result) - n -= 1 - elif n < 0: - while n < 0: - result = _decrement(result) - n += 1 - else: - # n == 0, roll forward - result = _rollf(result) - return result - - @apply_index_wraps - def apply_index(self, i): - # convert month anchor to annual period tuple - return self._end_apply_index(i, self.freqstr) - - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - wkday, days_in_month = tslib.monthrange(dt.year, self.month) - return self.month == dt.month and dt.day == days_in_month + _day_opt = 'end' class YearBegin(YearOffset): - """DateOffset increments between calendar year begin dates""" + """ + DateOffset increments between calendar year begin dates. + """ _default_month = 1 _prefix = 'AS' + _day_opt = 'start' - @apply_wraps - def apply(self, other): - def _increment(date, n): - year = date.year + n - 1 - if date.month >= self.month: - year += 1 - return datetime(year, self.month, 1, date.hour, date.minute, - date.second, date.microsecond) - - def _decrement(date, n): - year = date.year + n + 1 - if date.month < self.month or (date.month == self.month and - date.day == 1): - year -= 1 - return datetime(year, self.month, 1, date.hour, date.minute, - date.second, date.microsecond) - - def _rollf(date): - if (date.month != self.month) or date.day > 1: - date = _increment(date, 1) - return date - - n = self.n - result = other - if n > 0: - result = _increment(result, n) - elif n < 0: - result = _decrement(result, n) - else: - # n == 0, roll forward - result = _rollf(result) - return result - - @apply_index_wraps - def apply_index(self, i): - freq_month = 12 if self.month == 1 else self.month - 1 - freqstr = 'A-%s' % (_int_to_month[freq_month],) - return self._beg_apply_index(i, freqstr) - - def onOffset(self, dt): - if self.normalize and not _is_normalized(dt): - return False - return dt.month == self.month and dt.day == 1 +# --------------------------------------------------------------------- +# Special Offset Classes class FY5253(DateOffset): """ @@ -2267,8 +1804,7 @@ class FY5253(DateOffset): such as retail, manufacturing and parking industry. For more information see: - http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar - + http://en.wikipedia.org/wiki/4-4-5_calendar The year may either: - end on the last X day of the Y month. @@ -2292,39 +1828,29 @@ class FY5253(DateOffset): variation : str {"nearest", "last"} for "LastOfMonth" or "NearestEndMonth" """ - _prefix = 'RE' - _suffix_prefix_last = 'L' - _suffix_prefix_nearest = 'N' _adjust_dst = True + _attributes = frozenset(['weekday', 'startingMonth', 'variation']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - self.startingMonth = kwds['startingMonth'] - self.weekday = kwds["weekday"] - - self.variation = kwds["variation"] + def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, + variation="nearest"): + BaseOffset.__init__(self, n, normalize) + object.__setattr__(self, "startingMonth", startingMonth) + object.__setattr__(self, "weekday", weekday) - self.kwds = kwds + object.__setattr__(self, "variation", variation) if self.n == 0: raise ValueError('N cannot be 0') if self.variation not in ["nearest", "last"]: - raise ValueError('%s is not a valid variation' % self.variation) - - if self.variation == "nearest": - weekday_offset = weekday(self.weekday) - self._rd_forward = relativedelta(weekday=weekday_offset) - self._rd_backward = relativedelta(weekday=weekday_offset(-1)) - else: - self._offset_lwom = LastWeekOfMonth(n=1, weekday=self.weekday) + raise ValueError('{variation} is not a valid variation' + .format(variation=self.variation)) def isAnchored(self): - return self.n == 1 \ - and self.startingMonth is not None \ - and self.weekday is not None + return (self.n == 1 and + self.startingMonth is not None and + self.weekday is not None) def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -2334,13 +1860,15 @@ def onOffset(self, dt): if self.variation == "nearest": # We have to check the year end of "this" cal year AND the previous - return year_end == dt or \ - self.get_year_end(dt - relativedelta(months=1)) == dt + return (year_end == dt or + self.get_year_end(shift_month(dt, -1, None)) == dt) else: return year_end == dt @apply_wraps def apply(self, other): + norm = Timestamp(other).normalize() + n = self.n prev_year = self.get_year_end( datetime(other.year - 1, self.startingMonth, 1)) @@ -2348,112 +1876,89 @@ def apply(self, other): datetime(other.year, self.startingMonth, 1)) next_year = self.get_year_end( datetime(other.year + 1, self.startingMonth, 1)) - prev_year = tslib._localize_pydatetime(prev_year, other.tzinfo) - cur_year = tslib._localize_pydatetime(cur_year, other.tzinfo) - next_year = tslib._localize_pydatetime(next_year, other.tzinfo) - - if n > 0: - if other == prev_year: - year = other.year - 1 - elif other == cur_year: - year = other.year - elif other == next_year: - year = other.year + 1 - elif other < prev_year: - year = other.year - 1 - n -= 1 - elif other < cur_year: - year = other.year - n -= 1 - elif other < next_year: - year = other.year + 1 - n -= 1 - else: - assert False - result = self.get_year_end( - datetime(year + n, self.startingMonth, 1)) + prev_year = conversion.localize_pydatetime(prev_year, other.tzinfo) + cur_year = conversion.localize_pydatetime(cur_year, other.tzinfo) + next_year = conversion.localize_pydatetime(next_year, other.tzinfo) - result = datetime(result.year, result.month, result.day, - other.hour, other.minute, other.second, - other.microsecond) - return result - else: - n = -n - if other == prev_year: - year = other.year - 1 - elif other == cur_year: - year = other.year - elif other == next_year: - year = other.year + 1 - elif other > next_year: - year = other.year + 1 - n -= 1 - elif other > cur_year: - year = other.year + # Note: next_year.year == other.year + 1, so we will always + # have other < next_year + if norm == prev_year: + n -= 1 + elif norm == cur_year: + pass + elif n > 0: + if norm < prev_year: + n -= 2 + elif prev_year < norm < cur_year: n -= 1 - elif other > prev_year: - year = other.year - 1 + elif cur_year < norm < next_year: + pass + else: + if cur_year < norm < next_year: + n += 1 + elif prev_year < norm < cur_year: + pass + elif (norm.year == prev_year.year and norm < prev_year and + prev_year - norm <= timedelta(6)): + # GH#14774, error when next_year.year == cur_year.year + # e.g. prev_year == datetime(2004, 1, 3), + # other == datetime(2004, 1, 1) n -= 1 else: assert False - result = self.get_year_end( - datetime(year - n, self.startingMonth, 1)) - - result = datetime(result.year, result.month, result.day, - other.hour, other.minute, other.second, - other.microsecond) - return result + shifted = datetime(other.year + n, self.startingMonth, 1) + result = self.get_year_end(shifted) + result = datetime(result.year, result.month, result.day, + other.hour, other.minute, other.second, + other.microsecond) + return result def get_year_end(self, dt): - if self.variation == "nearest": - return self._get_year_end_nearest(dt) - else: - return self._get_year_end_last(dt) + assert dt.tzinfo is None - def get_target_month_end(self, dt): - target_month = datetime( - dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo) - next_month_first_of = target_month + relativedelta(months=+1) - return next_month_first_of + relativedelta(days=-1) - - def _get_year_end_nearest(self, dt): - target_date = self.get_target_month_end(dt) - if target_date.weekday() == self.weekday: + dim = ccalendar.get_days_in_month(dt.year, self.startingMonth) + target_date = datetime(dt.year, self.startingMonth, dim) + wkday_diff = self.weekday - target_date.weekday() + if wkday_diff == 0: + # year_end is the same for "last" and "nearest" cases return target_date - else: - forward = target_date + self._rd_forward - backward = target_date + self._rd_backward - if forward - target_date < target_date - backward: - return forward - else: - return backward + if self.variation == "last": + days_forward = (wkday_diff % 7) - 7 - def _get_year_end_last(self, dt): - current_year = datetime( - dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo) - return current_year + self._offset_lwom + # days_forward is always negative, so we always end up + # in the same year as dt + return target_date + timedelta(days=days_forward) + else: + # variation == "nearest": + days_forward = wkday_diff % 7 + if days_forward <= 3: + # The upcoming self.weekday is closer than the previous one + return target_date + timedelta(days_forward) + else: + # The previous self.weekday is closer than the upcoming one + return target_date + timedelta(days_forward - 7) @property def rule_code(self): + prefix = self._prefix suffix = self.get_rule_code_suffix() - return "%s-%s" % (self._get_prefix(), suffix) - - def _get_prefix(self): - return self._prefix + return "{prefix}-{suffix}".format(prefix=prefix, suffix=suffix) def _get_suffix_prefix(self): if self.variation == "nearest": - return self._suffix_prefix_nearest + return 'N' else: - return self._suffix_prefix_last + return 'L' def get_rule_code_suffix(self): - return '%s-%s-%s' % (self._get_suffix_prefix(), - _int_to_month[self.startingMonth], - _int_to_weekday[self.weekday]) + prefix = self._get_suffix_prefix() + month = ccalendar.MONTH_ALIASES[self.startingMonth] + weekday = ccalendar.int_to_weekday[self.weekday] + return '{prefix}-{month}-{weekday}'.format(prefix=prefix, month=month, + weekday=weekday) @classmethod def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code): @@ -2462,17 +1967,15 @@ def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code): elif varion_code == "L": variation = "last" else: - raise ValueError( - "Unable to parse varion_code: %s" % (varion_code,)) + raise ValueError("Unable to parse varion_code: " + "{code}".format(code=varion_code)) - startingMonth = _month_to_int[startingMonth_code] - weekday = _weekday_to_int[weekday_code] + startingMonth = ccalendar.MONTH_TO_CAL_NUM[startingMonth_code] + weekday = ccalendar.weekday_to_int[weekday_code] - return { - "weekday": weekday, - "startingMonth": startingMonth, - "variation": variation, - } + return {"weekday": weekday, + "startingMonth": startingMonth, + "variation": variation} @classmethod def _from_name(cls, *args): @@ -2492,7 +1995,7 @@ class FY5253Quarter(DateOffset): such as retail, manufacturing and parking industry. For more information see: - http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar + http://en.wikipedia.org/wiki/4-4-5_calendar The year may either: - end on the last X day of the Y month. @@ -2525,66 +2028,104 @@ class FY5253Quarter(DateOffset): _prefix = 'REQ' _adjust_dst = True + _attributes = frozenset(['weekday', 'startingMonth', 'qtr_with_extra_week', + 'variation']) - def __init__(self, n=1, normalize=False, **kwds): - self.n = n - self.normalize = normalize - - self.qtr_with_extra_week = kwds["qtr_with_extra_week"] + def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, + qtr_with_extra_week=1, variation="nearest"): + BaseOffset.__init__(self, n, normalize) - self.kwds = kwds + object.__setattr__(self, "startingMonth", startingMonth) + object.__setattr__(self, "weekday", weekday) + object.__setattr__(self, "qtr_with_extra_week", qtr_with_extra_week) + object.__setattr__(self, "variation", variation) if self.n == 0: raise ValueError('N cannot be 0') - self._offset = FY5253( - startingMonth=kwds['startingMonth'], - weekday=kwds["weekday"], - variation=kwds["variation"]) + @cache_readonly + def _offset(self): + return FY5253(startingMonth=self.startingMonth, + weekday=self.weekday, + variation=self.variation) def isAnchored(self): return self.n == 1 and self._offset.isAnchored() + def _rollback_to_year(self, other): + """ + Roll `other` back to the most recent date that was on a fiscal year + end. + + Return the date of that year-end, the number of full quarters + elapsed between that year-end and other, and the remaining Timedelta + since the most recent quarter-end. + + Parameters + ---------- + other : datetime or Timestamp + + Returns + ------- + tuple of + prev_year_end : Timestamp giving most recent fiscal year end + num_qtrs : int + tdelta : Timedelta + """ + num_qtrs = 0 + + norm = Timestamp(other).tz_localize(None) + start = self._offset.rollback(norm) + # Note: start <= norm and self._offset.onOffset(start) + + if start < norm: + # roll adjustment + qtr_lens = self.get_weeks(norm) + + # check thet qtr_lens is consistent with self._offset addition + end = liboffsets.shift_day(start, days=7 * sum(qtr_lens)) + assert self._offset.onOffset(end), (start, end, qtr_lens) + + tdelta = norm - start + for qlen in qtr_lens: + if qlen * 7 <= tdelta.days: + num_qtrs += 1 + tdelta -= Timedelta(days=qlen * 7) + else: + break + else: + tdelta = Timedelta(0) + + # Note: we always have tdelta.value >= 0 + return start, num_qtrs, tdelta + @apply_wraps def apply(self, other): - base = other + # Note: self.n == 0 is not allowed. n = self.n - if n > 0: - while n > 0: - if not self._offset.onOffset(other): - qtr_lens = self.get_weeks(other) - start = other - self._offset - else: - start = other - qtr_lens = self.get_weeks(other + self._offset) + prev_year_end, num_qtrs, tdelta = self._rollback_to_year(other) + res = prev_year_end + n += num_qtrs + if self.n <= 0 and tdelta.value > 0: + n += 1 - for weeks in qtr_lens: - start += relativedelta(weeks=weeks) - if start > other: - other = start - n -= 1 - break + # Possible speedup by handling years first. + years = n // 4 + if years: + res += self._offset * years + n -= years * 4 - else: - n = -n - while n > 0: - if not self._offset.onOffset(other): - qtr_lens = self.get_weeks(other) - end = other + self._offset - else: - end = other - qtr_lens = self.get_weeks(other) - - for weeks in reversed(qtr_lens): - end -= relativedelta(weeks=weeks) - if end < other: - other = end - n -= 1 - break - other = datetime(other.year, other.month, other.day, - base.hour, base.minute, base.second, base.microsecond) - return other + # Add an extra day to make *sure* we are getting the quarter lengths + # for the upcoming year, not the previous year + qtr_lens = self.get_weeks(res + Timedelta(days=1)) + + # Note: we always have 0 <= n < 4 + weeks = sum(qtr_lens[:n]) + if weeks: + res = liboffsets.shift_day(res, days=weeks * 7) + + return res def get_weeks(self, dt): ret = [13] * 4 @@ -2597,16 +2138,15 @@ def get_weeks(self, dt): return ret def year_has_extra_week(self, dt): - if self._offset.onOffset(dt): - prev_year_end = dt - self._offset - next_year_end = dt - else: - next_year_end = dt + self._offset - prev_year_end = dt - self._offset - - week_in_year = (next_year_end - prev_year_end).days / 7 + # Avoid round-down errors --> normalize to get + # e.g. '370D' instead of '360D23H' + norm = Timestamp(dt).normalize().tz_localize(None) - return week_in_year == 53 + next_year_end = self._offset.rollforward(norm) + prev_year_end = norm - self._offset + weeks_in_year = (next_year_end - prev_year_end).days / 7 + assert weeks_in_year in [52, 53], weeks_in_year + return weeks_in_year == 53 def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -2619,8 +2159,8 @@ def onOffset(self, dt): qtr_lens = self.get_weeks(dt) current = next_year_end - for qtr_len in qtr_lens[0:4]: - current += relativedelta(weeks=qtr_len) + for qtr_len in qtr_lens: + current = liboffsets.shift_day(current, days=qtr_len * 7) if dt == current: return True return False @@ -2628,8 +2168,9 @@ def onOffset(self, dt): @property def rule_code(self): suffix = self._offset.get_rule_code_suffix() - return "%s-%s" % (self._prefix, - "%s-%d" % (suffix, self.qtr_with_extra_week)) + qtr = self.qtr_with_extra_week + return "{prefix}-{suffix}-{qtr}".format(prefix=self._prefix, + suffix=suffix, qtr=qtr) @classmethod def _from_name(cls, *args): @@ -2639,36 +2180,33 @@ def _from_name(cls, *args): class Easter(DateOffset): """ - DateOffset for the Easter holiday using - logic defined in dateutil. Right now uses - the revised method which is valid in years - 1583-4099. + DateOffset for the Easter holiday using logic defined in dateutil. + + Right now uses the revised method which is valid in years 1583-4099. """ _adjust_dst = True + _attributes = frozenset(['n', 'normalize']) - def __init__(self, n=1, **kwds): - super(Easter, self).__init__(n, **kwds) + __init__ = BaseOffset.__init__ @apply_wraps def apply(self, other): - currentEaster = easter(other.year) - currentEaster = datetime( - currentEaster.year, currentEaster.month, currentEaster.day) - currentEaster = tslib._localize_pydatetime(currentEaster, other.tzinfo) + current_easter = easter(other.year) + current_easter = datetime(current_easter.year, + current_easter.month, current_easter.day) + current_easter = conversion.localize_pydatetime(current_easter, + other.tzinfo) + + n = self.n + if n >= 0 and other < current_easter: + n -= 1 + elif n < 0 and other > current_easter: + n += 1 + # TODO: Why does this handle the 0 case the opposite of others? # NOTE: easter returns a datetime.date so we have to convert to type of # other - if self.n >= 0: - if other >= currentEaster: - new = easter(other.year + self.n) - else: - new = easter(other.year + self.n - 1) - else: - if other > currentEaster: - new = easter(other.year + self.n + 1) - else: - new = easter(other.year + self.n) - + new = easter(other.year + n) new = datetime(new.year, new.month, new.day, other.hour, other.minute, other.second, other.microsecond) return new @@ -2683,21 +2221,36 @@ def onOffset(self, dt): def _tick_comp(op): + assert op not in [operator.eq, operator.ne] + def f(self, other): - return op(self.delta, other.delta) + try: + return op(self.delta, other.delta) + except AttributeError: + # comparing with a non-Tick object + raise TypeError("Invalid comparison between {cls} and {typ}" + .format(cls=type(self).__name__, + typ=type(other).__name__)) + f.__name__ = '__{opname}__'.format(opname=op.__name__) return f -class Tick(SingleConstructorOffset): +class Tick(liboffsets._Tick, SingleConstructorOffset): _inc = Timedelta(microseconds=1000) + _prefix = 'undefined' + _attributes = frozenset(['n', 'normalize']) + + def __init__(self, n=1, normalize=False): + BaseOffset.__init__(self, n, normalize) + if normalize: + raise ValueError("Tick offset with `normalize=True` are not " + "allowed.") # GH#21427 __gt__ = _tick_comp(operator.gt) __ge__ = _tick_comp(operator.ge) __lt__ = _tick_comp(operator.lt) __le__ = _tick_comp(operator.le) - __eq__ = _tick_comp(operator.eq) - __ne__ = _tick_comp(operator.ne) def __add__(self, other): if isinstance(other, Tick): @@ -2712,35 +2265,45 @@ def __add__(self, other): except ApplyTypeError: return NotImplemented except OverflowError: - raise OverflowError("the add operation between {} and {} " - "will overflow".format(self, other)) + raise OverflowError("the add operation between {self} and {other} " + "will overflow".format(self=self, other=other)) def __eq__(self, other): if isinstance(other, compat.string_types): from pandas.tseries.frequencies import to_offset - - other = to_offset(other) + try: + # GH#23524 if to_offset fails, we are dealing with an + # incomparable type so == is False and != is True + other = to_offset(other) + except ValueError: + # e.g. "infer" + return False if isinstance(other, Tick): return self.delta == other.delta else: - return DateOffset.__eq__(self, other) + return False # This is identical to DateOffset.__hash__, but has to be redefined here # for Python 3, because we've redefined __eq__. def __hash__(self): - return hash(self._params()) + return hash(self._params) def __ne__(self, other): if isinstance(other, compat.string_types): from pandas.tseries.frequencies import to_offset - - other = to_offset(other) + try: + # GH#23524 if to_offset fails, we are dealing with an + # incomparable type so == is False and != is True + other = to_offset(other) + except ValueError: + # e.g. "infer" + return True if isinstance(other, Tick): return self.delta != other.delta else: - return DateOffset.__ne__(self, other) + return True @property def delta(self): @@ -2748,8 +2311,9 @@ def delta(self): @property def nanos(self): - return _delta_to_nanoseconds(self.delta) + return delta_to_nanoseconds(self.delta) + # TODO: Should Tick have its own apply_index? def apply(self, other): # Timestamp can handle tz and nano sec, thus no need to use apply_wraps if isinstance(other, Timestamp): @@ -2771,16 +2335,16 @@ def apply(self, other): elif isinstance(other, type(self)): return type(self)(self.n + other.n) - raise ApplyTypeError('Unhandled type: %s' % type(other).__name__) - - _prefix = 'undefined' + raise ApplyTypeError('Unhandled type: {type_str}' + .format(type_str=type(other).__name__)) def isAnchored(self): return False def _delta_to_tick(delta): - if delta.microseconds == 0: + if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0: + # nanoseconds only for pd.Timedelta if delta.seconds == 0: return Day(delta.days) else: @@ -2792,7 +2356,7 @@ def _delta_to_tick(delta): else: return Second(seconds) else: - nanos = _delta_to_nanoseconds(delta) + nanos = delta_to_nanoseconds(delta) if nanos % 1000000 == 0: return Milli(nanos // 1000000) elif nanos % 1000 == 0: @@ -2801,9 +2365,6 @@ def _delta_to_tick(delta): return Nano(nanos) -_delta_to_nanoseconds = tslib._delta_to_nanoseconds - - class Day(Tick): _inc = Timedelta(days=1) _prefix = 'D' @@ -2846,35 +2407,21 @@ class Nano(Tick): CBMonthBegin = CustomBusinessMonthBegin CDay = CustomBusinessDay - -def _get_firstbday(wkday): - """ - wkday is the result of monthrange(year, month) - - If it's a saturday or sunday, increment first business day to reflect this - """ - first = 1 - if wkday == 5: # on Saturday - first = 3 - elif wkday == 6: # on Sunday - first = 2 - return first +# --------------------------------------------------------------------- -def generate_range(start=None, end=None, periods=None, - offset=BDay(), time_rule=None): +def generate_range(start=None, end=None, periods=None, offset=BDay()): """ Generates a sequence of dates corresponding to the specified time offset. Similar to dateutil.rrule except uses pandas DateOffset - objects to represent time increments + objects to represent time increments. Parameters ---------- start : datetime (default None) end : datetime (default None) - periods : int, optional - time_rule : (legacy) name of DateOffset object to be used, optional - Corresponds with names expected by tseries.frequencies.get_offset + periods : int, (default None) + offset : DateOffset, (default BDay()) Notes ----- @@ -2882,17 +2429,13 @@ def generate_range(start=None, end=None, periods=None, * At least two of (start, end, periods) must be specified. * If both start and end are specified, the returned dates will satisfy start <= date <= end. - * If both time_rule and offset are specified, time_rule supersedes offset. Returns ------- dates : generator object - """ - if time_rule is not None: - from pandas.tseries.frequencies import get_offset - - offset = get_offset(time_rule) + from pandas.tseries.frequencies import to_offset + offset = to_offset(offset) start = to_datetime(start) end = to_datetime(end) @@ -2903,7 +2446,7 @@ def generate_range(start=None, end=None, periods=None, elif end and not offset.onOffset(end): end = offset.rollback(end) - if periods is None and end < start: + if periods is None and end < start and offset.n >= 0: end = None periods = 0 @@ -2921,7 +2464,8 @@ def generate_range(start=None, end=None, periods=None, # faster than cur + offset next_date = offset.apply(cur) if next_date <= cur: - raise ValueError('Offset %s did not increment date' % offset) + raise ValueError('Offset {offset} did not increment date' + .format(offset=offset)) cur = next_date else: while cur >= end: @@ -2930,11 +2474,12 @@ def generate_range(start=None, end=None, periods=None, # faster than cur + offset next_date = offset.apply(cur) if next_date >= cur: - raise ValueError('Offset %s did not decrement date' % offset) + raise ValueError('Offset {offset} did not decrement date' + .format(offset=offset)) cur = next_date -prefix_mapping = dict((offset._prefix, offset) for offset in [ +prefix_mapping = {offset._prefix: offset for offset in [ YearBegin, # 'AS' YearEnd, # 'A' BYearBegin, # 'BAS' @@ -2951,6 +2496,7 @@ def generate_range(start=None, end=None, periods=None, CustomBusinessHour, # 'CBH' MonthEnd, # 'M' MonthBegin, # 'MS' + Nano, # 'N' SemiMonthEnd, # 'SM' SemiMonthBegin, # 'SMS' Week, # 'W' @@ -2964,7 +2510,5 @@ def generate_range(start=None, end=None, periods=None, Day, # 'D' WeekOfMonth, # 'WOM' FY5253, - FY5253Quarter, -]) - -prefix_mapping['N'] = Nano + FY5253Quarter +]} diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py deleted file mode 100644 index 1e1496bbe9c27..0000000000000 --- a/pandas/tseries/period.py +++ /dev/null @@ -1,1181 +0,0 @@ -# pylint: disable=E1101,E1103,W0232 -from datetime import datetime, timedelta -import numpy as np -import warnings - - -from pandas.core import common as com -from pandas.types.common import (is_integer, - is_float, - is_object_dtype, - is_integer_dtype, - is_float_dtype, - is_scalar, - is_datetime64_dtype, - is_datetime64tz_dtype, - is_timedelta64_dtype, - is_period_dtype, - is_bool_dtype, - pandas_dtype, - _ensure_object) -from pandas.types.dtypes import PeriodDtype -from pandas.types.generic import ABCSeries - -import pandas.tseries.frequencies as frequencies -from pandas.tseries.frequencies import get_freq_code as _gfc -from pandas.tseries.index import DatetimeIndex, Int64Index, Index -from pandas.tseries.tdi import TimedeltaIndex -from pandas.tseries.base import DatelikeOps, DatetimeIndexOpsMixin -from pandas.tseries.tools import parse_time_string -import pandas.tseries.offsets as offsets - -from pandas._libs.lib import infer_dtype -from pandas._libs import tslib, period -from pandas._libs.period import (Period, IncompatibleFrequency, - get_period_field_arr, _validate_end_alias, - _quarter_to_myear) - -from pandas.core.base import _shared_docs -from pandas.indexes.base import _index_shared_docs, _ensure_index - -from pandas import compat -from pandas.util.decorators import (Appender, Substitution, cache_readonly, - deprecate_kwarg) -from pandas.compat import zip, u - -import pandas.indexes.base as ibase -_index_doc_kwargs = dict(ibase._index_doc_kwargs) -_index_doc_kwargs.update( - dict(target_klass='PeriodIndex or list of Periods')) - - -def _field_accessor(name, alias, docstring=None): - def f(self): - base, mult = _gfc(self.freq) - result = get_period_field_arr(alias, self._values, base) - return Index(result, name=self.name) - f.__name__ = name - f.__doc__ = docstring - return property(f) - - -def dt64arr_to_periodarr(data, freq, tz): - if data.dtype != np.dtype('M8[ns]'): - raise ValueError('Wrong dtype: %s' % data.dtype) - - freq = Period._maybe_convert_freq(freq) - base, mult = _gfc(freq) - return period.dt64arr_to_periodarr(data.view('i8'), base, tz) - -# --- Period index sketch - - -_DIFFERENT_FREQ_INDEX = period._DIFFERENT_FREQ_INDEX - - -def _period_index_cmp(opname, nat_result=False): - """ - Wrap comparison operations to convert datetime-like to datetime64 - """ - - def wrapper(self, other): - if isinstance(other, Period): - func = getattr(self._values, opname) - other_base, _ = _gfc(other.freq) - if other.freq != self.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) - - result = func(other.ordinal) - elif isinstance(other, PeriodIndex): - if other.freq != self.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) - - result = getattr(self._values, opname)(other._values) - - mask = self._isnan | other._isnan - if mask.any(): - result[mask] = nat_result - - return result - elif other is tslib.NaT: - result = np.empty(len(self._values), dtype=bool) - result.fill(nat_result) - else: - other = Period(other, freq=self.freq) - func = getattr(self._values, opname) - result = func(other.ordinal) - - if self.hasnans: - result[self._isnan] = nat_result - - return result - return wrapper - - -def _new_PeriodIndex(cls, **d): - # GH13277 for unpickling - if d['data'].dtype == 'int64': - values = d.pop('data') - return cls._from_ordinals(values=values, **d) - - -class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index): - """ - Immutable ndarray holding ordinal values indicating regular periods in - time such as particular years, quarters, months, etc. A value of 1 is the - period containing the Gregorian proleptic datetime Jan 1, 0001 00:00:00. - This ordinal representation is from the scikits.timeseries project. - - For instance, - # construct period for day 1/1/1 and get the first second - i = Period(year=1,month=1,day=1,freq='D').asfreq('S', 'S') - i.ordinal - ===> 1 - - Index keys are boxed to Period objects which carries the metadata (eg, - frequency information). - - Parameters - ---------- - data : array-like (1-dimensional), optional - Optional period-like data to construct index with - copy : bool - Make a copy of input ndarray - freq : string or period object, optional - One of pandas period strings or corresponding objects - start : starting value, period-like, optional - If data is None, used as the start point in generating regular - period data. - periods : int, optional, > 0 - Number of periods to generate, if generating index. Takes precedence - over end argument - end : end value, period-like, optional - If periods is none, generated index will extend to first conforming - period on or just past end argument - year : int, array, or Series, default None - month : int, array, or Series, default None - quarter : int, array, or Series, default None - day : int, array, or Series, default None - hour : int, array, or Series, default None - minute : int, array, or Series, default None - second : int, array, or Series, default None - tz : object, default None - Timezone for converting datetime64 data to Periods - dtype : str or PeriodDtype, default None - - Examples - -------- - >>> idx = PeriodIndex(year=year_arr, quarter=q_arr) - - >>> idx2 = PeriodIndex(start='2000', end='2010', freq='A') - """ - _box_scalars = True - _typ = 'periodindex' - _attributes = ['name', 'freq'] - - # define my properties & methods for delegation - _other_ops = [] - _bool_ops = ['is_leap_year'] - _object_ops = ['start_time', 'end_time', 'freq'] - _field_ops = ['year', 'month', 'day', 'hour', 'minute', 'second', - 'weekofyear', 'weekday', 'week', 'dayofweek', - 'dayofyear', 'quarter', 'qyear', - 'days_in_month', 'daysinmonth'] - _datetimelike_ops = _field_ops + _object_ops + _bool_ops - _datetimelike_methods = ['strftime', 'to_timestamp', 'asfreq'] - - _is_numeric_dtype = False - _infer_as_myclass = True - - freq = None - - __eq__ = _period_index_cmp('__eq__') - __ne__ = _period_index_cmp('__ne__', nat_result=True) - __lt__ = _period_index_cmp('__lt__') - __gt__ = _period_index_cmp('__gt__') - __le__ = _period_index_cmp('__le__') - __ge__ = _period_index_cmp('__ge__') - - def __new__(cls, data=None, ordinal=None, freq=None, start=None, end=None, - periods=None, copy=False, name=None, tz=None, dtype=None, - **kwargs): - - if periods is not None: - if is_float(periods): - periods = int(periods) - elif not is_integer(periods): - raise ValueError('Periods must be a number, got %s' % - str(periods)) - - if name is None and hasattr(data, 'name'): - name = data.name - - if dtype is not None: - dtype = pandas_dtype(dtype) - if not is_period_dtype(dtype): - raise ValueError('dtype must be PeriodDtype') - if freq is None: - freq = dtype.freq - elif freq != dtype.freq: - msg = 'specified freq and dtype are different' - raise IncompatibleFrequency(msg) - - # coerce freq to freq object, otherwise it can be coerced elementwise - # which is slow - if freq: - freq = Period._maybe_convert_freq(freq) - - if data is None: - if ordinal is not None: - data = np.asarray(ordinal, dtype=np.int64) - else: - data, freq = cls._generate_range(start, end, periods, - freq, kwargs) - return cls._from_ordinals(data, name=name, freq=freq) - - if isinstance(data, PeriodIndex): - if freq is None or freq == data.freq: # no freq change - freq = data.freq - data = data._values - else: - base1, _ = _gfc(data.freq) - base2, _ = _gfc(freq) - data = period.period_asfreq_arr(data._values, - base1, base2, 1) - return cls._simple_new(data, name=name, freq=freq) - - # not array / index - if not isinstance(data, (np.ndarray, PeriodIndex, - DatetimeIndex, Int64Index)): - if is_scalar(data) or isinstance(data, Period): - cls._scalar_data_error(data) - - # other iterable of some kind - if not isinstance(data, (list, tuple)): - data = list(data) - - data = np.asarray(data) - - # datetime other than period - if is_datetime64_dtype(data.dtype): - data = dt64arr_to_periodarr(data, freq, tz) - return cls._from_ordinals(data, name=name, freq=freq) - - # check not floats - if infer_dtype(data) == 'floating' and len(data) > 0: - raise TypeError("PeriodIndex does not allow " - "floating point in construction") - - # anything else, likely an array of strings or periods - data = _ensure_object(data) - freq = freq or period.extract_freq(data) - data = period.extract_ordinals(data, freq) - return cls._from_ordinals(data, name=name, freq=freq) - - @classmethod - def _generate_range(cls, start, end, periods, freq, fields): - if freq is not None: - freq = Period._maybe_convert_freq(freq) - - field_count = len(fields) - if com._count_not_none(start, end) > 0: - if field_count > 0: - raise ValueError('Can either instantiate from fields ' - 'or endpoints, but not both') - subarr, freq = _get_ordinal_range(start, end, periods, freq) - elif field_count > 0: - subarr, freq = _range_from_fields(freq=freq, **fields) - else: - raise ValueError('Not enough parameters to construct ' - 'Period range') - - return subarr, freq - - @classmethod - def _simple_new(cls, values, name=None, freq=None, **kwargs): - """ - Values can be any type that can be coerced to Periods. - Ordinals in an ndarray are fastpath-ed to `_from_ordinals` - """ - if not is_integer_dtype(values): - values = np.array(values, copy=False) - if len(values) > 0 and is_float_dtype(values): - raise TypeError("PeriodIndex can't take floats") - return cls(values, name=name, freq=freq, **kwargs) - - return cls._from_ordinals(values, name, freq, **kwargs) - - @classmethod - def _from_ordinals(cls, values, name=None, freq=None, **kwargs): - """ - Values should be int ordinals - `__new__` & `_simple_new` cooerce to ordinals and call this method - """ - - values = np.array(values, dtype='int64', copy=False) - - result = object.__new__(cls) - result._data = values - result.name = name - if freq is None: - raise ValueError('freq is not specified and cannot be inferred') - result.freq = Period._maybe_convert_freq(freq) - result._reset_identity() - return result - - def _shallow_copy_with_infer(self, values=None, **kwargs): - """ we always want to return a PeriodIndex """ - return self._shallow_copy(values=values, **kwargs) - - def _shallow_copy(self, values=None, freq=None, **kwargs): - if freq is None: - freq = self.freq - if values is None: - values = self._values - return super(PeriodIndex, self)._shallow_copy(values=values, - freq=freq, **kwargs) - - def _coerce_scalar_to_index(self, item): - """ - we need to coerce a scalar to a compat for our index type - - Parameters - ---------- - item : scalar item to coerce - """ - return PeriodIndex([item], **self._get_attributes_dict()) - - def __contains__(self, key): - if isinstance(key, Period): - if key.freq != self.freq: - return False - else: - return key.ordinal in self._engine - else: - try: - self.get_loc(key) - return True - except Exception: - return False - return False - - @property - def asi8(self): - return self._values.view('i8') - - @cache_readonly - def _int64index(self): - return Int64Index(self.asi8, name=self.name, fastpath=True) - - @property - def values(self): - return self.asobject.values - - @property - def _values(self): - return self._data - - def __array__(self, dtype=None): - if is_integer_dtype(dtype): - return self.asi8 - else: - return self.asobject.values - - def __array_wrap__(self, result, context=None): - """ - Gets called after a ufunc. Needs additional handling as - PeriodIndex stores internal data as int dtype - - Replace this to __numpy_ufunc__ in future version - """ - if isinstance(context, tuple) and len(context) > 0: - func = context[0] - if (func is np.add): - pass - elif (func is np.subtract): - name = self.name - left = context[1][0] - right = context[1][1] - if (isinstance(left, PeriodIndex) and - isinstance(right, PeriodIndex)): - name = left.name if left.name == right.name else None - return Index(result, name=name) - elif isinstance(left, Period) or isinstance(right, Period): - return Index(result, name=name) - elif isinstance(func, np.ufunc): - if 'M->M' not in func.types: - msg = "ufunc '{0}' not supported for the PeriodIndex" - # This should be TypeError, but TypeError cannot be raised - # from here because numpy catches. - raise ValueError(msg.format(func.__name__)) - - if is_bool_dtype(result): - return result - # the result is object dtype array of Period - # cannot pass _simple_new as it is - return self._shallow_copy(result, freq=self.freq, name=self.name) - - @property - def _box_func(self): - return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) - - def _to_embed(self, keep_tz=False): - """ - return an array repr of this object, potentially casting to object - """ - return self.asobject.values - - @property - def _formatter_func(self): - return lambda x: "'%s'" % x - - def asof_locs(self, where, mask): - """ - where : array of timestamps - mask : array of booleans where data is not NA - - """ - where_idx = where - if isinstance(where_idx, DatetimeIndex): - where_idx = PeriodIndex(where_idx.values, freq=self.freq) - - locs = self._values[mask].searchsorted(where_idx._values, side='right') - - locs = np.where(locs > 0, locs - 1, 0) - result = np.arange(len(self))[mask].take(locs) - - first = mask.argmax() - result[(locs == 0) & (where_idx._values < self._values[first])] = -1 - - return result - - @Appender(_index_shared_docs['astype']) - def astype(self, dtype, copy=True, how='start'): - dtype = pandas_dtype(dtype) - if is_object_dtype(dtype): - return self.asobject - elif is_integer_dtype(dtype): - if copy: - return self._int64index.copy() - else: - return self._int64index - elif is_datetime64_dtype(dtype): - return self.to_timestamp(how=how) - elif is_datetime64tz_dtype(dtype): - return self.to_timestamp(how=how).tz_localize(dtype.tz) - elif is_period_dtype(dtype): - return self.asfreq(freq=dtype.freq) - raise ValueError('Cannot cast PeriodIndex to dtype %s' % dtype) - - @Substitution(klass='PeriodIndex') - @Appender(_shared_docs['searchsorted']) - @deprecate_kwarg(old_arg_name='key', new_arg_name='value') - def searchsorted(self, value, side='left', sorter=None): - if isinstance(value, Period): - if value.freq != self.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, value.freqstr) - raise IncompatibleFrequency(msg) - value = value.ordinal - elif isinstance(value, compat.string_types): - value = Period(value, freq=self.freq).ordinal - - return self._values.searchsorted(value, side=side, sorter=sorter) - - @property - def is_all_dates(self): - return True - - @property - def is_full(self): - """ - Returns True if there are any missing periods from start to end - """ - if len(self) == 0: - return True - if not self.is_monotonic: - raise ValueError('Index is not monotonic') - values = self.values - return ((values[1:] - values[:-1]) < 2).all() - - def asfreq(self, freq=None, how='E'): - """ - Convert the PeriodIndex to the specified frequency `freq`. - - Parameters - ---------- - - freq : str - a frequency - how : str {'E', 'S'} - 'E', 'END', or 'FINISH' for end, - 'S', 'START', or 'BEGIN' for start. - Whether the elements should be aligned to the end - or start within pa period. January 31st ('END') vs. - Janury 1st ('START') for example. - - Returns - ------- - - new : PeriodIndex with the new frequency - - Examples - -------- - >>> pidx = pd.period_range('2010-01-01', '2015-01-01', freq='A') - >>> pidx - - [2010, ..., 2015] - Length: 6, Freq: A-DEC - - >>> pidx.asfreq('M') - - [2010-12, ..., 2015-12] - Length: 6, Freq: M - - >>> pidx.asfreq('M', how='S') - - [2010-01, ..., 2015-01] - Length: 6, Freq: M - """ - how = _validate_end_alias(how) - - freq = Period._maybe_convert_freq(freq) - - base1, mult1 = _gfc(self.freq) - base2, mult2 = _gfc(freq) - - asi8 = self.asi8 - # mult1 can't be negative or 0 - end = how == 'E' - if end: - ordinal = asi8 + mult1 - 1 - else: - ordinal = asi8 - - new_data = period.period_asfreq_arr(ordinal, base1, base2, end) - - if self.hasnans: - new_data[self._isnan] = tslib.iNaT - - return self._simple_new(new_data, self.name, freq=freq) - - def to_datetime(self, dayfirst=False): - """ - DEPRECATED: use :meth:`to_timestamp` instead. - - Cast to DatetimeIndex. - """ - warnings.warn("to_datetime is deprecated. Use self.to_timestamp(...)", - FutureWarning, stacklevel=2) - return self.to_timestamp() - - year = _field_accessor('year', 0, "The year of the period") - month = _field_accessor('month', 3, "The month as January=1, December=12") - day = _field_accessor('day', 4, "The days of the period") - hour = _field_accessor('hour', 5, "The hour of the period") - minute = _field_accessor('minute', 6, "The minute of the period") - second = _field_accessor('second', 7, "The second of the period") - weekofyear = _field_accessor('week', 8, "The week ordinal of the year") - week = weekofyear - dayofweek = _field_accessor('dayofweek', 10, - "The day of the week with Monday=0, Sunday=6") - weekday = dayofweek - dayofyear = day_of_year = _field_accessor('dayofyear', 9, - "The ordinal day of the year") - quarter = _field_accessor('quarter', 2, "The quarter of the date") - qyear = _field_accessor('qyear', 1) - days_in_month = _field_accessor('days_in_month', 11, - "The number of days in the month") - daysinmonth = days_in_month - - @property - def is_leap_year(self): - """ Logical indicating if the date belongs to a leap year """ - return tslib._isleapyear_arr(np.asarray(self.year)) - - @property - def start_time(self): - return self.to_timestamp(how='start') - - @property - def end_time(self): - return self.to_timestamp(how='end') - - def _mpl_repr(self): - # how to represent ourselves to matplotlib - return self.asobject.values - - def to_timestamp(self, freq=None, how='start'): - """ - Cast to DatetimeIndex - - Parameters - ---------- - freq : string or DateOffset, default 'D' for week or longer, 'S' - otherwise - Target frequency - how : {'s', 'e', 'start', 'end'} - - Returns - ------- - DatetimeIndex - """ - how = _validate_end_alias(how) - - if freq is None: - base, mult = _gfc(self.freq) - freq = frequencies.get_to_timestamp_base(base) - else: - freq = Period._maybe_convert_freq(freq) - - base, mult = _gfc(freq) - new_data = self.asfreq(freq, how) - - new_data = period.periodarr_to_dt64arr(new_data._values, base) - return DatetimeIndex(new_data, freq='infer', name=self.name) - - def _maybe_convert_timedelta(self, other): - if isinstance(other, (timedelta, np.timedelta64, offsets.Tick)): - offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(offset, offsets.Tick): - nanos = tslib._delta_to_nanoseconds(other) - offset_nanos = tslib._delta_to_nanoseconds(offset) - if nanos % offset_nanos == 0: - return nanos // offset_nanos - elif isinstance(other, offsets.DateOffset): - freqstr = other.rule_code - base = frequencies.get_base_alias(freqstr) - if base == self.freq.rule_code: - return other.n - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) - elif isinstance(other, np.ndarray): - if is_integer_dtype(other): - return other - elif is_timedelta64_dtype(other): - offset = frequencies.to_offset(self.freq) - if isinstance(offset, offsets.Tick): - nanos = tslib._delta_to_nanoseconds(other) - offset_nanos = tslib._delta_to_nanoseconds(offset) - if (nanos % offset_nanos).all() == 0: - return nanos // offset_nanos - elif is_integer(other): - # integer is passed to .shift via - # _add_datetimelike_methods basically - # but ufunc may pass integer to _add_delta - return other - # raise when input doesn't have freq - msg = "Input has different freq from PeriodIndex(freq={0})" - raise IncompatibleFrequency(msg.format(self.freqstr)) - - def _add_delta(self, other): - ordinal_delta = self._maybe_convert_timedelta(other) - return self.shift(ordinal_delta) - - def _sub_datelike(self, other): - if other is tslib.NaT: - new_data = np.empty(len(self), dtype=np.int64) - new_data.fill(tslib.iNaT) - return TimedeltaIndex(new_data, name=self.name) - return NotImplemented - - def _sub_period(self, other): - if self.freq != other.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) - - asi8 = self.asi8 - new_data = asi8 - other.ordinal - - if self.hasnans: - new_data = new_data.astype(np.float64) - new_data[self._isnan] = np.nan - # result must be Int64Index or Float64Index - return Index(new_data, name=self.name) - - def shift(self, n): - """ - Specialized shift which produces an PeriodIndex - - Parameters - ---------- - n : int - Periods to shift by - - Returns - ------- - shifted : PeriodIndex - """ - values = self._values + n * self.freq.n - if self.hasnans: - values[self._isnan] = tslib.iNaT - return self._shallow_copy(values=values) - - @cache_readonly - def dtype(self): - return PeriodDtype.construct_from_string(self.freq) - - @property - def inferred_type(self): - # b/c data is represented as ints make sure we can't have ambiguous - # indexing - return 'period' - - def get_value(self, series, key): - """ - Fast lookup of value from 1-dimensional ndarray. Only use this if you - know what you're doing - """ - s = com._values_from_object(series) - try: - return com._maybe_box(self, - super(PeriodIndex, self).get_value(s, key), - series, key) - except (KeyError, IndexError): - try: - asdt, parsed, reso = parse_time_string(key, self.freq) - grp = frequencies.Resolution.get_freq_group(reso) - freqn = frequencies.get_freq_group(self.freq) - - vals = self._values - - # if our data is higher resolution than requested key, slice - if grp < freqn: - iv = Period(asdt, freq=(grp, 1)) - ord1 = iv.asfreq(self.freq, how='S').ordinal - ord2 = iv.asfreq(self.freq, how='E').ordinal - - if ord2 < vals[0] or ord1 > vals[-1]: - raise KeyError(key) - - pos = np.searchsorted(self._values, [ord1, ord2]) - key = slice(pos[0], pos[1] + 1) - return series[key] - elif grp == freqn: - key = Period(asdt, freq=self.freq).ordinal - return com._maybe_box(self, self._engine.get_value(s, key), - series, key) - else: - raise KeyError(key) - except TypeError: - pass - - key = Period(key, self.freq).ordinal - return com._maybe_box(self, self._engine.get_value(s, key), - series, key) - - @Appender(_index_shared_docs['get_indexer'] % _index_doc_kwargs) - def get_indexer(self, target, method=None, limit=None, tolerance=None): - target = _ensure_index(target) - - if hasattr(target, 'freq') and target.freq != self.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, target.freqstr) - raise IncompatibleFrequency(msg) - - if isinstance(target, PeriodIndex): - target = target.asi8 - - if tolerance is not None: - tolerance = self._convert_tolerance(tolerance) - return Index.get_indexer(self._int64index, target, method, - limit, tolerance) - - def _get_unique_index(self, dropna=False): - """ - wrap Index._get_unique_index to handle NaT - """ - res = super(PeriodIndex, self)._get_unique_index(dropna=dropna) - if dropna: - res = res.dropna() - return res - - def get_loc(self, key, method=None, tolerance=None): - """ - Get integer location for requested label - - Returns - ------- - loc : int - """ - try: - return self._engine.get_loc(key) - except KeyError: - if is_integer(key): - raise - - try: - asdt, parsed, reso = parse_time_string(key, self.freq) - key = asdt - except TypeError: - pass - - try: - key = Period(key, freq=self.freq) - except ValueError: - # we cannot construct the Period - # as we have an invalid type - raise KeyError(key) - - try: - ordinal = tslib.iNaT if key is tslib.NaT else key.ordinal - if tolerance is not None: - tolerance = self._convert_tolerance(tolerance) - return self._int64index.get_loc(ordinal, method, tolerance) - - except KeyError: - raise KeyError(key) - - def _maybe_cast_slice_bound(self, label, side, kind): - """ - If label is a string or a datetime, cast it to Period.ordinal according - to resolution. - - Parameters - ---------- - label : object - side : {'left', 'right'} - kind : {'ix', 'loc', 'getitem'} - - Returns - ------- - bound : Period or object - - Notes - ----- - Value of `side` parameter should be validated in caller. - - """ - assert kind in ['ix', 'loc', 'getitem'] - - if isinstance(label, datetime): - return Period(label, freq=self.freq) - elif isinstance(label, compat.string_types): - try: - _, parsed, reso = parse_time_string(label, self.freq) - bounds = self._parsed_string_to_bounds(reso, parsed) - return bounds[0 if side == 'left' else 1] - except Exception: - raise KeyError(label) - elif is_integer(label) or is_float(label): - self._invalid_indexer('slice', label) - - return label - - def _parsed_string_to_bounds(self, reso, parsed): - if reso == 'year': - t1 = Period(year=parsed.year, freq='A') - elif reso == 'month': - t1 = Period(year=parsed.year, month=parsed.month, freq='M') - elif reso == 'quarter': - q = (parsed.month - 1) // 3 + 1 - t1 = Period(year=parsed.year, quarter=q, freq='Q-DEC') - elif reso == 'day': - t1 = Period(year=parsed.year, month=parsed.month, day=parsed.day, - freq='D') - elif reso == 'hour': - t1 = Period(year=parsed.year, month=parsed.month, day=parsed.day, - hour=parsed.hour, freq='H') - elif reso == 'minute': - t1 = Period(year=parsed.year, month=parsed.month, day=parsed.day, - hour=parsed.hour, minute=parsed.minute, freq='T') - elif reso == 'second': - t1 = Period(year=parsed.year, month=parsed.month, day=parsed.day, - hour=parsed.hour, minute=parsed.minute, - second=parsed.second, freq='S') - else: - raise KeyError(reso) - return (t1.asfreq(self.freq, how='start'), - t1.asfreq(self.freq, how='end')) - - def _get_string_slice(self, key): - if not self.is_monotonic: - raise ValueError('Partial indexing only valid for ' - 'ordered time series') - - key, parsed, reso = parse_time_string(key, self.freq) - grp = frequencies.Resolution.get_freq_group(reso) - freqn = frequencies.get_freq_group(self.freq) - if reso in ['day', 'hour', 'minute', 'second'] and not grp < freqn: - raise KeyError(key) - - t1, t2 = self._parsed_string_to_bounds(reso, parsed) - return slice(self.searchsorted(t1.ordinal, side='left'), - self.searchsorted(t2.ordinal, side='right')) - - def _convert_tolerance(self, tolerance): - tolerance = DatetimeIndexOpsMixin._convert_tolerance(self, tolerance) - return self._maybe_convert_timedelta(tolerance) - - def insert(self, loc, item): - if not isinstance(item, Period) or self.freq != item.freq: - return self.asobject.insert(loc, item) - - idx = np.concatenate((self[:loc].asi8, np.array([item.ordinal]), - self[loc:].asi8)) - return self._shallow_copy(idx) - - def join(self, other, how='left', level=None, return_indexers=False): - """ - See Index.join - """ - self._assert_can_do_setop(other) - - result = Int64Index.join(self, other, how=how, level=level, - return_indexers=return_indexers) - - if return_indexers: - result, lidx, ridx = result - return self._apply_meta(result), lidx, ridx - return self._apply_meta(result) - - def _assert_can_do_setop(self, other): - super(PeriodIndex, self)._assert_can_do_setop(other) - - if not isinstance(other, PeriodIndex): - raise ValueError('can only call with other PeriodIndex-ed objects') - - if self.freq != other.freq: - msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr) - raise IncompatibleFrequency(msg) - - def _wrap_union_result(self, other, result): - name = self.name if self.name == other.name else None - result = self._apply_meta(result) - result.name = name - return result - - def _apply_meta(self, rawarr): - if not isinstance(rawarr, PeriodIndex): - rawarr = PeriodIndex._from_ordinals(rawarr, freq=self.freq, - name=self.name) - return rawarr - - def _format_native_types(self, na_rep=u('NaT'), date_format=None, - **kwargs): - - values = self.asobject.values - - if date_format: - formatter = lambda dt: dt.strftime(date_format) - else: - formatter = lambda dt: u('%s') % dt - - if self.hasnans: - mask = self._isnan - values[mask] = na_rep - imask = ~mask - values[imask] = np.array([formatter(dt) for dt - in values[imask]]) - else: - values = np.array([formatter(dt) for dt in values]) - return values - - def __setstate__(self, state): - """Necessary for making this object picklable""" - - if isinstance(state, dict): - super(PeriodIndex, self).__setstate__(state) - - elif isinstance(state, tuple): - - # < 0.15 compat - if len(state) == 2: - nd_state, own_state = state - data = np.empty(nd_state[1], dtype=nd_state[2]) - np.ndarray.__setstate__(data, nd_state) - - # backcompat - self.freq = Period._maybe_convert_freq(own_state[1]) - - else: # pragma: no cover - data = np.empty(state) - np.ndarray.__setstate__(self, state) - - self._data = data - - else: - raise Exception("invalid pickle state") - - _unpickle_compat = __setstate__ - - def tz_convert(self, tz): - """ - Convert tz-aware DatetimeIndex from one time zone to another (using - pytz/dateutil) - - Parameters - ---------- - tz : string, pytz.timezone, dateutil.tz.tzfile or None - Time zone for time. Corresponding timestamps would be converted to - time zone of the TimeSeries. - None will remove timezone holding UTC time. - - Returns - ------- - normalized : DatetimeIndex - - Note - ---- - Not currently implemented for PeriodIndex - """ - raise NotImplementedError("Not yet implemented for PeriodIndex") - - def tz_localize(self, tz, infer_dst=False): - """ - Localize tz-naive DatetimeIndex to given time zone (using - pytz/dateutil), or remove timezone from tz-aware DatetimeIndex - - Parameters - ---------- - tz : string, pytz.timezone, dateutil.tz.tzfile or None - Time zone for time. Corresponding timestamps would be converted to - time zone of the TimeSeries. - None will remove timezone holding local time. - infer_dst : boolean, default False - Attempt to infer fall dst-transition hours based on order - - Returns - ------- - localized : DatetimeIndex - - Note - ---- - Not currently implemented for PeriodIndex - """ - raise NotImplementedError("Not yet implemented for PeriodIndex") - - -PeriodIndex._add_numeric_methods_disabled() -PeriodIndex._add_logical_methods_disabled() -PeriodIndex._add_datetimelike_methods() - - -def _get_ordinal_range(start, end, periods, freq, mult=1): - if com._count_not_none(start, end, periods) < 2: - raise ValueError('Must specify 2 of start, end, periods') - - if freq is not None: - _, mult = _gfc(freq) - - if start is not None: - start = Period(start, freq) - if end is not None: - end = Period(end, freq) - - is_start_per = isinstance(start, Period) - is_end_per = isinstance(end, Period) - - if is_start_per and is_end_per and start.freq != end.freq: - raise ValueError('Start and end must have same freq') - if (start is tslib.NaT or end is tslib.NaT): - raise ValueError('Start and end must not be NaT') - - if freq is None: - if is_start_per: - freq = start.freq - elif is_end_per: - freq = end.freq - else: # pragma: no cover - raise ValueError('Could not infer freq from start/end') - - if periods is not None: - periods = periods * mult - if start is None: - data = np.arange(end.ordinal - periods + mult, - end.ordinal + 1, mult, - dtype=np.int64) - else: - data = np.arange(start.ordinal, start.ordinal + periods, mult, - dtype=np.int64) - else: - data = np.arange(start.ordinal, end.ordinal + 1, mult, dtype=np.int64) - - return data, freq - - -def _range_from_fields(year=None, month=None, quarter=None, day=None, - hour=None, minute=None, second=None, freq=None): - if hour is None: - hour = 0 - if minute is None: - minute = 0 - if second is None: - second = 0 - if day is None: - day = 1 - - ordinals = [] - - if quarter is not None: - if freq is None: - freq = 'Q' - base = frequencies.FreqGroup.FR_QTR - else: - base, mult = _gfc(freq) - if base != frequencies.FreqGroup.FR_QTR: - raise AssertionError("base must equal FR_QTR") - - year, quarter = _make_field_arrays(year, quarter) - for y, q in zip(year, quarter): - y, m = _quarter_to_myear(y, q, freq) - val = period.period_ordinal(y, m, 1, 1, 1, 1, 0, 0, base) - ordinals.append(val) - else: - base, mult = _gfc(freq) - arrays = _make_field_arrays(year, month, day, hour, minute, second) - for y, mth, d, h, mn, s in zip(*arrays): - ordinals.append(period.period_ordinal( - y, mth, d, h, mn, s, 0, 0, base)) - - return np.array(ordinals, dtype=np.int64), freq - - -def _make_field_arrays(*fields): - length = None - for x in fields: - if isinstance(x, (list, np.ndarray, ABCSeries)): - if length is not None and len(x) != length: - raise ValueError('Mismatched Period array lengths') - elif length is None: - length = len(x) - - arrays = [np.asarray(x) if isinstance(x, (np.ndarray, list, ABCSeries)) - else np.repeat(x, length) for x in fields] - - return arrays - - -def pnow(freq=None): - # deprecation, xref #13790 - import warnings - - warnings.warn("pd.pnow() and pandas.tseries.period.pnow() " - "are deprecated. Please use Period.now()", - FutureWarning, stacklevel=2) - return Period.now(freq=freq) - - -def period_range(start=None, end=None, periods=None, freq='D', name=None): - """ - Return a fixed frequency datetime index, with day (calendar) as the default - frequency - - - Parameters - ---------- - start : starting value, period-like, optional - end : ending value, period-like, optional - periods : int, default None - Number of periods in the index - freq : str/DateOffset, default 'D' - Frequency alias - name : str, default None - Name for the resulting PeriodIndex - - Returns - ------- - prng : PeriodIndex - """ - return PeriodIndex(start=start, end=end, periods=periods, - freq=freq, name=name) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 4eddf54701889..302016907635d 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -1,344 +1,3 @@ -""" -Period formatters and locators adapted from scikits.timeseries by -Pierre GF Gerard-Marchant & Matt Knox -""" +# flake8: noqa -# TODO: Use the fact that axis can have units to simplify the process - -import numpy as np - -from matplotlib import pylab -from pandas.tseries.period import Period -from pandas.tseries.offsets import DateOffset -import pandas.tseries.frequencies as frequencies -from pandas.tseries.index import DatetimeIndex -from pandas.tseries.period import PeriodIndex -from pandas.tseries.tdi import TimedeltaIndex -from pandas.formats.printing import pprint_thing -import pandas.compat as compat - -from pandas.tseries.converter import (TimeSeries_DateLocator, - TimeSeries_DateFormatter, - TimeSeries_TimedeltaFormatter) - -# --------------------------------------------------------------------- -# Plotting functions and monkey patches - - -def tsplot(series, plotf, ax=None, **kwargs): - """ - Plots a Series on the given Matplotlib axes or the current axes - - Parameters - ---------- - axes : Axes - series : Series - - Notes - _____ - Supports same kwargs as Axes.plot - - """ - # Used inferred freq is possible, need a test case for inferred - if ax is None: - import matplotlib.pyplot as plt - ax = plt.gca() - - freq, series = _maybe_resample(series, ax, kwargs) - - # Set ax with freq info - _decorate_axes(ax, freq, kwargs) - ax._plot_data.append((series, plotf, kwargs)) - lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs) - - # set date formatter, locators and rescale limits - format_dateaxis(ax, ax.freq, series.index) - return lines - - -def _maybe_resample(series, ax, kwargs): - # resample against axes freq if necessary - freq, ax_freq = _get_freq(ax, series) - - if freq is None: # pragma: no cover - raise ValueError('Cannot use dynamic axis without frequency info') - - # Convert DatetimeIndex to PeriodIndex - if isinstance(series.index, DatetimeIndex): - series = series.to_period(freq=freq) - - if ax_freq is not None and freq != ax_freq: - if frequencies.is_superperiod(freq, ax_freq): # upsample input - series = series.copy() - series.index = series.index.asfreq(ax_freq, how='s') - freq = ax_freq - elif _is_sup(freq, ax_freq): # one is weekly - how = kwargs.pop('how', 'last') - series = getattr(series.resample('D'), how)().dropna() - series = getattr(series.resample(ax_freq), how)().dropna() - freq = ax_freq - elif frequencies.is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq): - _upsample_others(ax, freq, kwargs) - ax_freq = freq - else: # pragma: no cover - raise ValueError('Incompatible frequency conversion') - return freq, series - - -def _is_sub(f1, f2): - return ((f1.startswith('W') and frequencies.is_subperiod('D', f2)) or - (f2.startswith('W') and frequencies.is_subperiod(f1, 'D'))) - - -def _is_sup(f1, f2): - return ((f1.startswith('W') and frequencies.is_superperiod('D', f2)) or - (f2.startswith('W') and frequencies.is_superperiod(f1, 'D'))) - - -def _upsample_others(ax, freq, kwargs): - legend = ax.get_legend() - lines, labels = _replot_ax(ax, freq, kwargs) - _replot_ax(ax, freq, kwargs) - - other_ax = None - if hasattr(ax, 'left_ax'): - other_ax = ax.left_ax - if hasattr(ax, 'right_ax'): - other_ax = ax.right_ax - - if other_ax is not None: - rlines, rlabels = _replot_ax(other_ax, freq, kwargs) - lines.extend(rlines) - labels.extend(rlabels) - - if (legend is not None and kwargs.get('legend', True) and - len(lines) > 0): - title = legend.get_title().get_text() - if title == 'None': - title = None - ax.legend(lines, labels, loc='best', title=title) - - -def _replot_ax(ax, freq, kwargs): - data = getattr(ax, '_plot_data', None) - - # clear current axes and data - ax._plot_data = [] - ax.clear() - - _decorate_axes(ax, freq, kwargs) - - lines = [] - labels = [] - if data is not None: - for series, plotf, kwds in data: - series = series.copy() - idx = series.index.asfreq(freq, how='S') - series.index = idx - ax._plot_data.append((series, plotf, kwds)) - - # for tsplot - if isinstance(plotf, compat.string_types): - from pandas.tools.plotting import _plot_klass - plotf = _plot_klass[plotf]._plot - - lines.append(plotf(ax, series.index._mpl_repr(), - series.values, **kwds)[0]) - labels.append(pprint_thing(series.name)) - - return lines, labels - - -def _decorate_axes(ax, freq, kwargs): - """Initialize axes for time-series plotting""" - if not hasattr(ax, '_plot_data'): - ax._plot_data = [] - - ax.freq = freq - xaxis = ax.get_xaxis() - xaxis.freq = freq - if not hasattr(ax, 'legendlabels'): - ax.legendlabels = [kwargs.get('label', None)] - else: - ax.legendlabels.append(kwargs.get('label', None)) - ax.view_interval = None - ax.date_axis_info = None - - -def _get_ax_freq(ax): - """ - Get the freq attribute of the ax object if set. - Also checks shared axes (eg when using secondary yaxis, sharex=True - or twinx) - """ - ax_freq = getattr(ax, 'freq', None) - if ax_freq is None: - # check for left/right ax in case of secondary yaxis - if hasattr(ax, 'left_ax'): - ax_freq = getattr(ax.left_ax, 'freq', None) - elif hasattr(ax, 'right_ax'): - ax_freq = getattr(ax.right_ax, 'freq', None) - if ax_freq is None: - # check if a shared ax (sharex/twinx) has already freq set - shared_axes = ax.get_shared_x_axes().get_siblings(ax) - if len(shared_axes) > 1: - for shared_ax in shared_axes: - ax_freq = getattr(shared_ax, 'freq', None) - if ax_freq is not None: - break - return ax_freq - - -def _get_freq(ax, series): - # get frequency from data - freq = getattr(series.index, 'freq', None) - if freq is None: - freq = getattr(series.index, 'inferred_freq', None) - - ax_freq = _get_ax_freq(ax) - - # use axes freq if no data freq - if freq is None: - freq = ax_freq - - # get the period frequency - if isinstance(freq, DateOffset): - freq = freq.rule_code - else: - freq = frequencies.get_base_alias(freq) - - freq = frequencies.get_period_alias(freq) - return freq, ax_freq - - -def _use_dynamic_x(ax, data): - freq = _get_index_freq(data) - ax_freq = _get_ax_freq(ax) - - if freq is None: # convert irregular if axes has freq info - freq = ax_freq - else: # do not use tsplot if irregular was plotted first - if (ax_freq is None) and (len(ax.get_lines()) > 0): - return False - - if freq is None: - return False - - if isinstance(freq, DateOffset): - freq = freq.rule_code - else: - freq = frequencies.get_base_alias(freq) - freq = frequencies.get_period_alias(freq) - - if freq is None: - return False - - # hack this for 0.10.1, creating more technical debt...sigh - if isinstance(data.index, DatetimeIndex): - base = frequencies.get_freq(freq) - x = data.index - if (base <= frequencies.FreqGroup.FR_DAY): - return x[:1].is_normalized - return Period(x[0], freq).to_timestamp(tz=x.tz) == x[0] - return True - - -def _get_index_freq(data): - freq = getattr(data.index, 'freq', None) - if freq is None: - freq = getattr(data.index, 'inferred_freq', None) - if freq == 'B': - weekdays = np.unique(data.index.dayofweek) - if (5 in weekdays) or (6 in weekdays): - freq = None - return freq - - -def _maybe_convert_index(ax, data): - # tsplot converts automatically, but don't want to convert index - # over and over for DataFrames - if isinstance(data.index, DatetimeIndex): - freq = getattr(data.index, 'freq', None) - - if freq is None: - freq = getattr(data.index, 'inferred_freq', None) - if isinstance(freq, DateOffset): - freq = freq.rule_code - - if freq is None: - freq = _get_ax_freq(ax) - - if freq is None: - raise ValueError('Could not get frequency alias for plotting') - - freq = frequencies.get_base_alias(freq) - freq = frequencies.get_period_alias(freq) - - data = data.to_period(freq=freq) - return data - - -# Patch methods for subplot. Only format_dateaxis is currently used. -# Do we need the rest for convenience? - -def format_timedelta_ticks(x, pos, n_decimals): - """ - Convert seconds to 'D days HH:MM:SS.F' - """ - s, ns = divmod(x, 1e9) - m, s = divmod(s, 60) - h, m = divmod(m, 60) - d, h = divmod(h, 24) - decimals = int(ns * 10**(n_decimals - 9)) - s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s)) - if n_decimals > 0: - s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals) - if d != 0: - s = '{:d} days '.format(int(d)) + s - return s - - -def format_dateaxis(subplot, freq, index): - """ - Pretty-formats the date axis (x-axis). - - Major and minor ticks are automatically set for the frequency of the - current underlying series. As the dynamic mode is activated by - default, changing the limits of the x axis will intelligently change - the positions of the ticks. - """ - - # handle index specific formatting - # Note: DatetimeIndex does not use this - # interface. DatetimeIndex uses matplotlib.date directly - if isinstance(index, PeriodIndex): - - majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, - minor_locator=False, - plot_obj=subplot) - minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, - minor_locator=True, - plot_obj=subplot) - subplot.xaxis.set_major_locator(majlocator) - subplot.xaxis.set_minor_locator(minlocator) - - majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, - minor_locator=False, - plot_obj=subplot) - minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, - minor_locator=True, - plot_obj=subplot) - subplot.xaxis.set_major_formatter(majformatter) - subplot.xaxis.set_minor_formatter(minformatter) - - # x and y coord info - subplot.format_coord = lambda t, y: ( - "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) - - elif isinstance(index, TimedeltaIndex): - subplot.xaxis.set_major_formatter( - TimeSeries_TimedeltaFormatter()) - else: - raise TypeError('index type not supported') - - pylab.draw_if_interactive() +from pandas.plotting._timeseries import tsplot diff --git a/pandas/tseries/resample.py b/pandas/tseries/resample.py deleted file mode 100755 index 2856b54ad9a8c..0000000000000 --- a/pandas/tseries/resample.py +++ /dev/null @@ -1,1398 +0,0 @@ -from datetime import timedelta -import numpy as np -import warnings -import copy - -import pandas as pd -from pandas.core.base import AbstractMethodError, GroupByMixin - -from pandas.core.groupby import (BinGrouper, Grouper, _GroupBy, GroupBy, - SeriesGroupBy, groupby, PanelGroupBy) - -from pandas.tseries.frequencies import to_offset, is_subperiod, is_superperiod -from pandas.tseries.index import DatetimeIndex, date_range -from pandas.tseries.tdi import TimedeltaIndex -from pandas.tseries.offsets import DateOffset, Tick, Day, _delta_to_nanoseconds -from pandas.tseries.period import PeriodIndex, period_range -import pandas.core.common as com -import pandas.core.algorithms as algos - -import pandas.compat as compat -from pandas.compat.numpy import function as nv - -from pandas._libs import lib, tslib -from pandas._libs.lib import Timestamp -from pandas._libs.period import IncompatibleFrequency - -from pandas.util.decorators import Appender -from pandas.core.generic import _shared_docs -_shared_docs_kwargs = dict() - - -class Resampler(_GroupBy): - - """ - Class for resampling datetimelike data, a groupby-like operation. - See aggregate, transform, and apply functions on this object. - - It's easiest to use obj.resample(...) to use Resampler. - - Parameters - ---------- - obj : pandas object - groupby : a TimeGrouper object - axis : int, default 0 - kind : str or None - 'period', 'timestamp' to override default index treatement - - Notes - ----- - After resampling, see aggregate, apply, and transform functions. - - Returns - ------- - a Resampler of the appropriate type - """ - - # to the groupby descriptor - _attributes = ['freq', 'axis', 'closed', 'label', 'convention', - 'loffset', 'base', 'kind'] - - # API compat of allowed attributes - _deprecated_valids = _attributes + ['__doc__', '_cache', '_attributes', - 'binner', 'grouper', 'groupby', - 'sort', 'kind', 'squeeze', 'keys', - 'group_keys', 'as_index', 'exclusions', - '_groupby'] - - # don't raise deprecation warning on attributes starting with these - # patterns - prevents warnings caused by IPython introspection - _deprecated_valid_patterns = ['_ipython', '_repr'] - - # API compat of disallowed attributes - _deprecated_invalids = ['iloc', 'loc', 'ix', 'iat', 'at'] - - def __init__(self, obj, groupby=None, axis=0, kind=None, **kwargs): - self.groupby = groupby - self.keys = None - self.sort = True - self.axis = axis - self.kind = kind - self.squeeze = False - self.group_keys = True - self.as_index = True - self.exclusions = set() - self.binner = None - self.grouper = None - - if self.groupby is not None: - self.groupby._set_grouper(self._convert_obj(obj), sort=True) - - def __unicode__(self): - """ provide a nice str repr of our rolling object """ - attrs = ["{k}={v}".format(k=k, v=getattr(self.groupby, k)) - for k in self._attributes if - getattr(self.groupby, k, None) is not None] - return "{klass} [{attrs}]".format(klass=self.__class__.__name__, - attrs=', '.join(attrs)) - - @property - def obj(self): - return self.groupby.obj - - @property - def ax(self): - return self.groupby.ax - - @property - def _typ(self): - """ masquerade for compat as a Series or a DataFrame """ - if isinstance(self._selected_obj, pd.Series): - return 'series' - return 'dataframe' - - @property - def _from_selection(self): - """ is the resampling from a DataFrame column or MultiIndex level """ - # upsampling and PeriodIndex resampling do not work - # with selection, this state used to catch and raise an error - return (self.groupby is not None and - (self.groupby.key is not None or - self.groupby.level is not None)) - - def _deprecated(self, op): - warnings.warn(("\n.resample() is now a deferred operation\n" - "You called {op}(...) on this deferred object " - "which materialized it into a {klass}\nby implicitly " - "taking the mean. Use .resample(...).mean() " - "instead").format(op=op, klass=self._typ), - FutureWarning, stacklevel=3) - return self.mean() - - def _make_deprecated_binop(op): - # op is a string - - def _evaluate_numeric_binop(self, other): - result = self._deprecated(op) - return getattr(result, op)(other) - return _evaluate_numeric_binop - - def _make_deprecated_unary(op, name): - # op is a callable - - def _evaluate_numeric_unary(self): - result = self._deprecated(name) - return op(result) - return _evaluate_numeric_unary - - def __array__(self): - return self._deprecated('__array__').__array__() - - __gt__ = _make_deprecated_binop('__gt__') - __ge__ = _make_deprecated_binop('__ge__') - __lt__ = _make_deprecated_binop('__lt__') - __le__ = _make_deprecated_binop('__le__') - __eq__ = _make_deprecated_binop('__eq__') - __ne__ = _make_deprecated_binop('__ne__') - - __add__ = __radd__ = _make_deprecated_binop('__add__') - __sub__ = __rsub__ = _make_deprecated_binop('__sub__') - __mul__ = __rmul__ = _make_deprecated_binop('__mul__') - __floordiv__ = __rfloordiv__ = _make_deprecated_binop('__floordiv__') - __truediv__ = __rtruediv__ = _make_deprecated_binop('__truediv__') - if not compat.PY3: - __div__ = __rdiv__ = _make_deprecated_binop('__div__') - __neg__ = _make_deprecated_unary(lambda x: -x, '__neg__') - __pos__ = _make_deprecated_unary(lambda x: x, '__pos__') - __abs__ = _make_deprecated_unary(lambda x: np.abs(x), '__abs__') - __inv__ = _make_deprecated_unary(lambda x: -x, '__inv__') - - def __getattr__(self, attr): - if attr in self._internal_names_set: - return object.__getattribute__(self, attr) - if attr in self._attributes: - return getattr(self.groupby, attr) - if attr in self.obj: - return self[attr] - - if attr in self._deprecated_invalids: - raise ValueError(".resample() is now a deferred operation\n" - "\tuse .resample(...).mean() instead of " - ".resample(...)") - - matches_pattern = any(attr.startswith(x) for x - in self._deprecated_valid_patterns) - if not matches_pattern and attr not in self._deprecated_valids: - self = self._deprecated(attr) - - return object.__getattribute__(self, attr) - - def __setattr__(self, attr, value): - if attr not in self._deprecated_valids: - raise ValueError("cannot set values on {0}".format( - self.__class__.__name__)) - object.__setattr__(self, attr, value) - - def __getitem__(self, key): - try: - return super(Resampler, self).__getitem__(key) - except (KeyError, com.AbstractMethodError): - - # compat for deprecated - if isinstance(self.obj, com.ABCSeries): - return self._deprecated('__getitem__')[key] - - raise - - def __setitem__(self, attr, value): - raise ValueError("cannot set items on {0}".format( - self.__class__.__name__)) - - def _convert_obj(self, obj): - """ - provide any conversions for the object in order to correctly handle - - Parameters - ---------- - obj : the object to be resampled - - Returns - ------- - obj : converted object - """ - obj = obj._consolidate() - return obj - - def _get_binner_for_time(self): - raise AbstractMethodError(self) - - def _set_binner(self): - """ - setup our binners - cache these as we are an immutable object - """ - - if self.binner is None: - self.binner, self.grouper = self._get_binner() - - def _get_binner(self): - """ - create the BinGrouper, assume that self.set_grouper(obj) - has already been called - """ - - binner, bins, binlabels = self._get_binner_for_time() - bin_grouper = BinGrouper(bins, binlabels) - return binner, bin_grouper - - def _assure_grouper(self): - """ make sure that we are creating our binner & grouper """ - self._set_binner() - - def plot(self, *args, **kwargs): - # for compat with prior versions, we want to - # have the warnings shown here and just have this work - return self._deprecated('plot').plot(*args, **kwargs) - - def aggregate(self, arg, *args, **kwargs): - """ - Apply aggregation function or functions to resampled groups, yielding - most likely Series but in some cases DataFrame depending on the output - of the aggregation function - - Parameters - ---------- - func_or_funcs : function or list / dict of functions - List/dict of functions will produce DataFrame with column names - determined by the function names themselves (list) or the keys in - the dict - - Notes - ----- - agg is an alias for aggregate. Use it. - - Examples - -------- - >>> s = Series([1,2,3,4,5], - index=pd.date_range('20130101', - periods=5,freq='s')) - 2013-01-01 00:00:00 1 - 2013-01-01 00:00:01 2 - 2013-01-01 00:00:02 3 - 2013-01-01 00:00:03 4 - 2013-01-01 00:00:04 5 - Freq: S, dtype: int64 - - >>> r = s.resample('2s') - DatetimeIndexResampler [freq=<2 * Seconds>, axis=0, closed=left, - label=left, convention=start, base=0] - - >>> r.agg(np.sum) - 2013-01-01 00:00:00 3 - 2013-01-01 00:00:02 7 - 2013-01-01 00:00:04 5 - Freq: 2S, dtype: int64 - - >>> r.agg(['sum','mean','max']) - sum mean max - 2013-01-01 00:00:00 3 1.5 2 - 2013-01-01 00:00:02 7 3.5 4 - 2013-01-01 00:00:04 5 5.0 5 - - >>> r.agg({'result' : lambda x: x.mean() / x.std(), - 'total' : np.sum}) - total result - 2013-01-01 00:00:00 3 2.121320 - 2013-01-01 00:00:02 7 4.949747 - 2013-01-01 00:00:04 5 NaN - - See also - -------- - transform - - Returns - ------- - Series or DataFrame - """ - - self._set_binner() - result, how = self._aggregate(arg, *args, **kwargs) - if result is None: - result = self._groupby_and_aggregate(arg, - *args, - **kwargs) - - result = self._apply_loffset(result) - return result - - agg = aggregate - apply = aggregate - - def transform(self, arg, *args, **kwargs): - """ - Call function producing a like-indexed Series on each group and return - a Series with the transformed values - - Parameters - ---------- - func : function - To apply to each group. Should return a Series with the same index - - Examples - -------- - >>> resampled.transform(lambda x: (x - x.mean()) / x.std()) - - Returns - ------- - transformed : Series - """ - return self._selected_obj.groupby(self.groupby).transform( - arg, *args, **kwargs) - - def _downsample(self, f): - raise AbstractMethodError(self) - - def _upsample(self, f, limit=None, fill_value=None): - raise AbstractMethodError(self) - - def _gotitem(self, key, ndim, subset=None): - """ - sub-classes to define - return a sliced object - - Parameters - ---------- - key : string / list of selections - ndim : 1,2 - requested ndim of result - subset : object, default None - subset to act on - """ - self._set_binner() - grouper = self.grouper - if subset is None: - subset = self.obj - grouped = groupby(subset, by=None, grouper=grouper, axis=self.axis) - - # try the key selection - try: - return grouped[key] - except KeyError: - return grouped - - def _groupby_and_aggregate(self, how, grouper=None, *args, **kwargs): - """ re-evaluate the obj with a groupby aggregation """ - - if grouper is None: - self._set_binner() - grouper = self.grouper - - obj = self._selected_obj - - try: - grouped = groupby(obj, by=None, grouper=grouper, axis=self.axis) - except TypeError: - - # panel grouper - grouped = PanelGroupBy(obj, grouper=grouper, axis=self.axis) - - try: - result = grouped.aggregate(how, *args, **kwargs) - except Exception: - - # we have a non-reducing function - # try to evaluate - result = grouped.apply(how, *args, **kwargs) - - result = self._apply_loffset(result) - return self._wrap_result(result) - - def _apply_loffset(self, result): - """ - if loffset is set, offset the result index - - This is NOT an idempotent routine, it will be applied - exactly once to the result. - - Parameters - ---------- - result : Series or DataFrame - the result of resample - """ - - needs_offset = ( - isinstance(self.loffset, (DateOffset, timedelta)) and - isinstance(result.index, DatetimeIndex) and - len(result.index) > 0 - ) - - if needs_offset: - result.index = result.index + self.loffset - - self.loffset = None - return result - - def _get_resampler_for_grouping(self, groupby, **kwargs): - """ return the correct class for resampling with groupby """ - return self._resampler_for_grouping(self, groupby=groupby, **kwargs) - - def _wrap_result(self, result): - """ potentially wrap any results """ - if isinstance(result, com.ABCSeries) and self._selection is not None: - result.name = self._selection - - return result - - def pad(self, limit=None): - """ - Forward fill the values - - Parameters - ---------- - limit : integer, optional - limit of how many values to fill - - See Also - -------- - Series.fillna - DataFrame.fillna - """ - return self._upsample('pad', limit=limit) - ffill = pad - - def backfill(self, limit=None): - """ - Backward fill the values - - Parameters - ---------- - limit : integer, optional - limit of how many values to fill - - See Also - -------- - Series.fillna - DataFrame.fillna - """ - return self._upsample('backfill', limit=limit) - bfill = backfill - - def fillna(self, method, limit=None): - """ - Fill missing values - - Parameters - ---------- - method : str, method of resampling ('ffill', 'bfill') - limit : integer, optional - limit of how many values to fill - - See Also - -------- - Series.fillna - DataFrame.fillna - """ - return self._upsample(method, limit=limit) - - @Appender(_shared_docs['interpolate'] % _shared_docs_kwargs) - def interpolate(self, method='linear', axis=0, limit=None, inplace=False, - limit_direction='forward', downcast=None, **kwargs): - """ - Interpolate values according to different methods. - - .. versionadded:: 0.18.1 - """ - result = self._upsample(None) - return result.interpolate(method=method, axis=axis, limit=limit, - inplace=inplace, - limit_direction=limit_direction, - downcast=downcast, **kwargs) - - def asfreq(self, fill_value=None): - """ - return the values at the new freq, - essentially a reindex - - Parameters - ---------- - fill_value: scalar, optional - Value to use for missing values, applied during upsampling (note - this does not fill NaNs that already were present). - - .. versionadded:: 0.20.0 - - See Also - -------- - Series.asfreq - DataFrame.asfreq - """ - return self._upsample('asfreq', fill_value=fill_value) - - def std(self, ddof=1, *args, **kwargs): - """ - Compute standard deviation of groups, excluding missing values - - Parameters - ---------- - ddof : integer, default 1 - degrees of freedom - """ - nv.validate_resampler_func('std', args, kwargs) - return self._downsample('std', ddof=ddof) - - def var(self, ddof=1, *args, **kwargs): - """ - Compute variance of groups, excluding missing values - - Parameters - ---------- - ddof : integer, default 1 - degrees of freedom - """ - nv.validate_resampler_func('var', args, kwargs) - return self._downsample('var', ddof=ddof) - - -Resampler._deprecated_valids += dir(Resampler) - -# downsample methods -for method in ['min', 'max', 'first', 'last', 'sum', 'mean', 'sem', - 'median', 'prod', 'ohlc']: - - def f(self, _method=method, *args, **kwargs): - nv.validate_resampler_func(_method, args, kwargs) - return self._downsample(_method) - f.__doc__ = getattr(GroupBy, method).__doc__ - setattr(Resampler, method, f) - -# groupby & aggregate methods -for method in ['count', 'size']: - - def f(self, _method=method): - return self._downsample(_method) - f.__doc__ = getattr(GroupBy, method).__doc__ - setattr(Resampler, method, f) - -# series only methods -for method in ['nunique']: - def f(self, _method=method): - return self._downsample(_method) - f.__doc__ = getattr(SeriesGroupBy, method).__doc__ - setattr(Resampler, method, f) - - -def _maybe_process_deprecations(r, how=None, fill_method=None, limit=None): - """ potentially we might have a deprecation warning, show it - but call the appropriate methods anyhow """ - - if how is not None: - - # .resample(..., how='sum') - if isinstance(how, compat.string_types): - method = "{0}()".format(how) - - # .resample(..., how=lambda x: ....) - else: - method = ".apply()" - - # if we have both a how and fill_method, then show - # the following warning - if fill_method is None: - warnings.warn("how in .resample() is deprecated\n" - "the new syntax is " - ".resample(...).{method}".format( - method=method), - FutureWarning, stacklevel=3) - r = r.aggregate(how) - - if fill_method is not None: - - # show the prior function call - method = '.' + method if how is not None else '' - - args = "limit={0}".format(limit) if limit is not None else "" - warnings.warn("fill_method is deprecated to .resample()\n" - "the new syntax is .resample(...){method}" - ".{fill_method}({args})".format( - method=method, - fill_method=fill_method, - args=args), - FutureWarning, stacklevel=3) - - if how is not None: - r = getattr(r, fill_method)(limit=limit) - else: - r = r.aggregate(fill_method, limit=limit) - - return r - - -class _GroupByMixin(GroupByMixin): - """ provide the groupby facilities """ - - def __init__(self, obj, *args, **kwargs): - - parent = kwargs.pop('parent', None) - groupby = kwargs.pop('groupby', None) - if parent is None: - parent = obj - - # initialize our GroupByMixin object with - # the resampler attributes - for attr in self._attributes: - setattr(self, attr, kwargs.get(attr, getattr(parent, attr))) - - super(_GroupByMixin, self).__init__(None) - self._groupby = groupby - self._groupby.mutated = True - self._groupby.grouper.mutated = True - self.groupby = copy.copy(parent.groupby) - - def _apply(self, f, **kwargs): - """ - dispatch to _upsample; we are stripping all of the _upsample kwargs and - performing the original function call on the grouped object - """ - - def func(x): - x = self._shallow_copy(x, groupby=self.groupby) - - if isinstance(f, compat.string_types): - return getattr(x, f)(**kwargs) - - return x.apply(f, **kwargs) - - result = self._groupby.apply(func) - return self._wrap_result(result) - - _upsample = _apply - _downsample = _apply - _groupby_and_aggregate = _apply - - -class DatetimeIndexResampler(Resampler): - - @property - def _resampler_for_grouping(self): - return DatetimeIndexResamplerGroupby - - def _get_binner_for_time(self): - - # this is how we are actually creating the bins - if self.kind == 'period': - return self.groupby._get_time_period_bins(self.ax) - return self.groupby._get_time_bins(self.ax) - - def _downsample(self, how, **kwargs): - """ - Downsample the cython defined function - - Parameters - ---------- - how : string / cython mapped function - **kwargs : kw args passed to how function - """ - self._set_binner() - how = self._is_cython_func(how) or how - ax = self.ax - obj = self._selected_obj - - if not len(ax): - # reset to the new freq - obj = obj.copy() - obj.index.freq = self.freq - return obj - - # do we have a regular frequency - if ax.freq is not None or ax.inferred_freq is not None: - - if len(self.grouper.binlabels) > len(ax) and how is None: - - # let's do an asfreq - return self.asfreq() - - # we are downsampling - # we want to call the actual grouper method here - result = obj.groupby( - self.grouper, axis=self.axis).aggregate(how, **kwargs) - - result = self._apply_loffset(result) - return self._wrap_result(result) - - def _adjust_binner_for_upsample(self, binner): - """ adjust our binner when upsampling """ - if self.closed == 'right': - binner = binner[1:] - else: - binner = binner[:-1] - return binner - - def _upsample(self, method, limit=None, fill_value=None): - """ - method : string {'backfill', 'bfill', 'pad', - 'ffill', 'asfreq'} method for upsampling - limit : int, default None - Maximum size gap to fill when reindexing - fill_value : scalar, default None - Value to use for missing values - - See also - -------- - .fillna - - """ - self._set_binner() - if self.axis: - raise AssertionError('axis must be 0') - if self._from_selection: - raise ValueError("Upsampling from level= or on= selection" - " is not supported, use .set_index(...)" - " to explicitly set index to" - " datetime-like") - - ax = self.ax - obj = self._selected_obj - binner = self.binner - res_index = self._adjust_binner_for_upsample(binner) - - # if we have the same frequency as our axis, then we are equal sampling - if limit is None and to_offset(ax.inferred_freq) == self.freq: - result = obj.copy() - result.index = res_index - else: - result = obj.reindex(res_index, method=method, - limit=limit, fill_value=fill_value) - - return self._wrap_result(result) - - def _wrap_result(self, result): - result = super(DatetimeIndexResampler, self)._wrap_result(result) - - # we may have a different kind that we were asked originally - # convert if needed - if self.kind == 'period' and not isinstance(result.index, PeriodIndex): - result.index = result.index.to_period(self.freq) - return result - - -class DatetimeIndexResamplerGroupby(_GroupByMixin, DatetimeIndexResampler): - """ - Provides a resample of a groupby implementation - - .. versionadded:: 0.18.1 - - """ - @property - def _constructor(self): - return DatetimeIndexResampler - - -class PeriodIndexResampler(DatetimeIndexResampler): - - @property - def _resampler_for_grouping(self): - return PeriodIndexResamplerGroupby - - def _convert_obj(self, obj): - obj = super(PeriodIndexResampler, self)._convert_obj(obj) - - offset = to_offset(self.freq) - if offset.n > 1: - if self.kind == 'period': # pragma: no cover - print('Warning: multiple of frequency -> timestamps') - - # Cannot have multiple of periods, convert to timestamp - self.kind = 'timestamp' - - # convert to timestamp - if not (self.kind is None or self.kind == 'period'): - if self._from_selection: - # see GH 14008, GH 12871 - msg = ("Resampling from level= or on= selection" - " with a PeriodIndex is not currently supported," - " use .set_index(...) to explicitly set index") - raise NotImplementedError(msg) - else: - obj = obj.to_timestamp(how=self.convention) - - return obj - - def aggregate(self, arg, *args, **kwargs): - result, how = self._aggregate(arg, *args, **kwargs) - if result is None: - result = self._downsample(arg, *args, **kwargs) - - result = self._apply_loffset(result) - return result - - agg = aggregate - - def _get_new_index(self): - """ return our new index """ - ax = self.ax - - if len(ax) == 0: - values = [] - else: - start = ax[0].asfreq(self.freq, how=self.convention) - end = ax[-1].asfreq(self.freq, how='end') - values = period_range(start, end, freq=self.freq).asi8 - - return ax._shallow_copy(values, freq=self.freq) - - def _downsample(self, how, **kwargs): - """ - Downsample the cython defined function - - Parameters - ---------- - how : string / cython mapped function - **kwargs : kw args passed to how function - """ - - # we may need to actually resample as if we are timestamps - if self.kind == 'timestamp': - return super(PeriodIndexResampler, self)._downsample(how, **kwargs) - - how = self._is_cython_func(how) or how - ax = self.ax - - new_index = self._get_new_index() - - # Start vs. end of period - memb = ax.asfreq(self.freq, how=self.convention) - - if is_subperiod(ax.freq, self.freq): - # Downsampling - if len(new_index) == 0: - bins = [] - else: - i8 = memb.asi8 - rng = np.arange(i8[0], i8[-1] + 1) - bins = memb.searchsorted(rng, side='right') - grouper = BinGrouper(bins, new_index) - return self._groupby_and_aggregate(how, grouper=grouper) - elif is_superperiod(ax.freq, self.freq): - return self.asfreq() - elif ax.freq == self.freq: - return self.asfreq() - - raise IncompatibleFrequency( - 'Frequency {} cannot be resampled to {}, as they are not ' - 'sub or super periods'.format(ax.freq, self.freq)) - - def _upsample(self, method, limit=None, fill_value=None): - """ - method : string {'backfill', 'bfill', 'pad', 'ffill'} - method for upsampling - limit : int, default None - Maximum size gap to fill when reindexing - fill_value : scalar, default None - Value to use for missing values - - See also - -------- - .fillna - - """ - if self._from_selection: - raise ValueError("Upsampling from level= or on= selection" - " is not supported, use .set_index(...)" - " to explicitly set index to" - " datetime-like") - # we may need to actually resample as if we are timestamps - if self.kind == 'timestamp': - return super(PeriodIndexResampler, self)._upsample( - method, limit=limit, fill_value=fill_value) - - ax = self.ax - obj = self.obj - new_index = self._get_new_index() - - # Start vs. end of period - memb = ax.asfreq(self.freq, how=self.convention) - - # Get the fill indexer - indexer = memb.get_indexer(new_index, method=method, limit=limit) - return self._wrap_result(_take_new_index( - obj, indexer, new_index, axis=self.axis)) - - -class PeriodIndexResamplerGroupby(_GroupByMixin, PeriodIndexResampler): - """ - Provides a resample of a groupby implementation - - .. versionadded:: 0.18.1 - - """ - @property - def _constructor(self): - return PeriodIndexResampler - - -class TimedeltaIndexResampler(DatetimeIndexResampler): - - @property - def _resampler_for_grouping(self): - return TimedeltaIndexResamplerGroupby - - def _get_binner_for_time(self): - return self.groupby._get_time_delta_bins(self.ax) - - def _adjust_binner_for_upsample(self, binner): - """ adjust our binner when upsampling """ - ax = self.ax - - if is_subperiod(ax.freq, self.freq): - # We are actually downsampling - # but are in the asfreq path - # GH 12926 - if self.closed == 'right': - binner = binner[1:] - else: - binner = binner[:-1] - return binner - - -class TimedeltaIndexResamplerGroupby(_GroupByMixin, TimedeltaIndexResampler): - """ - Provides a resample of a groupby implementation - - .. versionadded:: 0.18.1 - - """ - @property - def _constructor(self): - return TimedeltaIndexResampler - - -def resample(obj, kind=None, **kwds): - """ create a TimeGrouper and return our resampler """ - tg = TimeGrouper(**kwds) - return tg._get_resampler(obj, kind=kind) - - -resample.__doc__ = Resampler.__doc__ - - -def get_resampler_for_grouping(groupby, rule, how=None, fill_method=None, - limit=None, kind=None, **kwargs): - """ return our appropriate resampler when grouping as well """ - - # .resample uses 'on' similar to how .groupby uses 'key' - kwargs['key'] = kwargs.pop('on', None) - - tg = TimeGrouper(freq=rule, **kwargs) - resampler = tg._get_resampler(groupby.obj, kind=kind) - r = resampler._get_resampler_for_grouping(groupby=groupby) - return _maybe_process_deprecations(r, - how=how, - fill_method=fill_method, - limit=limit) - - -class TimeGrouper(Grouper): - """ - Custom groupby class for time-interval grouping - - Parameters - ---------- - freq : pandas date offset or offset alias for identifying bin edges - closed : closed end of interval; left or right - label : interval boundary to use for labeling; left or right - nperiods : optional, integer - convention : {'start', 'end', 'e', 's'} - If axis is PeriodIndex - - Notes - ----- - Use begin, end, nperiods to generate intervals that cannot be derived - directly from the associated object - """ - - def __init__(self, freq='Min', closed=None, label=None, how='mean', - nperiods=None, axis=0, - fill_method=None, limit=None, loffset=None, kind=None, - convention=None, base=0, **kwargs): - freq = to_offset(freq) - - end_types = set(['M', 'A', 'Q', 'BM', 'BA', 'BQ', 'W']) - rule = freq.rule_code - if (rule in end_types or - ('-' in rule and rule[:rule.find('-')] in end_types)): - if closed is None: - closed = 'right' - if label is None: - label = 'right' - else: - if closed is None: - closed = 'left' - if label is None: - label = 'left' - - self.closed = closed - self.label = label - self.nperiods = nperiods - self.kind = kind - - self.convention = convention or 'E' - self.convention = self.convention.lower() - - if isinstance(loffset, compat.string_types): - loffset = to_offset(loffset) - self.loffset = loffset - - self.how = how - self.fill_method = fill_method - self.limit = limit - self.base = base - - # always sort time groupers - kwargs['sort'] = True - - super(TimeGrouper, self).__init__(freq=freq, axis=axis, **kwargs) - - def _get_resampler(self, obj, kind=None): - """ - return my resampler or raise if we have an invalid axis - - Parameters - ---------- - obj : input object - kind : string, optional - 'period','timestamp','timedelta' are valid - - Returns - ------- - a Resampler - - Raises - ------ - TypeError if incompatible axis - - """ - self._set_grouper(obj) - - ax = self.ax - if isinstance(ax, DatetimeIndex): - return DatetimeIndexResampler(obj, - groupby=self, - kind=kind, - axis=self.axis) - elif isinstance(ax, PeriodIndex) or kind == 'period': - return PeriodIndexResampler(obj, - groupby=self, - kind=kind, - axis=self.axis) - elif isinstance(ax, TimedeltaIndex): - return TimedeltaIndexResampler(obj, - groupby=self, - axis=self.axis) - - raise TypeError("Only valid with DatetimeIndex, " - "TimedeltaIndex or PeriodIndex, " - "but got an instance of %r" % type(ax).__name__) - - def _get_grouper(self, obj): - # create the resampler and return our binner - r = self._get_resampler(obj) - r._set_binner() - return r.binner, r.grouper, r.obj - - def _get_binner_for_resample(self, kind=None): - # create the BinGrouper - # assume that self.set_grouper(obj) has already been called - - ax = self.ax - if kind is None: - kind = self.kind - if kind is None or kind == 'timestamp': - self.binner, bins, binlabels = self._get_time_bins(ax) - elif kind == 'timedelta': - self.binner, bins, binlabels = self._get_time_delta_bins(ax) - else: - self.binner, bins, binlabels = self._get_time_period_bins(ax) - - self.grouper = BinGrouper(bins, binlabels) - return self.binner, self.grouper, self.obj - - def _get_binner_for_grouping(self, obj): - # return an ordering of the transformed group labels, - # suitable for multi-grouping, e.g the labels for - # the resampled intervals - binner, grouper, obj = self._get_grouper(obj) - - l = [] - for key, group in grouper.get_iterator(self.ax): - l.extend([key] * len(group)) - - if isinstance(self.ax, PeriodIndex): - grouper = binner.__class__(l, freq=binner.freq, name=binner.name) - else: - # resampling causes duplicated values, specifying freq is invalid - grouper = binner.__class__(l, name=binner.name) - - # since we may have had to sort - # may need to reorder groups here - if self.indexer is not None: - indexer = self.indexer.argsort(kind='quicksort') - grouper = grouper.take(indexer) - return grouper - - def _get_time_bins(self, ax): - if not isinstance(ax, DatetimeIndex): - raise TypeError('axis must be a DatetimeIndex, but got ' - 'an instance of %r' % type(ax).__name__) - - if len(ax) == 0: - binner = labels = DatetimeIndex( - data=[], freq=self.freq, name=ax.name) - return binner, [], labels - - first, last = ax.min(), ax.max() - first, last = _get_range_edges(first, last, self.freq, - closed=self.closed, - base=self.base) - tz = ax.tz - # GH #12037 - # use first/last directly instead of call replace() on them - # because replace() will swallow the nanosecond part - # thus last bin maybe slightly before the end if the end contains - # nanosecond part and lead to `Values falls after last bin` error - binner = labels = DatetimeIndex(freq=self.freq, - start=first, - end=last, - tz=tz, - name=ax.name) - - # a little hack - trimmed = False - if (len(binner) > 2 and binner[-2] == last and - self.closed == 'right'): - - binner = binner[:-1] - trimmed = True - - ax_values = ax.asi8 - binner, bin_edges = self._adjust_bin_edges(binner, ax_values) - - # general version, knowing nothing about relative frequencies - bins = lib.generate_bins_dt64( - ax_values, bin_edges, self.closed, hasnans=ax.hasnans) - - if self.closed == 'right': - labels = binner - if self.label == 'right': - labels = labels[1:] - elif not trimmed: - labels = labels[:-1] - else: - if self.label == 'right': - labels = labels[1:] - elif not trimmed: - labels = labels[:-1] - - if ax.hasnans: - binner = binner.insert(0, tslib.NaT) - labels = labels.insert(0, tslib.NaT) - - # if we end up with more labels than bins - # adjust the labels - # GH4076 - if len(bins) < len(labels): - labels = labels[:len(bins)] - - return binner, bins, labels - - def _adjust_bin_edges(self, binner, ax_values): - # Some hacks for > daily data, see #1471, #1458, #1483 - - bin_edges = binner.asi8 - - if self.freq != 'D' and is_superperiod(self.freq, 'D'): - day_nanos = _delta_to_nanoseconds(timedelta(1)) - if self.closed == 'right': - bin_edges = bin_edges + day_nanos - 1 - - # intraday values on last day - if bin_edges[-2] > ax_values.max(): - bin_edges = bin_edges[:-1] - binner = binner[:-1] - - return binner, bin_edges - - def _get_time_delta_bins(self, ax): - if not isinstance(ax, TimedeltaIndex): - raise TypeError('axis must be a TimedeltaIndex, but got ' - 'an instance of %r' % type(ax).__name__) - - if not len(ax): - binner = labels = TimedeltaIndex( - data=[], freq=self.freq, name=ax.name) - return binner, [], labels - - start = ax[0] - end = ax[-1] - labels = binner = TimedeltaIndex(start=start, - end=end, - freq=self.freq, - name=ax.name) - - end_stamps = labels + 1 - bins = ax.searchsorted(end_stamps, side='left') - - # Addresses GH #10530 - if self.base > 0: - labels += type(self.freq)(self.base) - - return binner, bins, labels - - def _get_time_period_bins(self, ax): - if not isinstance(ax, DatetimeIndex): - raise TypeError('axis must be a DatetimeIndex, but got ' - 'an instance of %r' % type(ax).__name__) - - if not len(ax): - binner = labels = PeriodIndex( - data=[], freq=self.freq, name=ax.name) - return binner, [], labels - - labels = binner = PeriodIndex(start=ax[0], - end=ax[-1], - freq=self.freq, - name=ax.name) - - end_stamps = (labels + 1).asfreq(self.freq, 's').to_timestamp() - if ax.tzinfo: - end_stamps = end_stamps.tz_localize(ax.tzinfo) - bins = ax.searchsorted(end_stamps, side='left') - - return binner, bins, labels - - -def _take_new_index(obj, indexer, new_index, axis=0): - from pandas.core.api import Series, DataFrame - - if isinstance(obj, Series): - new_values = algos.take_1d(obj.values, indexer) - return Series(new_values, index=new_index, name=obj.name) - elif isinstance(obj, DataFrame): - if axis == 1: - raise NotImplementedError("axis 1 is not supported") - return DataFrame(obj._data.reindex_indexer( - new_axis=new_index, indexer=indexer, axis=1)) - else: - raise ValueError("'obj' should be either a Series or a DataFrame") - - -def _get_range_edges(first, last, offset, closed='left', base=0): - if isinstance(offset, compat.string_types): - offset = to_offset(offset) - - if isinstance(offset, Tick): - is_day = isinstance(offset, Day) - day_nanos = _delta_to_nanoseconds(timedelta(1)) - - # #1165 - if (is_day and day_nanos % offset.nanos == 0) or not is_day: - return _adjust_dates_anchored(first, last, offset, - closed=closed, base=base) - - if not isinstance(offset, Tick): # and first.time() != last.time(): - # hack! - first = first.normalize() - last = last.normalize() - - if closed == 'left': - first = Timestamp(offset.rollback(first)) - else: - first = Timestamp(first - offset) - - last = Timestamp(last + offset) - - return first, last - - -def _adjust_dates_anchored(first, last, offset, closed='right', base=0): - # First and last offsets should be calculated from the start day to fix an - # error cause by resampling across multiple days when a one day period is - # not a multiple of the frequency. - # - # See https://github.com/pandas-dev/pandas/issues/8683 - - # 14682 - Since we need to drop the TZ information to perform - # the adjustment in the presence of a DST change, - # save TZ Info and the DST state of the first and last parameters - # so that we can accurately rebuild them at the end. - first_tzinfo = first.tzinfo - last_tzinfo = last.tzinfo - first_dst = bool(first.dst()) - last_dst = bool(last.dst()) - - first = first.tz_localize(None) - last = last.tz_localize(None) - - start_day_nanos = first.normalize().value - - base_nanos = (base % offset.n) * offset.nanos // offset.n - start_day_nanos += base_nanos - - foffset = (first.value - start_day_nanos) % offset.nanos - loffset = (last.value - start_day_nanos) % offset.nanos - - if closed == 'right': - if foffset > 0: - # roll back - fresult = first.value - foffset - else: - fresult = first.value - offset.nanos - - if loffset > 0: - # roll forward - lresult = last.value + (offset.nanos - loffset) - else: - # already the end of the road - lresult = last.value - else: # closed == 'left' - if foffset > 0: - fresult = first.value - foffset - else: - # start of the road - fresult = first.value - - if loffset > 0: - # roll forward - lresult = last.value + (offset.nanos - loffset) - else: - lresult = last.value + offset.nanos - - return (Timestamp(fresult).tz_localize(first_tzinfo, ambiguous=first_dst), - Timestamp(lresult).tz_localize(last_tzinfo, ambiguous=last_dst)) - - -def asfreq(obj, freq, method=None, how=None, normalize=False, fill_value=None): - """ - Utility frequency conversion method for Series/DataFrame - """ - if isinstance(obj.index, PeriodIndex): - if method is not None: - raise NotImplementedError("'method' argument is not supported") - - if how is None: - how = 'E' - - new_obj = obj.copy() - new_obj.index = obj.index.asfreq(freq, how=how) - - elif len(obj.index) == 0: - new_obj = obj.copy() - new_obj.index = obj.index._shallow_copy(freq=to_offset(freq)) - - else: - dti = date_range(obj.index[0], obj.index[-1], freq=freq) - dti.name = obj.index.name - new_obj = obj.reindex(dti, method=method, fill_value=fill_value) - if normalize: - new_obj.index = new_obj.index.normalize() - - return new_obj diff --git a/pandas/tseries/tdi.py b/pandas/tseries/tdi.py deleted file mode 100644 index 5d062dd38f9fc..0000000000000 --- a/pandas/tseries/tdi.py +++ /dev/null @@ -1,989 +0,0 @@ -""" implement the TimedeltaIndex """ - -from datetime import timedelta -import numpy as np -from pandas.types.common import (_TD_DTYPE, - is_integer, is_float, - is_bool_dtype, - is_list_like, - is_scalar, - is_integer_dtype, - is_object_dtype, - is_timedelta64_dtype, - is_timedelta64_ns_dtype, - _ensure_int64) -from pandas.types.missing import isnull -from pandas.types.generic import ABCSeries -from pandas.core.common import _maybe_box, _values_from_object, is_bool_indexer - -from pandas.core.index import Index, Int64Index -import pandas.compat as compat -from pandas.compat import u -from pandas.tseries.frequencies import to_offset -from pandas.core.algorithms import checked_add_with_arr -from pandas.core.base import _shared_docs -from pandas.indexes.base import _index_shared_docs -import pandas.core.common as com -import pandas.types.concat as _concat -from pandas.util.decorators import Appender, Substitution, deprecate_kwarg -from pandas.tseries.base import TimelikeOps, DatetimeIndexOpsMixin -from pandas.tseries.timedeltas import (to_timedelta, - _coerce_scalar_to_timedelta_type) -from pandas.tseries.offsets import Tick, DateOffset -from pandas._libs import (lib, index as libindex, tslib as libts, - join as libjoin, Timedelta, NaT, iNaT) - - -def _td_index_cmp(opname, nat_result=False): - """ - Wrap comparison operations to convert timedelta-like to timedelta64 - """ - - def wrapper(self, other): - msg = "cannot compare a TimedeltaIndex with type {0}" - func = getattr(super(TimedeltaIndex, self), opname) - if _is_convertible_to_td(other) or other is NaT: - try: - other = _to_m8(other) - except ValueError: - # failed to parse as timedelta - raise TypeError(msg.format(type(other))) - result = func(other) - if isnull(other): - result.fill(nat_result) - else: - if not is_list_like(other): - raise TypeError(msg.format(type(other))) - - other = TimedeltaIndex(other).values - result = func(other) - result = _values_from_object(result) - - if isinstance(other, Index): - o_mask = other.values.view('i8') == iNaT - else: - o_mask = other.view('i8') == iNaT - - if o_mask.any(): - result[o_mask] = nat_result - - if self.hasnans: - result[self._isnan] = nat_result - - # support of bool dtype indexers - if is_bool_dtype(result): - return result - return Index(result) - - return wrapper - - -class TimedeltaIndex(DatetimeIndexOpsMixin, TimelikeOps, Int64Index): - """ - Immutable ndarray of timedelta64 data, represented internally as int64, and - which can be boxed to timedelta objects - - Parameters - ---------- - data : array-like (1-dimensional), optional - Optional timedelta-like data to construct index with - unit: unit of the arg (D,h,m,s,ms,us,ns) denote the unit, optional - which is an integer/float number - freq: a frequency for the index, optional - copy : bool - Make a copy of input ndarray - start : starting value, timedelta-like, optional - If data is None, start is used as the start point in generating regular - timedelta data. - periods : int, optional, > 0 - Number of periods to generate, if generating index. Takes precedence - over end argument - end : end time, timedelta-like, optional - If periods is none, generated index will extend to first conforming - time on or just past end argument - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - name : object - Name to be stored in the index - - Notes - ----- - - To learn more about the frequency strings, please see `this link - `__. - """ - - _typ = 'timedeltaindex' - _join_precedence = 10 - - def _join_i8_wrapper(joinf, **kwargs): - return DatetimeIndexOpsMixin._join_i8_wrapper( - joinf, dtype='m8[ns]', **kwargs) - - _inner_indexer = _join_i8_wrapper(libjoin.inner_join_indexer_int64) - _outer_indexer = _join_i8_wrapper(libjoin.outer_join_indexer_int64) - _left_indexer = _join_i8_wrapper(libjoin.left_join_indexer_int64) - _left_indexer_unique = _join_i8_wrapper( - libjoin.left_join_indexer_unique_int64, with_indexers=False) - _arrmap = None - - # define my properties & methods for delegation - _other_ops = [] - _bool_ops = [] - _object_ops = ['freq'] - _field_ops = ['days', 'seconds', 'microseconds', 'nanoseconds'] - _datetimelike_ops = _field_ops + _object_ops + _bool_ops - _datetimelike_methods = ["to_pytimedelta", "total_seconds", - "round", "floor", "ceil"] - - __eq__ = _td_index_cmp('__eq__') - __ne__ = _td_index_cmp('__ne__', nat_result=True) - __lt__ = _td_index_cmp('__lt__') - __gt__ = _td_index_cmp('__gt__') - __le__ = _td_index_cmp('__le__') - __ge__ = _td_index_cmp('__ge__') - - _engine_type = libindex.TimedeltaEngine - - _comparables = ['name', 'freq'] - _attributes = ['name', 'freq'] - _is_numeric_dtype = True - _infer_as_myclass = True - - freq = None - - def __new__(cls, data=None, unit=None, - freq=None, start=None, end=None, periods=None, - copy=False, name=None, - closed=None, verify_integrity=True, **kwargs): - - if isinstance(data, TimedeltaIndex) and freq is None and name is None: - if copy: - return data.copy() - else: - return data._shallow_copy() - - freq_infer = False - if not isinstance(freq, DateOffset): - - # if a passed freq is None, don't infer automatically - if freq != 'infer': - freq = to_offset(freq) - else: - freq_infer = True - freq = None - - if periods is not None: - if is_float(periods): - periods = int(periods) - elif not is_integer(periods): - raise ValueError('Periods must be a number, got %s' % - str(periods)) - - if data is None and freq is None: - raise ValueError("Must provide freq argument if no data is " - "supplied") - - if data is None: - return cls._generate(start, end, periods, name, freq, - closed=closed) - - if unit is not None: - data = to_timedelta(data, unit=unit, box=False) - - if not isinstance(data, (np.ndarray, Index, ABCSeries)): - if is_scalar(data): - raise ValueError('TimedeltaIndex() must be called with a ' - 'collection of some kind, %s was passed' - % repr(data)) - - # convert if not already - if getattr(data, 'dtype', None) != _TD_DTYPE: - data = to_timedelta(data, unit=unit, box=False) - elif copy: - data = np.array(data, copy=True) - - # check that we are matching freqs - if verify_integrity and len(data) > 0: - if freq is not None and not freq_infer: - index = cls._simple_new(data, name=name) - inferred = index.inferred_freq - if inferred != freq.freqstr: - on_freq = cls._generate( - index[0], None, len(index), name, freq) - if not np.array_equal(index.asi8, on_freq.asi8): - raise ValueError('Inferred frequency {0} from passed ' - 'timedeltas does not conform to ' - 'passed frequency {1}' - .format(inferred, freq.freqstr)) - index.freq = freq - return index - - if freq_infer: - index = cls._simple_new(data, name=name) - inferred = index.inferred_freq - if inferred: - index.freq = to_offset(inferred) - return index - - return cls._simple_new(data, name=name, freq=freq) - - @classmethod - def _generate(cls, start, end, periods, name, offset, closed=None): - if com._count_not_none(start, end, periods) != 2: - raise ValueError('Must specify two of start, end, or periods') - - if start is not None: - start = Timedelta(start) - - if end is not None: - end = Timedelta(end) - - left_closed = False - right_closed = False - - if start is None and end is None: - if closed is not None: - raise ValueError("Closed has to be None if not both of start" - "and end are defined") - - if closed is None: - left_closed = True - right_closed = True - elif closed == "left": - left_closed = True - elif closed == "right": - right_closed = True - else: - raise ValueError("Closed has to be either 'left', 'right' or None") - - index = _generate_regular_range(start, end, periods, offset) - index = cls._simple_new(index, name=name, freq=offset) - - if not left_closed: - index = index[1:] - if not right_closed: - index = index[:-1] - - return index - - @property - def _box_func(self): - return lambda x: Timedelta(x, unit='ns') - - @classmethod - def _simple_new(cls, values, name=None, freq=None, **kwargs): - values = np.array(values, copy=False) - if values.dtype == np.object_: - values = libts.array_to_timedelta64(values) - if values.dtype != _TD_DTYPE: - values = _ensure_int64(values).view(_TD_DTYPE) - - result = object.__new__(cls) - result._data = values - result.name = name - result.freq = freq - result._reset_identity() - return result - - @property - def _formatter_func(self): - from pandas.formats.format import _get_format_timedelta64 - return _get_format_timedelta64(self, box=True) - - def __setstate__(self, state): - """Necessary for making this object picklable""" - if isinstance(state, dict): - super(TimedeltaIndex, self).__setstate__(state) - else: - raise Exception("invalid pickle state") - _unpickle_compat = __setstate__ - - def _maybe_update_attributes(self, attrs): - """ Update Index attributes (e.g. freq) depending on op """ - freq = attrs.get('freq', None) - if freq is not None: - # no need to infer if freq is None - attrs['freq'] = 'infer' - return attrs - - def _add_delta(self, delta): - if isinstance(delta, (Tick, timedelta, np.timedelta64)): - new_values = self._add_delta_td(delta) - name = self.name - elif isinstance(delta, TimedeltaIndex): - new_values = self._add_delta_tdi(delta) - # update name when delta is index - name = com._maybe_match_name(self, delta) - else: - raise ValueError("cannot add the type {0} to a TimedeltaIndex" - .format(type(delta))) - - result = TimedeltaIndex(new_values, freq='infer', name=name) - return result - - def _evaluate_with_timedelta_like(self, other, op, opstr): - - # allow division by a timedelta - if opstr in ['__div__', '__truediv__']: - if _is_convertible_to_td(other): - other = Timedelta(other) - if isnull(other): - raise NotImplementedError( - "division by pd.NaT not implemented") - - i8 = self.asi8 - result = i8 / float(other.value) - result = self._maybe_mask_results(result, convert='float64') - return Index(result, name=self.name, copy=False) - - return NotImplemented - - def _add_datelike(self, other): - # adding a timedeltaindex to a datetimelike - from pandas import Timestamp, DatetimeIndex - if other is NaT: - result = self._nat_new(box=False) - else: - other = Timestamp(other) - i8 = self.asi8 - result = checked_add_with_arr(i8, other.value) - result = self._maybe_mask_results(result, fill_value=iNaT) - return DatetimeIndex(result, name=self.name, copy=False) - - def _sub_datelike(self, other): - from pandas import DatetimeIndex - if other is NaT: - result = self._nat_new(box=False) - else: - raise TypeError("cannot subtract a datelike from a TimedeltaIndex") - return DatetimeIndex(result, name=self.name, copy=False) - - def _format_native_types(self, na_rep=u('NaT'), - date_format=None, **kwargs): - from pandas.formats.format import Timedelta64Formatter - return Timedelta64Formatter(values=self, - nat_rep=na_rep, - justify='all').get_result() - - def _get_field(self, m): - - values = self.asi8 - hasnans = self.hasnans - if hasnans: - result = np.empty(len(self), dtype='float64') - mask = self._isnan - imask = ~mask - result.flat[imask] = np.array( - [getattr(Timedelta(val), m) for val in values[imask]]) - result[mask] = np.nan - else: - result = np.array([getattr(Timedelta(val), m) - for val in values], dtype='int64') - return Index(result, name=self.name) - - @property - def days(self): - """ Number of days for each element. """ - return self._get_field('days') - - @property - def seconds(self): - """ Number of seconds (>= 0 and less than 1 day) for each element. """ - return self._get_field('seconds') - - @property - def microseconds(self): - """ - Number of microseconds (>= 0 and less than 1 second) for each - element. """ - return self._get_field('microseconds') - - @property - def nanoseconds(self): - """ - Number of nanoseconds (>= 0 and less than 1 microsecond) for each - element. - """ - return self._get_field('nanoseconds') - - @property - def components(self): - """ - Return a dataframe of the components (days, hours, minutes, - seconds, milliseconds, microseconds, nanoseconds) of the Timedeltas. - - Returns - ------- - a DataFrame - """ - from pandas import DataFrame - - columns = ['days', 'hours', 'minutes', 'seconds', - 'milliseconds', 'microseconds', 'nanoseconds'] - hasnans = self.hasnans - if hasnans: - def f(x): - if isnull(x): - return [np.nan] * len(columns) - return x.components - else: - def f(x): - return x.components - - result = DataFrame([f(x) for x in self]) - result.columns = columns - if not hasnans: - result = result.astype('int64') - return result - - def total_seconds(self): - """ - Total duration of each element expressed in seconds. - - .. versionadded:: 0.17.0 - """ - return Index(self._maybe_mask_results(1e-9 * self.asi8), - name=self.name) - - def to_pytimedelta(self): - """ - Return TimedeltaIndex as object ndarray of datetime.timedelta objects - - Returns - ------- - datetimes : ndarray - """ - return libts.ints_to_pytimedelta(self.asi8) - - @Appender(_index_shared_docs['astype']) - def astype(self, dtype, copy=True): - dtype = np.dtype(dtype) - - if is_object_dtype(dtype): - return self.asobject - elif is_timedelta64_ns_dtype(dtype): - if copy is True: - return self.copy() - return self - elif is_timedelta64_dtype(dtype): - # return an index (essentially this is division) - result = self.values.astype(dtype, copy=copy) - if self.hasnans: - return Index(self._maybe_mask_results(result, - convert='float64'), - name=self.name) - return Index(result.astype('i8'), name=self.name) - elif is_integer_dtype(dtype): - return Index(self.values.astype('i8', copy=copy), dtype='i8', - name=self.name) - raise ValueError('Cannot cast TimedeltaIndex to dtype %s' % dtype) - - def union(self, other): - """ - Specialized union for TimedeltaIndex objects. If combine - overlapping ranges with the same DateOffset, will be much - faster than Index.union - - Parameters - ---------- - other : TimedeltaIndex or array-like - - Returns - ------- - y : Index or TimedeltaIndex - """ - self._assert_can_do_setop(other) - if not isinstance(other, TimedeltaIndex): - try: - other = TimedeltaIndex(other) - except (TypeError, ValueError): - pass - this, other = self, other - - if this._can_fast_union(other): - return this._fast_union(other) - else: - result = Index.union(this, other) - if isinstance(result, TimedeltaIndex): - if result.freq is None: - result.freq = to_offset(result.inferred_freq) - return result - - def join(self, other, how='left', level=None, return_indexers=False): - """ - See Index.join - """ - if _is_convertible_to_index(other): - try: - other = TimedeltaIndex(other) - except (TypeError, ValueError): - pass - - return Index.join(self, other, how=how, level=level, - return_indexers=return_indexers) - - def _wrap_joined_index(self, joined, other): - name = self.name if self.name == other.name else None - if (isinstance(other, TimedeltaIndex) and self.freq == other.freq and - self._can_fast_union(other)): - joined = self._shallow_copy(joined, name=name) - return joined - else: - return self._simple_new(joined, name) - - def _can_fast_union(self, other): - if not isinstance(other, TimedeltaIndex): - return False - - freq = self.freq - - if freq is None or freq != other.freq: - return False - - if not self.is_monotonic or not other.is_monotonic: - return False - - if len(self) == 0 or len(other) == 0: - return True - - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - right_start = right[0] - left_end = left[-1] - - # Only need to "adjoin", not overlap - return (right_start == left_end + freq) or right_start in left - - def _fast_union(self, other): - if len(other) == 0: - return self.view(type(self)) - - if len(self) == 0: - return other.view(type(self)) - - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - left_end = left[-1] - right_end = right[-1] - - # concatenate - if left_end < right_end: - loc = right.searchsorted(left_end, side='right') - right_chunk = right.values[loc:] - dates = _concat._concat_compat((left.values, right_chunk)) - return self._shallow_copy(dates) - else: - return left - - def _wrap_union_result(self, other, result): - name = self.name if self.name == other.name else None - return self._simple_new(result, name=name, freq=None) - - def intersection(self, other): - """ - Specialized intersection for TimedeltaIndex objects. May be much faster - than Index.intersection - - Parameters - ---------- - other : TimedeltaIndex or array-like - - Returns - ------- - y : Index or TimedeltaIndex - """ - self._assert_can_do_setop(other) - if not isinstance(other, TimedeltaIndex): - try: - other = TimedeltaIndex(other) - except (TypeError, ValueError): - pass - result = Index.intersection(self, other) - return result - - if len(self) == 0: - return self - if len(other) == 0: - return other - # to make our life easier, "sort" the two ranges - if self[0] <= other[0]: - left, right = self, other - else: - left, right = other, self - - end = min(left[-1], right[-1]) - start = right[0] - - if end < start: - return type(self)(data=[]) - else: - lslice = slice(*left.slice_locs(start, end)) - left_chunk = left.values[lslice] - return self._shallow_copy(left_chunk) - - def _maybe_promote(self, other): - if other.inferred_type == 'timedelta': - other = TimedeltaIndex(other) - return self, other - - def get_value(self, series, key): - """ - Fast lookup of value from 1-dimensional ndarray. Only use this if you - know what you're doing - """ - - if _is_convertible_to_td(key): - key = Timedelta(key) - return self.get_value_maybe_box(series, key) - - try: - return _maybe_box(self, Index.get_value(self, series, key), - series, key) - except KeyError: - try: - loc = self._get_string_slice(key) - return series[loc] - except (TypeError, ValueError, KeyError): - pass - - try: - return self.get_value_maybe_box(series, key) - except (TypeError, ValueError, KeyError): - raise KeyError(key) - - def get_value_maybe_box(self, series, key): - if not isinstance(key, Timedelta): - key = Timedelta(key) - values = self._engine.get_value(_values_from_object(series), key) - return _maybe_box(self, values, series, key) - - def get_loc(self, key, method=None, tolerance=None): - """ - Get integer location for requested label - - Returns - ------- - loc : int - """ - - if is_bool_indexer(key): - raise TypeError - - if isnull(key): - key = NaT - - if tolerance is not None: - # try converting tolerance now, so errors don't get swallowed by - # the try/except clauses below - tolerance = self._convert_tolerance(tolerance) - - if _is_convertible_to_td(key): - key = Timedelta(key) - return Index.get_loc(self, key, method, tolerance) - - try: - return Index.get_loc(self, key, method, tolerance) - except (KeyError, ValueError, TypeError): - try: - return self._get_string_slice(key) - except (TypeError, KeyError, ValueError): - pass - - try: - stamp = Timedelta(key) - return Index.get_loc(self, stamp, method, tolerance) - except (KeyError, ValueError): - raise KeyError(key) - - def _maybe_cast_slice_bound(self, label, side, kind): - """ - If label is a string, cast it to timedelta according to resolution. - - - Parameters - ---------- - label : object - side : {'left', 'right'} - kind : {'ix', 'loc', 'getitem'} - - Returns - ------- - label : object - - """ - assert kind in ['ix', 'loc', 'getitem', None] - - if isinstance(label, compat.string_types): - parsed = _coerce_scalar_to_timedelta_type(label, box=True) - lbound = parsed.round(parsed.resolution) - if side == 'left': - return lbound - else: - return (lbound + to_offset(parsed.resolution) - - Timedelta(1, 'ns')) - elif is_integer(label) or is_float(label): - self._invalid_indexer('slice', label) - - return label - - def _get_string_slice(self, key, use_lhs=True, use_rhs=True): - freq = getattr(self, 'freqstr', - getattr(self, 'inferred_freq', None)) - if is_integer(key) or is_float(key) or key is NaT: - self._invalid_indexer('slice', key) - loc = self._partial_td_slice(key, freq, use_lhs=use_lhs, - use_rhs=use_rhs) - return loc - - def _partial_td_slice(self, key, freq, use_lhs=True, use_rhs=True): - - # given a key, try to figure out a location for a partial slice - if not isinstance(key, compat.string_types): - return key - - raise NotImplementedError - - # TODO(wesm): dead code - # parsed = _coerce_scalar_to_timedelta_type(key, box=True) - - # is_monotonic = self.is_monotonic - - # # figure out the resolution of the passed td - # # and round to it - - # # t1 = parsed.round(reso) - - # t2 = t1 + to_offset(parsed.resolution) - Timedelta(1, 'ns') - - # stamps = self.asi8 - - # if is_monotonic: - - # # we are out of range - # if (len(stamps) and ((use_lhs and t1.value < stamps[0] and - # t2.value < stamps[0]) or - # ((use_rhs and t1.value > stamps[-1] and - # t2.value > stamps[-1])))): - # raise KeyError - - # # a monotonic (sorted) series can be sliced - # left = (stamps.searchsorted(t1.value, side='left') - # if use_lhs else None) - # right = (stamps.searchsorted(t2.value, side='right') - # if use_rhs else None) - - # return slice(left, right) - - # lhs_mask = (stamps >= t1.value) if use_lhs else True - # rhs_mask = (stamps <= t2.value) if use_rhs else True - - # # try to find a the dates - # return (lhs_mask & rhs_mask).nonzero()[0] - - @Substitution(klass='TimedeltaIndex') - @Appender(_shared_docs['searchsorted']) - @deprecate_kwarg(old_arg_name='key', new_arg_name='value') - def searchsorted(self, value, side='left', sorter=None): - if isinstance(value, (np.ndarray, Index)): - value = np.array(value, dtype=_TD_DTYPE, copy=False) - else: - value = _to_m8(value) - - return self.values.searchsorted(value, side=side, sorter=sorter) - - def is_type_compatible(self, typ): - return typ == self.inferred_type or typ == 'timedelta' - - @property - def inferred_type(self): - return 'timedelta64' - - @property - def dtype(self): - return _TD_DTYPE - - @property - def is_all_dates(self): - return True - - def insert(self, loc, item): - """ - Make new Index inserting new item at location - - Parameters - ---------- - loc : int - item : object - if not either a Python datetime or a numpy integer-like, returned - Index dtype will be object rather than datetime. - - Returns - ------- - new_index : Index - """ - - # try to convert if possible - if _is_convertible_to_td(item): - try: - item = Timedelta(item) - except: - pass - - freq = None - if isinstance(item, (Timedelta, libts.NaTType)): - - # check freq can be preserved on edge cases - if self.freq is not None: - if ((loc == 0 or loc == -len(self)) and - item + self.freq == self[0]): - freq = self.freq - elif (loc == len(self)) and item - self.freq == self[-1]: - freq = self.freq - item = _to_m8(item) - - try: - new_tds = np.concatenate((self[:loc].asi8, [item.view(np.int64)], - self[loc:].asi8)) - return TimedeltaIndex(new_tds, name=self.name, freq=freq) - - except (AttributeError, TypeError): - - # fall back to object index - if isinstance(item, compat.string_types): - return self.asobject.insert(loc, item) - raise TypeError( - "cannot insert TimedeltaIndex with incompatible label") - - def delete(self, loc): - """ - Make a new DatetimeIndex with passed location(s) deleted. - - Parameters - ---------- - loc: int, slice or array of ints - Indicate which sub-arrays to remove. - - Returns - ------- - new_index : TimedeltaIndex - """ - new_tds = np.delete(self.asi8, loc) - - freq = 'infer' - if is_integer(loc): - if loc in (0, -len(self), -1, len(self) - 1): - freq = self.freq - else: - if is_list_like(loc): - loc = lib.maybe_indices_to_slice( - _ensure_int64(np.array(loc)), len(self)) - if isinstance(loc, slice) and loc.step in (1, None): - if (loc.start in (0, None) or loc.stop in (len(self), None)): - freq = self.freq - - return TimedeltaIndex(new_tds, name=self.name, freq=freq) - - -TimedeltaIndex._add_numeric_methods() -TimedeltaIndex._add_logical_methods_disabled() -TimedeltaIndex._add_datetimelike_methods() - - -def _is_convertible_to_index(other): - """ - return a boolean whether I can attempt conversion to a TimedeltaIndex - """ - if isinstance(other, TimedeltaIndex): - return True - elif (len(other) > 0 and - other.inferred_type not in ('floating', 'mixed-integer', 'integer', - 'mixed-integer-float', 'mixed')): - return True - return False - - -def _is_convertible_to_td(key): - return isinstance(key, (DateOffset, timedelta, Timedelta, - np.timedelta64, compat.string_types)) - - -def _to_m8(key): - """ - Timedelta-like => dt64 - """ - if not isinstance(key, Timedelta): - # this also converts strings - key = Timedelta(key) - - # return an type that can be compared - return np.int64(key.value).view(_TD_DTYPE) - - -def _generate_regular_range(start, end, periods, offset): - stride = offset.nanos - if periods is None: - b = Timedelta(start).value - e = Timedelta(end).value - e += stride - e % stride - elif start is not None: - b = Timedelta(start).value - e = b + periods * stride - elif end is not None: - e = Timedelta(end).value + stride - b = e - periods * stride - else: - raise ValueError("at least 'start' or 'end' should be specified " - "if a 'period' is given.") - - data = np.arange(b, e, stride, dtype=np.int64) - data = TimedeltaIndex._simple_new(data, None) - - return data - - -def timedelta_range(start=None, end=None, periods=None, freq='D', - name=None, closed=None): - """ - Return a fixed frequency timedelta index, with day as the default - frequency - - Parameters - ---------- - start : string or timedelta-like, default None - Left bound for generating dates - end : string or datetime-like, default None - Right bound for generating dates - periods : integer or None, default None - If None, must specify start and end - freq : string or DateOffset, default 'D' (calendar daily) - Frequency strings can have multiples, e.g. '5H' - name : str, default None - Name of the resulting index - closed : string or None, default None - Make the interval closed with respect to the given frequency to - the 'left', 'right', or both sides (None) - - Returns - ------- - rng : TimedeltaIndex - - Notes - ----- - 2 of start, end, or periods must be specified. - - To learn more about the frequency strings, please see `this link - `__. - """ - return TimedeltaIndex(start=start, end=end, periods=periods, - freq=freq, name=name, - closed=closed) diff --git a/pandas/tseries/timedeltas.py b/pandas/tseries/timedeltas.py deleted file mode 100644 index ead602ee80e32..0000000000000 --- a/pandas/tseries/timedeltas.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -timedelta support tools -""" - -import numpy as np -import pandas as pd -import pandas._libs.tslib as tslib - -from pandas.types.common import (_ensure_object, - is_integer_dtype, - is_timedelta64_dtype, - is_list_like) -from pandas.types.generic import ABCSeries, ABCIndexClass - - -def to_timedelta(arg, unit='ns', box=True, errors='raise'): - """ - Convert argument to timedelta - - Parameters - ---------- - arg : string, timedelta, list, tuple, 1-d array, or Series - unit : unit of the arg (D,h,m,s,ms,us,ns) denote the unit, which is an - integer/float number - box : boolean, default True - - If True returns a Timedelta/TimedeltaIndex of the results - - if False returns a np.timedelta64 or ndarray of values of dtype - timedelta64[ns] - errors : {'ignore', 'raise', 'coerce'}, default 'raise' - - If 'raise', then invalid parsing will raise an exception - - If 'coerce', then invalid parsing will be set as NaT - - If 'ignore', then invalid parsing will return the input - - Returns - ------- - ret : timedelta64/arrays of timedelta64 if parsing succeeded - - Examples - -------- - - Parsing a single string to a Timedelta: - - >>> pd.to_timedelta('1 days 06:05:01.00003') - Timedelta('1 days 06:05:01.000030') - >>> pd.to_timedelta('15.5us') - Timedelta('0 days 00:00:00.000015') - - Parsing a list or array of strings: - - >>> pd.to_timedelta(['1 days 06:05:01.00003', '15.5us', 'nan']) - TimedeltaIndex(['1 days 06:05:01.000030', '0 days 00:00:00.000015', NaT], - dtype='timedelta64[ns]', freq=None) - - Converting numbers by specifying the `unit` keyword argument: - - >>> pd.to_timedelta(np.arange(5), unit='s') - TimedeltaIndex(['00:00:00', '00:00:01', '00:00:02', - '00:00:03', '00:00:04'], - dtype='timedelta64[ns]', freq=None) - >>> pd.to_timedelta(np.arange(5), unit='d') - TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'], - dtype='timedelta64[ns]', freq=None) - """ - unit = _validate_timedelta_unit(unit) - - if errors not in ('ignore', 'raise', 'coerce'): - raise ValueError("errors must be one of 'ignore', " - "'raise', or 'coerce'}") - - if arg is None: - return arg - elif isinstance(arg, ABCSeries): - from pandas import Series - values = _convert_listlike(arg._values, unit=unit, - box=False, errors=errors) - return Series(values, index=arg.index, name=arg.name) - elif isinstance(arg, ABCIndexClass): - return _convert_listlike(arg, unit=unit, box=box, - errors=errors, name=arg.name) - elif is_list_like(arg) and getattr(arg, 'ndim', 1) == 1: - return _convert_listlike(arg, unit=unit, box=box, errors=errors) - elif getattr(arg, 'ndim', 1) > 1: - raise TypeError('arg must be a string, timedelta, list, tuple, ' - '1-d array, or Series') - - # ...so it must be a scalar value. Return scalar. - return _coerce_scalar_to_timedelta_type(arg, unit=unit, - box=box, errors=errors) - - -_unit_map = { - 'Y': 'Y', - 'y': 'Y', - 'W': 'W', - 'w': 'W', - 'D': 'D', - 'd': 'D', - 'days': 'D', - 'Days': 'D', - 'day': 'D', - 'Day': 'D', - 'M': 'M', - 'H': 'h', - 'h': 'h', - 'm': 'm', - 'T': 'm', - 'S': 's', - 's': 's', - 'L': 'ms', - 'MS': 'ms', - 'ms': 'ms', - 'US': 'us', - 'us': 'us', - 'NS': 'ns', - 'ns': 'ns', -} - - -def _validate_timedelta_unit(arg): - """ provide validation / translation for timedelta short units """ - try: - return _unit_map[arg] - except: - if arg is None: - return 'ns' - raise ValueError("invalid timedelta unit {0} provided".format(arg)) - - -def _coerce_scalar_to_timedelta_type(r, unit='ns', box=True, errors='raise'): - """Convert string 'r' to a timedelta object.""" - - try: - result = tslib.convert_to_timedelta64(r, unit) - except ValueError: - if errors == 'raise': - raise - elif errors == 'ignore': - return r - - # coerce - result = pd.NaT - - if box: - result = tslib.Timedelta(result) - return result - - -def _convert_listlike(arg, unit='ns', box=True, errors='raise', name=None): - """Convert a list of objects to a timedelta index object.""" - - if isinstance(arg, (list, tuple)) or not hasattr(arg, 'dtype'): - arg = np.array(list(arg), dtype='O') - - # these are shortcut-able - if is_timedelta64_dtype(arg): - value = arg.astype('timedelta64[ns]') - elif is_integer_dtype(arg): - value = arg.astype('timedelta64[{0}]'.format( - unit)).astype('timedelta64[ns]', copy=False) - else: - try: - value = tslib.array_to_timedelta64(_ensure_object(arg), - unit=unit, errors=errors) - value = value.astype('timedelta64[ns]', copy=False) - except ValueError: - if errors == 'ignore': - return arg - else: - # This else-block accounts for the cases when errors='raise' - # and errors='coerce'. If errors == 'raise', these errors - # should be raised. If errors == 'coerce', we shouldn't - # expect any errors to be raised, since all parsing errors - # cause coercion to pd.NaT. However, if an error / bug is - # introduced that causes an Exception to be raised, we would - # like to surface it. - raise - - if box: - from pandas import TimedeltaIndex - value = TimedeltaIndex(value, unit='ns', name=name) - return value diff --git a/pandas/tseries/tools.py b/pandas/tseries/tools.py deleted file mode 100644 index 5dc9746c6d6f9..0000000000000 --- a/pandas/tseries/tools.py +++ /dev/null @@ -1,785 +0,0 @@ -from datetime import datetime, timedelta, time -import numpy as np -from collections import MutableMapping - -from pandas._libs import lib, tslib - -from pandas.types.common import (_ensure_object, - is_datetime64_ns_dtype, - is_datetime64_dtype, - is_datetime64tz_dtype, - is_integer_dtype, - is_list_like) -from pandas.types.generic import (ABCIndexClass, ABCSeries, - ABCDataFrame) -from pandas.types.missing import notnull -from pandas.core import algorithms - -import pandas.compat as compat - -_DATEUTIL_LEXER_SPLIT = None -try: - # Since these are private methods from dateutil, it is safely imported - # here so in case this interface changes, pandas will just fallback - # to not using the functionality - from dateutil.parser import _timelex - - if hasattr(_timelex, 'split'): - def _lexer_split_from_str(dt_str): - # The StringIO(str(_)) is for dateutil 2.2 compatibility - return _timelex.split(compat.StringIO(str(dt_str))) - - _DATEUTIL_LEXER_SPLIT = _lexer_split_from_str -except (ImportError, AttributeError): - pass - - -def _infer_tzinfo(start, end): - def _infer(a, b): - tz = a.tzinfo - if b and b.tzinfo: - if not (tslib.get_timezone(tz) == tslib.get_timezone(b.tzinfo)): - raise AssertionError('Inputs must both have the same timezone,' - ' {0} != {1}'.format(tz, b.tzinfo)) - return tz - - tz = None - if start is not None: - tz = _infer(start, end) - elif end is not None: - tz = _infer(end, start) - return tz - - -def _guess_datetime_format(dt_str, dayfirst=False, - dt_str_parse=compat.parse_date, - dt_str_split=_DATEUTIL_LEXER_SPLIT): - """ - Guess the datetime format of a given datetime string. - - Parameters - ---------- - dt_str : string, datetime string to guess the format of - dayfirst : boolean, default False - If True parses dates with the day first, eg 20/01/2005 - Warning: dayfirst=True is not strict, but will prefer to parse - with day first (this is a known bug). - dt_str_parse : function, defaults to `compat.parse_date` (dateutil) - This function should take in a datetime string and return - a `datetime.datetime` guess that the datetime string represents - dt_str_split : function, defaults to `_DATEUTIL_LEXER_SPLIT` (dateutil) - This function should take in a datetime string and return - a list of strings, the guess of the various specific parts - e.g. '2011/12/30' -> ['2011', '/', '12', '/', '30'] - - Returns - ------- - ret : datetime format string (for `strftime` or `strptime`) - """ - if dt_str_parse is None or dt_str_split is None: - return None - - if not isinstance(dt_str, compat.string_types): - return None - - day_attribute_and_format = (('day',), '%d', 2) - - # attr name, format, padding (if any) - datetime_attrs_to_format = [ - (('year', 'month', 'day'), '%Y%m%d', 0), - (('year',), '%Y', 0), - (('month',), '%B', 0), - (('month',), '%b', 0), - (('month',), '%m', 2), - day_attribute_and_format, - (('hour',), '%H', 2), - (('minute',), '%M', 2), - (('second',), '%S', 2), - (('microsecond',), '%f', 6), - (('second', 'microsecond'), '%S.%f', 0), - ] - - if dayfirst: - datetime_attrs_to_format.remove(day_attribute_and_format) - datetime_attrs_to_format.insert(0, day_attribute_and_format) - - try: - parsed_datetime = dt_str_parse(dt_str, dayfirst=dayfirst) - except: - # In case the datetime can't be parsed, its format cannot be guessed - return None - - if parsed_datetime is None: - return None - - try: - tokens = dt_str_split(dt_str) - except: - # In case the datetime string can't be split, its format cannot - # be guessed - return None - - format_guess = [None] * len(tokens) - found_attrs = set() - - for attrs, attr_format, padding in datetime_attrs_to_format: - # If a given attribute has been placed in the format string, skip - # over other formats for that same underlying attribute (IE, month - # can be represented in multiple different ways) - if set(attrs) & found_attrs: - continue - - if all(getattr(parsed_datetime, attr) is not None for attr in attrs): - for i, token_format in enumerate(format_guess): - token_filled = tokens[i].zfill(padding) - if (token_format is None and - token_filled == parsed_datetime.strftime(attr_format)): - format_guess[i] = attr_format - tokens[i] = token_filled - found_attrs.update(attrs) - break - - # Only consider it a valid guess if we have a year, month and day - if len(set(['year', 'month', 'day']) & found_attrs) != 3: - return None - - output_format = [] - for i, guess in enumerate(format_guess): - if guess is not None: - # Either fill in the format placeholder (like %Y) - output_format.append(guess) - else: - # Or just the token separate (IE, the dashes in "01-01-2013") - try: - # If the token is numeric, then we likely didn't parse it - # properly, so our guess is wrong - float(tokens[i]) - return None - except ValueError: - pass - - output_format.append(tokens[i]) - - guessed_format = ''.join(output_format) - - # rebuild string, capturing any inferred padding - dt_str = ''.join(tokens) - if parsed_datetime.strftime(guessed_format) == dt_str: - return guessed_format - - -def _guess_datetime_format_for_array(arr, **kwargs): - # Try to guess the format based on the first non-NaN element - non_nan_elements = notnull(arr).nonzero()[0] - if len(non_nan_elements): - return _guess_datetime_format(arr[non_nan_elements[0]], **kwargs) - - -def to_datetime(arg, errors='raise', dayfirst=False, yearfirst=False, - utc=None, box=True, format=None, exact=True, - unit=None, infer_datetime_format=False): - """ - Convert argument to datetime. - - Parameters - ---------- - arg : integer, float, string, datetime, list, tuple, 1-d array, Series - - .. versionadded: 0.18.1 - - or DataFrame/dict-like - - errors : {'ignore', 'raise', 'coerce'}, default 'raise' - - - If 'raise', then invalid parsing will raise an exception - - If 'coerce', then invalid parsing will be set as NaT - - If 'ignore', then invalid parsing will return the input - dayfirst : boolean, default False - Specify a date parse order if `arg` is str or its list-likes. - If True, parses dates with the day first, eg 10/11/12 is parsed as - 2012-11-10. - Warning: dayfirst=True is not strict, but will prefer to parse - with day first (this is a known bug, based on dateutil behavior). - yearfirst : boolean, default False - Specify a date parse order if `arg` is str or its list-likes. - - - If True parses dates with the year first, eg 10/11/12 is parsed as - 2010-11-12. - - If both dayfirst and yearfirst are True, yearfirst is preceded (same - as dateutil). - - Warning: yearfirst=True is not strict, but will prefer to parse - with year first (this is a known bug, based on dateutil beahavior). - - .. versionadded: 0.16.1 - - utc : boolean, default None - Return UTC DatetimeIndex if True (converting any tz-aware - datetime.datetime objects as well). - box : boolean, default True - - - If True returns a DatetimeIndex - - If False returns ndarray of values. - format : string, default None - strftime to parse time, eg "%d/%m/%Y", note that "%f" will parse - all the way up to nanoseconds. - exact : boolean, True by default - - - If True, require an exact format match. - - If False, allow the format to match anywhere in the target string. - - unit : string, default 'ns' - unit of the arg (D,s,ms,us,ns) denote the unit in epoch - (e.g. a unix timestamp), which is an integer/float number. - infer_datetime_format : boolean, default False - If True and no `format` is given, attempt to infer the format of the - datetime strings, and if it can be inferred, switch to a faster - method of parsing them. In some cases this can increase the parsing - speed by ~5-10x. - - Returns - ------- - ret : datetime if parsing succeeded. - Return type depends on input: - - - list-like: DatetimeIndex - - Series: Series of datetime64 dtype - - scalar: Timestamp - - In case when it is not possible to return designated types (e.g. when - any element of input is before Timestamp.min or after Timestamp.max) - return will have datetime.datetime type (or correspoding array/Series). - - Examples - -------- - - Assembling a datetime from multiple columns of a DataFrame. The keys can be - common abbreviations like ['year', 'month', 'day', 'minute', 'second', - 'ms', 'us', 'ns']) or plurals of the same - - >>> df = pd.DataFrame({'year': [2015, 2016], - 'month': [2, 3], - 'day': [4, 5]}) - >>> pd.to_datetime(df) - 0 2015-02-04 - 1 2016-03-05 - dtype: datetime64[ns] - - If a date does not meet the `timestamp limitations - `_, passing errors='ignore' - will return the original input instead of raising any exception. - - Passing errors='coerce' will force an out-of-bounds date to NaT, - in addition to forcing non-dates (or non-parseable dates) to NaT. - - >>> pd.to_datetime('13000101', format='%Y%m%d', errors='ignore') - datetime.datetime(1300, 1, 1, 0, 0) - >>> pd.to_datetime('13000101', format='%Y%m%d', errors='coerce') - NaT - - Passing infer_datetime_format=True can often-times speedup a parsing - if its not an ISO8601 format exactly, but in a regular format. - - >>> s = pd.Series(['3/11/2000', '3/12/2000', '3/13/2000']*1000) - - >>> s.head() - 0 3/11/2000 - 1 3/12/2000 - 2 3/13/2000 - 3 3/11/2000 - 4 3/12/2000 - dtype: object - - >>> %timeit pd.to_datetime(s,infer_datetime_format=True) - 100 loops, best of 3: 10.4 ms per loop - - >>> %timeit pd.to_datetime(s,infer_datetime_format=False) - 1 loop, best of 3: 471 ms per loop - - """ - - from pandas.tseries.index import DatetimeIndex - - tz = 'utc' if utc else None - - def _convert_listlike(arg, box, format, name=None, tz=tz): - - if isinstance(arg, (list, tuple)): - arg = np.array(arg, dtype='O') - - # these are shortcutable - if is_datetime64tz_dtype(arg): - if not isinstance(arg, DatetimeIndex): - return DatetimeIndex(arg, tz=tz, name=name) - if utc: - arg = arg.tz_convert(None).tz_localize('UTC') - return arg - - elif is_datetime64_ns_dtype(arg): - if box and not isinstance(arg, DatetimeIndex): - try: - return DatetimeIndex(arg, tz=tz, name=name) - except ValueError: - pass - - return arg - - elif unit is not None: - if format is not None: - raise ValueError("cannot specify both format and unit") - arg = getattr(arg, 'values', arg) - result = tslib.array_with_unit_to_datetime(arg, unit, - errors=errors) - if box: - if errors == 'ignore': - from pandas import Index - return Index(result) - - return DatetimeIndex(result, tz=tz, name=name) - return result - elif getattr(arg, 'ndim', 1) > 1: - raise TypeError('arg must be a string, datetime, list, tuple, ' - '1-d array, or Series') - - arg = _ensure_object(arg) - require_iso8601 = False - - if infer_datetime_format and format is None: - format = _guess_datetime_format_for_array(arg, dayfirst=dayfirst) - - if format is not None: - # There is a special fast-path for iso8601 formatted - # datetime strings, so in those cases don't use the inferred - # format because this path makes process slower in this - # special case - format_is_iso8601 = _format_is_iso(format) - if format_is_iso8601: - require_iso8601 = not infer_datetime_format - format = None - - try: - result = None - - if format is not None: - # shortcut formatting here - if format == '%Y%m%d': - try: - result = _attempt_YYYYMMDD(arg, errors=errors) - except: - raise ValueError("cannot convert the input to " - "'%Y%m%d' date format") - - # fallback - if result is None: - try: - result = tslib.array_strptime(arg, format, exact=exact, - errors=errors) - except tslib.OutOfBoundsDatetime: - if errors == 'raise': - raise - result = arg - except ValueError: - # if format was inferred, try falling back - # to array_to_datetime - terminate here - # for specified formats - if not infer_datetime_format: - if errors == 'raise': - raise - result = arg - - if result is None and (format is None or infer_datetime_format): - result = tslib.array_to_datetime( - arg, - errors=errors, - utc=utc, - dayfirst=dayfirst, - yearfirst=yearfirst, - require_iso8601=require_iso8601 - ) - - if is_datetime64_dtype(result) and box: - result = DatetimeIndex(result, tz=tz, name=name) - return result - - except ValueError as e: - try: - values, tz = tslib.datetime_to_datetime64(arg) - return DatetimeIndex._simple_new(values, name=name, tz=tz) - except (ValueError, TypeError): - raise e - - if arg is None: - return arg - elif isinstance(arg, tslib.Timestamp): - return arg - elif isinstance(arg, ABCSeries): - from pandas import Series - values = _convert_listlike(arg._values, False, format) - return Series(values, index=arg.index, name=arg.name) - elif isinstance(arg, (ABCDataFrame, MutableMapping)): - return _assemble_from_unit_mappings(arg, errors=errors) - elif isinstance(arg, ABCIndexClass): - return _convert_listlike(arg, box, format, name=arg.name) - elif is_list_like(arg): - return _convert_listlike(arg, box, format) - - return _convert_listlike(np.array([arg]), box, format)[0] - - -# mappings for assembling units -_unit_map = {'year': 'year', - 'years': 'year', - 'month': 'month', - 'months': 'month', - 'day': 'day', - 'days': 'day', - 'hour': 'h', - 'hours': 'h', - 'minute': 'm', - 'minutes': 'm', - 'second': 's', - 'seconds': 's', - 'ms': 'ms', - 'millisecond': 'ms', - 'milliseconds': 'ms', - 'us': 'us', - 'microsecond': 'us', - 'microseconds': 'us', - 'ns': 'ns', - 'nanosecond': 'ns', - 'nanoseconds': 'ns' - } - - -def _assemble_from_unit_mappings(arg, errors): - """ - assemble the unit specifed fields from the arg (DataFrame) - Return a Series for actual parsing - - Parameters - ---------- - arg : DataFrame - errors : {'ignore', 'raise', 'coerce'}, default 'raise' - - - If 'raise', then invalid parsing will raise an exception - - If 'coerce', then invalid parsing will be set as NaT - - If 'ignore', then invalid parsing will return the input - - Returns - ------- - Series - """ - from pandas import to_timedelta, to_numeric, DataFrame - arg = DataFrame(arg) - if not arg.columns.is_unique: - raise ValueError("cannot assemble with duplicate keys") - - # replace passed unit with _unit_map - def f(value): - if value in _unit_map: - return _unit_map[value] - - # m is case significant - if value.lower() in _unit_map: - return _unit_map[value.lower()] - - return value - - unit = {k: f(k) for k in arg.keys()} - unit_rev = {v: k for k, v in unit.items()} - - # we require at least Ymd - required = ['year', 'month', 'day'] - req = sorted(list(set(required) - set(unit_rev.keys()))) - if len(req): - raise ValueError("to assemble mappings requires at " - "least that [year, month, day] be specified: " - "[{0}] is missing".format(','.join(req))) - - # keys we don't recognize - excess = sorted(list(set(unit_rev.keys()) - set(_unit_map.values()))) - if len(excess): - raise ValueError("extra keys have been passed " - "to the datetime assemblage: " - "[{0}]".format(','.join(excess))) - - def coerce(values): - # we allow coercion to if errors allows - values = to_numeric(values, errors=errors) - - # prevent overflow in case of int8 or int16 - if is_integer_dtype(values): - values = values.astype('int64', copy=False) - return values - - values = (coerce(arg[unit_rev['year']]) * 10000 + - coerce(arg[unit_rev['month']]) * 100 + - coerce(arg[unit_rev['day']])) - try: - values = to_datetime(values, format='%Y%m%d', errors=errors) - except (TypeError, ValueError) as e: - raise ValueError("cannot assemble the " - "datetimes: {0}".format(e)) - - for u in ['h', 'm', 's', 'ms', 'us', 'ns']: - value = unit_rev.get(u) - if value is not None and value in arg: - try: - values += to_timedelta(coerce(arg[value]), - unit=u, - errors=errors) - except (TypeError, ValueError) as e: - raise ValueError("cannot assemble the datetimes " - "[{0}]: {1}".format(value, e)) - - return values - - -def _attempt_YYYYMMDD(arg, errors): - """ try to parse the YYYYMMDD/%Y%m%d format, try to deal with NaT-like, - arg is a passed in as an object dtype, but could really be ints/strings - with nan-like/or floats (e.g. with nan) - - Parameters - ---------- - arg : passed value - errors : 'raise','ignore','coerce' - """ - - def calc(carg): - # calculate the actual result - carg = carg.astype(object) - parsed = lib.try_parse_year_month_day(carg / 10000, - carg / 100 % 100, - carg % 100) - return tslib.array_to_datetime(parsed, errors=errors) - - def calc_with_mask(carg, mask): - result = np.empty(carg.shape, dtype='M8[ns]') - iresult = result.view('i8') - iresult[~mask] = tslib.iNaT - result[mask] = calc(carg[mask].astype(np.float64).astype(np.int64)). \ - astype('M8[ns]') - return result - - # try intlike / strings that are ints - try: - return calc(arg.astype(np.int64)) - except: - pass - - # a float with actual np.nan - try: - carg = arg.astype(np.float64) - return calc_with_mask(carg, notnull(carg)) - except: - pass - - # string with NaN-like - try: - mask = ~algorithms.isin(arg, list(tslib._nat_strings)) - return calc_with_mask(arg, mask) - except: - pass - - return None - - -def _format_is_iso(f): - """ - Does format match the iso8601 set that can be handled by the C parser? - Generally of form YYYY-MM-DDTHH:MM:SS - date separator can be different - but must be consistent. Leading 0s in dates and times are optional. - """ - iso_template = '%Y{date_sep}%m{date_sep}%d{time_sep}%H:%M:%S.%f'.format - excluded_formats = ['%Y%m%d', '%Y%m', '%Y'] - - for date_sep in [' ', '/', '\\', '-', '.', '']: - for time_sep in [' ', 'T']: - if (iso_template(date_sep=date_sep, - time_sep=time_sep - ).startswith(f) and f not in excluded_formats): - return True - return False - - -def parse_time_string(arg, freq=None, dayfirst=None, yearfirst=None): - """ - Try hard to parse datetime string, leveraging dateutil plus some extra - goodies like quarter recognition. - - Parameters - ---------- - arg : compat.string_types - freq : str or DateOffset, default None - Helps with interpreting time string if supplied - dayfirst : bool, default None - If None uses default from print_config - yearfirst : bool, default None - If None uses default from print_config - - Returns - ------- - datetime, datetime/dateutil.parser._result, str - """ - from pandas.core.config import get_option - if not isinstance(arg, compat.string_types): - return arg - - from pandas.tseries.offsets import DateOffset - if isinstance(freq, DateOffset): - freq = freq.rule_code - - if dayfirst is None: - dayfirst = get_option("display.date_dayfirst") - if yearfirst is None: - yearfirst = get_option("display.date_yearfirst") - - return tslib.parse_datetime_string_with_reso(arg, freq=freq, - dayfirst=dayfirst, - yearfirst=yearfirst) - - -DateParseError = tslib.DateParseError -normalize_date = tslib.normalize_date - -# Fixed time formats for time parsing -_time_formats = ["%H:%M", "%H%M", "%I:%M%p", "%I%M%p", - "%H:%M:%S", "%H%M%S", "%I:%M:%S%p", "%I%M%S%p"] - - -def _guess_time_format_for_array(arr): - # Try to guess the format based on the first non-NaN element - non_nan_elements = notnull(arr).nonzero()[0] - if len(non_nan_elements): - element = arr[non_nan_elements[0]] - for time_format in _time_formats: - try: - datetime.strptime(element, time_format) - return time_format - except ValueError: - pass - - return None - - -def to_time(arg, format=None, infer_time_format=False, errors='raise'): - """ - Parse time strings to time objects using fixed strptime formats ("%H:%M", - "%H%M", "%I:%M%p", "%I%M%p", "%H:%M:%S", "%H%M%S", "%I:%M:%S%p", - "%I%M%S%p") - - Use infer_time_format if all the strings are in the same format to speed - up conversion. - - Parameters - ---------- - arg : string in time format, datetime.time, list, tuple, 1-d array, Series - format : str, default None - Format used to convert arg into a time object. If None, fixed formats - are used. - infer_time_format: bool, default False - Infer the time format based on the first non-NaN element. If all - strings are in the same format, this will speed up conversion. - errors : {'ignore', 'raise', 'coerce'}, default 'raise' - - If 'raise', then invalid parsing will raise an exception - - If 'coerce', then invalid parsing will be set as None - - If 'ignore', then invalid parsing will return the input - - Returns - ------- - datetime.time - """ - from pandas.core.series import Series - - def _convert_listlike(arg, format): - - if isinstance(arg, (list, tuple)): - arg = np.array(arg, dtype='O') - - elif getattr(arg, 'ndim', 1) > 1: - raise TypeError('arg must be a string, datetime, list, tuple, ' - '1-d array, or Series') - - arg = _ensure_object(arg) - - if infer_time_format and format is None: - format = _guess_time_format_for_array(arg) - - times = [] - if format is not None: - for element in arg: - try: - times.append(datetime.strptime(element, format).time()) - except (ValueError, TypeError): - if errors == 'raise': - raise ValueError("Cannot convert %s to a time with " - "given format %s" % (element, format)) - elif errors == 'ignore': - return arg - else: - times.append(None) - else: - formats = _time_formats[:] - format_found = False - for element in arg: - time_object = None - for time_format in formats: - try: - time_object = datetime.strptime(element, - time_format).time() - if not format_found: - # Put the found format in front - fmt = formats.pop(formats.index(time_format)) - formats.insert(0, fmt) - format_found = True - break - except (ValueError, TypeError): - continue - - if time_object is not None: - times.append(time_object) - elif errors == 'raise': - raise ValueError("Cannot convert arg {arg} to " - "a time".format(arg=arg)) - elif errors == 'ignore': - return arg - else: - times.append(None) - - return times - - if arg is None: - return arg - elif isinstance(arg, time): - return arg - elif isinstance(arg, Series): - values = _convert_listlike(arg._values, format) - return Series(values, index=arg.index, name=arg.name) - elif isinstance(arg, ABCIndexClass): - return _convert_listlike(arg, format) - elif is_list_like(arg): - return _convert_listlike(arg, format) - - return _convert_listlike(np.array([arg]), format)[0] - - -def format(dt): - """Returns date in YYYYMMDD format.""" - return dt.strftime('%Y%m%d') - - -OLE_TIME_ZERO = datetime(1899, 12, 30, 0, 0, 0) - - -def ole2datetime(oledt): - """function for converting excel date to normal date format""" - val = float(oledt) - - # Excel has a bug where it thinks the date 2/29/1900 exists - # we just reject any date before 3/1/1900. - if val < 61: - raise ValueError("Value is outside of acceptable range: %s " % val) - - return OLE_TIME_ZERO + timedelta(days=val) diff --git a/pandas/tseries/util.py b/pandas/tseries/util.py deleted file mode 100644 index da3bb075dd02c..0000000000000 --- a/pandas/tseries/util.py +++ /dev/null @@ -1,104 +0,0 @@ -import warnings - -from pandas.compat import lrange -import numpy as np -from pandas.types.common import _ensure_platform_int -from pandas.core.frame import DataFrame -import pandas.core.algorithms as algorithms - - -def pivot_annual(series, freq=None): - """ - Deprecated. Use ``pivot_table`` instead. - - Group a series by years, taking leap years into account. - - The output has as many rows as distinct years in the original series, - and as many columns as the length of a leap year in the units corresponding - to the original frequency (366 for daily frequency, 366*24 for hourly...). - The fist column of the output corresponds to Jan. 1st, 00:00:00, - while the last column corresponds to Dec, 31st, 23:59:59. - Entries corresponding to Feb. 29th are masked for non-leap years. - - For example, if the initial series has a daily frequency, the 59th column - of the output always corresponds to Feb. 28th, the 61st column to Mar. 1st, - and the 60th column is masked for non-leap years. - With a hourly initial frequency, the (59*24)th column of the output always - correspond to Feb. 28th 23:00, the (61*24)th column to Mar. 1st, 00:00, and - the 24 columns between (59*24) and (61*24) are masked. - - If the original frequency is less than daily, the output is equivalent to - ``series.convert('A', func=None)``. - - Parameters - ---------- - series : Series - freq : string or None, default None - - Returns - ------- - annual : DataFrame - """ - - msg = "pivot_annual is deprecated. Use pivot_table instead" - warnings.warn(msg, FutureWarning) - - index = series.index - year = index.year - years = algorithms.unique1d(year) - - if freq is not None: - freq = freq.upper() - else: - freq = series.index.freq - - if freq == 'D': - width = 366 - offset = np.asarray(index.dayofyear) - 1 - - # adjust for leap year - offset[(~isleapyear(year)) & (offset >= 59)] += 1 - - columns = lrange(1, 367) - # todo: strings like 1/1, 1/25, etc.? - elif freq in ('M', 'BM'): - width = 12 - offset = np.asarray(index.month) - 1 - columns = lrange(1, 13) - elif freq == 'H': - width = 8784 - grouped = series.groupby(series.index.year) - defaulted = grouped.apply(lambda x: x.reset_index(drop=True)) - defaulted.index = defaulted.index.droplevel(0) - offset = np.asarray(defaulted.index) - offset[~isleapyear(year) & (offset >= 1416)] += 24 - columns = lrange(1, 8785) - else: - raise NotImplementedError(freq) - - flat_index = (year - years.min()) * width + offset - flat_index = _ensure_platform_int(flat_index) - - values = np.empty((len(years), width)) - values.fill(np.nan) - values.put(flat_index, series.values) - - return DataFrame(values, index=years, columns=columns) - - -def isleapyear(year): - """ - Returns true if year is a leap year. - - Parameters - ---------- - year : integer / sequence - A given (list of) year(s). - """ - - msg = "isleapyear is deprecated. Use .is_leap_year property instead" - warnings.warn(msg, FutureWarning) - - year = np.asarray(year) - return np.logical_or(year % 400 == 0, - np.logical_and(year % 4 == 0, year % 100 > 0)) diff --git a/pandas/tslib.py b/pandas/tslib.py deleted file mode 100644 index 3ecbffa20700d..0000000000000 --- a/pandas/tslib.py +++ /dev/null @@ -1,8 +0,0 @@ -# flake8: noqa - -import warnings -warnings.warn("The pandas.tslib module is deprecated and will be " - "removed in a future version. Please import from " - "the pandas._libs.tslib instead", FutureWarning, stacklevel=2) -from pandas._libs.tslib import (Timestamp, Timedelta, - NaT, OutOfBoundsDatetime) diff --git a/pandas/types/api.py b/pandas/types/api.py deleted file mode 100644 index c809cb3614a8c..0000000000000 --- a/pandas/types/api.py +++ /dev/null @@ -1,58 +0,0 @@ -# flake8: noqa - -import numpy as np - -from .common import (pandas_dtype, - is_dtype_equal, - is_extension_type, - - # categorical - is_categorical, - is_categorical_dtype, - - # datetimelike - is_datetimetz, - is_datetime64_dtype, - is_datetime64tz_dtype, - is_datetime64_any_dtype, - is_datetime64_ns_dtype, - is_timedelta64_dtype, - is_timedelta64_ns_dtype, - is_period, - is_period_dtype, - - # string-like - is_string_dtype, - is_object_dtype, - - # sparse - is_sparse, - - # numeric types - is_scalar, - is_sparse, - is_bool, - is_integer, - is_float, - is_complex, - is_number, - is_any_int_dtype, - is_integer_dtype, - is_int64_dtype, - is_numeric_dtype, - is_float_dtype, - is_floating_dtype, - is_bool_dtype, - is_complex_dtype, - is_signed_integer_dtype, - is_unsigned_integer_dtype, - - # like - is_re, - is_re_compilable, - is_dict_like, - is_iterator, - is_list_like, - is_hashable, - is_named_tuple, - is_sequence) diff --git a/pandas/types/common.py b/pandas/types/common.py deleted file mode 100644 index a1f03e59a5e6e..0000000000000 --- a/pandas/types/common.py +++ /dev/null @@ -1,507 +0,0 @@ -""" common type operations """ - -import numpy as np -from pandas.compat import (string_types, text_type, binary_type, - PY3, PY36) -from pandas._libs import algos, lib -from .dtypes import (CategoricalDtype, CategoricalDtypeType, - DatetimeTZDtype, DatetimeTZDtypeType, - PeriodDtype, PeriodDtypeType, - ExtensionDtype) -from .generic import (ABCCategorical, ABCPeriodIndex, - ABCDatetimeIndex, ABCSeries, - ABCSparseArray, ABCSparseSeries) -from .inference import is_string_like -from .inference import * # noqa - - -_POSSIBLY_CAST_DTYPES = set([np.dtype(t).name - for t in ['O', 'int8', 'uint8', 'int16', 'uint16', - 'int32', 'uint32', 'int64', 'uint64']]) - -_NS_DTYPE = np.dtype('M8[ns]') -_TD_DTYPE = np.dtype('m8[ns]') -_INT64_DTYPE = np.dtype(np.int64) - -# oh the troubles to reduce import time -_is_scipy_sparse = None - -_ensure_float64 = algos.ensure_float64 -_ensure_float32 = algos.ensure_float32 - - -def _ensure_float(arr): - if issubclass(arr.dtype.type, (np.integer, np.bool_)): - arr = arr.astype(float) - return arr - - -_ensure_uint64 = algos.ensure_uint64 -_ensure_int64 = algos.ensure_int64 -_ensure_int32 = algos.ensure_int32 -_ensure_int16 = algos.ensure_int16 -_ensure_int8 = algos.ensure_int8 -_ensure_platform_int = algos.ensure_platform_int -_ensure_object = algos.ensure_object - - -def _ensure_categorical(arr): - if not is_categorical(arr): - from pandas import Categorical - arr = Categorical(arr) - return arr - - -def is_object_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.object_) - - -def is_sparse(array): - """ return if we are a sparse array """ - return isinstance(array, (ABCSparseArray, ABCSparseSeries)) - - -def is_scipy_sparse(array): - """ return if we are a scipy.sparse.spmatrix """ - global _is_scipy_sparse - if _is_scipy_sparse is None: - try: - from scipy.sparse import issparse as _is_scipy_sparse - except ImportError: - _is_scipy_sparse = lambda _: False - return _is_scipy_sparse(array) - - -def is_categorical(array): - """ return if we are a categorical possibility """ - return isinstance(array, ABCCategorical) or is_categorical_dtype(array) - - -def is_datetimetz(array): - """ return if we are a datetime with tz array """ - return ((isinstance(array, ABCDatetimeIndex) and - getattr(array, 'tz', None) is not None) or - is_datetime64tz_dtype(array)) - - -def is_period(array): - """ return if we are a period array """ - return isinstance(array, ABCPeriodIndex) or is_period_arraylike(array) - - -def is_datetime64_dtype(arr_or_dtype): - try: - tipo = _get_dtype_type(arr_or_dtype) - except TypeError: - return False - return issubclass(tipo, np.datetime64) - - -def is_datetime64tz_dtype(arr_or_dtype): - return DatetimeTZDtype.is_dtype(arr_or_dtype) - - -def is_timedelta64_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.timedelta64) - - -def is_period_dtype(arr_or_dtype): - return PeriodDtype.is_dtype(arr_or_dtype) - - -def is_categorical_dtype(arr_or_dtype): - return CategoricalDtype.is_dtype(arr_or_dtype) - - -def is_string_dtype(arr_or_dtype): - dtype = _get_dtype(arr_or_dtype) - return dtype.kind in ('O', 'S', 'U') and not is_period_dtype(dtype) - - -def is_period_arraylike(arr): - """ return if we are period arraylike / PeriodIndex """ - if isinstance(arr, ABCPeriodIndex): - return True - elif isinstance(arr, (np.ndarray, ABCSeries)): - return arr.dtype == object and lib.infer_dtype(arr) == 'period' - return getattr(arr, 'inferred_type', None) == 'period' - - -def is_datetime_arraylike(arr): - """ return if we are datetime arraylike / DatetimeIndex """ - if isinstance(arr, ABCDatetimeIndex): - return True - elif isinstance(arr, (np.ndarray, ABCSeries)): - return arr.dtype == object and lib.infer_dtype(arr) == 'datetime' - return getattr(arr, 'inferred_type', None) == 'datetime' - - -def is_datetimelike(arr): - return (is_datetime64_dtype(arr) or is_datetime64tz_dtype(arr) or - is_timedelta64_dtype(arr) or - isinstance(arr, ABCPeriodIndex) or - is_datetimetz(arr)) - - -def is_dtype_equal(source, target): - """ return a boolean if the dtypes are equal """ - try: - source = _get_dtype(source) - target = _get_dtype(target) - return source == target - except (TypeError, AttributeError): - - # invalid comparison - # object == category will hit this - return False - - -def is_any_int_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.integer) - - -def is_integer_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return (issubclass(tipo, np.integer) and - not issubclass(tipo, (np.datetime64, np.timedelta64))) - - -def is_signed_integer_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return (issubclass(tipo, np.signedinteger) and - not issubclass(tipo, (np.datetime64, np.timedelta64))) - - -def is_unsigned_integer_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return (issubclass(tipo, np.unsignedinteger) and - not issubclass(tipo, (np.datetime64, np.timedelta64))) - - -def is_int64_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.int64) - - -def is_int_or_datetime_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return (issubclass(tipo, np.integer) or - issubclass(tipo, (np.datetime64, np.timedelta64))) - - -def is_datetime64_any_dtype(arr_or_dtype): - return (is_datetime64_dtype(arr_or_dtype) or - is_datetime64tz_dtype(arr_or_dtype)) - - -def is_datetime64_ns_dtype(arr_or_dtype): - try: - tipo = _get_dtype(arr_or_dtype) - except TypeError: - if is_datetime64tz_dtype(arr_or_dtype): - tipo = _get_dtype(arr_or_dtype.dtype) - else: - return False - return tipo == _NS_DTYPE or getattr(tipo, 'base', None) == _NS_DTYPE - - -def is_timedelta64_ns_dtype(arr_or_dtype): - tipo = _get_dtype(arr_or_dtype) - return tipo == _TD_DTYPE - - -def is_datetime_or_timedelta_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, (np.datetime64, np.timedelta64)) - - -def _is_unorderable_exception(e): - """ - return a boolean if we an unorderable exception error message - - These are different error message for PY>=3<=3.5 and PY>=3.6 - """ - if PY36: - return "'>' not supported between instances of" in str(e) - - elif PY3: - return 'unorderable' in str(e) - return False - - -def is_numeric_v_string_like(a, b): - """ - numpy doesn't like to compare numeric arrays vs scalar string-likes - - return a boolean result if this is the case for a,b or b,a - - """ - is_a_array = isinstance(a, np.ndarray) - is_b_array = isinstance(b, np.ndarray) - - is_a_numeric_array = is_a_array and is_numeric_dtype(a) - is_b_numeric_array = is_b_array and is_numeric_dtype(b) - is_a_string_array = is_a_array and is_string_like_dtype(a) - is_b_string_array = is_b_array and is_string_like_dtype(b) - - is_a_scalar_string_like = not is_a_array and is_string_like(a) - is_b_scalar_string_like = not is_b_array and is_string_like(b) - - return ((is_a_numeric_array and is_b_scalar_string_like) or - (is_b_numeric_array and is_a_scalar_string_like) or - (is_a_numeric_array and is_b_string_array) or - (is_b_numeric_array and is_a_string_array)) - - -def is_datetimelike_v_numeric(a, b): - # return if we have an i8 convertible and numeric comparison - if not hasattr(a, 'dtype'): - a = np.asarray(a) - if not hasattr(b, 'dtype'): - b = np.asarray(b) - - def is_numeric(x): - return is_integer_dtype(x) or is_float_dtype(x) - - is_datetimelike = needs_i8_conversion - return ((is_datetimelike(a) and is_numeric(b)) or - (is_datetimelike(b) and is_numeric(a))) - - -def is_datetimelike_v_object(a, b): - # return if we have an i8 convertible and object comparsion - if not hasattr(a, 'dtype'): - a = np.asarray(a) - if not hasattr(b, 'dtype'): - b = np.asarray(b) - - def f(x): - return is_object_dtype(x) - - def is_object(x): - return is_integer_dtype(x) or is_float_dtype(x) - - is_datetimelike = needs_i8_conversion - return ((is_datetimelike(a) and is_object(b)) or - (is_datetimelike(b) and is_object(a))) - - -def needs_i8_conversion(arr_or_dtype): - return (is_datetime_or_timedelta_dtype(arr_or_dtype) or - is_datetime64tz_dtype(arr_or_dtype) or - is_period_dtype(arr_or_dtype)) - - -def is_numeric_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return (issubclass(tipo, (np.number, np.bool_)) and - not issubclass(tipo, (np.datetime64, np.timedelta64))) - - -def is_string_like_dtype(arr_or_dtype): - # exclude object as its a mixed dtype - dtype = _get_dtype(arr_or_dtype) - return dtype.kind in ('S', 'U') - - -def is_float_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.floating) - - -def is_floating_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return isinstance(tipo, np.floating) - - -def is_bool_dtype(arr_or_dtype): - try: - tipo = _get_dtype_type(arr_or_dtype) - except ValueError: - # this isn't even a dtype - return False - return issubclass(tipo, np.bool_) - - -def is_extension_type(value): - """ - if we are a klass that is preserved by the internals - these are internal klasses that we represent (and don't use a np.array) - """ - if is_categorical(value): - return True - elif is_sparse(value): - return True - elif is_datetimetz(value): - return True - return False - - -def is_complex_dtype(arr_or_dtype): - tipo = _get_dtype_type(arr_or_dtype) - return issubclass(tipo, np.complexfloating) - - -def _coerce_to_dtype(dtype): - """ coerce a string / np.dtype to a dtype """ - if is_categorical_dtype(dtype): - dtype = CategoricalDtype() - elif is_datetime64tz_dtype(dtype): - dtype = DatetimeTZDtype(dtype) - elif is_period_dtype(dtype): - dtype = PeriodDtype(dtype) - else: - dtype = np.dtype(dtype) - return dtype - - -def _get_dtype(arr_or_dtype): - if isinstance(arr_or_dtype, np.dtype): - return arr_or_dtype - elif isinstance(arr_or_dtype, type): - return np.dtype(arr_or_dtype) - elif isinstance(arr_or_dtype, CategoricalDtype): - return arr_or_dtype - elif isinstance(arr_or_dtype, DatetimeTZDtype): - return arr_or_dtype - elif isinstance(arr_or_dtype, PeriodDtype): - return arr_or_dtype - elif isinstance(arr_or_dtype, string_types): - if is_categorical_dtype(arr_or_dtype): - return CategoricalDtype.construct_from_string(arr_or_dtype) - elif is_datetime64tz_dtype(arr_or_dtype): - return DatetimeTZDtype.construct_from_string(arr_or_dtype) - elif is_period_dtype(arr_or_dtype): - return PeriodDtype.construct_from_string(arr_or_dtype) - - if hasattr(arr_or_dtype, 'dtype'): - arr_or_dtype = arr_or_dtype.dtype - return np.dtype(arr_or_dtype) - - -def _get_dtype_type(arr_or_dtype): - if isinstance(arr_or_dtype, np.dtype): - return arr_or_dtype.type - elif isinstance(arr_or_dtype, type): - return np.dtype(arr_or_dtype).type - elif isinstance(arr_or_dtype, CategoricalDtype): - return CategoricalDtypeType - elif isinstance(arr_or_dtype, DatetimeTZDtype): - return DatetimeTZDtypeType - elif isinstance(arr_or_dtype, PeriodDtype): - return PeriodDtypeType - elif isinstance(arr_or_dtype, string_types): - if is_categorical_dtype(arr_or_dtype): - return CategoricalDtypeType - elif is_datetime64tz_dtype(arr_or_dtype): - return DatetimeTZDtypeType - elif is_period_dtype(arr_or_dtype): - return PeriodDtypeType - return _get_dtype_type(np.dtype(arr_or_dtype)) - try: - return arr_or_dtype.dtype.type - except AttributeError: - return type(None) - - -def _get_dtype_from_object(dtype): - """Get a numpy dtype.type-style object. This handles the datetime64[ns] - and datetime64[ns, TZ] compat - - Notes - ----- - If nothing can be found, returns ``object``. - """ - - # type object from a dtype - if isinstance(dtype, type) and issubclass(dtype, np.generic): - return dtype - elif is_categorical(dtype): - return CategoricalDtype().type - elif is_datetimetz(dtype): - return DatetimeTZDtype(dtype).type - elif isinstance(dtype, np.dtype): # dtype object - try: - _validate_date_like_dtype(dtype) - except TypeError: - # should still pass if we don't have a datelike - pass - return dtype.type - elif isinstance(dtype, string_types): - if dtype in ['datetimetz', 'datetime64tz']: - return DatetimeTZDtype.type - elif dtype in ['period']: - raise NotImplementedError - - if dtype == 'datetime' or dtype == 'timedelta': - dtype += '64' - - try: - return _get_dtype_from_object(getattr(np, dtype)) - except (AttributeError, TypeError): - # handles cases like _get_dtype(int) - # i.e., python objects that are valid dtypes (unlike user-defined - # types, in general) - # TypeError handles the float16 typecode of 'e' - # further handle internal types - pass - - return _get_dtype_from_object(np.dtype(dtype)) - - -def _validate_date_like_dtype(dtype): - try: - typ = np.datetime_data(dtype)[0] - except ValueError as e: - raise TypeError('%s' % e) - if typ != 'generic' and typ != 'ns': - raise ValueError('%r is too specific of a frequency, try passing %r' % - (dtype.name, dtype.type.__name__)) - - -_string_dtypes = frozenset(map(_get_dtype_from_object, (binary_type, - text_type))) - - -def pandas_dtype(dtype): - """ - Converts input into a pandas only dtype object or a numpy dtype object. - - Parameters - ---------- - dtype : object to be converted - - Returns - ------- - np.dtype or a pandas dtype - """ - if isinstance(dtype, DatetimeTZDtype): - return dtype - elif isinstance(dtype, PeriodDtype): - return dtype - elif isinstance(dtype, CategoricalDtype): - return dtype - elif isinstance(dtype, string_types): - try: - return DatetimeTZDtype.construct_from_string(dtype) - except TypeError: - pass - - if dtype.startswith('period[') or dtype.startswith('Period['): - # do not parse string like U as period[U] - try: - return PeriodDtype.construct_from_string(dtype) - except TypeError: - pass - - try: - return CategoricalDtype.construct_from_string(dtype) - except TypeError: - pass - elif isinstance(dtype, ExtensionDtype): - return dtype - - return np.dtype(dtype) diff --git a/pandas/types/concat.py b/pandas/types/concat.py deleted file mode 100644 index b098bbb75d984..0000000000000 --- a/pandas/types/concat.py +++ /dev/null @@ -1,488 +0,0 @@ -""" -Utility functions related to concat -""" - -import numpy as np -import pandas._libs.tslib as tslib -from pandas import compat -from pandas.core.algorithms import take_1d -from .common import (is_categorical_dtype, - is_sparse, - is_datetimetz, - is_datetime64_dtype, - is_timedelta64_dtype, - is_period_dtype, - is_object_dtype, - is_bool_dtype, - is_dtype_equal, - _NS_DTYPE, - _TD_DTYPE) -from pandas.types.generic import (ABCDatetimeIndex, ABCTimedeltaIndex, - ABCPeriodIndex) - - -def get_dtype_kinds(l): - """ - Parameters - ---------- - l : list of arrays - - Returns - ------- - a set of kinds that exist in this list of arrays - """ - - typs = set() - for arr in l: - - dtype = arr.dtype - if is_categorical_dtype(dtype): - typ = 'category' - elif is_sparse(arr): - typ = 'sparse' - elif is_datetimetz(arr): - # if to_concat contains different tz, - # the result must be object dtype - typ = str(arr.dtype) - elif is_datetime64_dtype(dtype): - typ = 'datetime' - elif is_timedelta64_dtype(dtype): - typ = 'timedelta' - elif is_object_dtype(dtype): - typ = 'object' - elif is_bool_dtype(dtype): - typ = 'bool' - elif is_period_dtype(dtype): - typ = str(arr.dtype) - else: - typ = dtype.kind - typs.add(typ) - return typs - - -def _get_series_result_type(result): - """ - return appropriate class of Series concat - input is either dict or array-like - """ - if isinstance(result, dict): - # concat Series with axis 1 - if all(is_sparse(c) for c in compat.itervalues(result)): - from pandas.sparse.api import SparseDataFrame - return SparseDataFrame - else: - from pandas.core.frame import DataFrame - return DataFrame - - elif is_sparse(result): - # concat Series with axis 1 - from pandas.sparse.api import SparseSeries - return SparseSeries - else: - from pandas.core.series import Series - return Series - - -def _get_frame_result_type(result, objs): - """ - return appropriate class of DataFrame-like concat - if any block is SparseBlock, return SparseDataFrame - otherwise, return 1st obj - """ - if any(b.is_sparse for b in result.blocks): - from pandas.sparse.api import SparseDataFrame - return SparseDataFrame - else: - return objs[0] - - -def _concat_compat(to_concat, axis=0): - """ - provide concatenation of an array of arrays each of which is a single - 'normalized' dtypes (in that for example, if it's object, then it is a - non-datetimelike and provide a combined dtype for the resulting array that - preserves the overall dtype if possible) - - Parameters - ---------- - to_concat : array of arrays - axis : axis to provide concatenation - - Returns - ------- - a single array, preserving the combined dtypes - """ - - # filter empty arrays - # 1-d dtypes always are included here - def is_nonempty(x): - try: - return x.shape[axis] > 0 - except Exception: - return True - - nonempty = [x for x in to_concat if is_nonempty(x)] - - # If all arrays are empty, there's nothing to convert, just short-cut to - # the concatenation, #3121. - # - # Creating an empty array directly is tempting, but the winnings would be - # marginal given that it would still require shape & dtype calculation and - # np.concatenate which has them both implemented is compiled. - - typs = get_dtype_kinds(to_concat) - - _contains_datetime = any(typ.startswith('datetime') for typ in typs) - _contains_period = any(typ.startswith('period') for typ in typs) - - if 'category' in typs: - # this must be priort to _concat_datetime, - # to support Categorical + datetime-like - return _concat_categorical(to_concat, axis=axis) - - elif _contains_datetime or 'timedelta' in typs or _contains_period: - return _concat_datetime(to_concat, axis=axis, typs=typs) - - # these are mandated to handle empties as well - elif 'sparse' in typs: - return _concat_sparse(to_concat, axis=axis, typs=typs) - - if not nonempty: - # we have all empties, but may need to coerce the result dtype to - # object if we have non-numeric type operands (numpy would otherwise - # cast this to float) - typs = get_dtype_kinds(to_concat) - if len(typs) != 1: - - if (not len(typs - set(['i', 'u', 'f'])) or - not len(typs - set(['bool', 'i', 'u']))): - # let numpy coerce - pass - else: - # coerce to object - to_concat = [x.astype('object') for x in to_concat] - - return np.concatenate(to_concat, axis=axis) - - -def _concat_categorical(to_concat, axis=0): - """Concatenate an object/categorical array of arrays, each of which is a - single dtype - - Parameters - ---------- - to_concat : array of arrays - axis : int - Axis to provide concatenation in the current implementation this is - always 0, e.g. we only have 1D categoricals - - Returns - ------- - Categorical - A single array, preserving the combined dtypes - """ - - def _concat_asobject(to_concat): - to_concat = [x.get_values() if is_categorical_dtype(x.dtype) - else x.ravel() for x in to_concat] - res = _concat_compat(to_concat) - if axis == 1: - return res.reshape(1, len(res)) - else: - return res - - # we could have object blocks and categoricals here - # if we only have a single categoricals then combine everything - # else its a non-compat categorical - categoricals = [x for x in to_concat if is_categorical_dtype(x.dtype)] - - # validate the categories - if len(categoricals) != len(to_concat): - pass - else: - # when all categories are identical - first = to_concat[0] - if all(first.is_dtype_equal(other) for other in to_concat[1:]): - return union_categoricals(categoricals) - - return _concat_asobject(to_concat) - - -def union_categoricals(to_union, sort_categories=False, ignore_order=False): - """ - Combine list-like of Categorical-like, unioning categories. All - categories must have the same dtype. - - .. versionadded:: 0.19.0 - - Parameters - ---------- - to_union : list-like of Categorical, CategoricalIndex, - or Series with dtype='category' - sort_categories : boolean, default False - If true, resulting categories will be lexsorted, otherwise - they will be ordered as they appear in the data. - ignore_order: boolean, default False - If true, the ordered attribute of the Categoricals will be ignored. - Results in an unordered categorical. - - .. versionadded:: 0.20.0 - - Returns - ------- - result : Categorical - - Raises - ------ - TypeError - - all inputs do not have the same dtype - - all inputs do not have the same ordered property - - all inputs are ordered and their categories are not identical - - sort_categories=True and Categoricals are ordered - ValueError - Empty list of categoricals passed - """ - from pandas import Index, Categorical, CategoricalIndex, Series - - if len(to_union) == 0: - raise ValueError('No Categoricals to union') - - def _maybe_unwrap(x): - if isinstance(x, (CategoricalIndex, Series)): - return x.values - elif isinstance(x, Categorical): - return x - else: - raise TypeError("all components to combine must be Categorical") - - to_union = [_maybe_unwrap(x) for x in to_union] - first = to_union[0] - - if not all(is_dtype_equal(other.categories.dtype, first.categories.dtype) - for other in to_union[1:]): - raise TypeError("dtype of categories must be the same") - - ordered = False - if all(first.is_dtype_equal(other) for other in to_union[1:]): - # identical categories - fastpath - categories = first.categories - ordered = first.ordered - new_codes = np.concatenate([c.codes for c in to_union]) - - if sort_categories and not ignore_order and ordered: - raise TypeError("Cannot use sort_categories=True with " - "ordered Categoricals") - - if sort_categories and not categories.is_monotonic_increasing: - categories = categories.sort_values() - indexer = categories.get_indexer(first.categories) - new_codes = take_1d(indexer, new_codes, fill_value=-1) - elif ignore_order or all(not c.ordered for c in to_union): - # different categories - union and recode - cats = first.categories.append([c.categories for c in to_union[1:]]) - categories = Index(cats.unique()) - if sort_categories: - categories = categories.sort_values() - - new_codes = [] - for c in to_union: - if len(c.categories) > 0: - indexer = categories.get_indexer(c.categories) - new_codes.append(take_1d(indexer, c.codes, fill_value=-1)) - else: - # must be all NaN - new_codes.append(c.codes) - new_codes = np.concatenate(new_codes) - else: - # ordered - to show a proper error message - if all(c.ordered for c in to_union): - msg = ("to union ordered Categoricals, " - "all categories must be the same") - raise TypeError(msg) - else: - raise TypeError('Categorical.ordered must be the same') - - if ignore_order: - ordered = False - - return Categorical(new_codes, categories=categories, ordered=ordered, - fastpath=True) - - -def _concat_datetime(to_concat, axis=0, typs=None): - """ - provide concatenation of an datetimelike array of arrays each of which is a - single M8[ns], datetimet64[ns, tz] or m8[ns] dtype - - Parameters - ---------- - to_concat : array of arrays - axis : axis to provide concatenation - typs : set of to_concat dtypes - - Returns - ------- - a single array, preserving the combined dtypes - """ - - def convert_to_pydatetime(x, axis): - # coerce to an object dtype - - # if dtype is of datetimetz or timezone - if x.dtype.kind == _NS_DTYPE.kind: - if getattr(x, 'tz', None) is not None: - x = x.asobject.values - else: - shape = x.shape - x = tslib.ints_to_pydatetime(x.view(np.int64).ravel(), - box=True) - x = x.reshape(shape) - - elif x.dtype == _TD_DTYPE: - shape = x.shape - x = tslib.ints_to_pytimedelta(x.view(np.int64).ravel(), box=True) - x = x.reshape(shape) - - if axis == 1: - x = np.atleast_2d(x) - return x - - if typs is None: - typs = get_dtype_kinds(to_concat) - - # must be single dtype - if len(typs) == 1: - _contains_datetime = any(typ.startswith('datetime') for typ in typs) - _contains_period = any(typ.startswith('period') for typ in typs) - - if _contains_datetime: - - if 'datetime' in typs: - new_values = np.concatenate([x.view(np.int64) for x in - to_concat], axis=axis) - return new_values.view(_NS_DTYPE) - else: - # when to_concat has different tz, len(typs) > 1. - # thus no need to care - return _concat_datetimetz(to_concat) - - elif 'timedelta' in typs: - new_values = np.concatenate([x.view(np.int64) for x in to_concat], - axis=axis) - return new_values.view(_TD_DTYPE) - - elif _contains_period: - # PeriodIndex must be handled by PeriodIndex, - # Thus can't meet this condition ATM - # Must be changed when we adding PeriodDtype - raise NotImplementedError - - # need to coerce to object - to_concat = [convert_to_pydatetime(x, axis) for x in to_concat] - return np.concatenate(to_concat, axis=axis) - - -def _concat_datetimetz(to_concat, name=None): - """ - concat DatetimeIndex with the same tz - all inputs must be DatetimeIndex - it is used in DatetimeIndex.append also - """ - # do not pass tz to set because tzlocal cannot be hashed - if len(set([str(x.dtype) for x in to_concat])) != 1: - raise ValueError('to_concat must have the same tz') - tz = to_concat[0].tz - # no need to localize because internal repr will not be changed - new_values = np.concatenate([x.asi8 for x in to_concat]) - return to_concat[0]._simple_new(new_values, tz=tz, name=name) - - -def _concat_index_asobject(to_concat, name=None): - """ - concat all inputs as object. DatetimeIndex, TimedeltaIndex and - PeriodIndex are converted to object dtype before concatenation - """ - - klasses = ABCDatetimeIndex, ABCTimedeltaIndex, ABCPeriodIndex - to_concat = [x.asobject if isinstance(x, klasses) else x - for x in to_concat] - - from pandas import Index - self = to_concat[0] - attribs = self._get_attributes_dict() - attribs['name'] = name - - to_concat = [x._values if isinstance(x, Index) else x - for x in to_concat] - return self._shallow_copy_with_infer(np.concatenate(to_concat), **attribs) - - -def _concat_sparse(to_concat, axis=0, typs=None): - """ - provide concatenation of an sparse/dense array of arrays each of which is a - single dtype - - Parameters - ---------- - to_concat : array of arrays - axis : axis to provide concatenation - typs : set of to_concat dtypes - - Returns - ------- - a single array, preserving the combined dtypes - """ - - from pandas.sparse.array import SparseArray, _make_index - - def convert_sparse(x, axis): - # coerce to native type - if isinstance(x, SparseArray): - x = x.get_values() - x = x.ravel() - if axis > 0: - x = np.atleast_2d(x) - return x - - if typs is None: - typs = get_dtype_kinds(to_concat) - - if len(typs) == 1: - # concat input as it is if all inputs are sparse - # and have the same fill_value - fill_values = set(c.fill_value for c in to_concat) - if len(fill_values) == 1: - sp_values = [c.sp_values for c in to_concat] - indexes = [c.sp_index.to_int_index() for c in to_concat] - - indices = [] - loc = 0 - for idx in indexes: - indices.append(idx.indices + loc) - loc += idx.length - sp_values = np.concatenate(sp_values) - indices = np.concatenate(indices) - sp_index = _make_index(loc, indices, kind=to_concat[0].sp_index) - - return SparseArray(sp_values, sparse_index=sp_index, - fill_value=to_concat[0].fill_value) - - # input may be sparse / dense mixed and may have different fill_value - # input must contain sparse at least 1 - sparses = [c for c in to_concat if is_sparse(c)] - fill_values = [c.fill_value for c in sparses] - sp_indexes = [c.sp_index for c in sparses] - - # densify and regular concat - to_concat = [convert_sparse(x, axis) for x in to_concat] - result = np.concatenate(to_concat, axis=axis) - - if not len(typs - set(['sparse', 'f', 'i'])): - # sparsify if inputs are sparse and dense numerics - # first sparse input's fill_value and SparseIndex is used - result = SparseArray(result.ravel(), fill_value=fill_values[0], - kind=sp_indexes[0]) - else: - # coerce to object if needed - result = result.astype('object') - return result diff --git a/pandas/types/dtypes.py b/pandas/types/dtypes.py deleted file mode 100644 index 43135ba94ab46..0000000000000 --- a/pandas/types/dtypes.py +++ /dev/null @@ -1,367 +0,0 @@ -""" define extension dtypes """ - -import re -import numpy as np -from pandas import compat - - -class ExtensionDtype(object): - """ - A np.dtype duck-typed class, suitable for holding a custom dtype. - - THIS IS NOT A REAL NUMPY DTYPE - """ - name = None - names = None - type = None - subdtype = None - kind = None - str = None - num = 100 - shape = tuple() - itemsize = 8 - base = None - isbuiltin = 0 - isnative = 0 - _metadata = [] - - def __unicode__(self): - return self.name - - def __str__(self): - """ - Return a string representation for a particular Object - - Invoked by str(df) in both py2/py3. - Yields Bytestring in Py2, Unicode String in py3. - """ - - if compat.PY3: - return self.__unicode__() - return self.__bytes__() - - def __bytes__(self): - """ - Return a string representation for a particular object. - - Invoked by bytes(obj) in py3 only. - Yields a bytestring in both py2/py3. - """ - from pandas.core.config import get_option - - encoding = get_option("display.encoding") - return self.__unicode__().encode(encoding, 'replace') - - def __repr__(self): - """ - Return a string representation for a particular object. - - Yields Bytestring in Py2, Unicode String in py3. - """ - return str(self) - - def __hash__(self): - raise NotImplementedError("sub-classes should implement an __hash__ " - "method") - - def __eq__(self, other): - raise NotImplementedError("sub-classes should implement an __eq__ " - "method") - - def __ne__(self, other): - return not self.__eq__(other) - - @classmethod - def is_dtype(cls, dtype): - """ Return a boolean if the passed type is an actual dtype that - we can match (via string or type) - """ - if hasattr(dtype, 'dtype'): - dtype = dtype.dtype - if isinstance(dtype, cls): - return True - elif isinstance(dtype, np.dtype): - return False - try: - return cls.construct_from_string(dtype) is not None - except: - return False - - -class CategoricalDtypeType(type): - """ - the type of CategoricalDtype, this metaclass determines subclass ability - """ - pass - - -class CategoricalDtype(ExtensionDtype): - - """ - A np.dtype duck-typed class, suitable for holding a custom categorical - dtype. - - THIS IS NOT A REAL NUMPY DTYPE, but essentially a sub-class of np.object - """ - name = 'category' - type = CategoricalDtypeType - kind = 'O' - str = '|O08' - base = np.dtype('O') - _cache = {} - - def __new__(cls): - - try: - return cls._cache[cls.name] - except KeyError: - c = object.__new__(cls) - cls._cache[cls.name] = c - return c - - def __hash__(self): - # make myself hashable - return hash(str(self)) - - def __eq__(self, other): - if isinstance(other, compat.string_types): - return other == self.name - - return isinstance(other, CategoricalDtype) - - @classmethod - def construct_from_string(cls, string): - """ attempt to construct this type from a string, raise a TypeError if - it's not possible """ - try: - if string == 'category': - return cls() - except: - pass - - raise TypeError("cannot construct a CategoricalDtype") - - -class DatetimeTZDtypeType(type): - """ - the type of DatetimeTZDtype, this metaclass determines subclass ability - """ - pass - - -class DatetimeTZDtype(ExtensionDtype): - - """ - A np.dtype duck-typed class, suitable for holding a custom datetime with tz - dtype. - - THIS IS NOT A REAL NUMPY DTYPE, but essentially a sub-class of - np.datetime64[ns] - """ - type = DatetimeTZDtypeType - kind = 'M' - str = '|M8[ns]' - num = 101 - base = np.dtype('M8[ns]') - _metadata = ['unit', 'tz'] - _match = re.compile("(datetime64|M8)\[(?P.+), (?P.+)\]") - _cache = {} - - def __new__(cls, unit=None, tz=None): - """ Create a new unit if needed, otherwise return from the cache - - Parameters - ---------- - unit : string unit that this represents, currently must be 'ns' - tz : string tz that this represents - """ - - if isinstance(unit, DatetimeTZDtype): - unit, tz = unit.unit, unit.tz - - elif unit is None: - # we are called as an empty constructor - # generally for pickle compat - return object.__new__(cls) - - elif tz is None: - - # we were passed a string that we can construct - try: - m = cls._match.search(unit) - if m is not None: - unit = m.groupdict()['unit'] - tz = m.groupdict()['tz'] - except: - raise ValueError("could not construct DatetimeTZDtype") - - elif isinstance(unit, compat.string_types): - - if unit != 'ns': - raise ValueError("DatetimeTZDtype only supports ns units") - - unit = unit - tz = tz - - if tz is None: - raise ValueError("DatetimeTZDtype constructor must have a tz " - "supplied") - - # set/retrieve from cache - key = (unit, str(tz)) - try: - return cls._cache[key] - except KeyError: - u = object.__new__(cls) - u.unit = unit - u.tz = tz - cls._cache[key] = u - return u - - @classmethod - def construct_from_string(cls, string): - """ attempt to construct this type from a string, raise a TypeError if - it's not possible - """ - try: - return cls(unit=string) - except ValueError: - raise TypeError("could not construct DatetimeTZDtype") - - def __unicode__(self): - # format the tz - return "datetime64[{unit}, {tz}]".format(unit=self.unit, tz=self.tz) - - @property - def name(self): - return str(self) - - def __hash__(self): - # make myself hashable - return hash(str(self)) - - def __eq__(self, other): - if isinstance(other, compat.string_types): - return other == self.name - - return (isinstance(other, DatetimeTZDtype) and - self.unit == other.unit and - str(self.tz) == str(other.tz)) - - -class PeriodDtypeType(type): - """ - the type of PeriodDtype, this metaclass determines subclass ability - """ - pass - - -class PeriodDtype(ExtensionDtype): - __metaclass__ = PeriodDtypeType - """ - A Period duck-typed class, suitable for holding a period with freq dtype. - - THIS IS NOT A REAL NUMPY DTYPE, but essentially a sub-class of np.int64. - """ - type = PeriodDtypeType - kind = 'O' - str = '|O08' - base = np.dtype('O') - num = 102 - _metadata = ['freq'] - _match = re.compile("(P|p)eriod\[(?P.+)\]") - _cache = {} - - def __new__(cls, freq=None): - """ - Parameters - ---------- - freq : frequency - """ - - if isinstance(freq, PeriodDtype): - return freq - - elif freq is None: - # empty constructor for pickle compat - return object.__new__(cls) - - from pandas.tseries.offsets import DateOffset - if not isinstance(freq, DateOffset): - freq = cls._parse_dtype_strict(freq) - - try: - return cls._cache[freq.freqstr] - except KeyError: - u = object.__new__(cls) - u.freq = freq - cls._cache[freq.freqstr] = u - return u - - @classmethod - def _parse_dtype_strict(cls, freq): - if isinstance(freq, compat.string_types): - if freq.startswith('period[') or freq.startswith('Period['): - m = cls._match.search(freq) - if m is not None: - freq = m.group('freq') - from pandas.tseries.frequencies import to_offset - freq = to_offset(freq) - if freq is not None: - return freq - - raise ValueError("could not construct PeriodDtype") - - @classmethod - def construct_from_string(cls, string): - """ - attempt to construct this type from a string, raise a TypeError - if its not possible - """ - from pandas.tseries.offsets import DateOffset - if isinstance(string, (compat.string_types, DateOffset)): - # avoid tuple to be regarded as freq - try: - return cls(freq=string) - except ValueError: - pass - raise TypeError("could not construct PeriodDtype") - - def __unicode__(self): - return "period[{freq}]".format(freq=self.freq.freqstr) - - @property - def name(self): - return str(self) - - def __hash__(self): - # make myself hashable - return hash(str(self)) - - def __eq__(self, other): - if isinstance(other, compat.string_types): - return other == self.name or other == self.name.title() - - return isinstance(other, PeriodDtype) and self.freq == other.freq - - @classmethod - def is_dtype(cls, dtype): - """ - Return a boolean if we if the passed type is an actual dtype that we - can match (via string or type) - """ - - if isinstance(dtype, compat.string_types): - # PeriodDtype can be instanciated from freq string like "U", - # but dosn't regard freq str like "U" as dtype. - if dtype.startswith('period[') or dtype.startswith('Period['): - try: - if cls._parse_dtype_strict(dtype) is not None: - return True - else: - return False - except ValueError: - return False - else: - return False - return super(PeriodDtype, cls).is_dtype(dtype) diff --git a/pandas/types/inference.py b/pandas/types/inference.py deleted file mode 100644 index d8e3b3ee7329b..0000000000000 --- a/pandas/types/inference.py +++ /dev/null @@ -1,106 +0,0 @@ -""" basic inference routines """ - -import collections -import re -import numpy as np -from numbers import Number -from pandas.compat import (string_types, text_type, - string_and_binary_types) -from pandas._libs import lib - -is_bool = lib.is_bool - -is_integer = lib.is_integer - -is_float = lib.is_float - -is_complex = lib.is_complex - -is_scalar = lib.isscalar - -is_decimal = lib.is_decimal - - -def is_number(obj): - return isinstance(obj, (Number, np.number)) - - -def is_string_like(obj): - return isinstance(obj, (text_type, string_types)) - - -def _iterable_not_string(x): - return (isinstance(x, collections.Iterable) and - not isinstance(x, string_types)) - - -def is_iterator(obj): - # python 3 generators have __next__ instead of next - return hasattr(obj, 'next') or hasattr(obj, '__next__') - - -def is_re(obj): - return isinstance(obj, re._pattern_type) - - -def is_re_compilable(obj): - try: - re.compile(obj) - except TypeError: - return False - else: - return True - - -def is_list_like(arg): - return (hasattr(arg, '__iter__') and - not isinstance(arg, string_and_binary_types)) - - -def is_dict_like(arg): - return hasattr(arg, '__getitem__') and hasattr(arg, 'keys') - - -def is_named_tuple(arg): - return isinstance(arg, tuple) and hasattr(arg, '_fields') - - -def is_hashable(arg): - """Return True if hash(arg) will succeed, False otherwise. - - Some types will pass a test against collections.Hashable but fail when they - are actually hashed with hash(). - - Distinguish between these and other types by trying the call to hash() and - seeing if they raise TypeError. - - Examples - -------- - >>> a = ([],) - >>> isinstance(a, collections.Hashable) - True - >>> is_hashable(a) - False - """ - # unfortunately, we can't use isinstance(arg, collections.Hashable), which - # can be faster than calling hash, because numpy scalars on Python 3 fail - # this test - - # reconsider this decision once this numpy bug is fixed: - # https://github.com/numpy/numpy/issues/5562 - - try: - hash(arg) - except TypeError: - return False - else: - return True - - -def is_sequence(x): - try: - iter(x) - len(x) # it has a length - return not isinstance(x, string_and_binary_types) - except (TypeError, AttributeError): - return False diff --git a/pandas/types/missing.py b/pandas/types/missing.py deleted file mode 100644 index cc8b5edc27542..0000000000000 --- a/pandas/types/missing.py +++ /dev/null @@ -1,393 +0,0 @@ -""" -missing types & inference -""" -import numpy as np -from pandas._libs import lib -from pandas._libs.tslib import NaT, iNaT -from .generic import (ABCMultiIndex, ABCSeries, - ABCIndexClass, ABCGeneric) -from .common import (is_string_dtype, is_datetimelike, - is_datetimelike_v_numeric, is_float_dtype, - is_datetime64_dtype, is_datetime64tz_dtype, - is_timedelta64_dtype, - is_complex_dtype, is_categorical_dtype, - is_string_like_dtype, is_bool_dtype, - is_integer_dtype, is_dtype_equal, - needs_i8_conversion, _ensure_object, - pandas_dtype, - is_scalar, - is_object_dtype, - is_integer, - _TD_DTYPE, - _NS_DTYPE) -from .inference import is_list_like - - -def isnull(obj): - """Detect missing values (NaN in numeric arrays, None/NaN in object arrays) - - Parameters - ---------- - arr : ndarray or object value - Object to check for null-ness - - Returns - ------- - isnulled : array-like of bool or bool - Array or bool indicating whether an object is null or if an array is - given which of the element is null. - - See also - -------- - pandas.notnull: boolean inverse of pandas.isnull - """ - return _isnull(obj) - - -def _isnull_new(obj): - if is_scalar(obj): - return lib.checknull(obj) - # hack (for now) because MI registers as ndarray - elif isinstance(obj, ABCMultiIndex): - raise NotImplementedError("isnull is not defined for MultiIndex") - elif isinstance(obj, (ABCSeries, np.ndarray, ABCIndexClass)): - return _isnull_ndarraylike(obj) - elif isinstance(obj, ABCGeneric): - return obj._constructor(obj._data.isnull(func=isnull)) - elif isinstance(obj, list) or hasattr(obj, '__array__'): - return _isnull_ndarraylike(np.asarray(obj)) - else: - return obj is None - - -def _isnull_old(obj): - """Detect missing values. Treat None, NaN, INF, -INF as null. - - Parameters - ---------- - arr: ndarray or object value - - Returns - ------- - boolean ndarray or boolean - """ - if is_scalar(obj): - return lib.checknull_old(obj) - # hack (for now) because MI registers as ndarray - elif isinstance(obj, ABCMultiIndex): - raise NotImplementedError("isnull is not defined for MultiIndex") - elif isinstance(obj, (ABCSeries, np.ndarray, ABCIndexClass)): - return _isnull_ndarraylike_old(obj) - elif isinstance(obj, ABCGeneric): - return obj._constructor(obj._data.isnull(func=_isnull_old)) - elif isinstance(obj, list) or hasattr(obj, '__array__'): - return _isnull_ndarraylike_old(np.asarray(obj)) - else: - return obj is None - - -_isnull = _isnull_new - - -def _use_inf_as_null(key): - """Option change callback for null/inf behaviour - Choose which replacement for numpy.isnan / -numpy.isfinite is used. - - Parameters - ---------- - flag: bool - True means treat None, NaN, INF, -INF as null (old way), - False means None and NaN are null, but INF, -INF are not null - (new way). - - Notes - ----- - This approach to setting global module values is discussed and - approved here: - - * http://stackoverflow.com/questions/4859217/ - programmatically-creating-variables-in-python/4859312#4859312 - """ - from pandas.core.config import get_option - flag = get_option(key) - if flag: - globals()['_isnull'] = _isnull_old - else: - globals()['_isnull'] = _isnull_new - - -def _isnull_ndarraylike(obj): - - values = getattr(obj, 'values', obj) - dtype = values.dtype - - if is_string_dtype(dtype): - if is_categorical_dtype(values): - from pandas import Categorical - if not isinstance(values, Categorical): - values = values.values - result = values.isnull() - else: - - # Working around NumPy ticket 1542 - shape = values.shape - - if is_string_like_dtype(dtype): - result = np.zeros(values.shape, dtype=bool) - else: - result = np.empty(shape, dtype=bool) - vec = lib.isnullobj(values.ravel()) - result[...] = vec.reshape(shape) - - elif needs_i8_conversion(obj): - # this is the NaT pattern - result = values.view('i8') == iNaT - else: - result = np.isnan(values) - - # box - if isinstance(obj, ABCSeries): - from pandas import Series - result = Series(result, index=obj.index, name=obj.name, copy=False) - - return result - - -def _isnull_ndarraylike_old(obj): - values = getattr(obj, 'values', obj) - dtype = values.dtype - - if is_string_dtype(dtype): - # Working around NumPy ticket 1542 - shape = values.shape - - if is_string_like_dtype(dtype): - result = np.zeros(values.shape, dtype=bool) - else: - result = np.empty(shape, dtype=bool) - vec = lib.isnullobj_old(values.ravel()) - result[:] = vec.reshape(shape) - - elif is_datetime64_dtype(dtype): - # this is the NaT pattern - result = values.view('i8') == iNaT - else: - result = ~np.isfinite(values) - - # box - if isinstance(obj, ABCSeries): - from pandas import Series - result = Series(result, index=obj.index, name=obj.name, copy=False) - - return result - - -def notnull(obj): - """Replacement for numpy.isfinite / -numpy.isnan which is suitable for use - on object arrays. - - Parameters - ---------- - arr : ndarray or object value - Object to check for *not*-null-ness - - Returns - ------- - isnulled : array-like of bool or bool - Array or bool indicating whether an object is *not* null or if an array - is given which of the element is *not* null. - - See also - -------- - pandas.isnull : boolean inverse of pandas.notnull - """ - res = isnull(obj) - if is_scalar(res): - return not res - return ~res - - -def is_null_datelike_scalar(other): - """ test whether the object is a null datelike, e.g. Nat - but guard against passing a non-scalar """ - if other is NaT or other is None: - return True - elif is_scalar(other): - - # a timedelta - if hasattr(other, 'dtype'): - return other.view('i8') == iNaT - elif is_integer(other) and other == iNaT: - return True - return isnull(other) - return False - - -def _is_na_compat(arr, fill_value=np.nan): - """ - Parameters - ---------- - arr: a numpy array - fill_value: fill value, default to np.nan - - Returns - ------- - True if we can fill using this fill_value - """ - dtype = arr.dtype - if isnull(fill_value): - return not (is_bool_dtype(dtype) or - is_integer_dtype(dtype)) - return True - - -def array_equivalent(left, right, strict_nan=False): - """ - True if two arrays, left and right, have equal non-NaN elements, and NaNs - in corresponding locations. False otherwise. It is assumed that left and - right are NumPy arrays of the same dtype. The behavior of this function - (particularly with respect to NaNs) is not defined if the dtypes are - different. - - Parameters - ---------- - left, right : ndarrays - strict_nan : bool, default False - If True, consider NaN and None to be different. - - Returns - ------- - b : bool - Returns True if the arrays are equivalent. - - Examples - -------- - >>> array_equivalent( - ... np.array([1, 2, np.nan]), - ... np.array([1, 2, np.nan])) - True - >>> array_equivalent( - ... np.array([1, np.nan, 2]), - ... np.array([1, 2, np.nan])) - False - """ - - left, right = np.asarray(left), np.asarray(right) - - # shape compat - if left.shape != right.shape: - return False - - # Object arrays can contain None, NaN and NaT. - # string dtypes must be come to this path for NumPy 1.7.1 compat - if is_string_dtype(left) or is_string_dtype(right): - - if not strict_nan: - # isnull considers NaN and None to be equivalent. - return lib.array_equivalent_object( - _ensure_object(left.ravel()), _ensure_object(right.ravel())) - - for left_value, right_value in zip(left, right): - if left_value is NaT and right_value is not NaT: - return False - - elif isinstance(left_value, float) and np.isnan(left_value): - if (not isinstance(right_value, float) or - not np.isnan(right_value)): - return False - else: - if left_value != right_value: - return False - return True - - # NaNs can occur in float and complex arrays. - if is_float_dtype(left) or is_complex_dtype(left): - return ((left == right) | (np.isnan(left) & np.isnan(right))).all() - - # numpy will will not allow this type of datetimelike vs integer comparison - elif is_datetimelike_v_numeric(left, right): - return False - - # M8/m8 - elif needs_i8_conversion(left) and needs_i8_conversion(right): - if not is_dtype_equal(left.dtype, right.dtype): - return False - - left = left.view('i8') - right = right.view('i8') - - # NaNs cannot occur otherwise. - try: - return np.array_equal(left, right) - except AttributeError: - # see gh-13388 - # - # NumPy v1.7.1 has a bug in its array_equal - # function that prevents it from correctly - # comparing two arrays with complex dtypes. - # This bug is corrected in v1.8.0, so remove - # this try-except block as soon as we stop - # supporting NumPy versions < 1.8.0 - if not is_dtype_equal(left.dtype, right.dtype): - return False - - left = left.tolist() - right = right.tolist() - - return left == right - - -def _infer_fill_value(val): - """ - infer the fill value for the nan/NaT from the provided - scalar/ndarray/list-like if we are a NaT, return the correct dtyped - element to provide proper block construction - """ - - if not is_list_like(val): - val = [val] - val = np.array(val, copy=False) - if is_datetimelike(val): - return np.array('NaT', dtype=val.dtype) - elif is_object_dtype(val.dtype): - dtype = lib.infer_dtype(_ensure_object(val)) - if dtype in ['datetime', 'datetime64']: - return np.array('NaT', dtype=_NS_DTYPE) - elif dtype in ['timedelta', 'timedelta64']: - return np.array('NaT', dtype=_TD_DTYPE) - return np.nan - - -def _maybe_fill(arr, fill_value=np.nan): - """ - if we have a compatiable fill_value and arr dtype, then fill - """ - if _is_na_compat(arr, fill_value): - arr.fill(fill_value) - return arr - - -def na_value_for_dtype(dtype): - """ - Return a dtype compat na value - - Parameters - ---------- - dtype : string / dtype - - Returns - ------- - np.dtype or a pandas dtype - """ - dtype = pandas_dtype(dtype) - - if (is_datetime64_dtype(dtype) or is_datetime64tz_dtype(dtype) or - is_timedelta64_dtype(dtype)): - return NaT - elif is_float_dtype(dtype): - return np.nan - elif is_integer_dtype(dtype): - return 0 - elif is_bool_dtype(dtype): - return False - return np.nan diff --git a/pandas/util/__init__.py b/pandas/util/__init__.py index e69de29bb2d1d..202e58c916e47 100644 --- a/pandas/util/__init__.py +++ b/pandas/util/__init__.py @@ -0,0 +1,2 @@ +from pandas.util._decorators import Appender, Substitution, cache_readonly # noqa +from pandas.core.util.hashing import hash_pandas_object, hash_array # noqa diff --git a/pandas/util/decorators.py b/pandas/util/_decorators.py similarity index 50% rename from pandas/util/decorators.py rename to pandas/util/_decorators.py index 4e1719958e8b7..e92051ebbea9a 100644 --- a/pandas/util/decorators.py +++ b/pandas/util/_decorators.py @@ -1,31 +1,91 @@ -from pandas.compat import StringIO, callable, signature -from pandas._libs.lib import cache_readonly # noqa -import types -import sys -import warnings +from functools import wraps +import inspect from textwrap import dedent -from functools import wraps, update_wrapper +import warnings +from pandas._libs.properties import cache_readonly # noqa +from pandas.compat import PY2, signature + + +def deprecate(name, alternative, version, alt_name=None, + klass=None, stacklevel=2, msg=None): + """ + Return a new function that emits a deprecation warning on use. + + To use this method for a deprecated function, another function + `alternative` with the same signature must exist. The deprecated + function will emit a deprecation warning, and in the docstring + it will contain the deprecation directive with the provided version + so it can be detected for future removal. + + Parameters + ---------- + name : str + Name of function to deprecate. + alternative : func + Function to use instead. + version : str + Version of pandas in which the method has been deprecated. + alt_name : str, optional + Name to use in preference of alternative.__name__. + klass : Warning, default FutureWarning + stacklevel : int, default 2 + msg : str + The message to display in the warning. + Default is '{name} is deprecated. Use {alt_name} instead.' + """ -def deprecate(name, alternative, alt_name=None): alt_name = alt_name or alternative.__name__ + klass = klass or FutureWarning + warning_msg = msg or '{} is deprecated, use {} instead'.format(name, + alt_name) + @wraps(alternative) def wrapper(*args, **kwargs): - warnings.warn("%s is deprecated. Use %s instead" % (name, alt_name), - FutureWarning, stacklevel=2) + warnings.warn(warning_msg, klass, stacklevel=stacklevel) return alternative(*args, **kwargs) + + # adding deprecated directive to the docstring + msg = msg or 'Use `{alt_name}` instead.'.format(alt_name=alt_name) + doc_error_msg = ('deprecate needs a correctly formatted docstring in ' + 'the target function (should have a one liner short ' + 'summary, and opening quotes should be in their own ' + 'line). Found:\n{}'.format(alternative.__doc__)) + + # when python is running in optimized mode (i.e. `-OO`), docstrings are + # removed, so we check that a docstring with correct formatting is used + # but we allow empty docstrings + if alternative.__doc__: + if alternative.__doc__.count('\n') < 3: + raise AssertionError(doc_error_msg) + empty1, summary, empty2, doc = alternative.__doc__.split('\n', 3) + if empty1 or empty2 and not summary: + raise AssertionError(doc_error_msg) + wrapper.__doc__ = dedent(""" + {summary} + + .. deprecated:: {depr_version} + {depr_msg} + + {rest_of_docstring}""").format(summary=summary.strip(), + depr_version=version, + depr_msg=msg, + rest_of_docstring=dedent(doc)) + return wrapper def deprecate_kwarg(old_arg_name, new_arg_name, mapping=None, stacklevel=2): - """Decorator to deprecate a keyword argument of a function + """ + Decorator to deprecate a keyword argument of a function. Parameters ---------- old_arg_name : str Name of argument in function to deprecate - new_arg_name : str - Name of prefered argument in function + new_arg_name : str or None + Name of preferred argument in function. Use None to raise warning that + ``old_arg_name`` keyword is deprecated. mapping : dict or callable If mapping is present, use it to translate old arguments to new arguments. A callable must do its own value checking; @@ -41,12 +101,15 @@ def deprecate_kwarg(old_arg_name, new_arg_name, mapping=None, stacklevel=2): ... >>> f(columns='should work ok') should work ok + >>> f(cols='should raise warning') FutureWarning: cols is deprecated, use columns instead warnings.warn(msg, FutureWarning) should raise warning + >>> f(cols='should error', columns="can\'t pass do both") TypeError: Can only specify 'cols' or 'columns', not both + >>> @deprecate_kwarg('old', 'new', {'yes': True, 'no': False}) ... def f(new=False): ... print('yes!' if new else 'no!') @@ -56,7 +119,25 @@ def deprecate_kwarg(old_arg_name, new_arg_name, mapping=None, stacklevel=2): warnings.warn(msg, FutureWarning) yes! + To raise a warning that a keyword will be removed entirely in the future + + >>> @deprecate_kwarg(old_arg_name='cols', new_arg_name=None) + ... def f(cols='', another_param=''): + ... print(cols) + ... + >>> f(cols='should raise warning') + FutureWarning: the 'cols' keyword is deprecated and will be removed in a + future version please takes steps to stop use of 'cols' + should raise warning + >>> f(another_param='should not raise warning') + should not raise warning + + >>> f(cols='should raise warning', another_param='') + FutureWarning: the 'cols' keyword is deprecated and will be removed in a + future version please takes steps to stop use of 'cols' + should raise warning """ + if mapping is not None and not hasattr(mapping, 'get') and \ not callable(mapping): raise TypeError("mapping from old to new argument values " @@ -66,6 +147,17 @@ def _deprecate_kwarg(func): @wraps(func) def wrapper(*args, **kwargs): old_arg_value = kwargs.pop(old_arg_name, None) + + if new_arg_name is None and old_arg_value is not None: + msg = ( + "the '{old_name}' keyword is deprecated and will be " + "removed in a future version. " + "Please take steps to stop the use of '{old_name}'" + ).format(old_name=old_arg_name) + warnings.warn(msg, FutureWarning, stacklevel=stacklevel) + kwargs[old_arg_name] = old_arg_value + return func(*args, **kwargs) + if old_arg_value is not None: if mapping is not None: if hasattr(mapping, 'get'): @@ -73,19 +165,24 @@ def wrapper(*args, **kwargs): old_arg_value) else: new_arg_value = mapping(old_arg_value) - msg = "the %s=%r keyword is deprecated, " \ - "use %s=%r instead" % \ - (old_arg_name, old_arg_value, - new_arg_name, new_arg_value) + msg = ("the {old_name}={old_val!r} keyword is deprecated, " + "use {new_name}={new_val!r} instead" + ).format(old_name=old_arg_name, + old_val=old_arg_value, + new_name=new_arg_name, + new_val=new_arg_value) else: new_arg_value = old_arg_value - msg = "the '%s' keyword is deprecated, " \ - "use '%s' instead" % (old_arg_name, new_arg_name) + msg = ("the '{old_name}' keyword is deprecated, " + "use '{new_name}' instead" + ).format(old_name=old_arg_name, + new_name=new_arg_name) warnings.warn(msg, FutureWarning, stacklevel=stacklevel) if kwargs.get(new_arg_name, None) is not None: - msg = ("Can only specify '%s' or '%s', not both" % - (old_arg_name, new_arg_name)) + msg = ("Can only specify '{old_name}' or '{new_name}', " + "not both").format(old_name=old_arg_name, + new_name=new_arg_name) raise TypeError(msg) else: kwargs[new_arg_name] = new_arg_value @@ -94,6 +191,31 @@ def wrapper(*args, **kwargs): return _deprecate_kwarg +def rewrite_axis_style_signature(name, extra_params): + def decorate(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + if not PY2: + kind = inspect.Parameter.POSITIONAL_OR_KEYWORD + params = [ + inspect.Parameter('self', kind), + inspect.Parameter(name, kind, default=None), + inspect.Parameter('index', kind, default=None), + inspect.Parameter('columns', kind, default=None), + inspect.Parameter('axis', kind, default=None), + ] + + for pname, default in extra_params: + params.append(inspect.Parameter(pname, kind, default=default)) + + sig = inspect.Signature(params) + + func.__signature__ = sig + return wrapper + return decorate + # Substitution and Appender are derived from matplotlib.docstring (1.1.0) # module http://matplotlib.org/users/license.html @@ -138,7 +260,12 @@ def __call__(self, func): return func def update(self, *args, **kwargs): - "Assume self.params is a dict and update it with supplied args" + """ + Update self.params with supplied args. + + If called, we assume self.params is a dict. + """ + self.params.update(*args, **kwargs) @classmethod @@ -196,86 +323,31 @@ def indent(text, indents=1): return jointext.join(text.split('\n')) -def suppress_stdout(f): - def wrapped(*args, **kwargs): - try: - sys.stdout = StringIO() - f(*args, **kwargs) - finally: - sys.stdout = sys.__stdout__ - - return wrapped - - def make_signature(func): """ - Returns a string repr of the arg list of a func call, with any defaults + Returns a tuple containing the paramenter list with defaults + and parameter list. Examples -------- - - >>> def f(a,b,c=2) : - >>> return a*b*c - >>> print(_make_signature(f)) - a,b,c=2 + >>> def f(a, b, c=2): + >>> return a * b * c + >>> print(make_signature(f)) + (['a', 'b', 'c=2'], ['a', 'b', 'c']) """ + spec = signature(func) if spec.defaults is None: n_wo_defaults = len(spec.args) defaults = ('',) * n_wo_defaults else: n_wo_defaults = len(spec.args) - len(spec.defaults) - defaults = ('',) * n_wo_defaults + spec.defaults + defaults = ('',) * n_wo_defaults + tuple(spec.defaults) args = [] - for i, (var, default) in enumerate(zip(spec.args, defaults)): + for var, default in zip(spec.args, defaults): args.append(var if default == '' else var + '=' + repr(default)) if spec.varargs: args.append('*' + spec.varargs) if spec.keywords: args.append('**' + spec.keywords) return args, spec.args - - -class docstring_wrapper(object): - """ - decorator to wrap a function, - provide a dynamically evaluated doc-string - - Parameters - ---------- - func : callable - creator : callable - return the doc-string - default : str, optional - return this doc-string on error - """ - _attrs = ['__module__', '__name__', - '__qualname__', '__annotations__'] - - def __init__(self, func, creator, default=None): - self.func = func - self.creator = creator - self.default = default - update_wrapper( - self, func, [attr for attr in self._attrs - if hasattr(func, attr)]) - - def __get__(self, instance, cls=None): - - # we are called with a class - if instance is None: - return self - - # we want to return the actual passed instance - return types.MethodType(self, instance) - - def __call__(self, *args, **kwargs): - return self.func(*args, **kwargs) - - @property - def __doc__(self): - try: - return self.creator() - except Exception as exc: - msg = self.default or str(exc) - return msg diff --git a/pandas/util/depr_module.py b/pandas/util/_depr_module.py similarity index 63% rename from pandas/util/depr_module.py rename to pandas/util/_depr_module.py index af7faf9dd96c8..2c8feec798c66 100644 --- a/pandas/util/depr_module.py +++ b/pandas/util/_depr_module.py @@ -3,8 +3,8 @@ It is for internal use only and should not be used beyond this purpose. """ -import warnings import importlib +import warnings class _DeprecatedModule(object): @@ -18,14 +18,19 @@ class _DeprecatedModule(object): be used when needed. removals : objects or methods in module that will no longer be accessible once module is removed. + moved : dict, optional + dictionary of function name -> new location for moved + objects """ - def __init__(self, deprmod, deprmodto=None, removals=None): + def __init__(self, deprmod, deprmodto=None, removals=None, + moved=None): self.deprmod = deprmod self.deprmodto = deprmodto self.removals = removals if self.removals is not None: self.removals = frozenset(self.removals) + self.moved = moved # For introspection purposes. self.self_dir = frozenset(dir(self.__class__)) @@ -60,17 +65,31 @@ def __getattr__(self, name): "{deprmod}.{name} is deprecated and will be removed in " "a future version.".format(deprmod=self.deprmod, name=name), FutureWarning, stacklevel=2) - else: - deprmodto = self.deprmodto - if deprmodto is None: - deprmodto = "{modname}.{name}".format( - modname=obj.__module__, name=name) - # The object is actually located in another module. + elif self.moved is not None and name in self.moved: warnings.warn( - "{deprmod}.{name} is deprecated. Please use " - "{deprmodto}.{name} instead.".format( - deprmod=self.deprmod, name=name, deprmodto=deprmodto), + "{deprmod} is deprecated and will be removed in " + "a future version.\nYou can access {name} as {moved}".format( + deprmod=self.deprmod, + name=name, + moved=self.moved[name]), FutureWarning, stacklevel=2) + else: + deprmodto = self.deprmodto + if deprmodto is False: + warnings.warn( + "{deprmod}.{name} is deprecated and will be removed in " + "a future version.".format( + deprmod=self.deprmod, name=name), + FutureWarning, stacklevel=2) + else: + if deprmodto is None: + deprmodto = obj.__module__ + # The object is actually located in another module. + warnings.warn( + "{deprmod}.{name} is deprecated. Please use " + "{deprmodto}.{name} instead.".format( + deprmod=self.deprmod, name=name, deprmodto=deprmodto), + FutureWarning, stacklevel=2) return obj diff --git a/pandas/util/doctools.py b/pandas/util/_doctools.py similarity index 87% rename from pandas/util/doctools.py rename to pandas/util/_doctools.py index 6df6444aeafab..4aee0a2e5350e 100644 --- a/pandas/util/doctools.py +++ b/pandas/util/_doctools.py @@ -1,7 +1,9 @@ import numpy as np -import pandas as pd + import pandas.compat as compat +import pandas as pd + class TablePlotter(object): """ @@ -15,17 +17,23 @@ def __init__(self, cell_width=0.37, cell_height=0.25, font_size=7.5): self.font_size = font_size def _shape(self, df): - """Calcurate table chape considering index levels""" + """ + Calculate table chape considering index levels. + """ + row, col = df.shape return row + df.columns.nlevels, col + df.index.nlevels def _get_cells(self, left, right, vertical): - """Calcurate appropriate figure size based on left and right data""" + """ + Calculate appropriate figure size based on left and right data. + """ + if vertical: - # calcurate required number of cells - vcells = max(sum([self._shape(l)[0] for l in left]), + # calculate required number of cells + vcells = max(sum(self._shape(l)[0] for l in left), self._shape(right)[0]) - hcells = (max([self._shape(l)[1] for l in left]) + + hcells = (max(self._shape(l)[1] for l in left) + self._shape(right)[1]) else: vcells = max([self._shape(l)[0] for l in left] + @@ -66,8 +74,8 @@ def plot(self, left, right, labels=None, vertical=True): if vertical: gs = gridspec.GridSpec(len(left), hcells) # left - max_left_cols = max([self._shape(l)[1] for l in left]) - max_left_rows = max([self._shape(l)[0] for l in left]) + max_left_cols = max(self._shape(l)[1] for l in left) + max_left_rows = max(self._shape(l)[0] for l in left) for i, (l, label) in enumerate(zip(left, labels)): ax = fig.add_subplot(gs[i, 0:max_left_cols]) self._make_table(ax, l, title=label, @@ -77,7 +85,7 @@ def plot(self, left, right, labels=None, vertical=True): self._make_table(ax, right, title='Result', height=1.05 / vcells) fig.subplots_adjust(top=0.9, bottom=0.05, left=0.05, right=0.95) else: - max_rows = max([self._shape(df)[0] for df in left + [right]]) + max_rows = max(self._shape(df)[0] for df in left + [right]) height = 1.0 / np.max(max_rows) gs = gridspec.GridSpec(1, hcells) # left @@ -131,7 +139,7 @@ def _make_table(self, ax, df, title, height=None): ax.set_visible(False) return - import pandas.tools.plotting as plotting + import pandas.plotting as plotting idx_nlevels = df.index.nlevels col_nlevels = df.columns.nlevels @@ -157,6 +165,14 @@ def _make_table(self, ax, df, title, height=None): ax.axis('off') +class _WritableDoc(type): + # Remove this when Python2 support is dropped + # __doc__ is not mutable for new-style classes in Python2, which means + # we can't use @Appender to share class docstrings. This can be used + # with `add_metaclass` to make cls.__doc__ mutable. + pass + + if __name__ == "__main__": import matplotlib.pyplot as plt diff --git a/pandas/util/_exceptions.py b/pandas/util/_exceptions.py new file mode 100644 index 0000000000000..953c8a43a21b8 --- /dev/null +++ b/pandas/util/_exceptions.py @@ -0,0 +1,16 @@ +import contextlib + + +@contextlib.contextmanager +def rewrite_exception(old_name, new_name): + """Rewrite the message of an exception.""" + try: + yield + except Exception as e: + msg = e.args[0] + msg = msg.replace(old_name, new_name) + args = (msg,) + if len(e.args) > 1: + args = args + e.args[1:] + e.args = args + raise diff --git a/pandas/util/print_versions.py b/pandas/util/_print_versions.py similarity index 77% rename from pandas/util/print_versions.py rename to pandas/util/_print_versions.py index ca75d4d02e927..a5c86c2cc80b3 100644 --- a/pandas/util/print_versions.py +++ b/pandas/util/_print_versions.py @@ -1,11 +1,11 @@ +import codecs +import importlib +import locale import os import platform -import sys import struct import subprocess -import codecs -import locale -import importlib +import sys def get_sys_info(): @@ -21,7 +21,7 @@ def get_sys_info(): stdout=subprocess.PIPE, stderr=subprocess.PIPE) so, serr = pipe.communicate() - except: + except (OSError, ValueError): pass else: if pipe.returncode == 0: @@ -38,20 +38,19 @@ def get_sys_info(): (sysname, nodename, release, version, machine, processor) = platform.uname() blob.extend([ - ("python", "%d.%d.%d.%s.%s" % sys.version_info[:]), + ("python", '.'.join(map(str, sys.version_info))), ("python-bits", struct.calcsize("P") * 8), - ("OS", "%s" % (sysname)), - ("OS-release", "%s" % (release)), - # ("Version", "%s" % (version)), - ("machine", "%s" % (machine)), - ("processor", "%s" % (processor)), - ("byteorder", "%s" % sys.byteorder), - ("LC_ALL", "%s" % os.environ.get('LC_ALL', "None")), - ("LANG", "%s" % os.environ.get('LANG', "None")), - ("LOCALE", "%s.%s" % locale.getlocale()), - + ("OS", "{sysname}".format(sysname=sysname)), + ("OS-release", "{release}".format(release=release)), + # ("Version", "{version}".format(version=version)), + ("machine", "{machine}".format(machine=machine)), + ("processor", "{processor}".format(processor=processor)), + ("byteorder", "{byteorder}".format(byteorder=sys.byteorder)), + ("LC_ALL", "{lc}".format(lc=os.environ.get('LC_ALL', "None"))), + ("LANG", "{lang}".format(lang=os.environ.get('LANG', "None"))), + ("LOCALE", '.'.join(map(str, locale.getlocale()))), ]) - except: + except (KeyError, ValueError): pass return blob @@ -69,6 +68,7 @@ def show_versions(as_json=False): ("Cython", lambda mod: mod.__version__), ("numpy", lambda mod: mod.version.version), ("scipy", lambda mod: mod.version.version), + ("pyarrow", lambda mod: mod.__version__), ("xarray", lambda mod: mod.__version__), ("IPython", lambda mod: mod.__version__), ("sphinx", lambda mod: mod.__version__), @@ -85,7 +85,7 @@ def show_versions(as_json=False): ("xlrd", lambda mod: mod.__VERSION__), ("xlwt", lambda mod: mod.__VERSION__), ("xlsxwriter", lambda mod: mod.__version__), - ("lxml", lambda mod: mod.etree.__version__), + ("lxml.etree", lambda mod: mod.__version__), ("bs4", lambda mod: mod.__version__), ("html5lib", lambda mod: mod.__version__), ("sqlalchemy", lambda mod: mod.__version__), @@ -93,8 +93,10 @@ def show_versions(as_json=False): ("psycopg2", lambda mod: mod.__version__), ("jinja2", lambda mod: mod.__version__), ("s3fs", lambda mod: mod.__version__), + ("fastparquet", lambda mod: mod.__version__), ("pandas_gbq", lambda mod: mod.__version__), - ("pandas_datareader", lambda mod: mod.__version__) + ("pandas_datareader", lambda mod: mod.__version__), + ("gcsfs", lambda mod: mod.__version__), ] deps_blob = list() @@ -106,13 +108,13 @@ def show_versions(as_json=False): mod = importlib.import_module(modname) ver = ver_f(mod) deps_blob.append((modname, ver)) - except: + except ImportError: deps_blob.append((modname, None)) if (as_json): try: import json - except: + except ImportError: import simplejson as json j = dict(system=dict(sys_info), dependencies=dict(deps_blob)) @@ -129,11 +131,11 @@ def show_versions(as_json=False): print("------------------") for k, stat in sys_info: - print("%s: %s" % (k, stat)) + print("{k}: {stat}".format(k=k, stat=stat)) print("") for k, stat in deps_blob: - print("%s: %s" % (k, stat)) + print("{k}: {stat}".format(k=k, stat=stat)) def main(): diff --git a/pandas/util/_test_decorators.py b/pandas/util/_test_decorators.py new file mode 100644 index 0000000000000..0331661c3131f --- /dev/null +++ b/pandas/util/_test_decorators.py @@ -0,0 +1,210 @@ +""" +This module provides decorator functions which can be applied to test objects +in order to skip those objects when certain conditions occur. A sample use case +is to detect if the platform is missing ``matplotlib``. If so, any test objects +which require ``matplotlib`` and decorated with ``@td.skip_if_no_mpl`` will be +skipped by ``pytest`` during the execution of the test suite. + +To illustrate, after importing this module: + +import pandas.util._test_decorators as td + +The decorators can be applied to classes: + +@td.skip_if_some_reason +class Foo(): + ... + +Or individual functions: + +@td.skip_if_some_reason +def test_foo(): + ... + +For more information, refer to the ``pytest`` documentation on ``skipif``. +""" +from distutils.version import LooseVersion +import locale + +import pytest + +from pandas.compat import ( + PY3, import_lzma, is_platform_32bit, is_platform_windows) +from pandas.compat.numpy import _np_version_under1p15 + +from pandas.core.computation.expressions import ( + _NUMEXPR_INSTALLED, _USE_NUMEXPR) + + +def safe_import(mod_name, min_version=None): + """ + Parameters: + ----------- + mod_name : str + Name of the module to be imported + min_version : str, default None + Minimum required version of the specified mod_name + + Returns: + -------- + object + The imported module if successful, or False + """ + try: + mod = __import__(mod_name) + except ImportError: + return False + + if not min_version: + return mod + else: + import sys + try: + version = getattr(sys.modules[mod_name], '__version__') + except AttributeError: + # xlrd uses a capitalized attribute name + version = getattr(sys.modules[mod_name], '__VERSION__') + if version: + from distutils.version import LooseVersion + if LooseVersion(version) >= LooseVersion(min_version): + return mod + + return False + + +def _skip_if_no_mpl(): + mod = safe_import("matplotlib") + if mod: + mod.use("Agg", warn=False) + else: + return True + + +def _skip_if_mpl_2_2(): + mod = safe_import("matplotlib") + + if mod: + v = mod.__version__ + if LooseVersion(v) > LooseVersion('2.1.2'): + return True + else: + mod.use("Agg", warn=False) + + +def _skip_if_has_locale(): + lang, _ = locale.getlocale() + if lang is not None: + return True + + +def _skip_if_not_us_locale(): + lang, _ = locale.getlocale() + if lang != 'en_US': + return True + + +def _skip_if_no_scipy(): + return not (safe_import('scipy.stats') and + safe_import('scipy.sparse') and + safe_import('scipy.interpolate') and + safe_import('scipy.signal')) + + +def _skip_if_no_lzma(): + try: + import_lzma() + except ImportError: + return True + + +def skip_if_no(package, min_version=None): + """ + Generic function to help skip test functions when required packages are not + present on the testing system. + + Intended for use as a decorator, this function will wrap the decorated + function with a pytest ``skip_if`` mark. During a pytest test suite + execution, that mark will attempt to import the specified ``package`` and + optionally ensure it meets the ``min_version``. If the import and version + check are unsuccessful, then the decorated function will be skipped. + + Parameters + ---------- + package: str + The name of the package required by the decorated function + min_version: str or None, default None + Optional minimum version of the package required by the decorated + function + + Returns + ------- + decorated_func: function + The decorated function wrapped within a pytest ``skip_if`` mark + """ + def decorated_func(func): + msg = "Could not import '{}'".format(package) + if min_version: + msg += " satisfying a min_version of {}".format(min_version) + return pytest.mark.skipif( + not safe_import(package, min_version=min_version), reason=msg + )(func) + return decorated_func + + +skip_if_no_mpl = pytest.mark.skipif(_skip_if_no_mpl(), + reason="Missing matplotlib dependency") +skip_if_np_lt_115 = pytest.mark.skipif(_np_version_under1p15, + reason="NumPy 1.15 or greater required") +skip_if_mpl = pytest.mark.skipif(not _skip_if_no_mpl(), + reason="matplotlib is present") +xfail_if_mpl_2_2 = pytest.mark.xfail(_skip_if_mpl_2_2(), + reason="matplotlib 2.2", + strict=False) +skip_if_32bit = pytest.mark.skipif(is_platform_32bit(), + reason="skipping for 32 bit") +skip_if_windows = pytest.mark.skipif(is_platform_windows(), + reason="Running on Windows") +skip_if_windows_python_3 = pytest.mark.skipif(is_platform_windows() and PY3, + reason=("not used on python3/" + "win32")) +skip_if_has_locale = pytest.mark.skipif(_skip_if_has_locale(), + reason="Specific locale is set {lang}" + .format(lang=locale.getlocale()[0])) +skip_if_not_us_locale = pytest.mark.skipif(_skip_if_not_us_locale(), + reason="Specific locale is set " + "{lang}".format( + lang=locale.getlocale()[0])) +skip_if_no_scipy = pytest.mark.skipif(_skip_if_no_scipy(), + reason="Missing SciPy requirement") +skip_if_no_lzma = pytest.mark.skipif(_skip_if_no_lzma(), + reason="need backports.lzma to run") +skip_if_no_ne = pytest.mark.skipif(not _USE_NUMEXPR, + reason="numexpr enabled->{enabled}, " + "installed->{installed}".format( + enabled=_USE_NUMEXPR, + installed=_NUMEXPR_INSTALLED)) + + +def parametrize_fixture_doc(*args): + """ + Intended for use as a decorator for parametrized fixture, + this function will wrap the decorated function with a pytest + ``parametrize_fixture_doc`` mark. That mark will format + initial fixture docstring by replacing placeholders {0}, {1} etc + with parameters passed as arguments. + + Parameters: + ---------- + args: iterable + Positional arguments for docstring. + + Returns: + ------- + documented_fixture: function + The decorated function wrapped within a pytest + ``parametrize_fixture_doc`` mark + """ + def documented_fixture(fixture): + fixture.__doc__ = fixture.__doc__.format(*args) + return fixture + return documented_fixture diff --git a/pandas/util/_tester.py b/pandas/util/_tester.py index aeb4259a9edae..19b1cc700261c 100644 --- a/pandas/util/_tester.py +++ b/pandas/util/_tester.py @@ -7,21 +7,23 @@ PKG = os.path.dirname(os.path.dirname(__file__)) -try: - import pytest -except ImportError: - def test(): - raise ImportError("Need pytest>=3.0 to run tests") -else: - def test(extra_args=None): - cmd = ['--skip-slow', '--skip-network'] - if extra_args: - if not isinstance(extra_args, list): - extra_args = [extra_args] - cmd = extra_args - cmd += [PKG] - print("running: pytest {}".format(' '.join(cmd))) - sys.exit(pytest.main(cmd)) +def test(extra_args=None): + try: + import pytest + except ImportError: + raise ImportError("Need pytest>=4.0.2 to run tests") + try: + import hypothesis # noqa + except ImportError: + raise ImportError("Need hypothesis>=3.58 to run tests") + cmd = ['--skip-slow', '--skip-network', '--skip-db'] + if extra_args: + if not isinstance(extra_args, list): + extra_args = [extra_args] + cmd = extra_args + cmd += [PKG] + print("running: pytest {}".format(' '.join(cmd))) + sys.exit(pytest.main(cmd)) __all__ = ['test'] diff --git a/pandas/util/validators.py b/pandas/util/_validators.py similarity index 59% rename from pandas/util/validators.py rename to pandas/util/_validators.py index f22412a2bcd17..1171478de2eb4 100644 --- a/pandas/util/validators.py +++ b/pandas/util/_validators.py @@ -2,8 +2,9 @@ Module that contains many useful utilities for validating data or function arguments """ +import warnings -from pandas.types.common import is_bool +from pandas.core.dtypes.common import is_bool def _check_arg_length(fname, args, max_fname_arg_count, compat_args): @@ -39,7 +40,7 @@ def _check_for_default_values(fname, arg_val_dict, compat_args): """ for key in arg_val_dict: # try checking equality directly with '=' operator, - # as comparison may have been overriden for the left + # as comparison may have been overridden for the left # hand object try: v1 = arg_val_dict[key] @@ -58,7 +59,7 @@ def _check_for_default_values(fname, arg_val_dict, compat_args): # could not compare them directly, so try comparison # using the 'is' operator - except: + except ValueError: match = (arg_val_dict[key] is compat_args[key]) if not match: @@ -195,8 +196,8 @@ def validate_args_and_kwargs(fname, args, kwargs, See Also -------- - validate_args : purely args validation - validate_kwargs : purely kwargs validation + validate_args : Purely args validation. + validate_kwargs : Purely kwargs validation. """ # Check that the total number of arguments passed in (i.e. @@ -220,7 +221,138 @@ def validate_args_and_kwargs(fname, args, kwargs, def validate_bool_kwarg(value, arg_name): """ Ensures that argument passed in arg_name is of type bool. """ if not (is_bool(value) or value is None): - raise ValueError('For argument "%s" expected type bool, ' - 'received type %s.' % - (arg_name, type(value).__name__)) + raise ValueError('For argument "{arg}" expected type bool, received ' + 'type {typ}.'.format(arg=arg_name, + typ=type(value).__name__)) return value + + +def validate_axis_style_args(data, args, kwargs, arg_name, method_name): + """Argument handler for mixed index, columns / axis functions + + In an attempt to handle both `.method(index, columns)`, and + `.method(arg, axis=.)`, we have to do some bad things to argument + parsing. This translates all arguments to `{index=., columns=.}` style. + + Parameters + ---------- + data : DataFrame or Panel + arg : tuple + All positional arguments from the user + kwargs : dict + All keyword arguments from the user + arg_name, method_name : str + Used for better error messages + + Returns + ------- + kwargs : dict + A dictionary of keyword arguments. Doesn't modify ``kwargs`` + inplace, so update them with the return value here. + + Examples + -------- + >>> df._validate_axis_style_args((str.upper,), {'columns': id}, + ... 'mapper', 'rename') + {'columns': , 'index': } + + This emits a warning + >>> df._validate_axis_style_args((str.upper, id), {}, + ... 'mapper', 'rename') + {'columns': , 'index': } + """ + # TODO(PY3): Change to keyword-only args and remove all this + + out = {} + # Goal: fill 'out' with index/columns-style arguments + # like out = {'index': foo, 'columns': bar} + + # Start by validating for consistency + if 'axis' in kwargs and any(x in kwargs for x in data._AXIS_NUMBERS): + msg = "Cannot specify both 'axis' and any of 'index' or 'columns'." + raise TypeError(msg) + + # First fill with explicit values provided by the user... + if arg_name in kwargs: + if args: + msg = ("{} got multiple values for argument " + "'{}'".format(method_name, arg_name)) + raise TypeError(msg) + + axis = data._get_axis_name(kwargs.get('axis', 0)) + out[axis] = kwargs[arg_name] + + # More user-provided arguments, now from kwargs + for k, v in kwargs.items(): + try: + ax = data._get_axis_name(k) + except ValueError: + pass + else: + out[ax] = v + + # All user-provided kwargs have been handled now. + # Now we supplement with positional arguments, emitting warnings + # when there's ambiguity and raising when there's conflicts + + if len(args) == 0: + pass # It's up to the function to decide if this is valid + elif len(args) == 1: + axis = data._get_axis_name(kwargs.get('axis', 0)) + out[axis] = args[0] + elif len(args) == 2: + if 'axis' in kwargs: + # Unambiguously wrong + msg = ("Cannot specify both 'axis' and any of 'index' " + "or 'columns'") + raise TypeError(msg) + + msg = ("Interpreting call\n\t'.{method_name}(a, b)' as " + "\n\t'.{method_name}(index=a, columns=b)'.\nUse named " + "arguments to remove any ambiguity. In the future, using " + "positional arguments for 'index' or 'columns' will raise " + " a 'TypeError'.") + warnings.warn(msg.format(method_name=method_name,), FutureWarning, + stacklevel=4) + out[data._AXIS_NAMES[0]] = args[0] + out[data._AXIS_NAMES[1]] = args[1] + else: + msg = "Cannot specify all of '{}', 'index', 'columns'." + raise TypeError(msg.format(arg_name)) + return out + + +def validate_fillna_kwargs(value, method, validate_scalar_dict_value=True): + """Validate the keyword arguments to 'fillna'. + + This checks that exactly one of 'value' and 'method' is specified. + If 'method' is specified, this validates that it's a valid method. + + Parameters + ---------- + value, method : object + The 'value' and 'method' keyword arguments for 'fillna'. + validate_scalar_dict_value : bool, default True + Whether to validate that 'value' is a scalar or dict. Specifically, + validate that it is not a list or tuple. + + Returns + ------- + value, method : object + """ + from pandas.core.missing import clean_fill_method + + if value is None and method is None: + raise ValueError("Must specify a fill 'value' or 'method'.") + elif value is None and method is not None: + method = clean_fill_method(method) + + elif value is not None and method is None: + if validate_scalar_dict_value and isinstance(value, (list, tuple)): + raise TypeError('"value" parameter must be a scalar or dict, but ' + 'you passed a "{0}"'.format(type(value).__name__)) + + elif value is not None and method is not None: + raise ValueError("Cannot specify both 'value' and 'method'.") + + return value, method diff --git a/pandas/util/move.c b/pandas/util/move.c index 9a8af5bbfbdf6..188d7b79b35d2 100644 --- a/pandas/util/move.c +++ b/pandas/util/move.c @@ -1,3 +1,12 @@ +/* +Copyright (c) 2019, PyData Development Team +All rights reserved. + +Distributed under the terms of the BSD Simplified License. + +The full license is in the LICENSE file, distributed with this software. +*/ + #include #define COMPILING_IN_PY2 (PY_VERSION_HEX <= 0x03000000) @@ -10,17 +19,17 @@ /* in python 3, we cannot intern bytes objects so this is always false */ #define PyString_CHECK_INTERNED(cs) 0 -#endif /* !COMPILING_IN_PY2 */ +#endif // !COMPILING_IN_PY2 #ifndef Py_TPFLAGS_HAVE_GETCHARBUFFER #define Py_TPFLAGS_HAVE_GETCHARBUFFER 0 -#endif +#endif // Py_TPFLAGS_HAVE_GETCHARBUFFER #ifndef Py_TPFLAGS_HAVE_NEWBUFFER #define Py_TPFLAGS_HAVE_NEWBUFFER 0 -#endif +#endif // Py_TPFLAGS_HAVE_NEWBUFFER -PyObject *badmove; /* bad move exception class */ +static PyObject *badmove; /* bad move exception class */ typedef struct { PyObject_HEAD @@ -28,18 +37,16 @@ typedef struct { PyObject *invalid_bytes; } stolenbufobject; -PyTypeObject stolenbuf_type; /* forward declare type */ +static PyTypeObject stolenbuf_type; /* forward declare type */ static void -stolenbuf_dealloc(stolenbufobject *self) -{ +stolenbuf_dealloc(stolenbufobject *self) { Py_DECREF(self->invalid_bytes); PyObject_Del(self); } static int -stolenbuf_getbuffer(stolenbufobject *self, Py_buffer *view, int flags) -{ +stolenbuf_getbuffer(stolenbufobject *self, Py_buffer *view, int flags) { return PyBuffer_FillInfo(view, (PyObject*) self, (void*) PyString_AS_STRING(self->invalid_bytes), @@ -51,8 +58,8 @@ stolenbuf_getbuffer(stolenbufobject *self, Py_buffer *view, int flags) #if COMPILING_IN_PY2 static Py_ssize_t -stolenbuf_getreadwritebuf(stolenbufobject *self, Py_ssize_t segment, void **out) -{ +stolenbuf_getreadwritebuf(stolenbufobject *self, + Py_ssize_t segment, void **out) { if (segment != 0) { PyErr_SetString(PyExc_SystemError, "accessing non-existent string segment"); @@ -63,15 +70,14 @@ stolenbuf_getreadwritebuf(stolenbufobject *self, Py_ssize_t segment, void **out) } static Py_ssize_t -stolenbuf_getsegcount(stolenbufobject *self, Py_ssize_t *len) -{ +stolenbuf_getsegcount(stolenbufobject *self, Py_ssize_t *len) { if (len) { *len = PyString_GET_SIZE(self->invalid_bytes); } return 1; } -PyBufferProcs stolenbuf_as_buffer = { +static PyBufferProcs stolenbuf_as_buffer = { (readbufferproc) stolenbuf_getreadwritebuf, (writebufferproc) stolenbuf_getreadwritebuf, (segcountproc) stolenbuf_getsegcount, @@ -79,19 +85,19 @@ PyBufferProcs stolenbuf_as_buffer = { (getbufferproc) stolenbuf_getbuffer, }; -#else /* Python 3 */ +#else // Python 3 -PyBufferProcs stolenbuf_as_buffer = { +static PyBufferProcs stolenbuf_as_buffer = { (getbufferproc) stolenbuf_getbuffer, NULL, }; -#endif /* COMPILING_IN_PY2 */ +#endif // COMPILING_IN_PY2 PyDoc_STRVAR(stolenbuf_doc, "A buffer that is wrapping a stolen bytes object's buffer."); -PyTypeObject stolenbuf_type = { +static PyTypeObject stolenbuf_type = { PyVarObject_HEAD_INIT(NULL, 0) "pandas.util._move.stolenbuf", /* tp_name */ sizeof(stolenbufobject), /* tp_basicsize */ @@ -157,8 +163,7 @@ PyDoc_STRVAR( however, if called through *unpacking like ``stolenbuf(*(a,))`` it would only have the one reference (the tuple). */ static PyObject* -move_into_mutable_buffer(PyObject *self, PyObject *bytes_rvalue) -{ +move_into_mutable_buffer(PyObject *self, PyObject *bytes_rvalue) { stolenbufobject *ret; if (!PyString_CheckExact(bytes_rvalue)) { @@ -185,7 +190,7 @@ move_into_mutable_buffer(PyObject *self, PyObject *bytes_rvalue) return (PyObject*) ret; } -PyMethodDef methods[] = { +static PyMethodDef methods[] = { {"move_into_mutable_buffer", (PyCFunction) move_into_mutable_buffer, METH_O, @@ -196,14 +201,14 @@ PyMethodDef methods[] = { #define MODULE_NAME "pandas.util._move" #if !COMPILING_IN_PY2 -PyModuleDef _move_module = { +static PyModuleDef move_module = { PyModuleDef_HEAD_INIT, MODULE_NAME, NULL, -1, methods, }; -#endif /* !COMPILING_IN_PY2 */ +#endif // !COMPILING_IN_PY2 PyDoc_STRVAR( badmove_doc, @@ -226,7 +231,7 @@ PyInit__move(void) #else #define ERROR_RETURN init_move(void) -#endif /* !COMPILING_IN_PY2 */ +#endif // !COMPILING_IN_PY2 { PyObject *m; @@ -242,10 +247,10 @@ init_move(void) } #if !COMPILING_IN_PY2 - if (!(m = PyModule_Create(&_move_module))) + if (!(m = PyModule_Create(&move_module))) #else if (!(m = Py_InitModule(MODULE_NAME, methods))) -#endif /* !COMPILING_IN_PY2 */ +#endif // !COMPILING_IN_PY2 { return ERROR_RETURN; } @@ -264,5 +269,5 @@ init_move(void) #if !COMPILING_IN_PY2 return m; -#endif /* !COMPILING_IN_PY2 */ +#endif // !COMPILING_IN_PY2 } diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 9a9f3c6c6b945..a5ae1f6a4d960 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -1,62 +1,53 @@ from __future__ import division -# pylint: disable-msg=W0402 +from collections import Counter +from contextlib import contextmanager +from datetime import datetime +from functools import wraps +import locale +import os import re +from shutil import rmtree import string +import subprocess import sys import tempfile -import warnings -import inspect -import os -import subprocess -import locale -import unittest import traceback +import warnings -from datetime import datetime -from functools import wraps, partial -from contextlib import contextmanager -from distutils.version import LooseVersion - -from numpy.random import randn, rand -import pytest import numpy as np +from numpy.random import rand, randn -import pandas as pd -from pandas.types.missing import array_equivalent -from pandas.types.common import (is_datetimelike_v_numeric, - is_datetimelike_v_object, - is_number, is_bool, - needs_i8_conversion, - is_categorical_dtype, - is_sequence, - is_list_like) -from pandas.formats.printing import pprint_thing -from pandas.core.algorithms import take_1d - +from pandas._libs import testing as _testing import pandas.compat as compat from pandas.compat import ( - filter, map, zip, range, unichr, lrange, lmap, lzip, u, callable, Counter, - raise_with_traceback, httplib, is_platform_windows, is_platform_32bit, - PY3 -) + PY2, PY3, filter, httplib, lmap, lrange, lzip, map, raise_with_traceback, + range, string_types, u, unichr, zip) -from pandas.computation import expressions as expr +from pandas.core.dtypes.common import ( + is_bool, is_categorical_dtype, is_datetime64_dtype, is_datetime64tz_dtype, + is_datetimelike_v_numeric, is_datetimelike_v_object, + is_extension_array_dtype, is_interval_dtype, is_list_like, is_number, + is_period_dtype, is_sequence, is_timedelta64_dtype, needs_i8_conversion) +from pandas.core.dtypes.missing import array_equivalent -from pandas import (bdate_range, CategoricalIndex, Categorical, DatetimeIndex, - TimedeltaIndex, PeriodIndex, RangeIndex, Index, MultiIndex, - Series, DataFrame, Panel, Panel4D) -from pandas.util.decorators import deprecate -from pandas.util import libtesting -from pandas.io.common import urlopen -slow = pytest.mark.slow +import pandas as pd +from pandas import ( + Categorical, CategoricalIndex, DataFrame, DatetimeIndex, Index, + IntervalIndex, MultiIndex, RangeIndex, Series, bdate_range) +from pandas.core.algorithms import take_1d +from pandas.core.arrays import ( + DatetimeArray, ExtensionArray, IntervalArray, PeriodArray, TimedeltaArray, + period_array) +import pandas.core.common as com +from pandas.io.common import urlopen +from pandas.io.formats.printing import pprint_thing N = 30 K = 4 _RAISE_NETWORK_ERROR_DEFAULT = False - # set testing_mode _testing_mode_warnings = (DeprecationWarning, compat.ResourceWarning) @@ -78,118 +69,298 @@ def reset_testing_mode(): set_testing_mode() -class TestCase(unittest.TestCase): +def reset_display_options(): + """ + Reset the display options for printing and representing objects. + """ - @classmethod - def setUpClass(cls): - pd.set_option('chained_assignment', 'raise') + pd.reset_option('^display.', silent=True) - @classmethod - def tearDownClass(cls): - pass - def reset_display_options(self): - # reset the display options - pd.reset_option('^display.', silent=True) +def round_trip_pickle(obj, path=None): + """ + Pickle an object and then read it again. + + Parameters + ---------- + obj : pandas object + The object to pickle and then re-read. + path : str, default None + The path where the pickled object is written and then read. + + Returns + ------- + round_trip_pickled_object : pandas object + The original object that was pickled and then re-read. + """ + + if path is None: + path = u('__{random_bytes}__.pickle'.format(random_bytes=rands(10))) + with ensure_clean(path) as path: + pd.to_pickle(obj, path) + return pd.read_pickle(path) - def round_trip_pickle(self, obj, path=None): - return round_trip_pickle(obj, path=path) - # https://docs.python.org/3/library/unittest.html#deprecated-aliases - def assertEquals(self, *args, **kwargs): - return deprecate('assertEquals', - self.assertEqual)(*args, **kwargs) +def round_trip_pathlib(writer, reader, path=None): + """ + Write an object to file specified by a pathlib.Path and read it back - def assertNotEquals(self, *args, **kwargs): - return deprecate('assertNotEquals', - self.assertNotEqual)(*args, **kwargs) + Parameters + ---------- + writer : callable bound to pandas object + IO writing function (e.g. DataFrame.to_csv ) + reader : callable + IO reading function (e.g. pd.read_csv ) + path : str, default None + The path where the object is written and then read. + + Returns + ------- + round_trip_object : pandas object + The original object that was serialized and then re-read. + """ - def assert_(self, *args, **kwargs): - return deprecate('assert_', - self.assertTrue)(*args, **kwargs) + import pytest + Path = pytest.importorskip('pathlib').Path + if path is None: + path = '___pathlib___' + with ensure_clean(path) as path: + writer(Path(path)) + obj = reader(Path(path)) + return obj - def assertAlmostEquals(self, *args, **kwargs): - return deprecate('assertAlmostEquals', - self.assertAlmostEqual)(*args, **kwargs) - def assertNotAlmostEquals(self, *args, **kwargs): - return deprecate('assertNotAlmostEquals', - self.assertNotAlmostEqual)(*args, **kwargs) +def round_trip_localpath(writer, reader, path=None): + """ + Write an object to file specified by a py.path LocalPath and read it back + Parameters + ---------- + writer : callable bound to pandas object + IO writing function (e.g. DataFrame.to_csv ) + reader : callable + IO reading function (e.g. pd.read_csv ) + path : str, default None + The path where the object is written and then read. -def round_trip_pickle(obj, path=None): + Returns + ------- + round_trip_object : pandas object + The original object that was serialized and then re-read. + """ + import pytest + LocalPath = pytest.importorskip('py.path').local if path is None: - path = u('__%s__.pickle' % rands(10)) + path = '___localpath___' with ensure_clean(path) as path: - pd.to_pickle(obj, path) - return pd.read_pickle(path) + writer(LocalPath(path)) + obj = reader(LocalPath(path)) + return obj -def assert_almost_equal(left, right, check_exact=False, - check_dtype='equiv', check_less_precise=False, - **kwargs): - """Check that left and right Index are equal. +@contextmanager +def decompress_file(path, compression): + """ + Open a compressed file and return a file object + + Parameters + ---------- + path : str + The path where the file is read from + + compression : {'gzip', 'bz2', 'zip', 'xz', None} + Name of the decompression to use + + Returns + ------- + f : file object + """ + + if compression is None: + f = open(path, 'rb') + elif compression == 'gzip': + import gzip + f = gzip.open(path, 'rb') + elif compression == 'bz2': + import bz2 + f = bz2.BZ2File(path, 'rb') + elif compression == 'xz': + lzma = compat.import_lzma() + f = lzma.LZMAFile(path, 'rb') + elif compression == 'zip': + import zipfile + zip_file = zipfile.ZipFile(path) + zip_names = zip_file.namelist() + if len(zip_names) == 1: + f = zip_file.open(zip_names.pop()) + else: + raise ValueError('ZIP file {} error. Only one file per ZIP.' + .format(path)) + else: + msg = 'Unrecognized compression type: {}'.format(compression) + raise ValueError(msg) + + try: + yield f + finally: + f.close() + if compression == "zip": + zip_file.close() + + +def write_to_compressed(compression, path, data, dest="test"): + """ + Write data to a compressed file. + + Parameters + ---------- + compression : {'gzip', 'bz2', 'zip', 'xz'} + The compression type to use. + path : str + The file path to write the data. + data : str + The data to write. + dest : str, default "test" + The destination file (for ZIP only) + + Raises + ------ + ValueError : An invalid compression value was passed in. + """ + + if compression == "zip": + import zipfile + compress_method = zipfile.ZipFile + elif compression == "gzip": + import gzip + compress_method = gzip.GzipFile + elif compression == "bz2": + import bz2 + compress_method = bz2.BZ2File + elif compression == "xz": + lzma = compat.import_lzma() + compress_method = lzma.LZMAFile + else: + msg = "Unrecognized compression type: {}".format(compression) + raise ValueError(msg) + + if compression == "zip": + mode = "w" + args = (dest, data) + method = "writestr" + else: + mode = "wb" + args = (data,) + method = "write" + + with compress_method(path, mode=mode) as f: + getattr(f, method)(*args) + + +def assert_almost_equal(left, right, check_dtype="equiv", + check_less_precise=False, **kwargs): + """ + Check that the left and right objects are approximately equal. + + By approximately equal, we refer to objects that are numbers or that + contain numbers which may be equivalent to specific levels of precision. Parameters ---------- left : object right : object - check_exact : bool, default True - Whether to compare number exactly. - check_dtype: bool, default True - check dtype if both a and b are the same type + check_dtype : bool / string {'equiv'}, default 'equiv' + Check dtype if both a and b are the same type. If 'equiv' is passed in, + then `RangeIndex` and `Int64Index` are also considered equivalent + when doing type checking. check_less_precise : bool or int, default False - Specify comparison precision. Only used when check_exact is False. - 5 digits (False) or 3 digits (True) after decimal points are compared. - If int, then specify the digits to compare + Specify comparison precision. 5 digits (False) or 3 digits (True) + after decimal points are compared. If int, then specify the number + of digits to compare. + + When comparing two numbers, if the first number has magnitude less + than 1e-5, we compare the two numbers directly and check whether + they are equivalent within the specified precision. Otherwise, we + compare the **ratio** of the second number to the first number and + check whether it is equivalent to 1 within the specified precision. """ + if isinstance(left, pd.Index): - return assert_index_equal(left, right, check_exact=check_exact, + return assert_index_equal(left, right, + check_exact=False, exact=check_dtype, check_less_precise=check_less_precise, **kwargs) elif isinstance(left, pd.Series): - return assert_series_equal(left, right, check_exact=check_exact, + return assert_series_equal(left, right, + check_exact=False, check_dtype=check_dtype, check_less_precise=check_less_precise, **kwargs) elif isinstance(left, pd.DataFrame): - return assert_frame_equal(left, right, check_exact=check_exact, + return assert_frame_equal(left, right, + check_exact=False, check_dtype=check_dtype, check_less_precise=check_less_precise, **kwargs) else: - # other sequences + # Other sequences. if check_dtype: if is_number(left) and is_number(right): - # do not compare numeric classes, like np.float64 and float + # Do not compare numeric classes, like np.float64 and float. pass elif is_bool(left) and is_bool(right): - # do not compare bool classes, like np.bool_ and bool + # Do not compare bool classes, like np.bool_ and bool. pass else: if (isinstance(left, np.ndarray) or isinstance(right, np.ndarray)): - obj = 'numpy array' + obj = "numpy array" else: - obj = 'Input' + obj = "Input" assert_class_equal(left, right, obj=obj) - return libtesting.assert_almost_equal( + return _testing.assert_almost_equal( left, right, check_dtype=check_dtype, check_less_precise=check_less_precise, **kwargs) -def assert_dict_equal(left, right, compare_keys=True): +def _check_isinstance(left, right, cls): + """ + Helper method for our assert_* methods that ensures that + the two objects being compared have the right type before + proceeding with the comparison. - assertIsInstance(left, dict, '[dict] ') - assertIsInstance(right, dict, '[dict] ') + Parameters + ---------- + left : The first object being compared. + right : The second object being compared. + cls : The class type to check against. + + Raises + ------ + AssertionError : Either `left` or `right` is not an instance of `cls`. + """ - return libtesting.assert_dict_equal(left, right, compare_keys=compare_keys) + err_msg = "{name} Expected type {exp_type}, found {act_type} instead" + cls_name = cls.__name__ + + if not isinstance(left, cls): + raise AssertionError(err_msg.format(name=cls_name, exp_type=cls, + act_type=type(left))) + if not isinstance(right, cls): + raise AssertionError(err_msg.format(name=cls_name, exp_type=cls, + act_type=type(right))) + + +def assert_dict_equal(left, right, compare_keys=True): + + _check_isinstance(left, right, dict) + return _testing.assert_dict_equal(left, right, compare_keys=compare_keys) def randbool(size=(), p=0.5): @@ -252,198 +423,6 @@ def close(fignum=None): _close(fignum) -def _skip_if_32bit(): - import pytest - if is_platform_32bit(): - pytest.skip("skipping for 32 bit") - - -def mplskip(cls): - """Skip a TestCase instance if matplotlib isn't installed""" - - @classmethod - def setUpClass(cls): - try: - import matplotlib as mpl - mpl.use("Agg", warn=False) - except ImportError: - import pytest - pytest.skip("matplotlib not installed") - - cls.setUpClass = setUpClass - return cls - - -def _skip_if_no_mpl(): - try: - import matplotlib # noqa - except ImportError: - import pytest - pytest.skip("matplotlib not installed") - - -def _skip_if_mpl_1_5(): - import matplotlib - v = matplotlib.__version__ - if v > LooseVersion('1.4.3') or v[0] == '0': - import pytest - pytest.skip("matplotlib 1.5") - - -def _skip_if_no_scipy(): - try: - import scipy.stats # noqa - except ImportError: - import pytest - pytest.skip("no scipy.stats module") - try: - import scipy.interpolate # noqa - except ImportError: - import pytest - pytest.skip('scipy.interpolate missing') - try: - import scipy.sparse # noqa - except ImportError: - import pytest - pytest.skip('scipy.sparse missing') - - -def _check_if_lzma(): - try: - return compat.import_lzma() - except ImportError: - return False - - -def _skip_if_no_lzma(): - return _check_if_lzma() or pytest.skip('need backports.lzma to run') - - -_mark_skipif_no_lzma = pytest.mark.skipif( - not _check_if_lzma(), - reason='need backports.lzma to run' -) - - -def _skip_if_no_xarray(): - try: - import xarray - except ImportError: - import pytest - pytest.skip("xarray not installed") - - v = xarray.__version__ - if v < LooseVersion('0.7.0'): - import pytest - pytest.skip("xarray not version is too low: {0}".format(v)) - - -def _skip_if_no_pytz(): - try: - import pytz # noqa - except ImportError: - import pytest - pytest.skip("pytz not installed") - - -def _skip_if_no_dateutil(): - try: - import dateutil # noqa - except ImportError: - import pytest - pytest.skip("dateutil not installed") - - -def _skip_if_windows_python_3(): - if PY3 and is_platform_windows(): - import pytest - pytest.skip("not used on python 3/win32") - - -def _skip_if_windows(): - if is_platform_windows(): - import pytest - pytest.skip("Running on Windows") - - -def _skip_if_no_pathlib(): - try: - from pathlib import Path # noqa - except ImportError: - import pytest - pytest.skip("pathlib not available") - - -def _skip_if_no_localpath(): - try: - from py.path import local as LocalPath # noqa - except ImportError: - import pytest - pytest.skip("py.path not installed") - - -def _incompat_bottleneck_version(method): - """ skip if we have bottleneck installed - and its >= 1.0 - as we don't match the nansum/nanprod behavior for all-nan - ops, see GH9422 - """ - if method not in ['sum', 'prod']: - return False - try: - import bottleneck as bn - return bn.__version__ >= LooseVersion('1.0') - except ImportError: - return False - - -def skip_if_no_ne(engine='numexpr'): - from pandas.computation.expressions import (_USE_NUMEXPR, - _NUMEXPR_INSTALLED) - - if engine == 'numexpr': - if not _USE_NUMEXPR: - import pytest - pytest.skip("numexpr enabled->{enabled}, " - "installed->{installed}".format( - enabled=_USE_NUMEXPR, - installed=_NUMEXPR_INSTALLED)) - - -def _skip_if_has_locale(): - import locale - lang, _ = locale.getlocale() - if lang is not None: - import pytest - pytest.skip("Specific locale is set {0}".format(lang)) - - -def _skip_if_not_us_locale(): - import locale - lang, _ = locale.getlocale() - if lang != 'en_US': - import pytest - pytest.skip("Specific locale is set {0}".format(lang)) - - -def _skip_if_no_mock(): - try: - import mock # noqa - except ImportError: - try: - from unittest import mock # noqa - except ImportError: - import nose - raise nose.SkipTest("mock is not installed") - - -def _skip_if_no_ipython(): - try: - import IPython # noqa - except ImportError: - import nose - raise nose.SkipTest("IPython not installed") - # ----------------------------------------------------------------------------- # locale utilities @@ -487,8 +466,8 @@ def _default_locale_getter(): try: raw_locales = check_output(['locale -a'], shell=True) except subprocess.CalledProcessError as e: - raise type(e)("%s, the 'locale -a' command cannot be found on your " - "system" % e) + raise type(e)("{exception}, the 'locale -a' command cannot be found " + "on your system".format(exception=e)) return raw_locales @@ -523,7 +502,7 @@ def get_locales(prefix=None, normalize=True, """ try: raw_locales = locale_getter() - except: + except Exception: return None try: @@ -545,7 +524,8 @@ def get_locales(prefix=None, normalize=True, if prefix is None: return _valid_locales(out_locales, normalize) - found = re.compile('%s.*' % prefix).findall('\n'.join(out_locales)) + pattern = re.compile('{prefix}.*'.format(prefix=prefix)) + found = pattern.findall('\n'.join(out_locales)) return _valid_locales(found, normalize) @@ -559,6 +539,8 @@ def set_locale(new_locale, lc_var=locale.LC_ALL): A string of the form .. For example to set the current locale to US English with a UTF8 encoding, you would pass "en_US.UTF-8". + lc_var : int, default `locale.LC_ALL` + The category of the locale being set. Notes ----- @@ -570,37 +552,38 @@ def set_locale(new_locale, lc_var=locale.LC_ALL): try: locale.setlocale(lc_var, new_locale) - - try: - normalized_locale = locale.getlocale() - except ValueError: - yield new_locale + normalized_locale = locale.getlocale() + if com._all_not_none(*normalized_locale): + yield '.'.join(normalized_locale) else: - if all(lc is not None for lc in normalized_locale): - yield '.'.join(normalized_locale) - else: - yield new_locale + yield new_locale finally: locale.setlocale(lc_var, current_locale) -def _can_set_locale(lc): - """Check to see if we can set a locale without throwing an exception. +def can_set_locale(lc, lc_var=locale.LC_ALL): + """ + Check to see if we can set a locale, and subsequently get the locale, + without raising an Exception. Parameters ---------- lc : str The locale to attempt to set. + lc_var : int, default `locale.LC_ALL` + The category of the locale being set. Returns ------- - isvalid : bool + is_valid : bool Whether the passed locale can be set """ + try: - with set_locale(lc): + with set_locale(lc, lc_var=lc_var): pass - except locale.Error: # horrible name for a Exception subclass + except (ValueError, + locale.Error): # horrible name for a Exception subclass return False else: return True @@ -627,7 +610,33 @@ def _valid_locales(locales, normalize): else: normalizer = lambda x: x.strip() - return list(filter(_can_set_locale, map(normalizer, locales))) + return list(filter(can_set_locale, map(normalizer, locales))) + +# ----------------------------------------------------------------------------- +# Stdout / stderr decorators + + +@contextmanager +def set_defaultencoding(encoding): + """ + Set default encoding (as given by sys.getdefaultencoding()) to the given + encoding; restore on exit. + + Parameters + ---------- + encoding : str + """ + if not PY2: + raise ValueError("set_defaultencoding context is only available " + "in Python 2.") + orig = sys.getdefaultencoding() + reload(sys) # noqa:F821 + sys.setdefaultencoding(encoding) + try: + yield + finally: + sys.setdefaultencoding(orig) + # ----------------------------------------------------------------------------- # Console debugging tools @@ -654,7 +663,7 @@ def set_trace(): from IPython.core.debugger import Pdb try: Pdb(color_scheme='Linux').set_trace(sys._getframe().f_back) - except: + except Exception: from pdb import Pdb as OldPdb OldPdb().set_trace(sys._getframe().f_back) @@ -700,24 +709,50 @@ def ensure_clean(filename=None, return_filelike=False): finally: try: os.close(fd) - except Exception as e: - print("Couldn't close file descriptor: %d (file: %s)" % - (fd, filename)) + except Exception: + print("Couldn't close file descriptor: {fdesc} (file: {fname})" + .format(fdesc=fd, fname=filename)) try: if os.path.exists(filename): os.remove(filename) except Exception as e: - print("Exception on removing file: %s" % e) + print("Exception on removing file: {error}".format(error=e)) + + +@contextmanager +def ensure_clean_dir(): + """ + Get a temporary directory path and agrees to remove on close. + Yields + ------ + Temporary directory path + """ + directory_name = tempfile.mkdtemp(suffix='') + try: + yield directory_name + finally: + try: + rmtree(directory_name) + except Exception: + pass -def get_data_path(f=''): - """Return the path of a data file, these are relative to the current test - directory. + +@contextmanager +def ensure_safe_environment_variables(): """ - # get our callers file - _, filename, _, _, _, _ = inspect.getouterframes(inspect.currentframe())[1] - base_dir = os.path.abspath(os.path.dirname(filename)) - return os.path.join(base_dir, 'data', f) + Get a context manager to safely set environment variables + + All changes will be undone on close, hence environment variables set + within this contextmanager will neither persist nor change global state. + """ + saved_environ = dict(os.environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(saved_environ) + # ----------------------------------------------------------------------------- # Comparators @@ -729,23 +764,6 @@ def equalContents(arr1, arr2): return frozenset(arr1) == frozenset(arr2) -def assert_equal(a, b, msg=""): - """asserts that a equals b, like nose's assert_equal, - but allows custom message to start. Passes a and b to - format string as well. So you can use '{0}' and '{1}' - to display a and b. - - Examples - -------- - >>> assert_equal(2, 2, "apples") - >>> assert_equal(5.2, 1.2, "{0} was really a dead parrot") - Traceback (most recent call last): - ... - AssertionError: 5.2 was really a dead parrot: 5.2 != 1.2 - """ - assert a == b, "%s: %r != %r" % (msg.format(a, b), a, b) - - def assert_index_equal(left, right, exact='equiv', check_names=True, check_less_precise=False, check_exact=True, check_categorical=True, obj='Index'): @@ -755,10 +773,10 @@ def assert_index_equal(left, right, exact='equiv', check_names=True, ---------- left : Index right : Index - exact : bool / string {'equiv'}, default False + exact : bool / string {'equiv'}, default 'equiv' Whether to check the Index class, dtype and inferred_type - are identical. If 'equiv', then RangeIndex can be substitued for - Int64Index as well + are identical. If 'equiv', then RangeIndex can be substituted for + Int64Index as well. check_names : bool, default True Whether to check the names attribute. check_less_precise : bool or int, default False @@ -773,43 +791,49 @@ def assert_index_equal(left, right, exact='equiv', check_names=True, Specify object name being compared, internally used to show appropriate assertion message """ + __tracebackhide__ = True def _check_types(l, r, obj='Index'): if exact: - assert_class_equal(left, right, exact=exact, obj=obj) - assert_attr_equal('dtype', l, r, obj=obj) + assert_class_equal(l, r, exact=exact, obj=obj) + + # Skip exact dtype checking when `check_categorical` is False + if check_categorical: + assert_attr_equal('dtype', l, r, obj=obj) + # allow string-like to have different inferred_types if l.inferred_type in ('string', 'unicode'): - assertIn(r.inferred_type, ('string', 'unicode')) + assert r.inferred_type in ('string', 'unicode') else: assert_attr_equal('inferred_type', l, r, obj=obj) def _get_ilevel_values(index, level): # accept level number only unique = index.levels[level] - labels = index.labels[level] + labels = index.codes[level] filled = take_1d(unique.values, labels, fill_value=unique._na_value) values = unique._shallow_copy(filled, name=index.names[level]) return values # instance validation - assertIsInstance(left, Index, '[index] ') - assertIsInstance(right, Index, '[index] ') + _check_isinstance(left, right, Index) # class / dtype comparison _check_types(left, right, obj=obj) # level comparison if left.nlevels != right.nlevels: - raise_assert_detail(obj, '{0} levels are different'.format(obj), - '{0}, {1}'.format(left.nlevels, left), - '{0}, {1}'.format(right.nlevels, right)) + msg1 = '{obj} levels are different'.format(obj=obj) + msg2 = '{nlevels}, {left}'.format(nlevels=left.nlevels, left=left) + msg3 = '{nlevels}, {right}'.format(nlevels=right.nlevels, right=right) + raise_assert_detail(obj, msg1, msg2, msg3) # length comparison if len(left) != len(right): - raise_assert_detail(obj, '{0} length are different'.format(obj), - '{0}, {1}'.format(len(left), left), - '{0}, {1}'.format(len(right), right)) + msg1 = '{obj} length are different'.format(obj=obj) + msg2 = '{length}, {left}'.format(length=len(left), left=left) + msg3 = '{length}, {right}'.format(length=len(right), right=right) + raise_assert_detail(obj, msg1, msg2, msg3) # MultiIndex special comparison for little-friendly error messages if left.nlevels > 1: @@ -818,7 +842,7 @@ def _get_ilevel_values(index, level): llevel = _get_ilevel_values(left, level) rlevel = _get_ilevel_values(right, level) - lobj = 'MultiIndex level [{0}]'.format(level) + lobj = 'MultiIndex level [{level}]'.format(level=level) assert_index_equal(llevel, rlevel, exact=exact, check_names=check_names, check_less_precise=check_less_precise, @@ -826,33 +850,38 @@ def _get_ilevel_values(index, level): # get_level_values may change dtype _check_types(left.levels[level], right.levels[level], obj=obj) - if check_exact: + # skip exact index checking when `check_categorical` is False + if check_exact and check_categorical: if not left.equals(right): diff = np.sum((left.values != right.values) .astype(int)) * 100.0 / len(left) - msg = '{0} values are different ({1} %)'\ - .format(obj, np.round(diff, 5)) + msg = '{obj} values are different ({pct} %)'.format( + obj=obj, pct=np.round(diff, 5)) raise_assert_detail(obj, msg, left, right) else: - libtesting.assert_almost_equal(left.values, right.values, - check_less_precise=check_less_precise, - check_dtype=exact, - obj=obj, lobj=left, robj=right) + _testing.assert_almost_equal(left.values, right.values, + check_less_precise=check_less_precise, + check_dtype=exact, + obj=obj, lobj=left, robj=right) # metadata comparison if check_names: assert_attr_equal('names', left, right, obj=obj) if isinstance(left, pd.PeriodIndex) or isinstance(right, pd.PeriodIndex): assert_attr_equal('freq', left, right, obj=obj) + if (isinstance(left, pd.IntervalIndex) or + isinstance(right, pd.IntervalIndex)): + assert_interval_array_equal(left.values, right.values) if check_categorical: if is_categorical_dtype(left) or is_categorical_dtype(right): assert_categorical_equal(left.values, right.values, - obj='{0} category'.format(obj)) + obj='{obj} category'.format(obj=obj)) def assert_class_equal(left, right, exact=True, obj='Input'): """checks classes are equal.""" + __tracebackhide__ = True def repr_class(x): if isinstance(x, Index): @@ -867,14 +896,14 @@ def repr_class(x): if exact == 'equiv': if type(left) != type(right): # allow equivalence of Int64Index/RangeIndex - types = set([type(left).__name__, type(right).__name__]) - if len(types - set(['Int64Index', 'RangeIndex'])): - msg = '{0} classes are not equivalent'.format(obj) + types = {type(left).__name__, type(right).__name__} + if len(types - {'Int64Index', 'RangeIndex'}): + msg = '{obj} classes are not equivalent'.format(obj=obj) raise_assert_detail(obj, msg, repr_class(left), repr_class(right)) elif exact: if type(left) != type(right): - msg = '{0} classes are different'.format(obj) + msg = '{obj} classes are different'.format(obj=obj) raise_assert_detail(obj, msg, repr_class(left), repr_class(right)) @@ -892,6 +921,7 @@ def assert_attr_equal(attr, left, right, obj='Attributes'): Specify object name being compared, internally used to show appropriate assertion message """ + __tracebackhide__ = True left_attr = getattr(left, attr) right_attr = getattr(right, attr) @@ -914,23 +944,22 @@ def assert_attr_equal(attr, left, right, obj='Attributes'): if result: return True else: - raise_assert_detail(obj, 'Attribute "{0}" are different'.format(attr), - left_attr, right_attr) + msg = 'Attribute "{attr}" are different'.format(attr=attr) + raise_assert_detail(obj, msg, left_attr, right_attr) def assert_is_valid_plot_return_object(objs): import matplotlib.pyplot as plt if isinstance(objs, (pd.Series, np.ndarray)): for el in objs.ravel(): - msg = ('one of \'objs\' is not a matplotlib Axes instance, ' - 'type encountered {0!r}') - assert isinstance(el, (plt.Axes, dict)), msg.format( - el.__class__.__name__) + msg = ("one of 'objs' is not a matplotlib Axes instance, type " + "encountered {name!r}").format(name=el.__class__.__name__) + assert isinstance(el, (plt.Axes, dict)), msg else: - assert isinstance(objs, (plt.Artist, tuple, dict)), \ - ('objs is neither an ndarray of Artist instances nor a ' - 'single Artist instance, tuple, or dict, "objs" is a {0!r} ' - ''.format(objs.__class__.__name__)) + assert isinstance(objs, (plt.Artist, tuple, dict)), ( + 'objs is neither an ndarray of Artist instances nor a ' + 'single Artist instance, tuple, or dict, "objs" is a {name!r}' + .format(name=objs.__class__.__name__)) def isiterable(obj): @@ -944,118 +973,131 @@ def is_sorted(seq): return assert_numpy_array_equal(seq, np.sort(np.array(seq))) -def assertIs(first, second, msg=''): - """Checks that 'first' is 'second'""" - a, b = first, second - assert a is b, "%s: %r is not %r" % (msg.format(a, b), a, b) - - -def assertIsNot(first, second, msg=''): - """Checks that 'first' is not 'second'""" - a, b = first, second - assert a is not b, "%s: %r is %r" % (msg.format(a, b), a, b) - - -def assertIn(first, second, msg=''): - """Checks that 'first' is in 'second'""" - a, b = first, second - assert a in b, "%s: %r is not in %r" % (msg.format(a, b), a, b) - - -def assertNotIn(first, second, msg=''): - """Checks that 'first' is not in 'second'""" - a, b = first, second - assert a not in b, "%s: %r is in %r" % (msg.format(a, b), a, b) - - -def assertIsNone(expr, msg=''): - """Checks that 'expr' is None""" - return assertIs(expr, None, msg) - - -def assertIsNotNone(expr, msg=''): - """Checks that 'expr' is not None""" - return assertIsNot(expr, None, msg) - - -def assertIsInstance(obj, cls, msg=''): - """Test that obj is an instance of cls - (which can be a class or a tuple of classes, - as supported by isinstance()).""" - if not isinstance(obj, cls): - err_msg = "{0}Expected type {1}, found {2} instead" - raise AssertionError(err_msg.format(msg, cls, type(obj))) - - -def assertNotIsInstance(obj, cls, msg=''): - """Test that obj is not an instance of cls - (which can be a class or a tuple of classes, - as supported by isinstance()).""" - if isinstance(obj, cls): - err_msg = "{0}Input must not be type {1}" - raise AssertionError(err_msg.format(msg, cls)) - - def assert_categorical_equal(left, right, check_dtype=True, - obj='Categorical', check_category_order=True): - """Test that categoricals are eqivalent + check_category_order=True, obj='Categorical'): + """Test that Categoricals are equivalent. Parameters ---------- - left, right : Categorical - Categoricals to compare + left : Categorical + right : Categorical check_dtype : bool, default True Check that integer dtype of the codes are the same - obj : str, default 'Categorical' - Specify object name being compared, internally used to show appropriate - assertion message check_category_order : bool, default True Whether the order of the categories should be compared, which implies identical integer codes. If False, only the resulting values are compared. The ordered attribute is checked regardless. + obj : str, default 'Categorical' + Specify object name being compared, internally used to show appropriate + assertion message """ - assertIsInstance(left, pd.Categorical, '[Categorical] ') - assertIsInstance(right, pd.Categorical, '[Categorical] ') + _check_isinstance(left, right, Categorical) if check_category_order: assert_index_equal(left.categories, right.categories, - obj='{0}.categories'.format(obj)) + obj='{obj}.categories'.format(obj=obj)) assert_numpy_array_equal(left.codes, right.codes, check_dtype=check_dtype, - obj='{0}.codes'.format(obj)) + obj='{obj}.codes'.format(obj=obj)) else: assert_index_equal(left.categories.sort_values(), right.categories.sort_values(), - obj='{0}.categories'.format(obj)) + obj='{obj}.categories'.format(obj=obj)) assert_index_equal(left.categories.take(left.codes), right.categories.take(right.codes), - obj='{0}.values'.format(obj)) + obj='{obj}.values'.format(obj=obj)) + + assert_attr_equal('ordered', left, right, obj=obj) + + +def assert_interval_array_equal(left, right, exact='equiv', + obj='IntervalArray'): + """Test that two IntervalArrays are equivalent. + + Parameters + ---------- + left, right : IntervalArray + The IntervalArrays to compare. + exact : bool / string {'equiv'}, default 'equiv' + Whether to check the Index class, dtype and inferred_type + are identical. If 'equiv', then RangeIndex can be substituted for + Int64Index as well. + obj : str, default 'IntervalArray' + Specify object name being compared, internally used to show appropriate + assertion message + """ + _check_isinstance(left, right, IntervalArray) + + assert_index_equal(left.left, right.left, exact=exact, + obj='{obj}.left'.format(obj=obj)) + assert_index_equal(left.right, right.right, exact=exact, + obj='{obj}.left'.format(obj=obj)) + assert_attr_equal('closed', left, right, obj=obj) + + +def assert_period_array_equal(left, right, obj='PeriodArray'): + _check_isinstance(left, right, PeriodArray) - assert_attr_equal('ordered', left, right, obj=obj) + assert_numpy_array_equal(left._data, right._data, + obj='{obj}.values'.format(obj=obj)) + assert_attr_equal('freq', left, right, obj=obj) + + +def assert_datetime_array_equal(left, right, obj='DatetimeArray'): + __tracebackhide__ = True + _check_isinstance(left, right, DatetimeArray) + + assert_numpy_array_equal(left._data, right._data, + obj='{obj}._data'.format(obj=obj)) + assert_attr_equal('freq', left, right, obj=obj) + assert_attr_equal('tz', left, right, obj=obj) + + +def assert_timedelta_array_equal(left, right, obj='TimedeltaArray'): + __tracebackhide__ = True + _check_isinstance(left, right, TimedeltaArray) + assert_numpy_array_equal(left._data, right._data, + obj='{obj}._data'.format(obj=obj)) + assert_attr_equal('freq', left, right, obj=obj) def raise_assert_detail(obj, message, left, right, diff=None): + __tracebackhide__ = True + if isinstance(left, np.ndarray): left = pprint_thing(left) + elif is_categorical_dtype(left): + left = repr(left) + + if PY2 and isinstance(left, string_types): + # left needs to be printable in native text type in python2 + left = left.encode('utf-8') + if isinstance(right, np.ndarray): right = pprint_thing(right) + elif is_categorical_dtype(right): + right = repr(right) + + if PY2 and isinstance(right, string_types): + # right needs to be printable in native text type in python2 + right = right.encode('utf-8') - msg = """{0} are different + msg = """{obj} are different -{1} -[left]: {2} -[right]: {3}""".format(obj, message, left, right) +{message} +[left]: {left} +[right]: {right}""".format(obj=obj, message=message, left=left, right=right) if diff is not None: - msg = msg + "\n[diff]: {diff}".format(diff=diff) + msg += "\n[diff]: {diff}".format(diff=diff) raise AssertionError(msg) def assert_numpy_array_equal(left, right, strict_nan=False, check_dtype=True, err_msg=None, - obj='numpy array', check_same=None): + check_same=None, obj='numpy array'): """ Checks that 'np.ndarray' is equivalent Parameters @@ -1068,33 +1110,42 @@ def assert_numpy_array_equal(left, right, strict_nan=False, check dtype if both a and b are np.ndarray err_msg : str, default None If provided, used as assertion message + check_same : None|'copy'|'same', default None + Ensure left and right refer/do not refer to the same memory area obj : str, default 'numpy array' Specify object name being compared, internally used to show appropriate assertion message - check_same : None|'copy'|'same', default None - Ensure left and right refer/do not refer to the same memory area """ + __tracebackhide__ = True # instance validation - # to show a detailed erorr message when classes are different + # Show a detailed error message when classes are different assert_class_equal(left, right, obj=obj) # both classes must be an np.ndarray - assertIsInstance(left, np.ndarray, '[ndarray] ') - assertIsInstance(right, np.ndarray, '[ndarray] ') + _check_isinstance(left, right, np.ndarray) def _get_base(obj): return obj.base if getattr(obj, 'base', None) is not None else obj + left_base = _get_base(left) + right_base = _get_base(right) + if check_same == 'same': - assertIs(_get_base(left), _get_base(right)) + if left_base is not right_base: + msg = "{left!r} is not {right!r}".format( + left=left_base, right=right_base) + raise AssertionError(msg) elif check_same == 'copy': - assertIsNot(_get_base(left), _get_base(right)) + if left_base is right_base: + msg = "{left!r} is {right!r}".format( + left=left_base, right=right_base) + raise AssertionError(msg) def _raise(left, right, err_msg): if err_msg is None: if left.shape != right.shape: - raise_assert_detail(obj, '{0} shapes are different' - .format(obj), left.shape, right.shape) + raise_assert_detail(obj, '{obj} shapes are different' + .format(obj=obj), left.shape, right.shape) diff = 0 for l, r in zip(left, right): @@ -1103,8 +1154,8 @@ def _raise(left, right, err_msg): diff += 1 diff = diff * 100.0 / left.size - msg = '{0} values are different ({1} %)'\ - .format(obj, np.round(diff, 5)) + msg = '{obj} values are different ({pct} %)'.format( + obj=obj, pct=np.round(diff, 5)) raise_assert_detail(obj, msg, left, right) raise AssertionError(err_msg) @@ -1120,6 +1171,55 @@ def _raise(left, right, err_msg): return True +def assert_extension_array_equal(left, right, check_dtype=True, + check_less_precise=False, + check_exact=False): + """Check that left and right ExtensionArrays are equal. + + Parameters + ---------- + left, right : ExtensionArray + The two arrays to compare + check_dtype : bool, default True + Whether to check if the ExtensionArray dtypes are identical. + check_less_precise : bool or int, default False + Specify comparison precision. Only used when check_exact is False. + 5 digits (False) or 3 digits (True) after decimal points are compared. + If int, then specify the digits to compare. + check_exact : bool, default False + Whether to compare number exactly. + + Notes + ----- + Missing values are checked separately from valid values. + A mask of missing values is computed for each and checked to match. + The remaining all-valid values are cast to object dtype and checked. + """ + assert isinstance(left, ExtensionArray), 'left is not an ExtensionArray' + assert isinstance(right, ExtensionArray), 'right is not an ExtensionArray' + if check_dtype: + assert_attr_equal('dtype', left, right, obj='ExtensionArray') + + if hasattr(left, "asi8") and type(right) == type(left): + # Avoid slow object-dtype comparisons + assert_numpy_array_equal(left.asi8, right.asi8) + return + + left_na = np.asarray(left.isna()) + right_na = np.asarray(right.isna()) + assert_numpy_array_equal(left_na, right_na, obj='ExtensionArray NA mask') + + left_valid = np.asarray(left[~left_na].astype(object)) + right_valid = np.asarray(right[~right_na].astype(object)) + if check_exact: + assert_numpy_array_equal(left_valid, right_valid, obj='ExtensionArray') + else: + _testing.assert_almost_equal(left_valid, right_valid, + check_dtype=check_dtype, + check_less_precise=check_less_precise, + obj='ExtensionArray') + + # This could be refactored to use the NDFrame.equals method def assert_series_equal(left, right, check_dtype=True, check_index_type='equiv', @@ -1146,35 +1246,35 @@ def assert_series_equal(left, right, check_dtype=True, check_less_precise : bool or int, default False Specify comparison precision. Only used when check_exact is False. 5 digits (False) or 3 digits (True) after decimal points are compared. - If int, then specify the digits to compare - check_exact : bool, default False - Whether to compare number exactly. + If int, then specify the digits to compare. check_names : bool, default True Whether to check the Series and Index names attribute. + check_exact : bool, default False + Whether to compare number exactly. check_datetimelike_compat : bool, default False Compare datetime-like which is comparable ignoring dtype. check_categorical : bool, default True Whether to compare internal Categorical exactly. obj : str, default 'Series' Specify object name being compared, internally used to show appropriate - assertion message + assertion message. """ + __tracebackhide__ = True # instance validation - assertIsInstance(left, Series, '[Series] ') - assertIsInstance(right, Series, '[Series] ') + _check_isinstance(left, right, Series) if check_series_type: # ToDo: There are some tests using rhs is sparse # lhs is dense. Should use assert_class_equal in future - assertIsInstance(left, type(right)) + assert isinstance(left, type(right)) # assert_class_equal(left, right, obj=obj) # length comparison if len(left) != len(right): - raise_assert_detail(obj, 'Series length are different', - '{0}, {1}'.format(len(left), left.index), - '{0}, {1}'.format(len(right), right.index)) + msg1 = '{len}, {left}'.format(len=len(left), left=left.index) + msg2 = '{len}, {right}'.format(len=len(right), right=right.index) + raise_assert_detail(obj, 'Series length are different', msg1, msg2) # index comparison assert_index_equal(left.index, right.index, exact=check_index_type, @@ -1182,15 +1282,22 @@ def assert_series_equal(left, right, check_dtype=True, check_less_precise=check_less_precise, check_exact=check_exact, check_categorical=check_categorical, - obj='{0}.index'.format(obj)) + obj='{obj}.index'.format(obj=obj)) if check_dtype: - assert_attr_equal('dtype', left, right) + # We want to skip exact dtype checking when `check_categorical` + # is False. We'll still raise if only one is a `Categorical`, + # regardless of `check_categorical` + if (is_categorical_dtype(left) and is_categorical_dtype(right) and + not check_categorical): + pass + else: + assert_attr_equal('dtype', left, right) if check_exact: assert_numpy_array_equal(left.get_values(), right.get_values(), check_dtype=check_dtype, - obj='{0}'.format(obj),) + obj='{obj}'.format(obj=obj),) elif check_datetimelike_compat: # we want to check only if we have compat dtypes # e.g. integer and M|m are NOT compat, but we can simply check @@ -1203,16 +1310,31 @@ def assert_series_equal(left, right, check_dtype=True, # datetimelike may have different objects (e.g. datetime.datetime # vs Timestamp) but will compare equal if not Index(left.values).equals(Index(right.values)): - msg = '[datetimelike_compat=True] {0} is not equal to {1}.' - raise AssertionError(msg.format(left.values, right.values)) + msg = ('[datetimelike_compat=True] {left} is not equal to ' + '{right}.').format(left=left.values, right=right.values) + raise AssertionError(msg) else: assert_numpy_array_equal(left.get_values(), right.get_values(), check_dtype=check_dtype) + elif is_interval_dtype(left) or is_interval_dtype(right): + assert_interval_array_equal(left.array, right.array) + + elif (is_extension_array_dtype(left.dtype) and + is_datetime64tz_dtype(left.dtype)): + # .values is an ndarray, but ._values is the ExtensionArray. + # TODO: Use .array + assert is_extension_array_dtype(right.dtype) + return assert_extension_array_equal(left._values, right._values) + + elif (is_extension_array_dtype(left) and not is_categorical_dtype(left) and + is_extension_array_dtype(right) and not is_categorical_dtype(right)): + return assert_extension_array_equal(left.array, right.array) + else: - libtesting.assert_almost_equal(left.get_values(), right.get_values(), - check_less_precise=check_less_precise, - check_dtype=check_dtype, - obj='{0}'.format(obj)) + _testing.assert_almost_equal(left.get_values(), right.get_values(), + check_less_precise=check_less_precise, + check_dtype=check_dtype, + obj='{obj}'.format(obj=obj)) # metadata comparison if check_names: @@ -1221,7 +1343,7 @@ def assert_series_equal(left, right, check_dtype=True, if check_categorical: if is_categorical_dtype(left) or is_categorical_dtype(right): assert_categorical_equal(left.values, right.values, - obj='{0} category'.format(obj)) + obj='{obj} category'.format(obj=obj)) # This could be refactored to use the NDFrame.equals method @@ -1237,28 +1359,41 @@ def assert_frame_equal(left, right, check_dtype=True, check_categorical=True, check_like=False, obj='DataFrame'): - """Check that left and right DataFrame are equal. + """ + Check that left and right DataFrame are equal. + + This function is intended to compare two DataFrames and output any + differences. Is is mostly intended for use in unit tests. + Additional parameters allow varying the strictness of the + equality checks performed. Parameters ---------- left : DataFrame + First DataFrame to compare. right : DataFrame + Second DataFrame to compare. check_dtype : bool, default True Whether to check the DataFrame dtype is identical. - check_index_type : bool / string {'equiv'}, default False + check_index_type : bool / string {'equiv'}, default 'equiv' Whether to check the Index class, dtype and inferred_type are identical. - check_column_type : bool / string {'equiv'}, default False + check_column_type : bool / string {'equiv'}, default 'equiv' Whether to check the columns class, dtype and inferred_type - are identical. - check_frame_type : bool, default False + are identical. Is passed as the ``exact`` argument of + :func:`assert_index_equal`. + check_frame_type : bool, default True Whether to check the DataFrame class is identical. check_less_precise : bool or int, default False Specify comparison precision. Only used when check_exact is False. 5 digits (False) or 3 digits (True) after decimal points are compared. - If int, then specify the digits to compare + If int, then specify the digits to compare. check_names : bool, default True - Whether to check the Index names attribute. + Whether to check that the `names` attribute for both the `index` + and `column` attributes of the DataFrame is identical, i.e. + + * left.index.names == right.index.names + * left.columns.names == right.columns.names by_blocks : bool, default False Specify how to compare internal data. If False, compare by columns. If True, compare by blocks. @@ -1269,28 +1404,59 @@ def assert_frame_equal(left, right, check_dtype=True, check_categorical : bool, default True Whether to compare internal Categorical exactly. check_like : bool, default False - If true, ignore the order of rows & columns + If True, ignore the order of index & columns. + Note: index labels must match their respective rows + (same as in columns) - same labels must be with the same data. obj : str, default 'DataFrame' Specify object name being compared, internally used to show appropriate - assertion message + assertion message. + + See Also + -------- + assert_series_equal : Equivalent method for asserting Series equality. + DataFrame.equals : Check DataFrame equality. + + Examples + -------- + This example shows comparing two DataFrames that are equal + but with columns of differing dtypes. + + >>> from pandas.util.testing import assert_frame_equal + >>> df1 = pd.DataFrame({'a': [1, 2], 'b': [3, 4]}) + >>> df2 = pd.DataFrame({'a': [1, 2], 'b': [3.0, 4.0]}) + + df1 equals itself. + >>> assert_frame_equal(df1, df1) + + df1 differs from df2 as column 'b' is of a different type. + >>> assert_frame_equal(df1, df2) + Traceback (most recent call last): + AssertionError: Attributes are different + + Attribute "dtype" are different + [left]: int64 + [right]: float64 + + Ignore differing dtypes in columns with check_dtype. + >>> assert_frame_equal(df1, df2, check_dtype=False) """ + __tracebackhide__ = True # instance validation - assertIsInstance(left, DataFrame, '[DataFrame] ') - assertIsInstance(right, DataFrame, '[DataFrame] ') + _check_isinstance(left, right, DataFrame) if check_frame_type: # ToDo: There are some tests using rhs is SparseDataFrame # lhs is DataFrame. Should use assert_class_equal in future - assertIsInstance(left, type(right)) + assert isinstance(left, type(right)) # assert_class_equal(left, right, obj=obj) # shape comparison if left.shape != right.shape: raise_assert_detail(obj, 'DataFrame shape mismatch', - '({0}, {1})'.format(*left.shape), - '({0}, {1})'.format(*right.shape)) + '{shape!r}'.format(shape=left.shape), + '{shape!r}'.format(shape=right.shape)) if check_like: left, right = left.reindex_like(right), right @@ -1301,7 +1467,7 @@ def assert_frame_equal(left, right, check_dtype=True, check_less_precise=check_less_precise, check_exact=check_exact, check_categorical=check_categorical, - obj='{0}.index'.format(obj)) + obj='{obj}.index'.format(obj=obj)) # column comparison assert_index_equal(left.columns, right.columns, exact=check_column_type, @@ -1309,12 +1475,12 @@ def assert_frame_equal(left, right, check_dtype=True, check_less_precise=check_less_precise, check_exact=check_exact, check_categorical=check_categorical, - obj='{0}.columns'.format(obj)) + obj='{obj}.columns'.format(obj=obj)) # compare by blocks if by_blocks: - rblocks = right.blocks - lblocks = left.blocks + rblocks = right._to_dict_of_blocks() + lblocks = left._to_dict_of_blocks() for dtype in list(set(list(lblocks.keys()) + list(rblocks.keys()))): assert dtype in lblocks assert dtype in rblocks @@ -1334,83 +1500,102 @@ def assert_frame_equal(left, right, check_dtype=True, check_exact=check_exact, check_names=check_names, check_datetimelike_compat=check_datetimelike_compat, check_categorical=check_categorical, - obj='DataFrame.iloc[:, {0}]'.format(i)) + obj='DataFrame.iloc[:, {idx}]'.format(idx=i)) -def assert_panelnd_equal(left, right, - check_dtype=True, - check_panel_type=False, - check_less_precise=False, - assert_func=assert_frame_equal, - check_names=False, - by_blocks=False, - obj='Panel'): - """Check that left and right Panels are equal. +def assert_equal(left, right, **kwargs): + """ + Wrapper for tm.assert_*_equal to dispatch to the appropriate test function. Parameters ---------- - left : Panel (or nd) - right : Panel (or nd) - check_dtype : bool, default True - Whether to check the Panel dtype is identical. - check_panel_type : bool, default False - Whether to check the Panel class is identical. - check_less_precise : bool or int, default False - Specify comparison precision. Only used when check_exact is False. - 5 digits (False) or 3 digits (True) after decimal points are compared. - If int, then specify the digits to compare - assert_func : function for comparing data - check_names : bool, default True - Whether to check the Index names attribute. - by_blocks : bool, default False - Specify how to compare internal data. If False, compare by columns. - If True, compare by blocks. - obj : str, default 'Panel' - Specify the object name being compared, internally used to show - the appropriate assertion message. + left : Index, Series, DataFrame, ExtensionArray, or np.ndarray + right : Index, Series, DataFrame, ExtensionArray, or np.ndarray + **kwargs """ + __tracebackhide__ = True - if check_panel_type: - assert_class_equal(left, right, obj=obj) - - for axis in left._AXIS_ORDERS: - left_ind = getattr(left, axis) - right_ind = getattr(right, axis) - assert_index_equal(left_ind, right_ind, check_names=check_names) - - if by_blocks: - rblocks = right.blocks - lblocks = left.blocks - for dtype in list(set(list(lblocks.keys()) + list(rblocks.keys()))): - assert dtype in lblocks - assert dtype in rblocks - array_equivalent(lblocks[dtype].values, rblocks[dtype].values) + if isinstance(left, pd.Index): + assert_index_equal(left, right, **kwargs) + elif isinstance(left, pd.Series): + assert_series_equal(left, right, **kwargs) + elif isinstance(left, pd.DataFrame): + assert_frame_equal(left, right, **kwargs) + elif isinstance(left, IntervalArray): + assert_interval_array_equal(left, right, **kwargs) + elif isinstance(left, PeriodArray): + assert_period_array_equal(left, right, **kwargs) + elif isinstance(left, DatetimeArray): + assert_datetime_array_equal(left, right, **kwargs) + elif isinstance(left, TimedeltaArray): + assert_timedelta_array_equal(left, right, **kwargs) + elif isinstance(left, ExtensionArray): + assert_extension_array_equal(left, right, **kwargs) + elif isinstance(left, np.ndarray): + assert_numpy_array_equal(left, right, **kwargs) else: + raise NotImplementedError(type(left)) - # can potentially be slow - for i, item in enumerate(left._get_axis(0)): - assert item in right, "non-matching item (right) '%s'" % item - litem = left.iloc[i] - ritem = right.iloc[i] - assert_func(litem, ritem, check_less_precise=check_less_precise) - for i, item in enumerate(right._get_axis(0)): - assert item in left, "non-matching item (left) '%s'" % item +def box_expected(expected, box_cls, transpose=True): + """ + Helper function to wrap the expected output of a test in a given box_class. + Parameters + ---------- + expected : np.ndarray, Index, Series + box_cls : {Index, Series, DataFrame} -# TODO: strangely check_names fails in py3 ? -_panel_frame_equal = partial(assert_frame_equal, check_names=False) -assert_panel_equal = partial(assert_panelnd_equal, - assert_func=_panel_frame_equal) -assert_panel4d_equal = partial(assert_panelnd_equal, - assert_func=assert_panel_equal) + Returns + ------- + subclass of box_cls + """ + if box_cls is pd.Index: + expected = pd.Index(expected) + elif box_cls is pd.Series: + expected = pd.Series(expected) + elif box_cls is pd.DataFrame: + expected = pd.Series(expected).to_frame() + if transpose: + # for vector operations, we we need a DataFrame to be a single-row, + # not a single-column, in order to operate against non-DataFrame + # vectors of the same length. + expected = expected.T + elif box_cls is PeriodArray: + # the PeriodArray constructor is not as flexible as period_array + expected = period_array(expected) + elif box_cls is DatetimeArray: + expected = DatetimeArray(expected) + elif box_cls is TimedeltaArray: + expected = TimedeltaArray(expected) + elif box_cls is np.ndarray: + expected = np.array(expected) + elif box_cls is to_array: + expected = to_array(expected) + else: + raise NotImplementedError(box_cls) + return expected + + +def to_array(obj): + # temporary implementation until we get pd.array in place + if is_period_dtype(obj): + return period_array(obj) + elif is_datetime64_dtype(obj) or is_datetime64tz_dtype(obj): + return DatetimeArray._from_sequence(obj) + elif is_timedelta64_dtype(obj): + return TimedeltaArray._from_sequence(obj) + else: + return np.array(obj) # ----------------------------------------------------------------------------- # Sparse -def assert_sp_array_equal(left, right, check_dtype=True): +def assert_sp_array_equal(left, right, check_dtype=True, check_kind=True, + check_fill_value=True, + consolidate_block_indices=False): """Check that the left and right SparseArray are equal. Parameters @@ -1419,25 +1604,48 @@ def assert_sp_array_equal(left, right, check_dtype=True): right : SparseArray check_dtype : bool, default True Whether to check the data dtype is identical. + check_kind : bool, default True + Whether to just the kind of the sparse index for each column. + check_fill_value : bool, default True + Whether to check that left.fill_value matches right.fill_value + consolidate_block_indices : bool, default False + Whether to consolidate contiguous blocks for sparse arrays with + a BlockIndex. Some operations, e.g. concat, will end up with + block indices that could be consolidated. Setting this to true will + create a new BlockIndex for that array, with consolidated + block indices. """ - assertIsInstance(left, pd.SparseArray, '[SparseArray]') - assertIsInstance(right, pd.SparseArray, '[SparseArray]') + _check_isinstance(left, right, pd.SparseArray) assert_numpy_array_equal(left.sp_values, right.sp_values, check_dtype=check_dtype) # SparseIndex comparison - assertIsInstance(left.sp_index, - pd.sparse.libsparse.SparseIndex, '[SparseIndex]') - assertIsInstance(right.sp_index, - pd.sparse.libsparse.SparseIndex, '[SparseIndex]') + assert isinstance(left.sp_index, pd._libs.sparse.SparseIndex) + assert isinstance(right.sp_index, pd._libs.sparse.SparseIndex) + + if not check_kind: + left_index = left.sp_index.to_block_index() + right_index = right.sp_index.to_block_index() + else: + left_index = left.sp_index + right_index = right.sp_index + + if consolidate_block_indices and left.kind == 'block': + # we'll probably remove this hack... + left_index = left_index.to_int_index().to_block_index() + right_index = right_index.to_int_index().to_block_index() - if not left.sp_index.equals(right.sp_index): + if not left_index.equals(right_index): raise_assert_detail('SparseArray.index', 'index are not equal', - left.sp_index, right.sp_index) + left_index, right_index) + else: + # Just ensure a + pass - assert_attr_equal('fill_value', left, right) + if check_fill_value: + assert_attr_equal('fill_value', left, right) if check_dtype: assert_attr_equal('dtype', left, right) assert_numpy_array_equal(left.values, right.values, @@ -1446,6 +1654,9 @@ def assert_sp_array_equal(left, right, check_dtype=True): def assert_sp_series_equal(left, right, check_dtype=True, exact_indices=True, check_series_type=True, check_names=True, + check_kind=True, + check_fill_value=True, + consolidate_block_indices=False, obj='SparseSeries'): """Check that the left and right SparseSeries are equal. @@ -1460,31 +1671,47 @@ def assert_sp_series_equal(left, right, check_dtype=True, exact_indices=True, Whether to check the SparseSeries class is identical. check_names : bool, default True Whether to check the SparseSeries name attribute. + check_kind : bool, default True + Whether to just the kind of the sparse index for each column. + check_fill_value : bool, default True + Whether to check that left.fill_value matches right.fill_value + consolidate_block_indices : bool, default False + Whether to consolidate contiguous blocks for sparse arrays with + a BlockIndex. Some operations, e.g. concat, will end up with + block indices that could be consolidated. Setting this to true will + create a new BlockIndex for that array, with consolidated + block indices. obj : str, default 'SparseSeries' Specify the object name being compared, internally used to show the appropriate assertion message. """ - assertIsInstance(left, pd.SparseSeries, '[SparseSeries]') - assertIsInstance(right, pd.SparseSeries, '[SparseSeries]') + _check_isinstance(left, right, pd.SparseSeries) if check_series_type: assert_class_equal(left, right, obj=obj) assert_index_equal(left.index, right.index, - obj='{0}.index'.format(obj)) + obj='{obj}.index'.format(obj=obj)) - assert_sp_array_equal(left.block.values, right.block.values) + assert_sp_array_equal(left.values, right.values, + check_kind=check_kind, + check_fill_value=check_fill_value, + consolidate_block_indices=consolidate_block_indices) if check_names: assert_attr_equal('name', left, right) if check_dtype: assert_attr_equal('dtype', left, right) - assert_numpy_array_equal(left.values, right.values) + assert_numpy_array_equal(np.asarray(left.values), + np.asarray(right.values)) def assert_sp_frame_equal(left, right, check_dtype=True, exact_indices=True, - check_frame_type=True, obj='SparseDataFrame'): + check_frame_type=True, check_kind=True, + check_fill_value=True, + consolidate_block_indices=False, + obj='SparseDataFrame'): """Check that the left and right SparseDataFrame are equal. Parameters @@ -1498,54 +1725,62 @@ def assert_sp_frame_equal(left, right, check_dtype=True, exact_indices=True, otherwise just compare dense representations. check_frame_type : bool, default True Whether to check the SparseDataFrame class is identical. + check_kind : bool, default True + Whether to just the kind of the sparse index for each column. + check_fill_value : bool, default True + Whether to check that left.fill_value matches right.fill_value + consolidate_block_indices : bool, default False + Whether to consolidate contiguous blocks for sparse arrays with + a BlockIndex. Some operations, e.g. concat, will end up with + block indices that could be consolidated. Setting this to true will + create a new BlockIndex for that array, with consolidated + block indices. obj : str, default 'SparseDataFrame' Specify the object name being compared, internally used to show the appropriate assertion message. """ - assertIsInstance(left, pd.SparseDataFrame, '[SparseDataFrame]') - assertIsInstance(right, pd.SparseDataFrame, '[SparseDataFrame]') + _check_isinstance(left, right, pd.SparseDataFrame) if check_frame_type: assert_class_equal(left, right, obj=obj) assert_index_equal(left.index, right.index, - obj='{0}.index'.format(obj)) + obj='{obj}.index'.format(obj=obj)) assert_index_equal(left.columns, right.columns, - obj='{0}.columns'.format(obj)) + obj='{obj}.columns'.format(obj=obj)) + + if check_fill_value: + assert_attr_equal('default_fill_value', left, right, obj=obj) for col, series in compat.iteritems(left): assert (col in right) # trade-off? if exact_indices: - assert_sp_series_equal(series, right[col], - check_dtype=check_dtype) + assert_sp_series_equal( + series, right[col], + check_dtype=check_dtype, + check_kind=check_kind, + check_fill_value=check_fill_value, + consolidate_block_indices=consolidate_block_indices + ) else: assert_series_equal(series.to_dense(), right[col].to_dense(), check_dtype=check_dtype) - assert_attr_equal('default_fill_value', left, right, obj=obj) - # do I care? # assert(left.default_kind == right.default_kind) for col in right: assert (col in left) - -def assert_sp_list_equal(left, right): - assertIsInstance(left, pd.SparseList, '[SparseList]') - assertIsInstance(right, pd.SparseList, '[SparseList]') - - assert_sp_array_equal(left.to_array(), right.to_array()) - # ----------------------------------------------------------------------------- # Others def assert_contains_all(iterable, dic): for k in iterable: - assert k in dic, "Did not contain item: '%r'" % k + assert k in dic, "Did not contain item: '{key!r}'".format(key=k) def assert_copy(iter1, iter2, **eql_kwargs): @@ -1559,20 +1794,16 @@ def assert_copy(iter1, iter2, **eql_kwargs): """ for elem1, elem2 in zip(iter1, iter2): assert_almost_equal(elem1, elem2, **eql_kwargs) - assert elem1 is not elem2, ("Expected object %r and " - "object %r to be different " - "objects, were same." - % (type(elem1), type(elem2))) + msg = ("Expected object {obj1!r} and object {obj2!r} to be " + "different objects, but they were the same object." + ).format(obj1=type(elem1), obj2=type(elem2)) + assert elem1 is not elem2, msg def getCols(k): return string.ascii_uppercase[:k] -def getArangeMat(): - return np.arange(N * K).reshape((N, K)) - - # make index def makeStringIndex(k=10, name=None): return Index(rands_array(nchars=10, size=k), name=name) @@ -1582,10 +1813,16 @@ def makeUnicodeIndex(k=10, name=None): return Index(randu_array(nchars=10, size=k), name=name) -def makeCategoricalIndex(k=10, n=3, name=None): +def makeCategoricalIndex(k=10, n=3, name=None, **kwargs): """ make a length k index or n categories """ x = rands_array(nchars=4, size=n) - return CategoricalIndex(np.random.choice(x, k), name=name) + return CategoricalIndex(np.random.choice(x, k), name=name, **kwargs) + + +def makeIntervalIndex(k=10, name=None, **kwargs): + """ make a length k IntervalIndex """ + x = np.linspace(0, 100, num=(k + 1)) + return IntervalIndex.from_breaks(x, name=name, **kwargs) def makeBoolIndex(k=10, name=None): @@ -1604,8 +1841,8 @@ def makeUIntIndex(k=10, name=None): return Index([2**63 + i for i in lrange(k)], name=name) -def makeRangeIndex(k=10, name=None): - return RangeIndex(0, k, 1, name=name) +def makeRangeIndex(k=10, name=None, **kwargs): + return RangeIndex(0, k, 1, name=name, **kwargs) def makeFloatIndex(k=10, name=None): @@ -1613,22 +1850,28 @@ def makeFloatIndex(k=10, name=None): return Index(values * (10 ** np.random.randint(0, 9)), name=name) -def makeDateIndex(k=10, freq='B', name=None): +def makeDateIndex(k=10, freq='B', name=None, **kwargs): dt = datetime(2000, 1, 1) dr = bdate_range(dt, periods=k, freq=freq, name=name) - return DatetimeIndex(dr, name=name) + return DatetimeIndex(dr, name=name, **kwargs) -def makeTimedeltaIndex(k=10, freq='D', name=None): - return TimedeltaIndex(start='1 day', periods=k, freq=freq, name=name) +def makeTimedeltaIndex(k=10, freq='D', name=None, **kwargs): + return pd.timedelta_range(start='1 day', periods=k, freq=freq, + name=name, **kwargs) -def makePeriodIndex(k=10, name=None): +def makePeriodIndex(k=10, name=None, **kwargs): dt = datetime(2000, 1, 1) - dr = PeriodIndex(start=dt, periods=k, freq='B', name=name) + dr = pd.period_range(start=dt, periods=k, freq='B', name=name, **kwargs) return dr +def makeMultiIndex(k=10, names=None, **kwargs): + return MultiIndex.from_product( + (('foo', 'bar'), (1, 2)), names=names, **kwargs) + + def all_index_generator(k=10): """Generator which can be iterated over to get instances of all the various index classes. @@ -1639,12 +1882,24 @@ def all_index_generator(k=10): """ all_make_index_funcs = [makeIntIndex, makeFloatIndex, makeStringIndex, makeUnicodeIndex, makeDateIndex, makePeriodIndex, - makeTimedeltaIndex, makeBoolIndex, + makeTimedeltaIndex, makeBoolIndex, makeRangeIndex, + makeIntervalIndex, makeCategoricalIndex] for make_index_func in all_make_index_funcs: yield make_index_func(k=k) +def index_subclass_makers_generator(): + make_index_funcs = [ + makeDateIndex, makePeriodIndex, + makeTimedeltaIndex, makeRangeIndex, + makeIntervalIndex, makeCategoricalIndex, + makeMultiIndex + ] + for make_index_func in make_index_funcs: + yield make_index_func + + def all_timeseries_index_generator(k=10): """Generator which can be iterated over to get instances of all the classes which represent time-seires. @@ -1678,7 +1933,7 @@ def makeObjectSeries(name=None): def getSeriesData(): index = makeStringIndex(N) - return dict((c, Series(randn(N), index=index)) for c in getCols(K)) + return {c: Series(randn(N), index=index) for c in getCols(K)} def makeTimeSeries(nper=None, freq='B', name=None): @@ -1694,11 +1949,11 @@ def makePeriodSeries(nper=None, name=None): def getTimeSeriesData(nper=None, freq='B'): - return dict((c, makeTimeSeries(nper, freq)) for c in getCols(K)) + return {c: makeTimeSeries(nper, freq) for c in getCols(K)} def getPeriodData(nper=None): - return dict((c, makePeriodSeries(nper)) for c in getCols(K)) + return {c: makePeriodSeries(nper) for c in getCols(K)} # make frame @@ -1734,23 +1989,6 @@ def makePeriodFrame(nper=None): return DataFrame(data) -def makePanel(nper=None): - cols = ['Item' + c for c in string.ascii_uppercase[:K - 1]] - data = dict((c, makeTimeDataFrame(nper)) for c in cols) - return Panel.fromDict(data) - - -def makePeriodPanel(nper=None): - cols = ['Item' + c for c in string.ascii_uppercase[:K - 1]] - data = dict((c, makePeriodFrame(nper)) for c in cols) - return Panel.fromDict(data) - - -def makePanel4D(nper=None): - return Panel4D(dict(l1=makePanel(nper), l2=makePanel(nper), - l3=makePanel(nper))) - - def makeCustomIndex(nentries, nlevels, prefix='#', names=False, ndupe_l=None, idx_type=None): """Create an index/multindex with given dimensions, levels, names, etc' @@ -1780,8 +2018,9 @@ def makeCustomIndex(nentries, nlevels, prefix='#', names=False, ndupe_l=None, assert (is_sequence(ndupe_l) and len(ndupe_l) <= nlevels) assert (names is None or names is False or names is True or len(names) is nlevels) - assert idx_type is None or \ - (idx_type in ('i', 'f', 's', 'u', 'dt', 'p', 'td') and nlevels == 1) + assert idx_type is None or (idx_type in ('i', 'f', 's', 'u', + 'dt', 'p', 'td') + and nlevels == 1) if names is True: # build default names @@ -1806,27 +2045,28 @@ def makeCustomIndex(nentries, nlevels, prefix='#', names=False, ndupe_l=None, idx.name = names[0] return idx elif idx_type is not None: - raise ValueError('"%s" is not a legal value for `idx_type`, use ' - '"i"/"f"/"s"/"u"/"dt/"p"/"td".' % idx_type) + raise ValueError('"{idx_type}" is not a legal value for `idx_type`, ' + 'use "i"/"f"/"s"/"u"/"dt/"p"/"td".' + .format(idx_type=idx_type)) if len(ndupe_l) < nlevels: ndupe_l.extend([1] * (nlevels - len(ndupe_l))) assert len(ndupe_l) == nlevels - assert all([x > 0 for x in ndupe_l]) + assert all(x > 0 for x in ndupe_l) tuples = [] for i in range(nlevels): def keyfunc(x): import re - numeric_tuple = re.sub("[^\d_]_?", "", x).split("_") + numeric_tuple = re.sub(r"[^\d_]_?", "", x).split("_") return lmap(int, numeric_tuple) # build a list of lists to create the index from div_factor = nentries // ndupe_l[i] + 1 cnt = Counter() for j in range(div_factor): - label = prefix + '_l%d_g' % i + str(j) + label = '{prefix}_l{i}_g{j}'.format(prefix=prefix, i=i, j=j) cnt[label] = ndupe_l[i] # cute Counter trick result = list(sorted(cnt.elements(), key=keyfunc))[:nentries] @@ -1836,7 +2076,11 @@ def keyfunc(x): # convert tuples to index if nentries == 1: + # we have a single level of tuples, i.e. a regular Index index = Index(tuples[0], name=names[0]) + elif nlevels == 1: + name = None if names is None else names[0] + index = Index((x[0] for x in tuples), name=name) else: index = MultiIndex.from_tuples(tuples, names=names) return index @@ -1849,8 +2093,8 @@ def makeCustomDataframe(nrows, ncols, c_idx_names=True, r_idx_names=True, """ nrows, ncols - number of data rows/cols c_idx_names, idx_names - False/True/list of strings, yields No names , - default names or uses the provided names for the levels of the - corresponding index. You can provide a single string when + default names or uses the provided names for the levels of the + corresponding index. You can provide a single string when c_idx_nlevels ==1. c_idx_nlevels - number of levels in columns index. > 1 will yield MultiIndex r_idx_nlevels - number of levels in rows index. > 1 will yield MultiIndex @@ -1903,12 +2147,12 @@ def makeCustomDataframe(nrows, ncols, c_idx_names=True, r_idx_names=True, assert c_idx_nlevels > 0 assert r_idx_nlevels > 0 - assert r_idx_type is None or \ - (r_idx_type in ('i', 'f', 's', - 'u', 'dt', 'p', 'td') and r_idx_nlevels == 1) - assert c_idx_type is None or \ - (c_idx_type in ('i', 'f', 's', - 'u', 'dt', 'p', 'td') and c_idx_nlevels == 1) + assert r_idx_type is None or (r_idx_type in ('i', 'f', 's', + 'u', 'dt', 'p', 'td') + and r_idx_nlevels == 1) + assert c_idx_type is None or (c_idx_type in ('i', 'f', 's', + 'u', 'dt', 'p', 'td') + and c_idx_nlevels == 1) columns = makeCustomIndex(ncols, nlevels=c_idx_nlevels, prefix='C', names=c_idx_names, ndupe_l=c_ndupe_l, @@ -1919,7 +2163,7 @@ def makeCustomDataframe(nrows, ncols, c_idx_names=True, r_idx_names=True, # by default, generate data based on location if data_gen_f is None: - data_gen_f = lambda r, c: "R%dC%d" % (r, c) + data_gen_f = lambda r, c: "R{rows}C{cols}".format(rows=r, cols=c) data = [[data_gen_f(r, c) for c in range(ncols)] for r in range(nrows)] @@ -1992,83 +2236,12 @@ def makeMissingDataframe(density=.9, random_state=None): return df -def add_nans(panel): - I, J, N = panel.shape - for i, item in enumerate(panel.items): - dm = panel[item] - for j, col in enumerate(dm.columns): - dm[col][:i + j] = np.NaN - return panel - - -def add_nans_panel4d(panel4d): - for l, label in enumerate(panel4d.labels): - panel = panel4d[label] - add_nans(panel) - return panel4d - - class TestSubDict(dict): def __init__(self, *args, **kwargs): dict.__init__(self, *args, **kwargs) -# Dependency checker when running tests. -# -# Copied this from nipy/nipype -# Copyright of respective developers, License: BSD-3 -def skip_if_no_package(pkg_name, min_version=None, max_version=None, - app='pandas', checker=LooseVersion): - """Check that the min/max version of the required package is installed. - - If the package check fails, the test is automatically skipped. - - Parameters - ---------- - pkg_name : string - Name of the required package. - min_version : string, optional - Minimal version number for required package. - max_version : string, optional - Max version number for required package. - app : string, optional - Application that is performing the check. For instance, the - name of the tutorial being executed that depends on specific - packages. - checker : object, optional - The class that will perform the version checking. Default is - distutils.version.LooseVersion. - - Examples - -------- - package_check('numpy', '1.3') - - """ - - import pytest - if app: - msg = '%s requires %s' % (app, pkg_name) - else: - msg = 'module requires %s' % pkg_name - if min_version: - msg += ' with version >= %s' % (min_version,) - if max_version: - msg += ' with version < %s' % (max_version,) - try: - mod = __import__(pkg_name) - except ImportError: - mod = None - try: - have_version = mod.__version__ - except AttributeError: - pytest.skip('Cannot find version for %s' % pkg_name) - if min_version and checker(have_version) < checker(min_version): - pytest.skip(msg) - if max_version and checker(have_version) >= checker(max_version): - pytest.skip(msg) - - def optional_args(decorator): """allows a decorator to take optional positional and keyword arguments. Assumes that taking a single, callable, positional argument means that @@ -2112,6 +2285,7 @@ def dec(f): 'Temporary failure in name resolution', 'Name or service not known', 'Connection refused', + 'certificate verify', ) # or this e.errno/e.reason.errno @@ -2132,7 +2306,7 @@ def dec(f): # and conditionally raise on these exception types _network_error_classes = (IOError, httplib.HTTPException) -if sys.version_info >= (3, 3): +if PY3: _network_error_classes += (TimeoutError,) # noqa @@ -2200,7 +2374,7 @@ def network(t, url="http://www.google.com", _skip_on_messages: iterable of string any exception e for which one of the strings is a substring of str(e) will be skipped with an appropriate - message. Intended to supress errors where an errno isn't available. + message. Intended to suppress errors where an errno isn't available. Notes ----- @@ -2253,7 +2427,7 @@ def network(t, url="http://www.google.com", from pytest import skip t.network = True - @wraps(t) + @compat.wraps(t) def wrapper(*args, **kwargs): if check_before_test and not raise_on_error: if not can_connect(url, error_classes): @@ -2267,16 +2441,16 @@ def wrapper(*args, **kwargs): if errno in skip_errnos: skip("Skipping test due to known errno" - " and error %s" % e) + " and error {error}".format(error=e)) try: e_str = traceback.format_exc(e) - except: + except Exception: e_str = str(e) - if any([m.lower() in e_str.lower() for m in _skip_on_messages]): + if any(m.lower() in e_str.lower() for m in _skip_on_messages): skip("Skipping test because exception " - "message is known and error %s" % e) + "message is known and error {error}".format(error=e)) if not isinstance(e, error_classes): raise @@ -2285,7 +2459,7 @@ def wrapper(*args, **kwargs): raise else: skip("Skipping test due to lack of connectivity" - " and error %s" % e) + " and error {error}".format(error=e)) return wrapper @@ -2293,137 +2467,51 @@ def wrapper(*args, **kwargs): with_connectivity_check = network -class SimpleMock(object): - - """ - Poor man's mocking object - - Note: only works for new-style classes, assumes __getattribute__ exists. - - >>> a = type("Duck",(),{}) - >>> a.attr1,a.attr2 ="fizz","buzz" - >>> b = SimpleMock(a,"attr1","bar") - >>> b.attr1 == "bar" and b.attr2 == "buzz" - True - >>> a.attr1 == "fizz" and a.attr2 == "buzz" - True - """ - - def __init__(self, obj, *args, **kwds): - assert(len(args) % 2 == 0) - attrs = kwds.get("attrs", {}) - for k, v in zip(args[::2], args[1::2]): - # dict comprehensions break 2.6 - attrs[k] = v - self.attrs = attrs - self.obj = obj - - def __getattribute__(self, name): - attrs = object.__getattribute__(self, "attrs") - obj = object.__getattribute__(self, "obj") - return attrs.get(name, type(obj).__getattribute__(obj, name)) - - -@contextmanager -def stdin_encoding(encoding=None): - """ - Context manager for running bits of code while emulating an arbitrary - stdin encoding. - - >>> import sys - >>> _encoding = sys.stdin.encoding - >>> with stdin_encoding('AES'): sys.stdin.encoding - 'AES' - >>> sys.stdin.encoding==_encoding - True - - """ - import sys - - _stdin = sys.stdin - sys.stdin = SimpleMock(sys.stdin, "encoding", encoding) - yield - sys.stdin = _stdin - - -def assertRaises(_exception, _callable=None, *args, **kwargs): - """assertRaises that is usable as context manager or in a with statement - - Exceptions that don't match the given Exception type fall through:: - - >>> with assertRaises(ValueError): - ... raise TypeError("banana") - ... - Traceback (most recent call last): - ... - TypeError: banana - - If it raises the given Exception type, the test passes - >>> with assertRaises(KeyError): - ... dct = dict() - ... dct["apple"] - - If the expected error doesn't occur, it raises an error. - >>> with assertRaises(KeyError): - ... dct = {'apple':True} - ... dct["apple"] - Traceback (most recent call last): - ... - AssertionError: KeyError not raised. - - In addition to using it as a contextmanager, you can also use it as a - function, just like the normal assertRaises - - >>> assertRaises(TypeError, ",".join, [1, 3, 5]) - """ - manager = _AssertRaisesContextmanager(exception=_exception) - # don't return anything if used in function form - if _callable is not None: - with manager: - _callable(*args, **kwargs) - else: - return manager - - -def assertRaisesRegexp(_exception, _regexp, _callable=None, *args, **kwargs): - """ Port of assertRaisesRegexp from unittest in - Python 2.7 - used in with statement. +def assert_raises_regex(_exception, _regexp, _callable=None, + *args, **kwargs): + r""" + Check that the specified Exception is raised and that the error message + matches a given regular expression pattern. This may be a regular + expression object or a string containing a regular expression suitable + for use by `re.search()`. This is a port of the `assertRaisesRegexp` + function from unittest in Python 2.7. - Explanation from standard library: - Like assertRaises() but also tests that regexp matches on the - string representation of the raised exception. regexp may be a - regular expression object or a string containing a regular - expression suitable for use by re.search(). + .. deprecated:: 0.24.0 + Use `pytest.raises` instead. - You can pass either a regular expression - or a compiled regular expression object. - >>> assertRaisesRegexp(ValueError, 'invalid literal for.*XYZ', - ... int, 'XYZ') + Examples + -------- + >>> assert_raises_regex(ValueError, 'invalid literal for.*XYZ', int, 'XYZ') >>> import re - >>> assertRaisesRegexp(ValueError, re.compile('literal'), int, 'XYZ') + >>> assert_raises_regex(ValueError, re.compile('literal'), int, 'XYZ') If an exception of a different type is raised, it bubbles up. - >>> assertRaisesRegexp(TypeError, 'literal', int, 'XYZ') + >>> assert_raises_regex(TypeError, 'literal', int, 'XYZ') Traceback (most recent call last): ... ValueError: invalid literal for int() with base 10: 'XYZ' >>> dct = dict() - >>> assertRaisesRegexp(KeyError, 'pear', dct.__getitem__, 'apple') + >>> assert_raises_regex(KeyError, 'pear', dct.__getitem__, 'apple') Traceback (most recent call last): ... AssertionError: "pear" does not match "'apple'" You can also use this in a with statement. - >>> with assertRaisesRegexp(TypeError, 'unsupported operand type\(s\)'): + + >>> with assert_raises_regex(TypeError, r'unsupported operand type\(s\)'): ... 1 + {} - >>> with assertRaisesRegexp(TypeError, 'banana'): + >>> with assert_raises_regex(TypeError, 'banana'): ... 'apple'[0] = 'b' Traceback (most recent call last): ... AssertionError: "banana" does not match "'str' object does not support \ item assignment" """ + warnings.warn(("assert_raises_regex has been deprecated and will " + "be removed in the next release. Please use " + "`pytest.raises` instead."), FutureWarning, stacklevel=2) + manager = _AssertRaisesContextmanager(exception=_exception, regexp=_regexp) if _callable is not None: with manager: @@ -2434,52 +2522,124 @@ def assertRaisesRegexp(_exception, _regexp, _callable=None, *args, **kwargs): class _AssertRaisesContextmanager(object): """ - Handles the behind the scenes work - for assertRaises and assertRaisesRegexp + Context manager behind `assert_raises_regex`. """ - def __init__(self, exception, regexp=None, *args, **kwargs): + def __init__(self, exception, regexp=None): + """ + Initialize an _AssertRaisesContextManager instance. + + Parameters + ---------- + exception : class + The expected Exception class. + regexp : str, default None + The regex to compare against the Exception message. + """ + self.exception = exception + if regexp is not None and not hasattr(regexp, "search"): regexp = re.compile(regexp, re.DOTALL) + self.regexp = regexp def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type, exc_value, trace_back): expected = self.exception - if not exc_type: - name = getattr(expected, "__name__", str(expected)) - raise AssertionError("{0} not raised.".format(name)) - if issubclass(exc_type, expected): - return self.handle_success(exc_type, exc_value, traceback) - return self.handle_failure(exc_type, exc_value, traceback) - - def handle_failure(*args, **kwargs): - # Failed, so allow Exception to bubble up - return False - def handle_success(self, exc_type, exc_value, traceback): - if self.regexp is not None: - val = str(exc_value) - if not self.regexp.search(val): - e = AssertionError('"%s" does not match "%s"' % - (self.regexp.pattern, str(val))) - raise_with_traceback(e, traceback) - return True + if not exc_type: + exp_name = getattr(expected, "__name__", str(expected)) + raise AssertionError("{name} not raised.".format(name=exp_name)) + + return self.exception_matches(exc_type, exc_value, trace_back) + + def exception_matches(self, exc_type, exc_value, trace_back): + """ + Check that the Exception raised matches the expected Exception + and expected error message regular expression. + + Parameters + ---------- + exc_type : class + The type of Exception raised. + exc_value : Exception + The instance of `exc_type` raised. + trace_back : stack trace object + The traceback object associated with `exc_value`. + + Returns + ------- + is_matched : bool + Whether or not the Exception raised matches the expected + Exception class and expected error message regular expression. + + Raises + ------ + AssertionError : The error message provided does not match + the expected error message regular expression. + """ + + if issubclass(exc_type, self.exception): + if self.regexp is not None: + val = str(exc_value) + + if not self.regexp.search(val): + msg = '"{pat}" does not match "{val}"'.format( + pat=self.regexp.pattern, val=val) + e = AssertionError(msg) + raise_with_traceback(e, trace_back) + + return True + else: + # Failed, so allow Exception to bubble up. + return False @contextmanager def assert_produces_warning(expected_warning=Warning, filter_level="always", clear=None, check_stacklevel=True): """ - Context manager for running code that expects to raise (or not raise) - warnings. Checks that code raises the expected warning and only the - expected warning. Pass ``False`` or ``None`` to check that it does *not* - raise a warning. Defaults to ``exception.Warning``, baseclass of all - Warnings. (basically a wrapper around ``warnings.catch_warnings``). + Context manager for running code expected to either raise a specific + warning, or not raise any warnings. Verifies that the code raises the + expected warning, and that it does not raise any other unexpected + warnings. It is basically a wrapper around ``warnings.catch_warnings``. + + Parameters + ---------- + expected_warning : {Warning, False, None}, default Warning + The type of Exception raised. ``exception.Warning`` is the base + class for all warnings. To check that no warning is returned, + specify ``False`` or ``None``. + filter_level : str, default "always" + Specifies whether warnings are ignored, displayed, or turned + into errors. + Valid values are: + + * "error" - turns matching warnings into exceptions + * "ignore" - discard the warning + * "always" - always emit a warning + * "default" - print the warning the first time it is generated + from each location + * "module" - print the warning the first time it is generated + from each module + * "once" - print the warning the first time it is generated + + clear : str, default None + If not ``None`` then remove any previously raised warnings from + the ``__warningsregistry__`` to ensure that no warning messages are + suppressed by this context manager. If ``None`` is specified, + the ``__warningsregistry__`` keeps track of which warnings have been + shown, and does not show them again. + check_stacklevel : bool, default True + If True, displays the line that called the function containing + the warning to show were the function is called. Otherwise, the + line that implements the function is displayed. + Examples + -------- >>> import warnings >>> with assert_produces_warning(): ... warnings.warn(UserWarning()) @@ -2498,10 +2658,12 @@ def assert_produces_warning(expected_warning=Warning, filter_level="always", ..warn:: This is *not* thread-safe. """ + __tracebackhide__ = True + with warnings.catch_warnings(record=True) as w: if clear is not None: - # make sure that we are clearning these warnings + # make sure that we are clearing these warnings # if they have happened before # to guarantee that we will catch them if not is_list_like(clear): @@ -2509,7 +2671,7 @@ def assert_produces_warning(expected_warning=Warning, filter_level="always", for m in clear: try: m.__warningregistry__.clear() - except: + except Exception: pass saw_warning = False @@ -2528,18 +2690,23 @@ def assert_produces_warning(expected_warning=Warning, filter_level="always", from inspect import getframeinfo, stack caller = getframeinfo(stack()[2][0]) msg = ("Warning not set with correct stacklevel. " - "File where warning is raised: {0} != {1}. " - "Warning message: {2}".format( - actual_warning.filename, caller.filename, - actual_warning.message)) + "File where warning is raised: {actual} != " + "{caller}. Warning message: {message}" + ).format(actual=actual_warning.filename, + caller=caller.filename, + message=actual_warning.message) assert actual_warning.filename == caller.filename, msg else: - extra_warnings.append(actual_warning.category.__name__) + extra_warnings.append((actual_warning.category.__name__, + actual_warning.message, + actual_warning.filename, + actual_warning.lineno)) if expected_warning: - assert saw_warning, ("Did not see expected warning of class %r." - % expected_warning.__name__) - assert not extra_warnings, ("Caused unexpected warning(s): %r." - % extra_warnings) + msg = "Did not see expected warning of class {name!r}.".format( + name=expected_warning.__name__) + assert saw_warning, msg + assert not extra_warnings, ("Caused unexpected warning(s): {extra!r}." + ).format(extra=extra_warnings) class RNGContext(object): @@ -2573,7 +2740,42 @@ def __exit__(self, exc_type, exc_value, traceback): @contextmanager -def use_numexpr(use, min_elements=expr._MIN_ELEMENTS): +def with_csv_dialect(name, **kwargs): + """ + Context manager to temporarily register a CSV dialect for parsing CSV. + + Parameters + ---------- + name : str + The name of the dialect. + kwargs : mapping + The parameters for the dialect. + + Raises + ------ + ValueError : the name of the dialect conflicts with a builtin one. + + See Also + -------- + csv : Python's CSV library. + """ + import csv + _BUILTIN_DIALECTS = {"excel", "excel-tab", "unix"} + + if name in _BUILTIN_DIALECTS: + raise ValueError("Cannot override builtin dialect.") + + csv.register_dialect(name, **kwargs) + yield + csv.unregister_dialect(name) + + +@contextmanager +def use_numexpr(use, min_elements=None): + from pandas.core.computation import expressions as expr + if min_elements is None: + min_elements = expr._MIN_ELEMENTS + olduse = expr._USE_NUMEXPR oldmin = expr._MIN_ELEMENTS expr.set_use_numexpr(use) @@ -2583,12 +2785,6 @@ def use_numexpr(use, min_elements=expr._MIN_ELEMENTS): expr.set_use_numexpr(olduse) -# Also provide all assert_* functions in the TestCase class -for name, obj in inspect.getmembers(sys.modules[__name__]): - if inspect.isfunction(obj) and name.startswith('assert'): - setattr(TestCase, name, staticmethod(obj)) - - def test_parallel(num_threads=2, kwargs_list=None): """Decorator to run the same function multiple times in parallel. @@ -2691,58 +2887,6 @@ def _constructor(self): return SubclassedCategorical -@contextmanager -def patch(ob, attr, value): - """Temporarily patch an attribute of an object. - - Parameters - ---------- - ob : any - The object to patch. This must support attribute assignment for `attr`. - attr : str - The name of the attribute to patch. - value : any - The temporary attribute to assign. - - Examples - -------- - >>> class C(object): - ... attribute = 'original' - ... - >>> C.attribute - 'original' - >>> with patch(C, 'attribute', 'patched'): - ... in_context = C.attribute - ... - >>> in_context - 'patched' - >>> C.attribute # the value is reset when the context manager exists - 'original' - - Correctly replaces attribute when the manager exits with an exception. - >>> with patch(C, 'attribute', 'patched'): - ... in_context = C.attribute - ... raise ValueError() - Traceback (most recent call last): - ... - ValueError - >>> in_context - 'patched' - >>> C.attribute - 'original' - """ - noattr = object() # mark that the attribute never existed - old = getattr(ob, attr, noattr) - setattr(ob, attr, value) - try: - yield - finally: - if old is noattr: - delattr(ob, attr) - else: - setattr(ob, attr, old) - - @contextmanager def set_timezone(tz): """Context manager for temporarily setting a timezone. @@ -2765,9 +2909,6 @@ def set_timezone(tz): ... 'EDT' """ - if is_platform_windows(): - import pytest - pytest.skip("timezone setting not supported on windows") import os import time @@ -2776,7 +2917,7 @@ def setTZ(tz): if tz is None: try: del os.environ['TZ'] - except: + except KeyError: pass else: os.environ['TZ'] = tz @@ -2788,3 +2929,52 @@ def setTZ(tz): yield finally: setTZ(orig_tz) + + +def _make_skipna_wrapper(alternative, skipna_alternative=None): + """Create a function for calling on an array. + + Parameters + ---------- + alternative : function + The function to be called on the array with no NaNs. + Only used when 'skipna_alternative' is None. + skipna_alternative : function + The function to be called on the original array + + Returns + ------- + skipna_wrapper : function + """ + if skipna_alternative: + def skipna_wrapper(x): + return skipna_alternative(x.values) + else: + def skipna_wrapper(x): + nona = x.dropna() + if len(nona) == 0: + return np.nan + return alternative(nona) + + return skipna_wrapper + + +def convert_rows_list_to_csv_str(rows_list): + """ + Convert list of CSV rows to single CSV-formatted string for current OS. + + This method is used for creating expected value of to_csv() method. + + Parameters + ---------- + rows_list : list + The list of string. Each element represents the row of csv. + + Returns + ------- + expected : string + Expected output of to_csv() in current OS + """ + sep = os.linesep + expected = sep.join(rows_list) + sep + return expected diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000000..be84c6f29fdeb --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,45 @@ +numpy>=1.15 +python-dateutil>=2.5.0 +pytz +asv +cython>=0.28.2 +flake8 +flake8-comprehensions +flake8-rst>=0.6.0,<=0.7.0 +gitpython +hypothesis>=3.82 +isort +moto +pytest>=4.0.2 +pytest-mock +sphinx +numpydoc +beautifulsoup4>=4.2.1 +blosc +botocore>=1.11 +boto3 +bottleneck>=1.2.0 +fastparquet>=0.2.1 +html5lib +ipython>=5.6.0 +ipykernel +jinja2 +lxml +matplotlib>=2.0.0 +nbsphinx +numexpr>=2.6.8 +openpyxl +pyarrow>=0.9.0 +tables>=3.4.2 +pytest-cov +pytest-xdist +s3fs +scipy>=1.1 +seaborn +sqlalchemy +statsmodels +xarray +xlrd +xlsxwriter +xlwt +cpplint \ No newline at end of file diff --git a/scripts/api_rst_coverage.py b/scripts/api_rst_coverage.py deleted file mode 100644 index 6bb5383509be6..0000000000000 --- a/scripts/api_rst_coverage.py +++ /dev/null @@ -1,43 +0,0 @@ -import pandas as pd -import inspect -import re - -def main(): - # classes whose members to check - classes = [pd.Series, pd.DataFrame, pd.Panel] - - def class_name_sort_key(x): - if x.startswith('Series'): - # make sure Series precedes DataFrame, and Panel. - return ' ' + x - else: - return x - - # class members - class_members = set() - for cls in classes: - class_members.update([cls.__name__ + '.' + x[0] for x in inspect.getmembers(cls)]) - - # class members referenced in api.rst - api_rst_members = set() - file_name = '../doc/source/api.rst' - with open(file_name, 'r') as f: - pattern = re.compile('({})\.(\w+)'.format('|'.join([cls.__name__ for cls in classes]))) - for line in f: - match = pattern.search(line) - if match: - api_rst_members.add(match.group(0)) - - print() - print("Documented members in api.rst that aren't actual class members:") - for x in sorted(api_rst_members.difference(class_members), key=class_name_sort_key): - print(x) - - print() - print("Class members (other than those beginning with '_') missing from api.rst:") - for x in sorted(class_members.difference(api_rst_members), key=class_name_sort_key): - if '._' not in x: - print(x) - -if __name__ == "__main__": - main() diff --git a/scripts/bench_join.R b/scripts/bench_join.R deleted file mode 100644 index edba277f0295c..0000000000000 --- a/scripts/bench_join.R +++ /dev/null @@ -1,50 +0,0 @@ -library(xts) - -iterations <- 50 - -ns = c(100, 1000, 10000, 100000, 1000000) -kinds = c("outer", "left", "inner") - -result = matrix(0, nrow=3, ncol=length(ns)) -n <- 100000 -pct.overlap <- 0.2 - -k <- 1 - -for (ni in 1:length(ns)){ - n <- ns[ni] - rng1 <- 1:n - offset <- as.integer(n * pct.overlap) - rng2 <- rng1 + offset - x <- xts(matrix(rnorm(n * k), nrow=n, ncol=k), - as.POSIXct(Sys.Date()) + rng1) - y <- xts(matrix(rnorm(n * k), nrow=n, ncol=k), - as.POSIXct(Sys.Date()) + rng2) - timing <- numeric() - for (i in 1:3) { - kind = kinds[i] - for(j in 1:iterations) { - gc() # just to be sure - timing[j] <- system.time(merge(x,y,join=kind))[3] - } - #timing <- system.time(for (j in 1:iterations) merge.xts(x, y, join=kind), - # gcFirst=F) - #timing <- as.list(timing) - result[i, ni] <- mean(timing) * 1000 - #result[i, ni] = (timing$elapsed / iterations) * 1000 - } -} - -rownames(result) <- kinds -colnames(result) <- log10(ns) - -mat <- matrix(rnorm(500000), nrow=100000, ncol=5) -set.seed(12345) -indexer <- sample(1:100000) - -timing <- rep(0, 10) -for (i in 1:10) { - gc() - timing[i] = system.time(mat[indexer,])[3] -} - diff --git a/scripts/bench_join.py b/scripts/bench_join.py deleted file mode 100644 index f9d43772766d8..0000000000000 --- a/scripts/bench_join.py +++ /dev/null @@ -1,211 +0,0 @@ -from pandas.compat import range, lrange -import numpy as np -import pandas._libs.lib as lib -from pandas import * -from copy import deepcopy -import time - -n = 1000000 -K = 1 -pct_overlap = 0.2 - -a = np.arange(n, dtype=np.int64) -b = np.arange(n * pct_overlap, n * (1 + pct_overlap), dtype=np.int64) - -dr1 = DatetimeIndex('1/1/2000', periods=n, offset=offsets.Minute()) -dr2 = DatetimeIndex( - dr1[int(pct_overlap * n)], periods=n, offset=offsets.Minute(2)) - -aobj = a.astype(object) -bobj = b.astype(object) - -av = np.random.randn(n) -bv = np.random.randn(n) - -avf = np.random.randn(n, K) -bvf = np.random.randn(n, K) - -a_series = Series(av, index=a) -b_series = Series(bv, index=b) - -a_frame = DataFrame(avf, index=a, columns=lrange(K)) -b_frame = DataFrame(bvf, index=b, columns=lrange(K, 2 * K)) - - -def do_left_join(a, b, av, bv): - out = np.empty((len(a), 2)) - lib.left_join_1d(a, b, av, bv, out) - return out - - -def do_outer_join(a, b, av, bv): - result_index, aindexer, bindexer = lib.outer_join_indexer(a, b) - result = np.empty((2, len(result_index))) - lib.take_1d(av, aindexer, result[0]) - lib.take_1d(bv, bindexer, result[1]) - return result_index, result - - -def do_inner_join(a, b, av, bv): - result_index, aindexer, bindexer = lib.inner_join_indexer(a, b) - result = np.empty((2, len(result_index))) - lib.take_1d(av, aindexer, result[0]) - lib.take_1d(bv, bindexer, result[1]) - return result_index, result - -from line_profiler import LineProfiler -prof = LineProfiler() - -from pandas.util.testing import set_trace - - -def do_left_join_python(a, b, av, bv): - indexer, mask = lib.ordered_left_join_int64(a, b) - - n, ak = av.shape - _, bk = bv.shape - result_width = ak + bk - - result = np.empty((result_width, n), dtype=np.float64) - result[:ak] = av.T - - bchunk = result[ak:] - _take_multi(bv.T, indexer, bchunk) - np.putmask(bchunk, np.tile(mask, bk), np.nan) - return result - - -def _take_multi(data, indexer, out): - if not data.flags.c_contiguous: - data = data.copy() - for i in range(data.shape[0]): - data[i].take(indexer, out=out[i]) - - -def do_left_join_multi(a, b, av, bv): - n, ak = av.shape - _, bk = bv.shape - result = np.empty((n, ak + bk), dtype=np.float64) - lib.left_join_2d(a, b, av, bv, result) - return result - - -def do_outer_join_multi(a, b, av, bv): - n, ak = av.shape - _, bk = bv.shape - result_index, rindexer, lindexer = lib.outer_join_indexer(a, b) - result = np.empty((len(result_index), ak + bk), dtype=np.float64) - lib.take_join_contiguous(av, bv, lindexer, rindexer, result) - # result = np.empty((ak + bk, len(result_index)), dtype=np.float64) - # lib.take_axis0(av, rindexer, out=result[:ak].T) - # lib.take_axis0(bv, lindexer, out=result[ak:].T) - return result_index, result - - -def do_inner_join_multi(a, b, av, bv): - n, ak = av.shape - _, bk = bv.shape - result_index, rindexer, lindexer = lib.inner_join_indexer(a, b) - result = np.empty((len(result_index), ak + bk), dtype=np.float64) - lib.take_join_contiguous(av, bv, lindexer, rindexer, result) - # result = np.empty((ak + bk, len(result_index)), dtype=np.float64) - # lib.take_axis0(av, rindexer, out=result[:ak].T) - # lib.take_axis0(bv, lindexer, out=result[ak:].T) - return result_index, result - - -def do_left_join_multi_v2(a, b, av, bv): - indexer, mask = lib.ordered_left_join_int64(a, b) - bv_taken = bv.take(indexer, axis=0) - np.putmask(bv_taken, mask.repeat(bv.shape[1]), np.nan) - return np.concatenate((av, bv_taken), axis=1) - - -def do_left_join_series(a, b): - return b.reindex(a.index) - - -def do_left_join_frame(a, b): - a.index._indexMap = None - b.index._indexMap = None - return a.join(b, how='left') - - -# a = np.array([1, 2, 3, 4, 5], dtype=np.int64) -# b = np.array([0, 3, 5, 7, 9], dtype=np.int64) -# print(lib.inner_join_indexer(a, b)) - -out = np.empty((10, 120000)) - - -def join(a, b, av, bv, how="left"): - func_dict = {'left': do_left_join_multi, - 'outer': do_outer_join_multi, - 'inner': do_inner_join_multi} - - f = func_dict[how] - return f(a, b, av, bv) - - -def bench_python(n=100000, pct_overlap=0.20, K=1): - import gc - ns = [2, 3, 4, 5, 6] - iterations = 200 - pct_overlap = 0.2 - kinds = ['outer', 'left', 'inner'] - - all_results = {} - for logn in ns: - n = 10 ** logn - a = np.arange(n, dtype=np.int64) - b = np.arange(n * pct_overlap, n * pct_overlap + n, dtype=np.int64) - - avf = np.random.randn(n, K) - bvf = np.random.randn(n, K) - - a_frame = DataFrame(avf, index=a, columns=lrange(K)) - b_frame = DataFrame(bvf, index=b, columns=lrange(K, 2 * K)) - - all_results[logn] = result = {} - - for kind in kinds: - gc.disable() - elapsed = 0 - _s = time.clock() - for i in range(iterations): - if i % 10 == 0: - elapsed += time.clock() - _s - gc.collect() - _s = time.clock() - a_frame.join(b_frame, how=kind) - # join(a, b, avf, bvf, how=kind) - elapsed += time.clock() - _s - gc.enable() - result[kind] = (elapsed / iterations) * 1000 - - return DataFrame(all_results, index=kinds) - - -def bench_xts(n=100000, pct_overlap=0.20): - from pandas.rpy.common import r - r('a <- 5') - - xrng = '1:%d' % n - - start = n * pct_overlap + 1 - end = n + start - 1 - yrng = '%d:%d' % (start, end) - - r('library(xts)') - - iterations = 500 - - kinds = ['left', 'outer', 'inner'] - result = {} - for kind in kinds: - r('x <- xts(rnorm(%d), as.POSIXct(Sys.Date()) + %s)' % (n, xrng)) - r('y <- xts(rnorm(%d), as.POSIXct(Sys.Date()) + %s)' % (n, yrng)) - stmt = 'for (i in 1:%d) merge(x, y, join="%s")' % (iterations, kind) - elapsed = r('as.list(system.time(%s, gcFirst=F))$elapsed' % stmt)[0] - result[kind] = (elapsed / iterations) * 1000 - return Series(result) diff --git a/scripts/bench_join_multi.py b/scripts/bench_join_multi.py deleted file mode 100644 index b19da6a2c47d8..0000000000000 --- a/scripts/bench_join_multi.py +++ /dev/null @@ -1,32 +0,0 @@ -from pandas import * - -import numpy as np -from pandas.compat import zip, range, lzip -from pandas.util.testing import rands -import pandas._libs.lib as lib - -N = 100000 - -key1 = [rands(10) for _ in range(N)] -key2 = [rands(10) for _ in range(N)] - -zipped = lzip(key1, key2) - - -def _zip(*args): - arr = np.empty(N, dtype=object) - arr[:] = lzip(*args) - return arr - - -def _zip2(*args): - return lib.list_to_object_array(lzip(*args)) - -index = MultiIndex.from_arrays([key1, key2]) -to_join = DataFrame({'j1': np.random.randn(100000)}, index=index) - -data = DataFrame({'A': np.random.randn(500000), - 'key1': np.repeat(key1, 5), - 'key2': np.repeat(key2, 5)}) - -# data.join(to_join, on=['key1', 'key2']) diff --git a/scripts/bench_refactor.py b/scripts/bench_refactor.py deleted file mode 100644 index dafba371e995a..0000000000000 --- a/scripts/bench_refactor.py +++ /dev/null @@ -1,51 +0,0 @@ -from pandas import * -from pandas.compat import range -try: - import pandas.core.internals as internals - reload(internals) - import pandas.core.frame as frame - reload(frame) - from pandas.core.frame import DataFrame as DataMatrix -except ImportError: - pass - -N = 1000 -K = 500 - - -def horribly_unconsolidated(): - index = np.arange(N) - - df = DataMatrix(index=index) - - for i in range(K): - df[i] = float(K) - - return df - - -def bench_reindex_index(df, it=100): - new_idx = np.arange(0, N, 2) - for i in range(it): - df.reindex(new_idx) - - -def bench_reindex_columns(df, it=100): - new_cols = np.arange(0, K, 2) - for i in range(it): - df.reindex(columns=new_cols) - - -def bench_join_index(df, it=10): - left = df.reindex(index=np.arange(0, N, 2), - columns=np.arange(K // 2)) - right = df.reindex(columns=np.arange(K // 2 + 1, K)) - for i in range(it): - joined = left.join(right) - -if __name__ == '__main__': - df = horribly_unconsolidated() - left = df.reindex(index=np.arange(0, N, 2), - columns=np.arange(K // 2)) - right = df.reindex(columns=np.arange(K // 2 + 1, K)) - bench_join_index(df) diff --git a/scripts/boxplot_test.py b/scripts/boxplot_test.py deleted file mode 100644 index 3704f7b60dc60..0000000000000 --- a/scripts/boxplot_test.py +++ /dev/null @@ -1,14 +0,0 @@ -import matplotlib.pyplot as plt - -import random -import pandas.util.testing as tm -tm.N = 1000 -df = tm.makeTimeDataFrame() -import string -foo = list(string.letters[:5]) * 200 -df['indic'] = list(string.letters[:5]) * 200 -random.shuffle(foo) -df['indic2'] = foo -df.boxplot(by=['indic', 'indic2'], fontsize=8, rot=90) - -plt.show() diff --git a/scripts/build_dist.sh b/scripts/build_dist.sh index c9c36c18bed9c..c3f849ce7a6eb 100755 --- a/scripts/build_dist.sh +++ b/scripts/build_dist.sh @@ -10,9 +10,7 @@ read -p "Ok to continue (y/n)? " answer case ${answer:0:1} in y|Y ) echo "Building distribution" - python setup.py clean - python setup.py build_ext --inplace - python setup.py sdist --formats=gztar + ./build_dist_for_release.sh ;; * ) echo "Not building distribution" diff --git a/scripts/build_dist_for_release.sh b/scripts/build_dist_for_release.sh new file mode 100755 index 0000000000000..bee0f23a68ec2 --- /dev/null +++ b/scripts/build_dist_for_release.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# this requires cython to be installed + +# this builds the release cleanly & is building on the current checkout +rm -rf dist +git clean -xfd +python setup.py clean --quiet +python setup.py cython --quiet +python setup.py sdist --formats=gztar --quiet diff --git a/scripts/count_code.sh b/scripts/count_code.sh deleted file mode 100755 index 991faf2e8711b..0000000000000 --- a/scripts/count_code.sh +++ /dev/null @@ -1 +0,0 @@ -cloc pandas --force-lang=Python,pyx --not-match-f="parser.c|lib.c|tslib.c|sandbox.c|hashtable.c|sparse.c|algos.c|index.c" \ No newline at end of file diff --git a/scripts/download_wheels.py b/scripts/download_wheels.py new file mode 100644 index 0000000000000..f5cdbbe36d90d --- /dev/null +++ b/scripts/download_wheels.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +"""Fetch wheels from wheels.scipy.org for a pandas version.""" +import argparse +import pathlib +import sys +import urllib.parse +import urllib.request + +from lxml import html + + +def parse_args(args=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", type=str, help="Pandas version (0.23.0)") + return parser.parse_args(args) + + +def fetch(version): + base = 'http://wheels.scipy.org' + tree = html.parse(base) + root = tree.getroot() + + dest = pathlib.Path('dist') + dest.mkdir(exist_ok=True) + + files = [x for x in root.xpath("//a/text()") + if x.startswith('pandas-{}'.format(version)) + and not dest.joinpath(x).exists()] + + N = len(files) + + for i, filename in enumerate(files, 1): + out = str(dest.joinpath(filename)) + link = urllib.request.urljoin(base, filename) + urllib.request.urlretrieve(link, out) + print("Downloaded {link} to {out} [{i}/{N}]".format( + link=link, out=out, i=i, N=N + )) + + +def main(args=None): + args = parse_args(args) + fetch(args.version) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/faster_xs.py b/scripts/faster_xs.py deleted file mode 100644 index 2bb6271124c4f..0000000000000 --- a/scripts/faster_xs.py +++ /dev/null @@ -1,15 +0,0 @@ -import numpy as np - -import pandas.util.testing as tm - -from pandas.core.internals import _interleaved_dtype - -df = tm.makeDataFrame() - -df['E'] = 'foo' -df['F'] = 'foo' -df['G'] = 2 -df['H'] = df['A'] > 0 - -blocks = df._data.blocks -items = df.columns diff --git a/scripts/file_sizes.py b/scripts/file_sizes.py deleted file mode 100644 index de03c72ffbd09..0000000000000 --- a/scripts/file_sizes.py +++ /dev/null @@ -1,208 +0,0 @@ -from __future__ import print_function -import os -import sys - -import numpy as np -import matplotlib.pyplot as plt - -from pandas import DataFrame -from pandas.util.testing import set_trace -from pandas import compat - -dirs = [] -names = [] -lengths = [] - -if len(sys.argv) > 1: - loc = sys.argv[1] -else: - loc = '.' -walked = os.walk(loc) - - -def _should_count_file(path): - return path.endswith('.py') or path.endswith('.pyx') - - -def _is_def_line(line): - """def/cdef/cpdef, but not `cdef class`""" - return (line.endswith(':') and not 'class' in line.split() and - (line.startswith('def ') or - line.startswith('cdef ') or - line.startswith('cpdef ') or - ' def ' in line or ' cdef ' in line or ' cpdef ' in line)) - - -class LengthCounter(object): - """ - should add option for subtracting nested function lengths?? - """ - def __init__(self, lines): - self.lines = lines - self.pos = 0 - self.counts = [] - self.n = len(lines) - - def get_counts(self): - self.pos = 0 - self.counts = [] - while self.pos < self.n: - line = self.lines[self.pos] - self.pos += 1 - if _is_def_line(line): - level = _get_indent_level(line) - self._count_function(indent_level=level) - return self.counts - - def _count_function(self, indent_level=1): - indent = ' ' * indent_level - - def _end_of_function(line): - return (line != '' and - not line.startswith(indent) and - not line.startswith('#')) - - start_pos = self.pos - while self.pos < self.n: - line = self.lines[self.pos] - if _end_of_function(line): - self._push_count(start_pos) - return - - self.pos += 1 - - if _is_def_line(line): - self._count_function(indent_level=indent_level + 1) - - # end of file - self._push_count(start_pos) - - def _push_count(self, start_pos): - func_lines = self.lines[start_pos:self.pos] - - if len(func_lines) > 300: - set_trace() - - # remove blank lines at end - while len(func_lines) > 0 and func_lines[-1] == '': - func_lines = func_lines[:-1] - - # remove docstrings and comments - clean_lines = [] - in_docstring = False - for line in func_lines: - line = line.strip() - if in_docstring and _is_triplequote(line): - in_docstring = False - continue - - if line.startswith('#'): - continue - - if _is_triplequote(line): - in_docstring = True - continue - - self.counts.append(len(func_lines)) - - -def _get_indent_level(line): - level = 0 - while line.startswith(' ' * level): - level += 1 - return level - - -def _is_triplequote(line): - return line.startswith('"""') or line.startswith("'''") - - -def _get_file_function_lengths(path): - lines = [x.rstrip() for x in open(path).readlines()] - counter = LengthCounter(lines) - return counter.get_counts() - -# def test_get_function_lengths(): -text = """ -class Foo: - -def foo(): - def bar(): - a = 1 - - b = 2 - - c = 3 - - foo = 'bar' - -def x(): - a = 1 - - b = 3 - - c = 7 - - pass -""" - -expected = [5, 8, 7] - -lines = [x.rstrip() for x in text.splitlines()] -counter = LengthCounter(lines) -result = counter.get_counts() -assert(result == expected) - - -def doit(): - for directory, _, files in walked: - print(directory) - for path in files: - if not _should_count_file(path): - continue - - full_path = os.path.join(directory, path) - print(full_path) - lines = len(open(full_path).readlines()) - - dirs.append(directory) - names.append(path) - lengths.append(lines) - - result = DataFrame({'dirs': dirs, 'names': names, - 'lengths': lengths}) - - -def doit2(): - counts = {} - for directory, _, files in walked: - print(directory) - for path in files: - if not _should_count_file(path) or path.startswith('test_'): - continue - - full_path = os.path.join(directory, path) - counts[full_path] = _get_file_function_lengths(full_path) - - return counts - -counts = doit2() - -# counts = _get_file_function_lengths('pandas/tests/test_series.py') - -all_counts = [] -for k, v in compat.iteritems(counts): - all_counts.extend(v) -all_counts = np.array(all_counts) - -fig = plt.figure(figsize=(10, 5)) -ax = fig.add_subplot(111) -ax.hist(all_counts, bins=100) -n = len(all_counts) -nmore = (all_counts > 50).sum() -ax.set_title('%s function lengths, n=%d' % ('pandas', n)) -ax.set_ylabel('N functions') -ax.set_xlabel('Function length') -ax.text(100, 300, '%.3f%% with > 50 lines' % ((n - nmore) / float(n)), - fontsize=18) -plt.show() diff --git a/scripts/find_commits_touching_func.py b/scripts/find_commits_touching_func.py index 099761f38bb44..a4583155b1bde 100755 --- a/scripts/find_commits_touching_func.py +++ b/scripts/find_commits_touching_func.py @@ -1,135 +1,148 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -# copryright 2013, y-p @ github - -from __future__ import print_function -from pandas.compat import range, lrange, map - -"""Search the git history for all commits touching a named method +# copyright 2013, y-p @ github +""" +Search the git history for all commits touching a named method You need the sh module to run this -WARNING: this script uses git clean -f, running it on a repo with untracked files -will probably erase them. +WARNING: this script uses git clean -f, running it on a repo with untracked +files will probably erase them. + +Usage:: + $ ./find_commits_touching_func.py (see arguments below) """ +from __future__ import print_function import logging import re import os +import argparse from collections import namedtuple -from pandas.compat import parse_date - +from pandas.compat import lrange, map, string_types, text_type, parse_date try: import sh except ImportError: - raise ImportError("The 'sh' package is required in order to run this script. ") + raise ImportError("The 'sh' package is required to run this script.") -import argparse desc = """ -Find all commits touching a sepcified function across the codebase. +Find all commits touching a specified function across the codebase. """.strip() argparser = argparse.ArgumentParser(description=desc) argparser.add_argument('funcname', metavar='FUNCNAME', - help='Name of function/method to search for changes on.') + help='Name of function/method to search for changes on') argparser.add_argument('-f', '--file-masks', metavar='f_re(,f_re)*', - default=["\.py.?$"], - help='comma seperated list of regexes to match filenames against\n'+ - 'defaults all .py? files') + default=[r"\.py.?$"], + help='comma separated list of regexes to match ' + 'filenames against\ndefaults all .py? files') argparser.add_argument('-d', '--dir-masks', metavar='d_re(,d_re)*', default=[], - help='comma seperated list of regexes to match base path against') + help='comma separated list of regexes to match base ' + 'path against') argparser.add_argument('-p', '--path-masks', metavar='p_re(,p_re)*', default=[], - help='comma seperated list of regexes to match full file path against') + help='comma separated list of regexes to match full ' + 'file path against') argparser.add_argument('-y', '--saw-the-warning', - action='store_true',default=False, - help='must specify this to run, acknowledge you realize this will erase untracked files') + action='store_true', default=False, + help='must specify this to run, acknowledge you ' + 'realize this will erase untracked files') argparser.add_argument('--debug-level', default="CRITICAL", - help='debug level of messages (DEBUG,INFO,etc...)') - + help='debug level of messages (DEBUG, INFO, etc...)') args = argparser.parse_args() lfmt = logging.Formatter(fmt='%(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M:%S' -) - + datefmt='%m-%d %H:%M:%S') shh = logging.StreamHandler() shh.setFormatter(lfmt) - -logger=logging.getLogger("findit") +logger = logging.getLogger("findit") logger.addHandler(shh) +Hit = namedtuple("Hit", "commit path") +HASH_LEN = 8 -Hit=namedtuple("Hit","commit path") -HASH_LEN=8 def clean_checkout(comm): - h,s,d = get_commit_vitals(comm) + h, s, d = get_commit_vitals(comm) if len(s) > 60: s = s[:60] + "..." - s=s.split("\n")[0] - logger.info("CO: %s %s" % (comm,s )) + s = s.split("\n")[0] + logger.info("CO: %s %s" % (comm, s)) - sh.git('checkout', comm ,_tty_out=False) + sh.git('checkout', comm, _tty_out=False) sh.git('clean', '-f') -def get_hits(defname,files=()): - cs=set() + +def get_hits(defname, files=()): + cs = set() for f in files: try: - r=sh.git('blame', '-L', '/def\s*{start}/,/def/'.format(start=defname),f,_tty_out=False) + r = sh.git('blame', + '-L', + r'/def\s*{start}/,/def/'.format(start=defname), + f, + _tty_out=False) except sh.ErrorReturnCode_128: logger.debug("no matches in %s" % f) continue lines = r.strip().splitlines()[:-1] # remove comment lines - lines = [x for x in lines if not re.search("^\w+\s*\(.+\)\s*#",x)] - hits = set(map(lambda x: x.split(" ")[0],lines)) - cs.update(set([Hit(commit=c,path=f) for c in hits])) + lines = [x for x in lines if not re.search(r"^\w+\s*\(.+\)\s*#", x)] + hits = set(map(lambda x: x.split(" ")[0], lines)) + cs.update({Hit(commit=c, path=f) for c in hits}) return cs -def get_commit_info(c,fmt,sep='\t'): - r=sh.git('log', "--format={}".format(fmt), '{}^..{}'.format(c,c),"-n","1",_tty_out=False) - return compat.text_type(r).split(sep) -def get_commit_vitals(c,hlen=HASH_LEN): - h,s,d= get_commit_info(c,'%H\t%s\t%ci',"\t") - return h[:hlen],s,parse_date(d) +def get_commit_info(c, fmt, sep='\t'): + r = sh.git('log', + "--format={}".format(fmt), + '{}^..{}'.format(c, c), + "-n", + "1", + _tty_out=False) + return text_type(r).split(sep) + -def file_filter(state,dirname,fnames): - if args.dir_masks and not any([re.search(x,dirname) for x in args.dir_masks]): +def get_commit_vitals(c, hlen=HASH_LEN): + h, s, d = get_commit_info(c, '%H\t%s\t%ci', "\t") + return h[:hlen], s, parse_date(d) + + +def file_filter(state, dirname, fnames): + if (args.dir_masks and + not any(re.search(x, dirname) for x in args.dir_masks)): return for f in fnames: - p = os.path.abspath(os.path.join(os.path.realpath(dirname),f)) - if any([re.search(x,f) for x in args.file_masks])\ - or any([re.search(x,p) for x in args.path_masks]): + p = os.path.abspath(os.path.join(os.path.realpath(dirname), f)) + if (any(re.search(x, f) for x in args.file_masks) or + any(re.search(x, p) for x in args.path_masks)): if os.path.isfile(p): state['files'].append(p) -def search(defname,head_commit="HEAD"): - HEAD,s = get_commit_vitals("HEAD")[:2] - logger.info("HEAD at %s: %s" % (HEAD,s)) + +def search(defname, head_commit="HEAD"): + HEAD, s = get_commit_vitals("HEAD")[:2] + logger.info("HEAD at %s: %s" % (HEAD, s)) done_commits = set() # allhits = set() files = [] state = dict(files=files) - os.path.walk('.',file_filter,state) + os.walk('.', file_filter, state) # files now holds a list of paths to files # seed with hits from q - allhits= set(get_hits(defname, files = files)) - q = set([HEAD]) + allhits = set(get_hits(defname, files=files)) + q = {HEAD} try: while q: - h=q.pop() + h = q.pop() clean_checkout(h) - hits = get_hits(defname, files = files) + hits = get_hits(defname, files=files) for x in hits: - prevc = get_commit_vitals(x.commit+"^")[0] + prevc = get_commit_vitals(x.commit + "^")[0] if prevc not in done_commits: q.add(prevc) allhits.update(hits) @@ -141,61 +154,63 @@ def search(defname,head_commit="HEAD"): clean_checkout(HEAD) return allhits + def pprint_hits(hits): - SUBJ_LEN=50 + SUBJ_LEN = 50 PATH_LEN = 20 - hits=list(hits) + hits = list(hits) max_p = 0 for hit in hits: - p=hit.path.split(os.path.realpath(os.curdir)+os.path.sep)[-1] - max_p=max(max_p,len(p)) + p = hit.path.split(os.path.realpath(os.curdir) + os.path.sep)[-1] + max_p = max(max_p, len(p)) if max_p < PATH_LEN: SUBJ_LEN += PATH_LEN - max_p PATH_LEN = max_p def sorter(i): - h,s,d=get_commit_vitals(hits[i].commit) - return hits[i].path,d + h, s, d = get_commit_vitals(hits[i].commit) + return hits[i].path, d - print("\nThese commits touched the %s method in these files on these dates:\n" \ - % args.funcname) - for i in sorted(lrange(len(hits)),key=sorter): + print(('\nThese commits touched the %s method in these files ' + 'on these dates:\n') % args.funcname) + for i in sorted(lrange(len(hits)), key=sorter): hit = hits[i] - h,s,d=get_commit_vitals(hit.commit) - p=hit.path.split(os.path.realpath(os.curdir)+os.path.sep)[-1] + h, s, d = get_commit_vitals(hit.commit) + p = hit.path.split(os.path.realpath(os.curdir) + os.path.sep)[-1] fmt = "{:%d} {:10} {:<%d} {:<%d}" % (HASH_LEN, SUBJ_LEN, PATH_LEN) if len(s) > SUBJ_LEN: - s = s[:SUBJ_LEN-5] + " ..." - print(fmt.format(h[:HASH_LEN],d.isoformat()[:10],s,p[-20:]) ) + s = s[:SUBJ_LEN - 5] + " ..." + print(fmt.format(h[:HASH_LEN], d.isoformat()[:10], s, p[-20:])) print("\n") + def main(): if not args.saw_the_warning: argparser.print_help() print(""" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -WARNING: this script uses git clean -f, running it on a repo with untracked files. +WARNING: +this script uses git clean -f, running it on a repo with untracked files. It's recommended that you make a fresh clone and run from its root directory. You must specify the -y argument to ignore this warning. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! """) return - if isinstance(args.file_masks,compat.string_types): + if isinstance(args.file_masks, string_types): args.file_masks = args.file_masks.split(',') - if isinstance(args.path_masks,compat.string_types): + if isinstance(args.path_masks, string_types): args.path_masks = args.path_masks.split(',') - if isinstance(args.dir_masks,compat.string_types): + if isinstance(args.dir_masks, string_types): args.dir_masks = args.dir_masks.split(',') - logger.setLevel(getattr(logging,args.debug_level)) + logger.setLevel(getattr(logging, args.debug_level)) - hits=search(args.funcname) + hits = search(args.funcname) pprint_hits(hits) - pass if __name__ == "__main__": import sys diff --git a/scripts/find_undoc_args.py b/scripts/find_undoc_args.py deleted file mode 100755 index 49273bacccf98..0000000000000 --- a/scripts/find_undoc_args.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import print_function - -from collections import namedtuple -from itertools import islice -import types -import os -import re -import argparse -#http://docs.python.org/2/library/argparse.html -# arg name is positional is not prefixed with - or -- - -parser = argparse.ArgumentParser(description='Program description.') -parser.add_argument('-p', '--path', metavar='PATH', type=str, required=False, - default=None, - help='full path relative to which paths wills be reported',action='store') -parser.add_argument('-m', '--module', metavar='MODULE', type=str,required=True, - help='name of package to import and examine',action='store') -parser.add_argument('-G', '--github_repo', metavar='REPO', type=str,required=False, - help='github project where the the code lives, e.g. "pandas-dev/pandas"', - default=None,action='store') - -args = parser.parse_args() - -Entry=namedtuple("Entry","func path lnum undoc_names missing_args nsig_names ndoc_names") - -def entry_gen(root_ns,module_name): - - q=[root_ns] - seen=set() - while q: - ns = q.pop() - for x in dir(ns): - cand = getattr(ns,x) - if (isinstance(cand,types.ModuleType) - and cand.__name__ not in seen - and cand.__name__.startswith(module_name)): - # print(cand.__name__) - seen.add(cand.__name__) - q.insert(0,cand) - elif (isinstance(cand,(types.MethodType,types.FunctionType)) and - cand not in seen and cand.__doc__): - seen.add(cand) - yield cand - -def cmp_docstring_sig(f): - def build_loc(f): - path=f.__code__.co_filename.split(args.path,1)[-1][1:] - return dict(path=path,lnum=f.__code__.co_firstlineno) - - import inspect - sig_names=set(inspect.getargspec(f).args) - doc = f.__doc__.lower() - doc = re.split("^\s*parameters\s*",doc,1,re.M)[-1] - doc = re.split("^\s*returns*",doc,1,re.M)[0] - doc_names={x.split(":")[0].strip() for x in doc.split("\n") - if re.match("\s+[\w_]+\s*:",x)} - sig_names.discard("self") - doc_names.discard("kwds") - doc_names.discard("kwargs") - doc_names.discard("args") - return Entry(func=f,path=build_loc(f)['path'],lnum=build_loc(f)['lnum'], - undoc_names=sig_names.difference(doc_names), - missing_args=doc_names.difference(sig_names),nsig_names=len(sig_names), - ndoc_names=len(doc_names)) - -def format_id(i): - return i - -def format_item_as_github_task_list( i,item,repo): - tmpl = "- [ ] {id}) [{file}:{lnum} ({func_name}())]({link}) - __Missing__[{nmissing}/{total_args}]: {undoc_names}" - - link_tmpl = "https://github.com/{repo}/blob/master/{file}#L{lnum}" - - link = link_tmpl.format(repo=repo,file=item.path ,lnum=item.lnum ) - - s = tmpl.format(id=i,file=item.path , - lnum=item.lnum, - func_name=item.func.__name__, - link=link, - nmissing=len(item.undoc_names), - total_args=item.nsig_names, - undoc_names=list(item.undoc_names)) - - if item.missing_args: - s+= " __Extra__(?): {missing_args}".format(missing_args=list(item.missing_args)) - - return s - -def format_item_as_plain(i,item): - tmpl = "+{lnum} {path} {func_name}(): Missing[{nmissing}/{total_args}]={undoc_names}" - - s = tmpl.format(path=item.path , - lnum=item.lnum, - func_name=item.func.__name__, - nmissing=len(item.undoc_names), - total_args=item.nsig_names, - undoc_names=list(item.undoc_names)) - - if item.missing_args: - s+= " Extra(?)={missing_args}".format(missing_args=list(item.missing_args)) - - return s - -def main(): - module = __import__(args.module) - if not args.path: - args.path=os.path.dirname(module.__file__) - collect=[cmp_docstring_sig(e) for e in entry_gen(module,module.__name__)] - # only include if there are missing arguments in the docstring (fewer false positives) - # and there are at least some documented arguments - collect = [e for e in collect if e.undoc_names and len(e.undoc_names) != e.nsig_names] - collect.sort(key=lambda x:x.path) - - if args.github_repo: - for i,item in enumerate(collect,1): - print( format_item_as_github_task_list(i,item,args.github_repo)) - else: - for i,item in enumerate(collect,1): - print( format_item_as_plain(i, item)) - -if __name__ == "__main__": - import sys - sys.exit(main()) diff --git a/scripts/gen_release_notes.py b/scripts/gen_release_notes.py deleted file mode 100644 index 7e4ffca59a0ab..0000000000000 --- a/scripts/gen_release_notes.py +++ /dev/null @@ -1,95 +0,0 @@ -from __future__ import print_function -import sys -import json -from pandas.io.common import urlopen -from datetime import datetime - - -class Milestone(object): - - def __init__(self, title, number): - self.title = title - self.number = number - - def __eq__(self, other): - if isinstance(other, Milestone): - return self.number == other.number - return False - - -class Issue(object): - - def __init__(self, title, labels, number, milestone, body, state): - self.title = title - self.labels = set([x['name'] for x in labels]) - self.number = number - self.milestone = milestone - self.body = body - self.closed = state == 'closed' - - def __eq__(self, other): - if isinstance(other, Issue): - return self.number == other.number - return False - - -def get_issues(): - all_issues = [] - page_number = 1 - while True: - iss = _get_page(page_number) - if len(iss) == 0: - break - page_number += 1 - all_issues.extend(iss) - return all_issues - - -def _get_page(page_number): - gh_url = ('https://api.github.com/repos/pandas-dev/pandas/issues?' - 'milestone=*&state=closed&assignee=*&page=%d') % page_number - with urlopen(gh_url) as resp: - rs = resp.readlines()[0] - jsondata = json.loads(rs) - issues = [Issue(x['title'], x['labels'], x['number'], - get_milestone(x['milestone']), x['body'], x['state']) - for x in jsondata] - return issues - - -def get_milestone(data): - if data is None: - return None - return Milestone(data['title'], data['number']) - - -def collate_label(issues, label): - lines = [] - for x in issues: - if label in x.labels: - lines.append('\t- %s(#%d)' % (x.title, x.number)) - - return '\n'.join(lines) - - -def release_notes(milestone): - issues = get_issues() - - headers = ['New Features', 'Improvements to existing features', - 'API Changes', 'Bug fixes'] - labels = ['New', 'Enhancement', 'API-Change', 'Bug'] - - rs = 'pandas %s' % milestone - rs += '\n' + ('=' * len(rs)) - rs += '\n\n **Release date:** %s' % datetime.today().strftime('%B %d, %Y') - for i, h in enumerate(headers): - rs += '\n\n**%s**\n\n' % h - l = labels[i] - rs += collate_label(issues, l) - - return rs - -if __name__ == '__main__': - - rs = release_notes(sys.argv[1]) - print(rs) diff --git a/scripts/generate_pip_deps_from_conda.py b/scripts/generate_pip_deps_from_conda.py new file mode 100755 index 0000000000000..7b6eb1f9a32b5 --- /dev/null +++ b/scripts/generate_pip_deps_from_conda.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +""" +Convert the conda environment.yml to the pip requirements-dev.txt, +or check that they have the same packages (for the CI) + +Usage: + + Generate `requirements-dev.txt` + $ ./conda_to_pip + + Compare and fail (exit status != 0) if `requirements-dev.txt` has not been + generated with this script: + $ ./conda_to_pip --compare +""" +import argparse +import os +import re +import sys +import yaml + + +EXCLUDE = {'python=3'} +RENAME = {'pytables': 'tables'} + + +def conda_package_to_pip(package): + """ + Convert a conda package to its pip equivalent. + + In most cases they are the same, those are the exceptions: + - Packages that should be excluded (in `EXCLUDE`) + - Packages that should be renamed (in `RENAME`) + - A package requiring a specific version, in conda is defined with a single + equal (e.g. ``pandas=1.0``) and in pip with two (e.g. ``pandas==1.0``) + """ + if package in EXCLUDE: + return + + package = re.sub('(?<=[^<>])=', '==', package).strip() + for compare in ('<=', '>=', '=='): + if compare not in package: + continue + + pkg, version = package.split(compare) + + if pkg in RENAME: + return ''.join((RENAME[pkg], compare, version)) + + break + + return package + + +def main(conda_fname, pip_fname, compare=False): + """ + Generate the pip dependencies file from the conda file, or compare that + they are synchronized (``compare=True``). + + Parameters + ---------- + conda_fname : str + Path to the conda file with dependencies (e.g. `environment.yml`). + pip_fname : str + Path to the pip file with dependencies (e.g. `requirements-dev.txt`). + compare : bool, default False + Whether to generate the pip file (``False``) or to compare if the + pip file has been generated with this script and the last version + of the conda file (``True``). + + Returns + ------- + bool + True if the comparison fails, False otherwise + """ + with open(conda_fname) as conda_fd: + deps = yaml.safe_load(conda_fd)['dependencies'] + + pip_deps = [] + for dep in deps: + if isinstance(dep, str): + conda_dep = conda_package_to_pip(dep) + if conda_dep: + pip_deps.append(conda_dep) + elif isinstance(dep, dict) and len(dep) == 1 and 'pip' in dep: + pip_deps += dep['pip'] + else: + raise ValueError('Unexpected dependency {}'.format(dep)) + + pip_content = '\n'.join(pip_deps) + + if compare: + with open(pip_fname) as pip_fd: + return pip_content != pip_fd.read() + else: + with open(pip_fname, 'w') as pip_fd: + pip_fd.write(pip_content) + return False + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser( + description='convert (or compare) conda file to pip') + argparser.add_argument('--compare', + action='store_true', + help='compare whether the two files are equivalent') + argparser.add_argument('--azure', + action='store_true', + help='show the output in azure-pipelines format') + args = argparser.parse_args() + + repo_path = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) + res = main(os.path.join(repo_path, 'environment.yml'), + os.path.join(repo_path, 'requirements-dev.txt'), + compare=args.compare) + if res: + msg = ('`requirements-dev.txt` has to be generated with `{}` after ' + '`environment.yml` is modified.\n'.format(sys.argv[0])) + if args.azure: + msg = ('##vso[task.logissue type=error;' + 'sourcepath=requirements-dev.txt]{}'.format(msg)) + sys.stderr.write(msg) + sys.exit(res) diff --git a/scripts/git-mrb b/scripts/git-mrb deleted file mode 100644 index c15e6dbf9f51a..0000000000000 --- a/scripts/git-mrb +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -"""git-mrb: merge remote branch. - -git mrb [remote:branch OR remote-branch] [onto] [upstream] - -remote must be locally available, and branch must exist in that remote. - -If 'onto' branch isn't given, default is 'master'. - -If 'upstream' repository isn't given, default is 'origin'. - -You can separate the remote and branch spec with either a : or a -. - -Taken from IPython project -""" -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -from subprocess import check_call -import sys - -#----------------------------------------------------------------------------- -# Functions -#----------------------------------------------------------------------------- - -def sh(cmd): - cmd = cmd.format(**shvars) - print('$', cmd) - check_call(cmd, shell=True) - -#----------------------------------------------------------------------------- -# Main Script -#----------------------------------------------------------------------------- - -argv = sys.argv[1:] -narg = len(argv) - -try: - branch_spec = argv[0] - sep = ':' if ':' in branch_spec else '-' - remote, branch = branch_spec.split(':', 1) - if not branch: - raise ValueError('Branch spec %s invalid, branch not found' % - branch_spec) -except: - import traceback as tb - tb.print_exc() - print(__doc__) - sys.exit(1) - -onto = argv[1] if narg >= 2 else 'master' -upstream = argv[1] if narg == 3 else 'origin' - -# Git doesn't like ':' in branch names. -if sep == ':': - branch_spec = branch_spec.replace(':', '-') - -# Global used by sh -shvars = dict(remote=remote, branch_spec=branch_spec, branch=branch, - onto=onto, upstream=upstream) - -# Start git calls. -sh('git fetch {remote}') -sh('git checkout -b {branch_spec} {onto}') -sh('git merge {remote}/{branch}') - -print(""" -************************************************************* - Run test suite. If tests pass, run the following to merge: - -git checkout {onto} -git merge {branch_spec} -git push {upstream} {onto} - -************************************************************* -""".format(**shvars)) - -ans = raw_input("Revert to master and delete temporary branch? [Y/n]: ") -if ans.strip().lower() in ('', 'y', 'yes'): - sh('git checkout {onto}') - sh('git branch -D {branch_spec}') \ No newline at end of file diff --git a/scripts/git_code_churn.py b/scripts/git_code_churn.py deleted file mode 100644 index 18c9b244a6ba0..0000000000000 --- a/scripts/git_code_churn.py +++ /dev/null @@ -1,34 +0,0 @@ -import subprocess -import os -import re -import sys - -import numpy as np - -from pandas import * - - -if __name__ == '__main__': - from vbench.git import GitRepo - repo = GitRepo('/Users/wesm/code/pandas') - churn = repo.get_churn_by_file() - - file_include = [] - for path in churn.major_axis: - if path.endswith('.pyx') or path.endswith('.py'): - file_include.append(path) - commits_include = [sha for sha in churn.minor_axis - if 'LF' not in repo.messages[sha]] - commits_include.remove('dcf3490') - - clean_churn = churn.reindex(major=file_include, minor=commits_include) - - by_commit = clean_churn.sum('major').sum(1) - - by_date = by_commit.groupby(repo.commit_date).sum() - - by_date = by_date.drop([datetime(2011, 6, 10)]) - - # clean out days where I touched Cython - - by_date = by_date[by_date < 5000] diff --git a/scripts/groupby_sample.py b/scripts/groupby_sample.py deleted file mode 100644 index 42008858d3cad..0000000000000 --- a/scripts/groupby_sample.py +++ /dev/null @@ -1,54 +0,0 @@ -from pandas import * -import numpy as np -import string -import pandas.compat as compat - -g1 = np.array(list(string.letters))[:-1] -g2 = np.arange(510) -df_small = DataFrame({'group1': ["a", "b", "a", "a", "b", "c", "c", "c", "c", - "c", "a", "a", "a", "b", "b", "b", "b"], - 'group2': [1, 2, 3, 4, 1, 3, 5, 6, 5, 4, 1, 2, 3, 4, 3, 2, 1], - 'value': ["apple", "pear", "orange", "apple", - "banana", "durian", "lemon", "lime", - "raspberry", "durian", "peach", "nectarine", - "banana", "lemon", "guava", "blackberry", - "grape"]}) -value = df_small['value'].values.repeat(3) -df = DataFrame({'group1': g1.repeat(4000 * 5), - 'group2': np.tile(g2, 400 * 5), - 'value': value.repeat(4000 * 5)}) - - -def random_sample(): - grouped = df.groupby(['group1', 'group2'])['value'] - from random import choice - choose = lambda group: choice(group.index) - indices = grouped.apply(choose) - return df.reindex(indices) - - -def random_sample_v2(): - grouped = df.groupby(['group1', 'group2'])['value'] - from random import choice - choose = lambda group: choice(group.index) - indices = [choice(v) for k, v in compat.iteritems(grouped.groups)] - return df.reindex(indices) - - -def do_shuffle(arr): - from random import shuffle - result = arr.copy().values - shuffle(result) - return result - - -def shuffle_uri(df, grouped): - perm = np.r_[tuple([np.random.permutation( - idxs) for idxs in compat.itervalues(grouped.groups)])] - df['state_permuted'] = np.asarray(df.ix[perm]['value']) - -df2 = df.copy() -grouped = df2.groupby('group1') -shuffle_uri(df2, grouped) - -df2['state_perm'] = grouped['value'].transform(do_shuffle) diff --git a/scripts/groupby_speed.py b/scripts/groupby_speed.py deleted file mode 100644 index 3be9fac12418e..0000000000000 --- a/scripts/groupby_speed.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import print_function -from pandas import * - -rng = DatetimeIndex('1/3/2011', '11/30/2011', offset=offsets.Minute()) - -df = DataFrame(np.random.randn(len(rng), 5), index=rng, - columns=list('OHLCV')) - -rng5 = DatetimeIndex('1/3/2011', '11/30/2011', offset=offsets.Minute(5)) -gp = rng5.asof -grouped = df.groupby(gp) - - -def get1(dt): - k = gp(dt) - return grouped.get_group(k) - - -def get2(dt): - k = gp(dt) - return df.ix[grouped.groups[k]] - - -def f(): - for i, date in enumerate(df.index): - if i % 10000 == 0: - print(i) - get1(date) - - -def g(): - for i, date in enumerate(df.index): - if i % 10000 == 0: - print(i) - get2(date) diff --git a/scripts/groupby_test.py b/scripts/groupby_test.py deleted file mode 100644 index f640a6ed79503..0000000000000 --- a/scripts/groupby_test.py +++ /dev/null @@ -1,145 +0,0 @@ -from collections import defaultdict - -from numpy import nan -import numpy as np - -from pandas import * - -import pandas._libs.lib as tseries -import pandas.core.groupby as gp -import pandas.util.testing as tm -from pandas.compat import range -reload(gp) - -""" - -k = 1000 -values = np.random.randn(8 * k) -key1 = np.array(['foo', 'bar', 'baz', 'bar', 'foo', 'baz', 'bar', 'baz'] * k, - dtype=object) -key2 = np.array(['b', 'b', 'b', 'b', 'a', 'a', 'a', 'a' ] * k, - dtype=object) -shape, labels, idicts = gp.labelize(key1, key2) - -print(tseries.group_labels(key1)) - -# print(shape) -# print(labels) -# print(idicts) - -result = tseries.group_aggregate(values, labels, shape) - -print(tseries.groupby_indices(key2)) - -df = DataFrame({'key1' : key1, - 'key2' : key2, - 'v1' : values, - 'v2' : values}) -k1 = df['key1'] -k2 = df['key2'] - -# del df['key1'] -# del df['key2'] - -# r2 = gp.multi_groupby(df, np.sum, k1, k2) - -# print(result) - -gen = gp.generate_groups(df['v1'], labels, shape, axis=1, - factory=DataFrame) - -res = defaultdict(dict) -for a, gen1 in gen: - for b, group in gen1: - print(a, b) - print(group) - # res[b][a] = group['values'].sum() - res[b][a] = group.sum() - -res = DataFrame(res) - -grouped = df.groupby(['key1', 'key2']) -""" - -# data = {'A' : [0, 0, 0, 0, 1, 1, 1, 1, 1, 1., nan, nan], -# 'B' : ['A', 'B'] * 6, -# 'C' : np.random.randn(12)} -# df = DataFrame(data) -# df['C'][2:10:2] = nan - -# single column -# grouped = df.drop(['B'], axis=1).groupby('A') -# exp = {} -# for cat, group in grouped: -# exp[cat] = group['C'].sum() -# exp = DataFrame({'C' : exp}) -# result = grouped.sum() - -# grouped = df.groupby(['A', 'B']) -# expd = {} -# for cat1, cat2, group in grouped: -# expd.setdefault(cat1, {})[cat2] = group['C'].sum() -# exp = DataFrame(expd).T.stack() -# result = grouped.sum()['C'] - -# print('wanted') -# print(exp) -# print('got') -# print(result) - -# tm.N = 10000 - -# mapping = {'A': 0, 'C': 1, 'B': 0, 'D': 1} -# tf = lambda x: x - x.mean() - -# df = tm.makeTimeDataFrame() -# ts = df['A'] - -# # grouped = df.groupby(lambda x: x.strftime('%m/%y')) -# grouped = df.groupby(mapping, axis=1) -# groupedT = df.T.groupby(mapping, axis=0) - -# r1 = groupedT.transform(tf).T -# r2 = grouped.transform(tf) - -# fillit = lambda x: x.fillna(method='pad') - -# f = lambda x: x - -# transformed = df.groupby(lambda x: x.strftime('%m/%y')).transform(lambda -# x: x) - -# def ohlc(group): -# return Series([group[0], group.max(), group.min(), group[-1]], -# index=['open', 'high', 'low', 'close']) -# grouper = [lambda x: x.year, lambda x: x.month] -# dr = DateRange('1/1/2000', '1/1/2002') -# ts = Series(np.random.randn(len(dr)), index=dr) - -# import string - -# k = 20 -# n = 1000 - -# keys = list(string.letters[:k]) - -# df = DataFrame({'A' : np.tile(keys, n), -# 'B' : np.repeat(keys[:k/2], n * 2), -# 'C' : np.random.randn(k * n)}) - -# def f(): -# for x in df.groupby(['A', 'B']): -# pass - -a = np.arange(100).repeat(100) -b = np.tile(np.arange(100), 100) -index = MultiIndex.from_arrays([a, b]) -s = Series(np.random.randn(len(index)), index) -df = DataFrame({'A': s}) -df['B'] = df.index.get_level_values(0) -df['C'] = df.index.get_level_values(1) - - -def f(): - for x in df.groupby(['B', 'B']): - pass diff --git a/scripts/hdfstore_panel_perf.py b/scripts/hdfstore_panel_perf.py deleted file mode 100644 index c66e9506fc4c5..0000000000000 --- a/scripts/hdfstore_panel_perf.py +++ /dev/null @@ -1,17 +0,0 @@ -from pandas import * -from pandas.util.testing import rands -from pandas.compat import range - -i, j, k = 7, 771, 5532 - -panel = Panel(np.random.randn(i, j, k), - items=[rands(10) for _ in range(i)], - major_axis=DatetimeIndex('1/1/2000', periods=j, - offset=offsets.Minute()), - minor_axis=[rands(10) for _ in range(k)]) - - -store = HDFStore('test.h5') -store.put('test_panel', panel, table=True) - -retrieved = store['test_panel'] diff --git a/scripts/json_manip.py b/scripts/json_manip.py deleted file mode 100644 index 7ff4547825568..0000000000000 --- a/scripts/json_manip.py +++ /dev/null @@ -1,423 +0,0 @@ -""" - -Tasks -------- - -Search and transform jsonable structures, specifically to make it 'easy' to make tabular/csv output for other consumers. - -Example -~~~~~~~~~~~~~ - - *give me a list of all the fields called 'id' in this stupid, gnarly - thing* - - >>> Q('id',gnarly_data) - ['id1','id2','id3'] - - -Observations: ---------------------- - -1) 'simple data structures' exist and are common. They are tedious - to search. - -2) The DOM is another nested / treeish structure, and jQuery selector is - a good tool for that. - -3a) R, Numpy, Excel and other analysis tools want 'tabular' data. These - analyses are valuable and worth doing. - -3b) Dot/Graphviz, NetworkX, and some other analyses *like* treeish/dicty - things, and those analyses are also worth doing! - -3c) Some analyses are best done using 'one-off' and custom code in C, Python, - or another 'real' programming language. - -4) Arbitrary transforms are tedious and error prone. SQL is one solution, - XSLT is another, - -5) the XPATH/XML/XSLT family is.... not universally loved :) They are - very complete, and the completeness can make simple cases... gross. - -6) For really complicated data structures, we can write one-off code. Getting - 80% of the way is mostly okay. There will always have to be programmers - in the loop. - -7) Re-inventing SQL is probably a failure mode. So is reinventing XPATH, XSLT - and the like. Be wary of mission creep! Re-use when possible (e.g., can - we put the thing into a DOM using - -8) If the interface is good, people can improve performance later. - - -Simplifying ---------------- - - -1) Assuming 'jsonable' structures - -2) keys are strings or stringlike. Python allows any hashable to be a key. - for now, we pretend that doesn't happen. - -3) assumes most dicts are 'well behaved'. DAG, no cycles! - -4) assume that if people want really specialized transforms, they can do it - themselves. - -""" -from __future__ import print_function - -from collections import namedtuple -import csv -import itertools -from itertools import product -from operator import attrgetter as aget, itemgetter as iget -import operator -import sys -from pandas.compat import map, u, callable, Counter -import pandas.compat as compat - - -## note 'url' appears multiple places and not all extensions have same struct -ex1 = { - 'name': 'Gregg', - 'extensions': [ - {'id':'hello', - 'url':'url1'}, - {'id':'gbye', - 'url':'url2', - 'more': dict(url='url3')}, - ] -} - -## much longer example -ex2 = {u('metadata'): {u('accessibilities'): [{u('name'): u('accessibility.tabfocus'), - u('value'): 7}, - {u('name'): u('accessibility.mouse_focuses_formcontrol'), u('value'): False}, - {u('name'): u('accessibility.browsewithcaret'), u('value'): False}, - {u('name'): u('accessibility.win32.force_disabled'), u('value'): False}, - {u('name'): u('accessibility.typeaheadfind.startlinksonly'), u('value'): False}, - {u('name'): u('accessibility.usebrailledisplay'), u('value'): u('')}, - {u('name'): u('accessibility.typeaheadfind.timeout'), u('value'): 5000}, - {u('name'): u('accessibility.typeaheadfind.enabletimeout'), u('value'): True}, - {u('name'): u('accessibility.tabfocus_applies_to_xul'), u('value'): False}, - {u('name'): u('accessibility.typeaheadfind.flashBar'), u('value'): 1}, - {u('name'): u('accessibility.typeaheadfind.autostart'), u('value'): True}, - {u('name'): u('accessibility.blockautorefresh'), u('value'): False}, - {u('name'): u('accessibility.browsewithcaret_shortcut.enabled'), - u('value'): True}, - {u('name'): u('accessibility.typeaheadfind.enablesound'), u('value'): True}, - {u('name'): u('accessibility.typeaheadfind.prefillwithselection'), - u('value'): True}, - {u('name'): u('accessibility.typeaheadfind.soundURL'), u('value'): u('beep')}, - {u('name'): u('accessibility.typeaheadfind'), u('value'): False}, - {u('name'): u('accessibility.typeaheadfind.casesensitive'), u('value'): 0}, - {u('name'): u('accessibility.warn_on_browsewithcaret'), u('value'): True}, - {u('name'): u('accessibility.usetexttospeech'), u('value'): u('')}, - {u('name'): u('accessibility.accesskeycausesactivation'), u('value'): True}, - {u('name'): u('accessibility.typeaheadfind.linksonly'), u('value'): False}, - {u('name'): u('isInstantiated'), u('value'): True}], - u('extensions'): [{u('id'): u('216ee7f7f4a5b8175374cd62150664efe2433a31'), - u('isEnabled'): True}, - {u('id'): u('1aa53d3b720800c43c4ced5740a6e82bb0b3813e'), u('isEnabled'): False}, - {u('id'): u('01ecfac5a7bd8c9e27b7c5499e71c2d285084b37'), u('isEnabled'): True}, - {u('id'): u('1c01f5b22371b70b312ace94785f7b0b87c3dfb2'), u('isEnabled'): True}, - {u('id'): u('fb723781a2385055f7d024788b75e959ad8ea8c3'), u('isEnabled'): True}], - u('fxVersion'): u('9.0'), - u('location'): u('zh-CN'), - u('operatingSystem'): u('WINNT Windows NT 5.1'), - u('surveyAnswers'): u(''), - u('task_guid'): u('d69fbd15-2517-45b5-8a17-bb7354122a75'), - u('tpVersion'): u('1.2'), - u('updateChannel'): u('beta')}, - u('survey_data'): { - u('extensions'): [{u('appDisabled'): False, - u('id'): u('testpilot?labs.mozilla.com'), - u('isCompatible'): True, - u('isEnabled'): True, - u('isPlatformCompatible'): True, - u('name'): u('Test Pilot')}, - {u('appDisabled'): True, - u('id'): u('dict?www.youdao.com'), - u('isCompatible'): False, - u('isEnabled'): False, - u('isPlatformCompatible'): True, - u('name'): u('Youdao Word Capturer')}, - {u('appDisabled'): False, - u('id'): u('jqs?sun.com'), - u('isCompatible'): True, - u('isEnabled'): True, - u('isPlatformCompatible'): True, - u('name'): u('Java Quick Starter')}, - {u('appDisabled'): False, - u('id'): u('?20a82645-c095-46ed-80e3-08825760534b?'), - u('isCompatible'): True, - u('isEnabled'): True, - u('isPlatformCompatible'): True, - u('name'): u('Microsoft .NET Framework Assistant')}, - {u('appDisabled'): False, - u('id'): u('?a0d7ccb3-214d-498b-b4aa-0e8fda9a7bf7?'), - u('isCompatible'): True, - u('isEnabled'): True, - u('isPlatformCompatible'): True, - u('name'): u('WOT')}], - u('version_number'): 1}} - -# class SurveyResult(object): - -# def __init__(self, record): -# self.record = record -# self.metadata, self.survey_data = self._flatten_results() - -# def _flatten_results(self): -# survey_data = self.record['survey_data'] -# extensions = DataFrame(survey_data['extensions']) - -def denorm(queries,iterable_of_things,default=None): - """ - 'repeat', or 'stutter' to 'tableize' for downstream. - (I have no idea what a good word for this is!) - - Think ``kronecker`` products, or: - - ``SELECT single,multiple FROM table;`` - - single multiple - ------- --------- - id1 val1 - id1 val2 - - - Args: - - queries: iterable of ``Q`` queries. - iterable_of_things: to be queried. - - Returns: - - list of 'stuttered' output, where if a query returns - a 'single', it gets repeated appropriately. - - - """ - - def _denorm(queries,thing): - fields = [] - results = [] - for q in queries: - #print(q) - r = Ql(q,thing) - #print("-- result: ", r) - if not r: - r = [default] - if isinstance(r[0], type({})): - fields.append(sorted(r[0].keys())) # dicty answers - else: - fields.append([q]) # stringy answer - - results.append(r) - - #print(results) - #print(fields) - flist = list(flatten(*map(iter,fields))) - - prod = itertools.product(*results) - for p in prod: - U = dict() - for (ii,thing) in enumerate(p): - #print(ii,thing) - if isinstance(thing, type({})): - U.update(thing) - else: - U[fields[ii][0]] = thing - - yield U - - return list(flatten(*[_denorm(queries,thing) for thing in iterable_of_things])) - - -def default_iget(fields,default=None,): - """ itemgetter with 'default' handling, that *always* returns lists - - API CHANGES from ``operator.itemgetter`` - - Note: Sorry to break the iget api... (fields vs *fields) - Note: *always* returns a list... unlike itemgetter, - which can return tuples or 'singles' - """ - myiget = operator.itemgetter(*fields) - L = len(fields) - def f(thing): - try: - ans = list(myiget(thing)) - if L < 2: - ans = [ans,] - return ans - except KeyError: - # slower! - return [thing.get(x,default) for x in fields] - - f.__doc__ = "itemgetter with default %r for fields %r" %(default,fields) - f.__name__ = "default_itemgetter" - return f - - -def flatten(*stack): - """ - helper function for flattening iterables of generators in a - sensible way. - """ - stack = list(stack) - while stack: - try: x = next(stack[0]) - except StopIteration: - stack.pop(0) - continue - if hasattr(x,'next') and callable(getattr(x,'next')): - stack.insert(0, x) - - #if isinstance(x, (GeneratorType,listerator)): - else: yield x - - -def _Q(filter_, thing): - """ underlying machinery for Q function recursion """ - T = type(thing) - if isinstance({}, T): - for k,v in compat.iteritems(thing): - #print(k,v) - if filter_ == k: - if isinstance(v, type([])): - yield iter(v) - else: - yield v - - if type(v) in (type({}),type([])): - yield Q(filter_,v) - - elif isinstance([], T): - for k in thing: - #print(k) - yield Q(filter_,k) - - else: - # no recursion. - pass - -def Q(filter_,thing): - """ - type(filter): - - list: a flattened list of all searches (one list) - - dict: dict with vals each of which is that search - - Notes: - - [1] 'parent thing', with space, will do a descendent - [2] this will come back 'flattened' jQuery style - [3] returns a generator. Use ``Ql`` if you want a list. - - """ - if isinstance(filter_, type([])): - return flatten(*[_Q(x,thing) for x in filter_]) - elif isinstance(filter_, type({})): - d = dict.fromkeys(list(filter_.keys())) - #print(d) - for k in d: - #print(flatten(Q(k,thing))) - d[k] = Q(k,thing) - - return d - - else: - if " " in filter_: # i.e. "antecendent post" - parts = filter_.strip().split() - r = None - for p in parts: - r = Ql(p,thing) - thing = r - - return r - - else: # simple. - return flatten(_Q(filter_,thing)) - -def Ql(filter_,thing): - """ same as Q, but returns a list, not a generator """ - res = Q(filter_,thing) - - if isinstance(filter_, type({})): - for k in res: - res[k] = list(res[k]) - return res - - else: - return list(res) - - - -def countit(fields,iter_of_iter,default=None): - """ - note: robust to fields not being in i_of_i, using ``default`` - """ - C = Counter() # needs hashables - T = namedtuple("Thing",fields) - get = default_iget(*fields,default=default) - return Counter( - (T(*get(thing)) for thing in iter_of_iter) - ) - - -## right now this works for one row... -def printout(queries,things,default=None, f=sys.stdout, **kwargs): - """ will print header and objects - - **kwargs go to csv.DictWriter - - help(csv.DictWriter) for more. - """ - - results = denorm(queries,things,default=None) - fields = set(itertools.chain(*(x.keys() for x in results))) - - W = csv.DictWriter(f=f,fieldnames=fields,**kwargs) - #print("---prod---") - #print(list(prod)) - W.writeheader() - for r in results: - W.writerow(r) - - -def test_run(): - print("\n>>> print(list(Q('url',ex1)))") - print(list(Q('url',ex1))) - assert list(Q('url',ex1)) == ['url1','url2','url3'] - assert Ql('url',ex1) == ['url1','url2','url3'] - - print("\n>>> print(list(Q(['name','id'],ex1)))") - print(list(Q(['name','id'],ex1))) - assert Ql(['name','id'],ex1) == ['Gregg','hello','gbye'] - - - print("\n>>> print(Ql('more url',ex1))") - print(Ql('more url',ex1)) - - - print("\n>>> list(Q('extensions',ex1))") - print(list(Q('extensions',ex1))) - - print("\n>>> print(Ql('extensions',ex1))") - print(Ql('extensions',ex1)) - - print("\n>>> printout(['name','extensions'],[ex1,], extrasaction='ignore')") - printout(['name','extensions'],[ex1,], extrasaction='ignore') - - print("\n\n") - - from pprint import pprint as pp - - print("-- note that the extension fields are also flattened! (and N/A) -- ") - pp(denorm(['location','fxVersion','notthere','survey_data extensions'],[ex2,], default="N/A")[:2]) - - -if __name__ == "__main__": - pass diff --git a/scripts/leak.py b/scripts/leak.py deleted file mode 100644 index 47f74bf020597..0000000000000 --- a/scripts/leak.py +++ /dev/null @@ -1,13 +0,0 @@ -from pandas import * -from pandas.compat import range -import numpy as np -import pandas.util.testing as tm -import os -import psutil - -pid = os.getpid() -proc = psutil.Process(pid) - -df = DataFrame(index=np.arange(100)) -for i in range(5000): - df[i] = 5 diff --git a/scripts/list_future_warnings.sh b/scripts/list_future_warnings.sh new file mode 100755 index 0000000000000..0c4046bbb5f49 --- /dev/null +++ b/scripts/list_future_warnings.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Check all future warnings in Python files, and report them with the version +# where the FutureWarning was added. +# +# This is useful to detect features that have been deprecated, and should be +# removed from the code. For example, if a line of code contains: +# +# warning.warn('Method deprecated', FutureWarning, stacklevel=2) +# +# Which is released in Pandas 0.20.0, then it is expected that the method +# is removed before releasing Pandas 0.24.0, including the warning. If it +# is not, this script will list this line, with the version 0.20.0, which +# will make it easy to detect that it had to be removed. +# +# In some cases this script can return false positives, for example in files +# where FutureWarning is used to detect deprecations, or similar. The EXCLUDE +# variable can be used to ignore files that use FutureWarning, but do not +# deprecate functionality. +# +# Usage: +# +# $ ./list_future_warnings.sh + +EXCLUDE="^pandas/tests/|" # tests validate that FutureWarnings are raised +EXCLUDE+="^pandas/util/_decorators.py$|" # generic deprecate function that raises warning +EXCLUDE+="^pandas/util/_depr_module.py$|" # generic deprecate module that raises warnings +EXCLUDE+="^pandas/util/testing.py$|" # contains function to evaluate if warning is raised +EXCLUDE+="^pandas/io/parsers.py$" # implements generic deprecation system in io reading + +BASE_DIR="$(dirname $0)/.." +cd $BASE_DIR +FILES=`grep -RIl "FutureWarning" pandas/* | grep -vE "$EXCLUDE"` +OUTPUT=() +IFS=$'\n' + +for FILE in $FILES; do + FILE_LINES=`git blame -sf $FILE | grep FutureWarning | tr -s " " | cut -d " " -f1,3` + for FILE_LINE in $FILE_LINES; do + TAG=$(git tag --contains $(echo $FILE_LINE | cut -d" " -f1) | head -n1) + OUTPUT_ROW=`printf "%-14s %-16s %s" ${TAG:-"(not released)"} $FILE_LINE $FILE` + OUTPUT+=($OUTPUT_ROW) + done +done + +printf "%s\n" "${OUTPUT[@]}" | sort -V diff --git a/scripts/merge-py.py b/scripts/merge-pr.py similarity index 80% rename from scripts/merge-py.py rename to scripts/merge-pr.py index b9350f8feceb8..31264cad52e4f 100755 --- a/scripts/merge-py.py +++ b/scripts/merge-pr.py @@ -22,7 +22,6 @@ # usage: ./apache-pr-merge.py (see config env vars below) # # Lightly modified from version of this script in incubator-parquet-format - from __future__ import print_function from subprocess import check_output @@ -99,6 +98,14 @@ def continue_maybe(prompt): fail("Okay, exiting") +def continue_maybe2(prompt): + result = input("\n%s (y/n): " % prompt) + if result.lower() != "y": + return False + else: + return True + + original_head = run_cmd("git rev-parse HEAD")[:8] @@ -152,7 +159,7 @@ def merge_pr(pr_num, target_ref): if body is not None: merge_message_flags += ["-m", '\n'.join(textwrap.wrap(body))] - authors = "\n".join(["Author: %s" % a for a in distinct_authors]) + authors = "\n".join("Author: %s" % a for a in distinct_authors) merge_message_flags += ["-m", authors] @@ -193,6 +200,40 @@ def merge_pr(pr_num, target_ref): return merge_hash +def update_pr(pr_num, user_login, base_ref): + + pr_branch_name = "%s_MERGE_PR_%s" % (BRANCH_PREFIX, pr_num) + + run_cmd("git fetch %s pull/%s/head:%s" % (PR_REMOTE_NAME, pr_num, + pr_branch_name)) + run_cmd("git checkout %s" % pr_branch_name) + + continue_maybe("Update ready (local ref %s)? Push to %s/%s?" % ( + pr_branch_name, user_login, base_ref)) + + push_user_remote = "https://github.com/%s/pandas.git" % user_login + + try: + run_cmd('git push %s %s:%s' % (push_user_remote, pr_branch_name, + base_ref)) + except Exception as e: + + if continue_maybe2("Force push?"): + try: + run_cmd( + 'git push -f %s %s:%s' % (push_user_remote, pr_branch_name, + base_ref)) + except Exception as e: + fail("Exception while pushing: %s" % e) + clean_up() + else: + fail("Exception while pushing: %s" % e) + clean_up() + + clean_up() + print("Pull request #%s updated!" % pr_num) + + def cherry_pick(pr_num, merge_hash, default_branch): pick_ref = input("Enter a branch name [%s]: " % default_branch) if pick_ref == "": @@ -233,6 +274,7 @@ def fix_version_from_branch(branch, versions): branch_ver = branch.replace("branch-", "") return filter(lambda x: x.name.startswith(branch_ver), versions)[-1] + pr_num = input("Which pull request would you like to merge? (e.g. 34): ") pr = get_json("%s/pulls/%s" % (GITHUB_API_BASE, pr_num)) @@ -255,10 +297,25 @@ def fix_version_from_branch(branch, versions): continue_maybe(msg) print("\n=== Pull Request #%s ===" % pr_num) -print("title\t%s\nsource\t%s\ntarget\t%s\nurl\t%s" - % (title, pr_repo_desc, target_ref, url)) -continue_maybe("Proceed with merging pull request #%s?" % pr_num) + +# we may have un-printable unicode in our title +try: + title = title.encode('raw_unicode_escape') +except Exception: + pass + +print("title\t{title}\nsource\t{source}\ntarget\t{target}\nurl\t{url}".format( + title=title, source=pr_repo_desc, target=target_ref, url=url)) + merged_refs = [target_ref] -merge_hash = merge_pr(pr_num, target_ref) +print("\nProceed with updating or merging pull request #%s?" % pr_num) +update = input("Update PR and push to remote (r), merge locally (l), " + "or do nothing (n) ?") +update = update.lower() + +if update == 'r': + merge_hash = update_pr(pr_num, user_login, base_ref) +elif update == 'l': + merge_hash = merge_pr(pr_num, target_ref) diff --git a/scripts/parser_magic.py b/scripts/parser_magic.py deleted file mode 100644 index 72fef39d8db65..0000000000000 --- a/scripts/parser_magic.py +++ /dev/null @@ -1,74 +0,0 @@ -from pandas.util.testing import set_trace -import pandas.util.testing as tm -import pandas.compat as compat - -from pandas import * -import ast -import inspect -import sys - - -def merge(a, b): - f, args, _ = parse_stmt(inspect.currentframe().f_back) - return DataFrame({args[0]: a, - args[1]: b}) - - -def parse_stmt(frame): - info = inspect.getframeinfo(frame) - call = info[-2][0] - mod = ast.parse(call) - body = mod.body[0] - if isinstance(body, (ast.Assign, ast.Expr)): - call = body.value - elif isinstance(body, ast.Call): - call = body - return _parse_call(call) - - -def _parse_call(call): - func = _maybe_format_attribute(call.func) - - str_args = [] - for arg in call.args: - if isinstance(arg, ast.Name): - str_args.append(arg.id) - elif isinstance(arg, ast.Call): - formatted = _format_call(arg) - str_args.append(formatted) - - return func, str_args, {} - - -def _format_call(call): - func, args, kwds = _parse_call(call) - content = '' - if args: - content += ', '.join(args) - if kwds: - fmt_kwds = ['%s=%s' % item for item in compat.iteritems(kwds)] - joined_kwds = ', '.join(fmt_kwds) - if args: - content = content + ', ' + joined_kwds - else: - content += joined_kwds - return '%s(%s)' % (func, content) - - -def _maybe_format_attribute(name): - if isinstance(name, ast.Attribute): - return _format_attribute(name) - return name.id - - -def _format_attribute(attr): - obj = attr.value - if isinstance(attr.value, ast.Attribute): - obj = _format_attribute(attr.value) - else: - obj = obj.id - return '.'.join((obj, attr.attr)) - -a = tm.makeTimeSeries() -b = tm.makeTimeSeries() -df = merge(a, b) diff --git a/scripts/preepoch_test.py b/scripts/preepoch_test.py deleted file mode 100644 index 36a3d768e671f..0000000000000 --- a/scripts/preepoch_test.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -from pandas import * - - -def panda_test(): - - # generate some data - data = np.random.rand(50, 5) - # generate some dates - dates = DatetimeIndex('1/1/1969', periods=50) - # generate column headings - cols = ['A', 'B', 'C', 'D', 'E'] - - df = DataFrame(data, index=dates, columns=cols) - - # save to HDF5Store - store = HDFStore('bugzilla.h5', mode='w') - store['df'] = df # This gives: OverflowError: mktime argument out of range - store.close() - - -if __name__ == '__main__': - panda_test() diff --git a/scripts/pypistats.py b/scripts/pypistats.py deleted file mode 100644 index 41343f6d30c76..0000000000000 --- a/scripts/pypistats.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -Calculates the total number of downloads that a particular PyPI package has -received across all versions tracked by PyPI -""" - -from datetime import datetime -import locale -import sys -import xmlrpclib -import pandas as pd - -locale.setlocale(locale.LC_ALL, '') - - -class PyPIDownloadAggregator(object): - - def __init__(self, package_name, include_hidden=True): - self.package_name = package_name - self.include_hidden = include_hidden - self.proxy = xmlrpclib.Server('http://pypi.python.org/pypi') - self._downloads = {} - - @property - def releases(self): - """Retrieves the release number for each uploaded release""" - - result = self.proxy.package_releases(self.package_name, - self.include_hidden) - - if len(result) == 0: - # no matching package--search for possibles, and limit to 15 - # results - results = self.proxy.search({ - 'name': self.package_name, - 'description': self.package_name - }, 'or')[:15] - - # make sure we only get unique package names - matches = [] - for match in results: - name = match['name'] - if name not in matches: - matches.append(name) - - # if only one package was found, return it - if len(matches) == 1: - self.package_name = matches[0] - return self.releases - - error = """No such package found: %s - -Possible matches include: -%s -""" % (self.package_name, '\n'.join('\t- %s' % n for n in matches)) - - sys.exit(error) - - return result - - def get_downloads(self): - """Calculate the total number of downloads for the package""" - downloads = {} - for release in self.releases: - urls = self.proxy.release_urls(self.package_name, release) - urls = pd.DataFrame(urls) - urls['version'] = release - downloads[release] = urls - - return pd.concat(downloads, ignore_index=True) - -if __name__ == '__main__': - agg = PyPIDownloadAggregator('pandas') - - data = agg.get_downloads() - - to_omit = ['0.2b1', '0.2beta'] - - isostrings = data['upload_time'].map(lambda x: x.value) - data['upload_time'] = pd.to_datetime(isostrings) - - totals = data.groupby('version').downloads.sum() - rollup = {'0.8.0rc1': '0.8.0', - '0.8.0rc2': '0.8.0', - '0.3.0.beta': '0.3.0', - '0.3.0.beta2': '0.3.0'} - downloads = totals.groupby(lambda x: rollup.get(x, x)).sum() - - first_upload = data.groupby('version').upload_time.min() - - result = pd.DataFrame({'downloads': totals, - 'release_date': first_upload}) - result = result.sort('release_date') - result = result.drop(to_omit + list(rollup.keys())) - result.index.name = 'release' - - by_date = result.reset_index().set_index('release_date').downloads - dummy = pd.Series(index=pd.DatetimeIndex([datetime(2012, 12, 27)])) - by_date = by_date.append(dummy).shift(1).fillna(0) diff --git a/scripts/roll_median_leak.py b/scripts/roll_median_leak.py deleted file mode 100644 index 03f39e2b18372..0000000000000 --- a/scripts/roll_median_leak.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import print_function -from pandas import * - -import numpy as np -import os - -from vbench.api import Benchmark -from pandas.util.testing import rands -from pandas.compat import range -import pandas._libs.lib as lib -import pandas._sandbox as sbx -import time - -import psutil - -pid = os.getpid() -proc = psutil.Process(pid) - -lst = SparseList() -lst.append([5] * 10000) -lst.append(np.repeat(np.nan, 1000000)) - -for _ in range(10000): - print(proc.get_memory_info()) - sdf = SparseDataFrame({'A': lst.to_array()}) - chunk = sdf[sdf['A'] == 5] diff --git a/scripts/runtests.py b/scripts/runtests.py deleted file mode 100644 index e14752b43116b..0000000000000 --- a/scripts/runtests.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import print_function -import os -print(os.getpid()) -import nose -nose.main('pandas.core') diff --git a/scripts/test_py27.bat b/scripts/test_py27.bat deleted file mode 100644 index 11e3056287e31..0000000000000 --- a/scripts/test_py27.bat +++ /dev/null @@ -1,6 +0,0 @@ -SET PATH=C:\MinGW\bin;C:\Python27;C:\Python27\Scripts;%PATH% - -python setup.py clean -python setup.py build_ext -c mingw32 --inplace - -nosetests pandas \ No newline at end of file diff --git a/scripts/testmed.py b/scripts/testmed.py deleted file mode 100644 index dd3b952d58c60..0000000000000 --- a/scripts/testmed.py +++ /dev/null @@ -1,171 +0,0 @@ -## {{{ Recipe 576930 (r10): Efficient Running Median using an Indexable Skiplist - -from random import random -from math import log, ceil -from pandas.compat import range -from numpy.random import randn -from pandas.lib.skiplist import rolling_median - - -class Node(object): - __slots__ = 'value', 'next', 'width' - - def __init__(self, value, next, width): - self.value, self.next, self.width = value, next, width - - -class End(object): - 'Sentinel object that always compares greater than another object' - def __cmp__(self, other): - return 1 - -NIL = Node(End(), [], []) # Singleton terminator node - - -class IndexableSkiplist: - 'Sorted collection supporting O(lg n) insertion, removal, and lookup by rank.' - - def __init__(self, expected_size=100): - self.size = 0 - self.maxlevels = int(1 + log(expected_size, 2)) - self.head = Node('HEAD', [NIL] * self.maxlevels, [1] * self.maxlevels) - - def __len__(self): - return self.size - - def __getitem__(self, i): - node = self.head - i += 1 - for level in reversed(range(self.maxlevels)): - while node.width[level] <= i: - i -= node.width[level] - node = node.next[level] - return node.value - - def insert(self, value): - # find first node on each level where node.next[levels].value > value - chain = [None] * self.maxlevels - steps_at_level = [0] * self.maxlevels - node = self.head - for level in reversed(range(self.maxlevels)): - while node.next[level].value <= value: - steps_at_level[level] += node.width[level] - node = node.next[level] - chain[level] = node - - # insert a link to the newnode at each level - d = min(self.maxlevels, 1 - int(log(random(), 2.0))) - newnode = Node(value, [None] * d, [None] * d) - steps = 0 - for level in range(d): - prevnode = chain[level] - newnode.next[level] = prevnode.next[level] - prevnode.next[level] = newnode - newnode.width[level] = prevnode.width[level] - steps - prevnode.width[level] = steps + 1 - steps += steps_at_level[level] - for level in range(d, self.maxlevels): - chain[level].width[level] += 1 - self.size += 1 - - def remove(self, value): - # find first node on each level where node.next[levels].value >= value - chain = [None] * self.maxlevels - node = self.head - for level in reversed(range(self.maxlevels)): - while node.next[level].value < value: - node = node.next[level] - chain[level] = node - if value != chain[0].next[0].value: - raise KeyError('Not Found') - - # remove one link at each level - d = len(chain[0].next[0].next) - for level in range(d): - prevnode = chain[level] - prevnode.width[level] += prevnode.next[level].width[level] - 1 - prevnode.next[level] = prevnode.next[level].next[level] - for level in range(d, self.maxlevels): - chain[level].width[level] -= 1 - self.size -= 1 - - def __iter__(self): - 'Iterate over values in sorted order' - node = self.head.next[0] - while node is not NIL: - yield node.value - node = node.next[0] - -from collections import deque -from itertools import islice - - -class RunningMedian: - 'Fast running median with O(lg n) updates where n is the window size' - - def __init__(self, n, iterable): - from pandas.lib.skiplist import IndexableSkiplist as skiplist - - self.it = iter(iterable) - self.queue = deque(islice(self.it, n)) - self.skiplist = IndexableSkiplist(n) - for elem in self.queue: - self.skiplist.insert(elem) - - def __iter__(self): - queue = self.queue - skiplist = self.skiplist - midpoint = len(queue) // 2 - yield skiplist[midpoint] - for newelem in self.it: - oldelem = queue.popleft() - skiplist.remove(oldelem) - queue.append(newelem) - skiplist.insert(newelem) - yield skiplist[midpoint] - -N = 100000 -K = 10000 - -import time - - -def test(): - from numpy.random import randn - - arr = randn(N) - - def _test(arr, k): - meds = RunningMedian(k, arr) - return list(meds) - - _test(arr, K) - - - -def test2(): - - arr = randn(N) - - return rolling_median(arr, K) - - -def runmany(f, arr, arglist): - timings = [] - - for arg in arglist: - tot = 0 - for i in range(5): - tot += _time(f, arr, arg) - timings.append(tot / 5) - - return timings - - -def _time(f, *args): - _start = time.clock() - result = f(*args) - return time.clock() - _start - -if __name__ == '__main__': - test2() diff --git a/scripts/tests/__init__.py b/scripts/tests/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/scripts/tests/conftest.py b/scripts/tests/conftest.py new file mode 100644 index 0000000000000..f8318b8d402af --- /dev/null +++ b/scripts/tests/conftest.py @@ -0,0 +1,3 @@ +def pytest_addoption(parser): + parser.addoption("--strict-data-files", action="store_true", + help="Unused. For compat with setup.cfg.") diff --git a/scripts/tests/test_validate_docstrings.py b/scripts/tests/test_validate_docstrings.py new file mode 100644 index 0000000000000..09fb5a30cbc3b --- /dev/null +++ b/scripts/tests/test_validate_docstrings.py @@ -0,0 +1,1131 @@ +import io +import random +import string +import textwrap +import pytest +import numpy as np +import pandas as pd + +import validate_docstrings +validate_one = validate_docstrings.validate_one + + +class GoodDocStrings(object): + """ + Collection of good doc strings. + + This class contains a lot of docstrings that should pass the validation + script without any errors. + """ + + def plot(self, kind, color='blue', **kwargs): + """ + Generate a plot. + + Render the data in the Series as a matplotlib plot of the + specified kind. + + Parameters + ---------- + kind : str + Kind of matplotlib plot. + color : str, default 'blue' + Color name or rgb code. + **kwargs + These parameters will be passed to the matplotlib plotting + function. + """ + pass + + def sample(self): + """ + Generate and return a random number. + + The value is sampled from a continuous uniform distribution between + 0 and 1. + + Returns + ------- + float + Random number generated. + """ + return random.random() + + def random_letters(self): + """ + Generate and return a sequence of random letters. + + The length of the returned string is also random, and is also + returned. + + Returns + ------- + length : int + Length of the returned string. + letters : str + String of random letters. + """ + length = random.randint(1, 10) + letters = "".join(random.sample(string.ascii_lowercase, length)) + return length, letters + + def sample_values(self): + """ + Generate an infinite sequence of random numbers. + + The values are sampled from a continuous uniform distribution between + 0 and 1. + + Yields + ------ + float + Random number generated. + """ + while True: + yield random.random() + + def head(self): + """ + Return the first 5 elements of the Series. + + This function is mainly useful to preview the values of the + Series without displaying the whole of it. + + Returns + ------- + Series + Subset of the original series with the 5 first values. + + See Also + -------- + Series.tail : Return the last 5 elements of the Series. + Series.iloc : Return a slice of the elements in the Series, + which can also be used to return the first or last n. + """ + return self.iloc[:5] + + def head1(self, n=5): + """ + Return the first elements of the Series. + + This function is mainly useful to preview the values of the + Series without displaying the whole of it. + + Parameters + ---------- + n : int + Number of values to return. + + Returns + ------- + Series + Subset of the original series with the n first values. + + See Also + -------- + tail : Return the last n elements of the Series. + + Examples + -------- + >>> s = pd.Series(['Ant', 'Bear', 'Cow', 'Dog', 'Falcon']) + >>> s.head() + 0 Ant + 1 Bear + 2 Cow + 3 Dog + 4 Falcon + dtype: object + + With the `n` parameter, we can change the number of returned rows: + + >>> s.head(n=3) + 0 Ant + 1 Bear + 2 Cow + dtype: object + """ + return self.iloc[:n] + + def contains(self, pat, case=True, na=np.nan): + """ + Return whether each value contains `pat`. + + In this case, we are illustrating how to use sections, even + if the example is simple enough and does not require them. + + Parameters + ---------- + pat : str + Pattern to check for within each element. + case : bool, default True + Whether check should be done with case sensitivity. + na : object, default np.nan + Fill value for missing data. + + Examples + -------- + >>> s = pd.Series(['Antelope', 'Lion', 'Zebra', np.nan]) + >>> s.str.contains(pat='a') + 0 False + 1 False + 2 True + 3 NaN + dtype: object + + **Case sensitivity** + + With `case_sensitive` set to `False` we can match `a` with both + `a` and `A`: + + >>> s.str.contains(pat='a', case=False) + 0 True + 1 False + 2 True + 3 NaN + dtype: object + + **Missing values** + + We can fill missing values in the output using the `na` parameter: + + >>> s.str.contains(pat='a', na=False) + 0 False + 1 False + 2 True + 3 False + dtype: bool + """ + pass + + def mode(self, axis, numeric_only): + """ + Ensure sphinx directives don't affect checks for trailing periods. + + Parameters + ---------- + axis : str + Sentence ending in period, followed by single directive. + + .. versionchanged:: 0.1.2 + + numeric_only : bool + Sentence ending in period, followed by multiple directives. + + .. versionadded:: 0.1.2 + .. deprecated:: 0.00.0 + A multiline description, + which spans another line. + """ + pass + + def good_imports(self): + """ + Ensure import other than numpy and pandas are fine. + + Examples + -------- + This example does not import pandas or import numpy. + >>> import datetime + >>> datetime.MAXYEAR + 9999 + """ + pass + + +class BadGenericDocStrings(object): + """Everything here has a bad docstring + """ + + def func(self): + + """Some function. + + With several mistakes in the docstring. + + It has a blank like after the signature `def func():`. + + The text 'Some function' should go in the line after the + opening quotes of the docstring, not in the same line. + + There is a blank line between the docstring and the first line + of code `foo = 1`. + + The closing quotes should be in the next line, not in this one.""" + + foo = 1 + bar = 2 + return foo + bar + + def astype(self, dtype): + """ + Casts Series type. + + Verb in third-person of the present simple, should be infinitive. + """ + pass + + def astype1(self, dtype): + """ + Method to cast Series type. + + Does not start with verb. + """ + pass + + def astype2(self, dtype): + """ + Cast Series type + + Missing dot at the end. + """ + pass + + def astype3(self, dtype): + """ + Cast Series type from its current type to the new type defined in + the parameter dtype. + + Summary is too verbose and doesn't fit in a single line. + """ + pass + + def two_linebreaks_between_sections(self, foo): + """ + Test linebreaks message GL03. + + Note 2 blank lines before parameters section. + + + Parameters + ---------- + foo : str + Description of foo parameter. + """ + pass + + def linebreak_at_end_of_docstring(self, foo): + """ + Test linebreaks message GL03. + + Note extra blank line at end of docstring. + + Parameters + ---------- + foo : str + Description of foo parameter. + + """ + pass + + def plot(self, kind, **kwargs): + """ + Generate a plot. + + Render the data in the Series as a matplotlib plot of the + specified kind. + + Note the blank line between the parameters title and the first + parameter. Also, note that after the name of the parameter `kind` + and before the colon, a space is missing. + + Also, note that the parameter descriptions do not start with a + capital letter, and do not finish with a dot. + + Finally, the `**kwargs` parameter is missing. + + Parameters + ---------- + + kind: str + kind of matplotlib plot + """ + pass + + def method(self, foo=None, bar=None): + """ + A sample DataFrame method. + + Do not import numpy and pandas. + + Try to use meaningful data, when it makes the example easier + to understand. + + Try to avoid positional arguments like in `df.method(1)`. They + can be alright if previously defined with a meaningful name, + like in `present_value(interest_rate)`, but avoid them otherwise. + + When presenting the behavior with different parameters, do not place + all the calls one next to the other. Instead, add a short sentence + explaining what the example shows. + + Examples + -------- + >>> import numpy as np + >>> import pandas as pd + >>> df = pd.DataFrame(np.ones((3, 3)), + ... columns=('a', 'b', 'c')) + >>> df.all(1) + 0 True + 1 True + 2 True + dtype: bool + >>> df.all(bool_only=True) + Series([], dtype: bool) + """ + pass + + def private_classes(self): + """ + This mentions NDFrame, which is not correct. + """ + + def unknown_section(self): + """ + This section has an unknown section title. + + Unknown Section + --------------- + This should raise an error in the validation. + """ + + def sections_in_wrong_order(self): + """ + This docstring has the sections in the wrong order. + + Parameters + ---------- + name : str + This section is in the right position. + + Examples + -------- + >>> print('So far Examples is good, as it goes before Parameters') + So far Examples is good, as it goes before Parameters + + See Also + -------- + function : This should generate an error, as See Also needs to go + before Examples. + """ + + def deprecation_in_wrong_order(self): + """ + This docstring has the deprecation warning in the wrong order. + + This is the extended summary. The correct order should be + summary, deprecation warning, extended summary. + + .. deprecated:: 1.0 + This should generate an error as it needs to go before + extended summary. + """ + + def method_wo_docstrings(self): + pass + + +class BadSummaries(object): + + def wrong_line(self): + """Exists on the wrong line""" + pass + + def no_punctuation(self): + """ + Has the right line but forgets punctuation + """ + pass + + def no_capitalization(self): + """ + provides a lowercase summary. + """ + pass + + def no_infinitive(self): + """ + Started with a verb that is not infinitive. + """ + + def multi_line(self): + """ + Extends beyond one line + which is not correct. + """ + + def two_paragraph_multi_line(self): + """ + Extends beyond one line + which is not correct. + + Extends beyond one line, which in itself is correct but the + previous short summary should still be an issue. + """ + + +class BadParameters(object): + """ + Everything here has a problem with its Parameters section. + """ + + def missing_params(self, kind, **kwargs): + """ + Lacks kwargs in Parameters. + + Parameters + ---------- + kind : str + Foo bar baz. + """ + + def bad_colon_spacing(self, kind): + """ + Has bad spacing in the type line. + + Parameters + ---------- + kind: str + Needs a space after kind. + """ + + def no_description_period(self, kind): + """ + Forgets to add a period to the description. + + Parameters + ---------- + kind : str + Doesn't end with a dot + """ + + def no_description_period_with_directive(self, kind): + """ + Forgets to add a period, and also includes a directive. + + Parameters + ---------- + kind : str + Doesn't end with a dot + + .. versionadded:: 0.00.0 + """ + + def no_description_period_with_directives(self, kind): + """ + Forgets to add a period, and also includes multiple directives. + + Parameters + ---------- + kind : str + Doesn't end with a dot + + .. versionchanged:: 0.00.0 + .. deprecated:: 0.00.0 + """ + + def parameter_capitalization(self, kind): + """ + Forgets to capitalize the description. + + Parameters + ---------- + kind : str + this is not capitalized. + """ + + def blank_lines(self, kind): + """ + Adds a blank line after the section header. + + Parameters + ---------- + + kind : str + Foo bar baz. + """ + pass + + def integer_parameter(self, kind): + """ + Uses integer instead of int. + + Parameters + ---------- + kind : integer + Foo bar baz. + """ + pass + + def string_parameter(self, kind): + """ + Uses string instead of str. + + Parameters + ---------- + kind : string + Foo bar baz. + """ + pass + + def boolean_parameter(self, kind): + """ + Uses boolean instead of bool. + + Parameters + ---------- + kind : boolean + Foo bar baz. + """ + pass + + def list_incorrect_parameter_type(self, kind): + """ + Uses list of boolean instead of list of bool. + + Parameters + ---------- + kind : list of boolean, integer, float or string + Foo bar baz. + """ + pass + + +class BadReturns(object): + + def return_not_documented(self): + """ + Lacks section for Returns + """ + return "Hello world!" + + def yield_not_documented(self): + """ + Lacks section for Yields + """ + yield "Hello world!" + + def no_type(self): + """ + Returns documented but without type. + + Returns + ------- + Some value. + """ + return "Hello world!" + + def no_description(self): + """ + Provides type but no descrption. + + Returns + ------- + str + """ + return "Hello world!" + + def no_punctuation(self): + """ + Provides type and description but no period. + + Returns + ------- + str + A nice greeting + """ + return "Hello world!" + + def named_single_return(self): + """ + Provides name but returns only one value. + + Returns + ------- + s : str + A nice greeting. + """ + return "Hello world!" + + def no_capitalization(self): + """ + Forgets capitalization in return values description. + + Returns + ------- + foo : str + The first returned string. + bar : str + the second returned string. + """ + return "Hello", "World!" + + def no_period_multi(self): + """ + Forgets period in return values description. + + Returns + ------- + foo : str + The first returned string + bar : str + The second returned string. + """ + return "Hello", "World!" + + +class BadSeeAlso(object): + + def desc_no_period(self): + """ + Return the first 5 elements of the Series. + + See Also + -------- + Series.tail : Return the last 5 elements of the Series. + Series.iloc : Return a slice of the elements in the Series, + which can also be used to return the first or last n + """ + pass + + def desc_first_letter_lowercase(self): + """ + Return the first 5 elements of the Series. + + See Also + -------- + Series.tail : return the last 5 elements of the Series. + Series.iloc : Return a slice of the elements in the Series, + which can also be used to return the first or last n. + """ + pass + + def prefix_pandas(self): + """ + Have `pandas` prefix in See Also section. + + See Also + -------- + pandas.Series.rename : Alter Series index labels or name. + DataFrame.head : The first `n` rows of the caller object. + """ + pass + + +class BadExamples(object): + + def unused_import(self): + """ + Examples + -------- + >>> import pandas as pdf + >>> df = pd.DataFrame(np.ones((3, 3)), columns=('a', 'b', 'c')) + """ + pass + + def missing_whitespace_around_arithmetic_operator(self): + """ + Examples + -------- + >>> 2+5 + 7 + """ + pass + + def indentation_is_not_a_multiple_of_four(self): + """ + Examples + -------- + >>> if 2 + 5: + ... pass + """ + pass + + def missing_whitespace_after_comma(self): + """ + Examples + -------- + >>> df = pd.DataFrame(np.ones((3,3)),columns=('a','b', 'c')) + """ + pass + + +class TestValidator(object): + + def _import_path(self, klass=None, func=None): + """ + Build the required import path for tests in this module. + + Parameters + ---------- + klass : str + Class name of object in module. + func : str + Function name of object in module. + + Returns + ------- + str + Import path of specified object in this module + """ + base_path = "scripts.tests.test_validate_docstrings" + + if klass: + base_path = ".".join([base_path, klass]) + + if func: + base_path = ".".join([base_path, func]) + + return base_path + + def test_good_class(self, capsys): + errors = validate_one(self._import_path( + klass='GoodDocStrings'))['errors'] + assert isinstance(errors, list) + assert not errors + + @pytest.mark.parametrize("func", [ + 'plot', 'sample', 'random_letters', 'sample_values', 'head', 'head1', + 'contains', 'mode', 'good_imports']) + def test_good_functions(self, capsys, func): + errors = validate_one(self._import_path( + klass='GoodDocStrings', func=func))['errors'] + assert isinstance(errors, list) + assert not errors + + def test_bad_class(self, capsys): + errors = validate_one(self._import_path( + klass='BadGenericDocStrings'))['errors'] + assert isinstance(errors, list) + assert errors + + @pytest.mark.parametrize("func", [ + 'func', 'astype', 'astype1', 'astype2', 'astype3', 'plot', 'method', + 'private_classes', + ]) + def test_bad_generic_functions(self, capsys, func): + errors = validate_one(self._import_path( # noqa:F821 + klass='BadGenericDocStrings', func=func))['errors'] + assert isinstance(errors, list) + assert errors + + @pytest.mark.parametrize("klass,func,msgs", [ + # See Also tests + ('BadGenericDocStrings', 'private_classes', + ("Private classes (NDFrame) should not be mentioned in public " + 'docstrings',)), + ('BadGenericDocStrings', 'unknown_section', + ('Found unknown section "Unknown Section".',)), + ('BadGenericDocStrings', 'sections_in_wrong_order', + ('Sections are in the wrong order. Correct order is: Parameters, ' + 'See Also, Examples',)), + ('BadGenericDocStrings', 'deprecation_in_wrong_order', + ('Deprecation warning should precede extended summary',)), + ('BadSeeAlso', 'desc_no_period', + ('Missing period at end of description for See Also "Series.iloc"',)), + ('BadSeeAlso', 'desc_first_letter_lowercase', + ('should be capitalized for See Also "Series.tail"',)), + # Summary tests + ('BadSummaries', 'wrong_line', + ('should start in the line immediately after the opening quotes',)), + ('BadSummaries', 'no_punctuation', + ('Summary does not end with a period',)), + ('BadSummaries', 'no_capitalization', + ('Summary does not start with a capital letter',)), + ('BadSummaries', 'no_capitalization', + ('Summary must start with infinitive verb',)), + ('BadSummaries', 'multi_line', + ('Summary should fit in a single line',)), + ('BadSummaries', 'two_paragraph_multi_line', + ('Summary should fit in a single line',)), + # Parameters tests + ('BadParameters', 'missing_params', + ('Parameters {**kwargs} not documented',)), + ('BadParameters', 'bad_colon_spacing', + ('Parameter "kind" requires a space before the colon ' + 'separating the parameter name and type',)), + ('BadParameters', 'no_description_period', + ('Parameter "kind" description should finish with "."',)), + ('BadParameters', 'no_description_period_with_directive', + ('Parameter "kind" description should finish with "."',)), + ('BadParameters', 'parameter_capitalization', + ('Parameter "kind" description should start with a capital letter',)), + ('BadParameters', 'integer_parameter', + ('Parameter "kind" type should use "int" instead of "integer"',)), + ('BadParameters', 'string_parameter', + ('Parameter "kind" type should use "str" instead of "string"',)), + ('BadParameters', 'boolean_parameter', + ('Parameter "kind" type should use "bool" instead of "boolean"',)), + ('BadParameters', 'list_incorrect_parameter_type', + ('Parameter "kind" type should use "bool" instead of "boolean"',)), + ('BadParameters', 'list_incorrect_parameter_type', + ('Parameter "kind" type should use "int" instead of "integer"',)), + ('BadParameters', 'list_incorrect_parameter_type', + ('Parameter "kind" type should use "str" instead of "string"',)), + pytest.param('BadParameters', 'blank_lines', ('No error yet?',), + marks=pytest.mark.xfail), + # Returns tests + ('BadReturns', 'return_not_documented', ('No Returns section found',)), + ('BadReturns', 'yield_not_documented', ('No Yields section found',)), + pytest.param('BadReturns', 'no_type', ('foo',), + marks=pytest.mark.xfail), + ('BadReturns', 'no_description', + ('Return value has no description',)), + ('BadReturns', 'no_punctuation', + ('Return value description should finish with "."',)), + ('BadReturns', 'named_single_return', + ('The first line of the Returns section should contain only the ' + 'type, unless multiple values are being returned',)), + ('BadReturns', 'no_capitalization', + ('Return value description should start with a capital ' + 'letter',)), + ('BadReturns', 'no_period_multi', + ('Return value description should finish with "."',)), + # Examples tests + ('BadGenericDocStrings', 'method', + ('Do not import numpy, as it is imported automatically',)), + ('BadGenericDocStrings', 'method', + ('Do not import pandas, as it is imported automatically',)), + ('BadGenericDocStrings', 'method_wo_docstrings', + ("The object does not have a docstring",)), + # See Also tests + ('BadSeeAlso', 'prefix_pandas', + ('pandas.Series.rename in `See Also` section ' + 'does not need `pandas` prefix',)), + # Examples tests + ('BadExamples', 'unused_import', + ("flake8 error: F401 'pandas as pdf' imported but unused",)), + ('BadExamples', 'indentation_is_not_a_multiple_of_four', + ('flake8 error: E111 indentation is not a multiple of four',)), + ('BadExamples', 'missing_whitespace_around_arithmetic_operator', + ('flake8 error: ' + 'E226 missing whitespace around arithmetic operator',)), + ('BadExamples', 'missing_whitespace_after_comma', + ("flake8 error: E231 missing whitespace after ',' (3 times)",)), + ('BadGenericDocStrings', 'two_linebreaks_between_sections', + ('Double line break found; please use only one blank line to ' + 'separate sections or paragraphs, and do not leave blank lines ' + 'at the end of docstrings',)), + ('BadGenericDocStrings', 'linebreak_at_end_of_docstring', + ('Double line break found; please use only one blank line to ' + 'separate sections or paragraphs, and do not leave blank lines ' + 'at the end of docstrings',)), + ]) + def test_bad_docstrings(self, capsys, klass, func, msgs): + result = validate_one(self._import_path(klass=klass, func=func)) + for msg in msgs: + assert msg in ' '.join(err[1] for err in result['errors']) + + def test_validate_all_ignore_deprecated(self, monkeypatch): + monkeypatch.setattr( + validate_docstrings, 'validate_one', lambda func_name: { + 'docstring': 'docstring1', + 'errors': [('ER01', 'err desc'), + ('ER02', 'err desc'), + ('ER03', 'err desc')], + 'warnings': [], + 'examples_errors': '', + 'deprecated': True}) + result = validate_docstrings.validate_all(prefix=None, + ignore_deprecated=True) + assert len(result) == 0 + + +class TestApiItems(object): + @property + def api_doc(self): + return io.StringIO(textwrap.dedent(''' + .. currentmodule:: itertools + + Itertools + --------- + + Infinite + ~~~~~~~~ + + .. autosummary:: + + cycle + count + + Finite + ~~~~~~ + + .. autosummary:: + + chain + + .. currentmodule:: random + + Random + ------ + + All + ~~~ + + .. autosummary:: + + seed + randint + ''')) + + @pytest.mark.parametrize('idx,name', [(0, 'itertools.cycle'), + (1, 'itertools.count'), + (2, 'itertools.chain'), + (3, 'random.seed'), + (4, 'random.randint')]) + def test_item_name(self, idx, name): + result = list(validate_docstrings.get_api_items(self.api_doc)) + assert result[idx][0] == name + + @pytest.mark.parametrize('idx,func', [(0, 'cycle'), + (1, 'count'), + (2, 'chain'), + (3, 'seed'), + (4, 'randint')]) + def test_item_function(self, idx, func): + result = list(validate_docstrings.get_api_items(self.api_doc)) + assert callable(result[idx][1]) + assert result[idx][1].__name__ == func + + @pytest.mark.parametrize('idx,section', [(0, 'Itertools'), + (1, 'Itertools'), + (2, 'Itertools'), + (3, 'Random'), + (4, 'Random')]) + def test_item_section(self, idx, section): + result = list(validate_docstrings.get_api_items(self.api_doc)) + assert result[idx][2] == section + + @pytest.mark.parametrize('idx,subsection', [(0, 'Infinite'), + (1, 'Infinite'), + (2, 'Finite'), + (3, 'All'), + (4, 'All')]) + def test_item_subsection(self, idx, subsection): + result = list(validate_docstrings.get_api_items(self.api_doc)) + assert result[idx][3] == subsection + + +class TestDocstringClass(object): + @pytest.mark.parametrize('name, expected_obj', + [('pandas.isnull', pd.isnull), + ('pandas.DataFrame', pd.DataFrame), + ('pandas.Series.sum', pd.Series.sum)]) + def test_resolves_class_name(self, name, expected_obj): + d = validate_docstrings.Docstring(name) + assert d.obj is expected_obj + + @pytest.mark.parametrize('invalid_name', ['panda', 'panda.DataFrame']) + def test_raises_for_invalid_module_name(self, invalid_name): + msg = 'No module can be imported from "{}"'.format(invalid_name) + with pytest.raises(ImportError, match=msg): + validate_docstrings.Docstring(invalid_name) + + @pytest.mark.parametrize('invalid_name', + ['pandas.BadClassName', + 'pandas.Series.bad_method_name']) + def test_raises_for_invalid_attribute_name(self, invalid_name): + name_components = invalid_name.split('.') + obj_name, invalid_attr_name = name_components[-2], name_components[-1] + msg = "'{}' has no attribute '{}'".format(obj_name, invalid_attr_name) + with pytest.raises(AttributeError, match=msg): + validate_docstrings.Docstring(invalid_name) + + +class TestMainFunction(object): + def test_exit_status_for_validate_one(self, monkeypatch): + monkeypatch.setattr( + validate_docstrings, 'validate_one', lambda func_name: { + 'docstring': 'docstring1', + 'errors': [('ER01', 'err desc'), + ('ER02', 'err desc'), + ('ER03', 'err desc')], + 'warnings': [], + 'examples_errors': ''}) + exit_status = validate_docstrings.main(func_name='docstring1', + prefix=None, + errors=[], + output_format='default', + ignore_deprecated=False) + assert exit_status == 0 + + def test_exit_status_errors_for_validate_all(self, monkeypatch): + monkeypatch.setattr( + validate_docstrings, 'validate_all', + lambda prefix, ignore_deprecated=False: { + 'docstring1': {'errors': [('ER01', 'err desc'), + ('ER02', 'err desc'), + ('ER03', 'err desc')], + 'file': 'module1.py', + 'file_line': 23}, + 'docstring2': {'errors': [('ER04', 'err desc'), + ('ER05', 'err desc')], + 'file': 'module2.py', + 'file_line': 925}}) + exit_status = validate_docstrings.main(func_name=None, + prefix=None, + errors=[], + output_format='default', + ignore_deprecated=False) + assert exit_status == 5 + + def test_no_exit_status_noerrors_for_validate_all(self, monkeypatch): + monkeypatch.setattr( + validate_docstrings, 'validate_all', + lambda prefix, ignore_deprecated=False: { + 'docstring1': {'errors': [], + 'warnings': [('WN01', 'warn desc')]}, + 'docstring2': {'errors': []}}) + exit_status = validate_docstrings.main(func_name=None, + prefix=None, + errors=[], + output_format='default', + ignore_deprecated=False) + assert exit_status == 0 + + def test_exit_status_for_validate_all_json(self, monkeypatch): + print('EXECUTED') + monkeypatch.setattr( + validate_docstrings, 'validate_all', + lambda prefix, ignore_deprecated=False: { + 'docstring1': {'errors': [('ER01', 'err desc'), + ('ER02', 'err desc'), + ('ER03', 'err desc')]}, + 'docstring2': {'errors': [('ER04', 'err desc'), + ('ER05', 'err desc')]}}) + exit_status = validate_docstrings.main(func_name=None, + prefix=None, + errors=[], + output_format='json', + ignore_deprecated=False) + assert exit_status == 0 + + def test_errors_param_filters_errors(self, monkeypatch): + monkeypatch.setattr( + validate_docstrings, 'validate_all', + lambda prefix, ignore_deprecated=False: { + 'Series.foo': {'errors': [('ER01', 'err desc'), + ('ER02', 'err desc'), + ('ER03', 'err desc')], + 'file': 'series.py', + 'file_line': 142}, + 'DataFrame.bar': {'errors': [('ER01', 'err desc'), + ('ER02', 'err desc')], + 'file': 'frame.py', + 'file_line': 598}, + 'Series.foobar': {'errors': [('ER01', 'err desc')], + 'file': 'series.py', + 'file_line': 279}}) + exit_status = validate_docstrings.main(func_name=None, + prefix=None, + errors=['ER01'], + output_format='default', + ignore_deprecated=False) + assert exit_status == 3 + + exit_status = validate_docstrings.main(func_name=None, + prefix=None, + errors=['ER03'], + output_format='default', + ignore_deprecated=False) + assert exit_status == 1 diff --git a/scripts/touchup_gh_issues.py b/scripts/touchup_gh_issues.py deleted file mode 100755 index 8aa6d426156f0..0000000000000 --- a/scripts/touchup_gh_issues.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import print_function -from collections import OrderedDict -import sys -import re - -""" -Reads in stdin, replace all occurences of '#num' or 'GH #num' with -links to github issue. dumps the issue anchors before the next -section header -""" - -pat = "((?:\s*GH\s*)?)#(\d{3,4})([^_]|$)?" -rep_pat = r"\1GH\2_\3" -anchor_pat = ".. _GH{id}: https://github.com/pandas-dev/pandas/issues/{id}" -section_pat = "^pandas\s[\d\.]+\s*$" - - -def main(): - issues = OrderedDict() - while True: - - line = sys.stdin.readline() - if not line: - break - - if re.search(section_pat, line): - for id in issues: - print(anchor_pat.format(id=id).rstrip()) - if issues: - print("\n") - issues = OrderedDict() - - for m in re.finditer(pat, line): - id = m.group(2) - if id not in issues: - issues[id] = True - print(re.sub(pat, rep_pat, line).rstrip()) - pass - -if __name__ == "__main__": - main() diff --git a/scripts/use_build_cache.py b/scripts/use_build_cache.py deleted file mode 100755 index f8c2df2a8a45d..0000000000000 --- a/scripts/use_build_cache.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os - -""" -This script should be run from the repo root dir, it rewrites setup.py -to use the build cache directory specified in the envar BUILD_CACHE_DIR -or in a file named .build_cache_dir in the repo root directory. - -Artifacts included in the cache: -- gcc artifacts -- The .c files resulting from cythonizing pyx/d files -- 2to3 refactoring results (when run under python3) - -Tested on releases back to 0.7.0. - -""" - -try: - import argparse - argparser = argparse.ArgumentParser(description=""" - 'Program description. - """.strip()) - - argparser.add_argument('-f', '--force-overwrite', - default=False, - help='Setting this will overwrite any existing cache results for the current commit', - action='store_true') - argparser.add_argument('-d', '--debug', - default=False, - help='Report cache hits/misses', - action='store_true') - - args = argparser.parse_args() -except: - class Foo(object): - debug=False - force_overwrite=False - - args = Foo() # for 2.6, no argparse - -#print(args.accumulate(args.integers)) - -shim=""" -import os -import sys -import shutil -import warnings -import re -""" - -shim += ("BC_FORCE_OVERWRITE = %s\n" % args.force_overwrite) -shim += ("BC_DEBUG = %s\n" % args.debug) - -shim += """ -try: - if not ("develop" in sys.argv) and not ("install" in sys.argv): - 1/0 - basedir = os.path.dirname(__file__) - dotfile = os.path.join(basedir,".build_cache_dir") - BUILD_CACHE_DIR = "" - if os.path.exists(dotfile): - BUILD_CACHE_DIR = open(dotfile).readline().strip() - BUILD_CACHE_DIR = os.environ.get('BUILD_CACHE_DIR',BUILD_CACHE_DIR) - - if os.path.isdir(BUILD_CACHE_DIR): - print("--------------------------------------------------------") - print("BUILD CACHE ACTIVATED (V2). be careful, this is experimental.") - print("BUILD_CACHE_DIR: " + BUILD_CACHE_DIR ) - print("--------------------------------------------------------") - else: - BUILD_CACHE_DIR = None - - # retrieve 2to3 artifacts - if sys.version_info[0] >= 3: - from lib2to3 import refactor - from hashlib import sha1 - import shutil - import multiprocessing - pyver = "%d.%d" % (sys.version_info[:2]) - fileq = ["pandas"] - to_process = dict() - - # retrieve the hashes existing in the cache - orig_hashes=dict() - post_hashes=dict() - for path,dirs,files in os.walk(os.path.join(BUILD_CACHE_DIR,'pandas')): - for f in files: - s=f.split(".py-")[-1] - try: - prev_h,post_h,ver = s.split('-') - if ver == pyver: - orig_hashes[prev_h] = os.path.join(path,f) - post_hashes[post_h] = os.path.join(path,f) - except: - pass - - while fileq: - f = fileq.pop() - - if os.path.isdir(f): - fileq.extend([os.path.join(f,x) for x in os.listdir(f)]) - else: - if not f.endswith(".py"): - continue - else: - try: - h = sha1(open(f,"rb").read()).hexdigest() - except IOError: - to_process[h] = f - else: - if h in orig_hashes and not BC_FORCE_OVERWRITE: - src = orig_hashes[h] - if BC_DEBUG: - print("2to3 cache hit %s,%s" % (f,h)) - shutil.copyfile(src,f) - elif h not in post_hashes: - # we're not in a dev dir with already processed files - if BC_DEBUG: - print("2to3 cache miss (will process) %s,%s" % (f,h)) - to_process[h] = f - - avail_fixes = set(refactor.get_fixers_from_package("lib2to3.fixes")) - avail_fixes.discard('lib2to3.fixes.fix_next') - t=refactor.RefactoringTool(avail_fixes) - if to_process: - print("Starting 2to3 refactoring...") - for orig_h,f in to_process.items(): - if BC_DEBUG: - print("2to3 on %s" % f) - try: - t.refactor([f],True) - post_h = sha1(open(f, "rb").read()).hexdigest() - cached_fname = f + '-' + orig_h + '-' + post_h + '-' + pyver - path = os.path.join(BUILD_CACHE_DIR, cached_fname) - pathdir =os.path.dirname(path) - if BC_DEBUG: - print("cache put %s in %s" % (f, path)) - try: - os.makedirs(pathdir) - except OSError as exc: - import errno - if exc.errno == errno.EEXIST and os.path.isdir(pathdir): - pass - else: - raise - - shutil.copyfile(f, path) - - except Exception as e: - print("While processing %s 2to3 raised: %s" % (f,str(e))) - - pass - print("2to3 done refactoring.") - -except Exception as e: - if not isinstance(e,ZeroDivisionError): - print( "Exception: " + str(e)) - BUILD_CACHE_DIR = None - -class CompilationCacheMixin(object): - def __init__(self, *args, **kwds): - cache_dir = kwds.pop("cache_dir", BUILD_CACHE_DIR) - self.cache_dir = cache_dir - if not os.path.isdir(cache_dir): - raise Exception("Error: path to Cache directory (%s) is not a dir" % cache_dir) - - def _copy_from_cache(self, hash, target): - src = os.path.join(self.cache_dir, hash) - if os.path.exists(src) and not BC_FORCE_OVERWRITE: - if BC_DEBUG: - print("Cache HIT: asked to copy file %s in %s" % - (src,os.path.abspath(target))) - s = "." - for d in target.split(os.path.sep)[:-1]: - s = os.path.join(s, d) - if not os.path.exists(s): - os.mkdir(s) - shutil.copyfile(src, target) - - return True - - return False - - def _put_to_cache(self, hash, src): - target = os.path.join(self.cache_dir, hash) - if BC_DEBUG: - print( "Cache miss: asked to copy file from %s to %s" % (src,target)) - s = "." - for d in target.split(os.path.sep)[:-1]: - s = os.path.join(s, d) - if not os.path.exists(s): - os.mkdir(s) - shutil.copyfile(src, target) - - def _hash_obj(self, obj): - try: - return hash(obj) - except: - raise NotImplementedError("You must override this method") - -class CompilationCacheExtMixin(CompilationCacheMixin): - def _hash_file(self, fname): - from hashlib import sha1 - f= None - try: - hash = sha1() - hash.update(self.build_lib.encode('utf-8')) - try: - if sys.version_info[0] >= 3: - import io - f = io.open(fname, "rb") - else: - f = open(fname) - - first_line = f.readline() - # ignore cython generation timestamp header - if "Generated by Cython" not in first_line.decode('utf-8'): - hash.update(first_line) - hash.update(f.read()) - return hash.hexdigest() - - except: - raise - return None - finally: - if f: - f.close() - - except IOError: - return None - - def _hash_obj(self, ext): - from hashlib import sha1 - - sources = ext.sources - if (sources is None or - (not hasattr(sources, '__iter__')) or - isinstance(sources, str) or - sys.version[0] == 2 and isinstance(sources, unicode)): # argh - return False - - sources = list(sources) + ext.depends - hash = sha1() - try: - for fname in sources: - fhash = self._hash_file(fname) - if fhash: - hash.update(fhash.encode('utf-8')) - except: - return None - - return hash.hexdigest() - - -class CachingBuildExt(build_ext, CompilationCacheExtMixin): - def __init__(self, *args, **kwds): - CompilationCacheExtMixin.__init__(self, *args, **kwds) - kwds.pop("cache_dir", None) - build_ext.__init__(self, *args, **kwds) - - def build_extension(self, ext, *args, **kwds): - ext_path = self.get_ext_fullpath(ext.name) - build_path = os.path.join(self.build_lib, os.path.basename(ext_path)) - - hash = self._hash_obj(ext) - if hash and self._copy_from_cache(hash, ext_path): - return - - build_ext.build_extension(self, ext, *args, **kwds) - - hash = self._hash_obj(ext) - if os.path.exists(build_path): - self._put_to_cache(hash, build_path) # build_ext - if os.path.exists(ext_path): - self._put_to_cache(hash, ext_path) # develop - - def cython_sources(self, sources, extension): - import re - cplus = self.cython_cplus or getattr(extension, 'cython_cplus', 0) or \ - (extension.language and extension.language.lower() == 'c++') - target_ext = '.c' - if cplus: - target_ext = '.cpp' - - for i, s in enumerate(sources): - if not re.search("\.(pyx|pxi|pxd)$", s): - continue - ext_dir = os.path.dirname(s) - ext_basename = re.sub("\.[^\.]+$", "", os.path.basename(s)) - ext_basename += target_ext - target = os.path.join(ext_dir, ext_basename) - hash = self._hash_file(s) - sources[i] = target - if hash and self._copy_from_cache(hash, target): - continue - build_ext.cython_sources(self, [s], extension) - self._put_to_cache(hash, target) - - sources = [x for x in sources if x.startswith("pandas") or "lib." in x] - - return sources - -if BUILD_CACHE_DIR: # use the cache - cmdclass['build_ext'] = CachingBuildExt - -try: - # recent - setuptools_kwargs['use_2to3'] = True if BUILD_CACHE_DIR is None else False -except: - pass - -try: - # pre eb2234231 , ~ 0.7.0, - setuptools_args['use_2to3'] = True if BUILD_CACHE_DIR is None else False -except: - pass - -""" -def main(): - opd = os.path.dirname - opj = os.path.join - s= None - with open(opj(opd(__file__),"..","setup.py")) as f: - s = f.read() - if s: - if "BUILD CACHE ACTIVATED (V2)" in s: - print( "setup.py already wired with V2 build_cache, skipping..") - else: - SEP="\nsetup(" - before,after = s.split(SEP) - with open(opj(opd(__file__),"..","setup.py"),"wb") as f: - f.write((before + shim + SEP + after).encode('ascii')) - print(""" - setup.py was rewritten to use a build cache. - Make sure you've put the following in your .bashrc: - - export BUILD_CACHE_DIR= - echo $BUILD_CACHE_DIR > pandas_repo_rootdir/.build_cache_dir - - Once active, build results (compilation, cythonizations and 2to3 artifacts) - will be cached in "$BUILD_CACHE_DIR" and subsequent builds should be - sped up if no changes requiring recompilation were made. - - Go ahead and run: - - python setup.py clean - python setup.py develop - - """) - -if __name__ == '__main__': - import sys - sys.exit(main()) diff --git a/scripts/validate_docstrings.py b/scripts/validate_docstrings.py new file mode 100755 index 0000000000000..20f32124a2532 --- /dev/null +++ b/scripts/validate_docstrings.py @@ -0,0 +1,954 @@ +#!/usr/bin/env python +""" +Analyze docstrings to detect errors. + +If no argument is provided, it does a quick check of docstrings and returns +a csv with all API functions and results of basic checks. + +If a function or method is provided in the form "pandas.function", +"pandas.module.class.method", etc. a list of all errors in the docstring for +the specified function or method. + +Usage:: + $ ./validate_docstrings.py + $ ./validate_docstrings.py pandas.DataFrame.head +""" +import os +import sys +import json +import re +import glob +import functools +import collections +import argparse +import pydoc +import inspect +import importlib +import doctest +import tempfile + +import flake8.main.application + +try: + from io import StringIO +except ImportError: + from cStringIO import StringIO + +# Template backend makes matplotlib to not plot anything. This is useful +# to avoid that plot windows are open from the doctests while running the +# script. Setting here before matplotlib is loaded. +# We don't warn for the number of open plots, as none is actually being opened +os.environ['MPLBACKEND'] = 'Template' +import matplotlib +matplotlib.rc('figure', max_open_warning=10000) + +import numpy + +BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +sys.path.insert(0, os.path.join(BASE_PATH)) +import pandas +from pandas.compat import signature + +sys.path.insert(1, os.path.join(BASE_PATH, 'doc', 'sphinxext')) +from numpydoc.docscrape import NumpyDocString +from pandas.io.formats.printing import pprint_thing + + +PRIVATE_CLASSES = ['NDFrame', 'IndexOpsMixin'] +DIRECTIVES = ['versionadded', 'versionchanged', 'deprecated'] +ALLOWED_SECTIONS = ['Parameters', 'Attributes', 'Methods', 'Returns', 'Yields', + 'Other Parameters', 'Raises', 'Warns', 'See Also', 'Notes', + 'References', 'Examples'] +ERROR_MSGS = { + 'GL01': 'Docstring text (summary) should start in the line immediately ' + 'after the opening quotes (not in the same line, or leaving a ' + 'blank line in between)', + 'GL02': 'Closing quotes should be placed in the line after the last text ' + 'in the docstring (do not close the quotes in the same line as ' + 'the text, or leave a blank line between the last text and the ' + 'quotes)', + 'GL03': 'Double line break found; please use only one blank line to ' + 'separate sections or paragraphs, and do not leave blank lines ' + 'at the end of docstrings', + 'GL04': 'Private classes ({mentioned_private_classes}) should not be ' + 'mentioned in public docstrings', + 'GL05': 'Tabs found at the start of line "{line_with_tabs}", please use ' + 'whitespace only', + 'GL06': 'Found unknown section "{section}". Allowed sections are: ' + '{allowed_sections}', + 'GL07': 'Sections are in the wrong order. Correct order is: ' + '{correct_sections}', + 'GL08': 'The object does not have a docstring', + 'GL09': 'Deprecation warning should precede extended summary', + 'SS01': 'No summary found (a short summary in a single line should be ' + 'present at the beginning of the docstring)', + 'SS02': 'Summary does not start with a capital letter', + 'SS03': 'Summary does not end with a period', + 'SS04': 'Summary contains heading whitespaces', + 'SS05': 'Summary must start with infinitive verb, not third person ' + '(e.g. use "Generate" instead of "Generates")', + 'SS06': 'Summary should fit in a single line', + 'ES01': 'No extended summary found', + 'PR01': 'Parameters {missing_params} not documented', + 'PR02': 'Unknown parameters {unknown_params}', + 'PR03': 'Wrong parameters order. Actual: {actual_params}. ' + 'Documented: {documented_params}', + 'PR04': 'Parameter "{param_name}" has no type', + 'PR05': 'Parameter "{param_name}" type should not finish with "."', + 'PR06': 'Parameter "{param_name}" type should use "{right_type}" instead ' + 'of "{wrong_type}"', + 'PR07': 'Parameter "{param_name}" has no description', + 'PR08': 'Parameter "{param_name}" description should start with a ' + 'capital letter', + 'PR09': 'Parameter "{param_name}" description should finish with "."', + 'PR10': 'Parameter "{param_name}" requires a space before the colon ' + 'separating the parameter name and type', + 'RT01': 'No Returns section found', + 'RT02': 'The first line of the Returns section should contain only the ' + 'type, unless multiple values are being returned', + 'RT03': 'Return value has no description', + 'RT04': 'Return value description should start with a capital letter', + 'RT05': 'Return value description should finish with "."', + 'YD01': 'No Yields section found', + 'SA01': 'See Also section not found', + 'SA02': 'Missing period at end of description for See Also ' + '"{reference_name}" reference', + 'SA03': 'Description should be capitalized for See Also ' + '"{reference_name}" reference', + 'SA04': 'Missing description for See Also "{reference_name}" reference', + 'SA05': '{reference_name} in `See Also` section does not need `pandas` ' + 'prefix, use {right_reference} instead.', + 'EX01': 'No examples section found', + 'EX02': 'Examples do not pass tests:\n{doctest_log}', + 'EX03': 'flake8 error: {error_code} {error_message}{times_happening}', + 'EX04': 'Do not import {imported_library}, as it is imported ' + 'automatically for the examples (numpy as np, pandas as pd)', +} + + +def error(code, **kwargs): + """ + Return a tuple with the error code and the message with variables replaced. + + This is syntactic sugar so instead of: + - `('EX02', ERROR_MSGS['EX02'].format(doctest_log=log))` + + We can simply use: + - `error('EX02', doctest_log=log)` + + Parameters + ---------- + code : str + Error code. + **kwargs + Values for the variables in the error messages + + Returns + ------- + code : str + Error code. + message : str + Error message with varaibles replaced. + """ + return (code, ERROR_MSGS[code].format(**kwargs)) + + +def get_api_items(api_doc_fd): + """ + Yield information about all public API items. + + Parse api.rst file from the documentation, and extract all the functions, + methods, classes, attributes... This should include all pandas public API. + + Parameters + ---------- + api_doc_fd : file descriptor + A file descriptor of the API documentation page, containing the table + of contents with all the public API. + + Yields + ------ + name : str + The name of the object (e.g. 'pandas.Series.str.upper). + func : function + The object itself. In most cases this will be a function or method, + but it can also be classes, properties, cython objects... + section : str + The name of the section in the API page where the object item is + located. + subsection : str + The name of the subsection in the API page where the object item is + located. + """ + current_module = 'pandas' + previous_line = current_section = current_subsection = '' + position = None + for line in api_doc_fd: + line = line.strip() + if len(line) == len(previous_line): + if set(line) == set('-'): + current_section = previous_line + continue + if set(line) == set('~'): + current_subsection = previous_line + continue + + if line.startswith('.. currentmodule::'): + current_module = line.replace('.. currentmodule::', '').strip() + continue + + if line == '.. autosummary::': + position = 'autosummary' + continue + + if position == 'autosummary': + if line == '': + position = 'items' + continue + + if position == 'items': + if line == '': + position = None + continue + item = line.strip() + func = importlib.import_module(current_module) + for part in item.split('.'): + func = getattr(func, part) + + yield ('.'.join([current_module, item]), func, + current_section, current_subsection) + + previous_line = line + + +class Docstring(object): + def __init__(self, name): + self.name = name + obj = self._load_obj(name) + self.obj = obj + self.code_obj = self._to_original_callable(obj) + self.raw_doc = obj.__doc__ or '' + self.clean_doc = pydoc.getdoc(obj) + self.doc = NumpyDocString(self.clean_doc) + + def __len__(self): + return len(self.raw_doc) + + @staticmethod + def _load_obj(name): + """ + Import Python object from its name as string. + + Parameters + ---------- + name : str + Object name to import (e.g. pandas.Series.str.upper) + + Returns + ------- + object + Python object that can be a class, method, function... + + Examples + -------- + >>> Docstring._load_obj('pandas.Series') + + """ + for maxsplit in range(1, name.count('.') + 1): + # TODO when py3 only replace by: module, *func_parts = ... + func_name_split = name.rsplit('.', maxsplit) + module = func_name_split[0] + func_parts = func_name_split[1:] + try: + obj = importlib.import_module(module) + except ImportError: + pass + else: + continue + + if 'obj' not in locals(): + raise ImportError('No module can be imported ' + 'from "{}"'.format(name)) + + for part in func_parts: + obj = getattr(obj, part) + return obj + + @staticmethod + def _to_original_callable(obj): + """ + Find the Python object that contains the source code of the object. + + This is useful to find the place in the source code (file and line + number) where a docstring is defined. It does not currently work for + all cases, but it should help find some (properties...). + """ + while True: + if inspect.isfunction(obj) or inspect.isclass(obj): + f = inspect.getfile(obj) + if f.startswith('<') and f.endswith('>'): + return None + return obj + if inspect.ismethod(obj): + obj = obj.__func__ + elif isinstance(obj, functools.partial): + obj = obj.func + elif isinstance(obj, property): + obj = obj.fget + else: + return None + + @property + def type(self): + return type(self.obj).__name__ + + @property + def is_function_or_method(self): + # TODO(py27): remove ismethod + return (inspect.isfunction(self.obj) + or inspect.ismethod(self.obj)) + + @property + def source_file_name(self): + """ + File name where the object is implemented (e.g. pandas/core/frame.py). + """ + try: + fname = inspect.getsourcefile(self.code_obj) + except TypeError: + # In some cases the object is something complex like a cython + # object that can't be easily introspected. An it's better to + # return the source code file of the object as None, than crash + pass + else: + if fname: + fname = os.path.relpath(fname, BASE_PATH) + return fname + + @property + def source_file_def_line(self): + """ + Number of line where the object is defined in its file. + """ + try: + return inspect.getsourcelines(self.code_obj)[-1] + except (OSError, TypeError): + # In some cases the object is something complex like a cython + # object that can't be easily introspected. An it's better to + # return the line number as None, than crash + pass + + @property + def github_url(self): + url = 'https://github.com/pandas-dev/pandas/blob/master/' + url += '{}#L{}'.format(self.source_file_name, + self.source_file_def_line) + return url + + @property + def start_blank_lines(self): + i = None + if self.raw_doc: + for i, row in enumerate(self.raw_doc.split('\n')): + if row.strip(): + break + return i + + @property + def end_blank_lines(self): + i = None + if self.raw_doc: + for i, row in enumerate(reversed(self.raw_doc.split('\n'))): + if row.strip(): + break + return i + + @property + def double_blank_lines(self): + prev = True + for row in self.raw_doc.split('\n'): + if not prev and not row.strip(): + return True + prev = row.strip() + return False + + @property + def section_titles(self): + sections = [] + self.doc._doc.reset() + while not self.doc._doc.eof(): + content = self.doc._read_to_next_section() + if (len(content) > 1 + and len(content[0]) == len(content[1]) + and set(content[1]) == {'-'}): + sections.append(content[0]) + return sections + + @property + def summary(self): + return ' '.join(self.doc['Summary']) + + @property + def num_summary_lines(self): + return len(self.doc['Summary']) + + @property + def extended_summary(self): + if not self.doc['Extended Summary'] and len(self.doc['Summary']) > 1: + return ' '.join(self.doc['Summary']) + return ' '.join(self.doc['Extended Summary']) + + @property + def needs_summary(self): + return not (bool(self.summary) and bool(self.extended_summary)) + + @property + def doc_parameters(self): + return collections.OrderedDict((name, (type_, ''.join(desc))) + for name, type_, desc + in self.doc['Parameters']) + + @property + def signature_parameters(self): + if inspect.isclass(self.obj): + if hasattr(self.obj, '_accessors') and ( + self.name.split('.')[-1] in + self.obj._accessors): + # accessor classes have a signature but don't want to show this + return tuple() + try: + sig = signature(self.obj) + except (TypeError, ValueError): + # Some objects, mainly in C extensions do not support introspection + # of the signature + return tuple() + params = sig.args + if sig.varargs: + params.append("*" + sig.varargs) + if sig.keywords: + params.append("**" + sig.keywords) + params = tuple(params) + if params and params[0] in ('self', 'cls'): + return params[1:] + return params + + @property + def parameter_mismatches(self): + errs = [] + signature_params = self.signature_parameters + doc_params = tuple(self.doc_parameters) + missing = set(signature_params) - set(doc_params) + if missing: + errs.append(error('PR01', missing_params=pprint_thing(missing))) + extra = set(doc_params) - set(signature_params) + if extra: + errs.append(error('PR02', unknown_params=pprint_thing(extra))) + if (not missing and not extra and signature_params != doc_params + and not (not signature_params and not doc_params)): + errs.append(error('PR03', + actual_params=signature_params, + documented_params=doc_params)) + + return errs + + @property + def correct_parameters(self): + return not bool(self.parameter_mismatches) + + def parameter_type(self, param): + return self.doc_parameters[param][0] + + def parameter_desc(self, param): + desc = self.doc_parameters[param][1] + # Find and strip out any sphinx directives + for directive in DIRECTIVES: + full_directive = '.. {}'.format(directive) + if full_directive in desc: + # Only retain any description before the directive + desc = desc[:desc.index(full_directive)] + return desc + + @property + def see_also(self): + return collections.OrderedDict((name, ''.join(desc)) + for name, desc, _ + in self.doc['See Also']) + + @property + def examples(self): + return self.doc['Examples'] + + @property + def returns(self): + return self.doc['Returns'] + + @property + def yields(self): + return self.doc['Yields'] + + @property + def method_source(self): + try: + return inspect.getsource(self.obj) + except TypeError: + return '' + + @property + def first_line_ends_in_dot(self): + if self.doc: + return self.doc.split('\n')[0][-1] == '.' + + @property + def deprecated_with_directive(self): + return '.. deprecated:: ' in (self.summary + self.extended_summary) + + @property + def deprecated(self): + return (self.name.startswith('pandas.Panel') + or self.deprecated_with_directive) + + @property + def mentioned_private_classes(self): + return [klass for klass in PRIVATE_CLASSES if klass in self.raw_doc] + + @property + def examples_errors(self): + flags = doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL + finder = doctest.DocTestFinder() + runner = doctest.DocTestRunner(optionflags=flags) + context = {'np': numpy, 'pd': pandas} + error_msgs = '' + for test in finder.find(self.raw_doc, self.name, globs=context): + f = StringIO() + runner.run(test, out=f.write) + error_msgs += f.getvalue() + return error_msgs + + @property + def examples_source_code(self): + lines = doctest.DocTestParser().get_examples(self.raw_doc) + return [line.source for line in lines] + + def validate_pep8(self): + if not self.examples: + return + + # F401 is needed to not generate flake8 errors in examples + # that do not user numpy or pandas + content = ''.join(('import numpy as np # noqa: F401\n', + 'import pandas as pd # noqa: F401\n', + *self.examples_source_code)) + + application = flake8.main.application.Application() + application.initialize(["--quiet"]) + + with tempfile.NamedTemporaryFile(mode='w') as file: + file.write(content) + file.flush() + application.run_checks([file.name]) + + # We need this to avoid flake8 printing the names of the files to + # the standard output + application.formatter.write = lambda line, source: None + application.report() + + yield from application.guide.stats.statistics_for('') + + +def get_validation_data(doc): + """ + Validate the docstring. + + Parameters + ---------- + doc : Docstring + A Docstring object with the given function name. + + Returns + ------- + tuple + errors : list of tuple + Errors occurred during validation. + warnings : list of tuple + Warnings occurred during validation. + examples_errs : str + Examples usage displayed along the error, otherwise empty string. + + Notes + ----- + The errors codes are defined as: + - First two characters: Section where the error happens: + * GL: Global (no section, like section ordering errors) + * SS: Short summary + * ES: Extended summary + * PR: Parameters + * RT: Returns + * YD: Yields + * RS: Raises + * WN: Warns + * SA: See Also + * NT: Notes + * RF: References + * EX: Examples + - Last two characters: Numeric error code inside the section + + For example, EX02 is the second codified error in the Examples section + (which in this case is assigned to examples that do not pass the tests). + + The error codes, their corresponding error messages, and the details on how + they are validated, are not documented more than in the source code of this + function. + """ + + errs = [] + wrns = [] + if not doc.raw_doc: + errs.append(error('GL08')) + return errs, wrns, '' + + if doc.start_blank_lines != 1: + errs.append(error('GL01')) + if doc.end_blank_lines != 1: + errs.append(error('GL02')) + if doc.double_blank_lines: + errs.append(error('GL03')) + mentioned_errs = doc.mentioned_private_classes + if mentioned_errs: + errs.append(error('GL04', + mentioned_private_classes=', '.join(mentioned_errs))) + for line in doc.raw_doc.splitlines(): + if re.match("^ *\t", line): + errs.append(error('GL05', line_with_tabs=line.lstrip())) + + unexpected_sections = [section for section in doc.section_titles + if section not in ALLOWED_SECTIONS] + for section in unexpected_sections: + errs.append(error('GL06', + section=section, + allowed_sections=', '.join(ALLOWED_SECTIONS))) + + correct_order = [section for section in ALLOWED_SECTIONS + if section in doc.section_titles] + if correct_order != doc.section_titles: + errs.append(error('GL07', + correct_sections=', '.join(correct_order))) + + if (doc.deprecated_with_directive + and not doc.extended_summary.startswith('.. deprecated:: ')): + errs.append(error('GL09')) + + if not doc.summary: + errs.append(error('SS01')) + else: + if not doc.summary[0].isupper(): + errs.append(error('SS02')) + if doc.summary[-1] != '.': + errs.append(error('SS03')) + if doc.summary != doc.summary.lstrip(): + errs.append(error('SS04')) + elif (doc.is_function_or_method + and doc.summary.split(' ')[0][-1] == 's'): + errs.append(error('SS05')) + if doc.num_summary_lines > 1: + errs.append(error('SS06')) + + if not doc.extended_summary: + wrns.append(('ES01', 'No extended summary found')) + + # PR01: Parameters not documented + # PR02: Unknown parameters + # PR03: Wrong parameters order + errs += doc.parameter_mismatches + + for param in doc.doc_parameters: + if not param.startswith("*"): # Check can ignore var / kwargs + if not doc.parameter_type(param): + if ':' in param: + errs.append(error('PR10', + param_name=param.split(':')[0])) + else: + errs.append(error('PR04', param_name=param)) + else: + if doc.parameter_type(param)[-1] == '.': + errs.append(error('PR05', param_name=param)) + common_type_errors = [('integer', 'int'), + ('boolean', 'bool'), + ('string', 'str')] + for wrong_type, right_type in common_type_errors: + if wrong_type in doc.parameter_type(param): + errs.append(error('PR06', + param_name=param, + right_type=right_type, + wrong_type=wrong_type)) + if not doc.parameter_desc(param): + errs.append(error('PR07', param_name=param)) + else: + if not doc.parameter_desc(param)[0].isupper(): + errs.append(error('PR08', param_name=param)) + if doc.parameter_desc(param)[-1] != '.': + errs.append(error('PR09', param_name=param)) + + if doc.is_function_or_method: + if not doc.returns: + if 'return' in doc.method_source: + errs.append(error('RT01')) + else: + if len(doc.returns) == 1 and doc.returns[0][1]: + errs.append(error('RT02')) + for name_or_type, type_, desc in doc.returns: + if not desc: + errs.append(error('RT03')) + else: + desc = ' '.join(desc) + if not desc[0].isupper(): + errs.append(error('RT04')) + if not desc.endswith('.'): + errs.append(error('RT05')) + + if not doc.yields and 'yield' in doc.method_source: + errs.append(error('YD01')) + + if not doc.see_also: + wrns.append(error('SA01')) + else: + for rel_name, rel_desc in doc.see_also.items(): + if rel_desc: + if not rel_desc.endswith('.'): + errs.append(error('SA02', reference_name=rel_name)) + if not rel_desc[0].isupper(): + errs.append(error('SA03', reference_name=rel_name)) + else: + errs.append(error('SA04', reference_name=rel_name)) + if rel_name.startswith('pandas.'): + errs.append(error('SA05', + reference_name=rel_name, + right_reference=rel_name[len('pandas.'):])) + + examples_errs = '' + if not doc.examples: + wrns.append(error('EX01')) + else: + examples_errs = doc.examples_errors + if examples_errs: + errs.append(error('EX02', doctest_log=examples_errs)) + for err in doc.validate_pep8(): + errs.append(error('EX03', + error_code=err.error_code, + error_message=err.message, + times_happening=' ({} times)'.format(err.count) + if err.count > 1 else '')) + examples_source_code = ''.join(doc.examples_source_code) + for wrong_import in ('numpy', 'pandas'): + if 'import {}'.format(wrong_import) in examples_source_code: + errs.append(error('EX04', imported_library=wrong_import)) + return errs, wrns, examples_errs + + +def validate_one(func_name): + """ + Validate the docstring for the given func_name + + Parameters + ---------- + func_name : function + Function whose docstring will be evaluated (e.g. pandas.read_csv). + + Returns + ------- + dict + A dictionary containing all the information obtained from validating + the docstring. + """ + doc = Docstring(func_name) + errs, wrns, examples_errs = get_validation_data(doc) + return {'type': doc.type, + 'docstring': doc.clean_doc, + 'deprecated': doc.deprecated, + 'file': doc.source_file_name, + 'file_line': doc.source_file_def_line, + 'github_link': doc.github_url, + 'errors': errs, + 'warnings': wrns, + 'examples_errors': examples_errs} + + +def validate_all(prefix, ignore_deprecated=False): + """ + Execute the validation of all docstrings, and return a dict with the + results. + + Parameters + ---------- + prefix : str or None + If provided, only the docstrings that start with this pattern will be + validated. If None, all docstrings will be validated. + ignore_deprecated: bool, default False + If True, deprecated objects are ignored when validating docstrings. + + Returns + ------- + dict + A dictionary with an item for every function/method... containing + all the validation information. + """ + result = {} + seen = {} + + # functions from the API docs + api_doc_fnames = os.path.join( + BASE_PATH, 'doc', 'source', 'reference', '*.rst') + api_items = [] + for api_doc_fname in glob.glob(api_doc_fnames): + with open(api_doc_fname) as f: + api_items += list(get_api_items(f)) + for func_name, func_obj, section, subsection in api_items: + if prefix and not func_name.startswith(prefix): + continue + doc_info = validate_one(func_name) + if ignore_deprecated and doc_info['deprecated']: + continue + result[func_name] = doc_info + + shared_code_key = doc_info['file'], doc_info['file_line'] + shared_code = seen.get(shared_code_key, '') + result[func_name].update({'in_api': True, + 'section': section, + 'subsection': subsection, + 'shared_code_with': shared_code}) + + seen[shared_code_key] = func_name + + # functions from introspecting Series, DataFrame and Panel + api_item_names = set(list(zip(*api_items))[0]) + for class_ in (pandas.Series, pandas.DataFrame, pandas.Panel): + for member in inspect.getmembers(class_): + func_name = 'pandas.{}.{}'.format(class_.__name__, member[0]) + if (not member[0].startswith('_') + and func_name not in api_item_names): + if prefix and not func_name.startswith(prefix): + continue + doc_info = validate_one(func_name) + if ignore_deprecated and doc_info['deprecated']: + continue + result[func_name] = doc_info + result[func_name]['in_api'] = False + + return result + + +def main(func_name, prefix, errors, output_format, ignore_deprecated): + def header(title, width=80, char='#'): + full_line = char * width + side_len = (width - len(title) - 2) // 2 + adj = '' if len(title) % 2 == 0 else ' ' + title_line = '{side} {title}{adj} {side}'.format(side=char * side_len, + title=title, + adj=adj) + + return '\n{full_line}\n{title_line}\n{full_line}\n\n'.format( + full_line=full_line, title_line=title_line) + + exit_status = 0 + if func_name is None: + result = validate_all(prefix, ignore_deprecated) + + if output_format == 'json': + output = json.dumps(result) + else: + if output_format == 'default': + output_format = '{text}\n' + elif output_format == 'azure': + output_format = ('##vso[task.logissue type=error;' + 'sourcepath={path};' + 'linenumber={row};' + 'code={code};' + ']{text}\n') + else: + raise ValueError('Unknown output_format "{}"'.format( + output_format)) + + output = '' + for name, res in result.items(): + for err_code, err_desc in res['errors']: + # The script would be faster if instead of filtering the + # errors after validating them, it didn't validate them + # initially. But that would complicate the code too much + if errors and err_code not in errors: + continue + exit_status += 1 + output += output_format.format( + name=name, + path=res['file'], + row=res['file_line'], + code=err_code, + text='{}: {}'.format(name, err_desc)) + + sys.stdout.write(output) + + else: + result = validate_one(func_name) + sys.stderr.write(header('Docstring ({})'.format(func_name))) + sys.stderr.write('{}\n'.format(result['docstring'])) + sys.stderr.write(header('Validation')) + if result['errors']: + sys.stderr.write('{} Errors found:\n'.format( + len(result['errors']))) + for err_code, err_desc in result['errors']: + # Failing examples are printed at the end + if err_code == 'EX02': + sys.stderr.write('\tExamples do not pass tests\n') + continue + sys.stderr.write('\t{}\n'.format(err_desc)) + if result['warnings']: + sys.stderr.write('{} Warnings found:\n'.format( + len(result['warnings']))) + for wrn_code, wrn_desc in result['warnings']: + sys.stderr.write('\t{}\n'.format(wrn_desc)) + + if not result['errors']: + sys.stderr.write('Docstring for "{}" correct. :)\n'.format( + func_name)) + + if result['examples_errors']: + sys.stderr.write(header('Doctests')) + sys.stderr.write(result['examples_errors']) + + return exit_status + + +if __name__ == '__main__': + format_opts = 'default', 'json', 'azure' + func_help = ('function or method to validate (e.g. pandas.DataFrame.head) ' + 'if not provided, all docstrings are validated and returned ' + 'as JSON') + argparser = argparse.ArgumentParser( + description='validate pandas docstrings') + argparser.add_argument('function', + nargs='?', + default=None, + help=func_help) + argparser.add_argument('--format', default='default', choices=format_opts, + help='format of the output when validating ' + 'multiple docstrings (ignored when validating one).' + 'It can be {}'.format(str(format_opts)[1:-1])) + argparser.add_argument('--prefix', default=None, help='pattern for the ' + 'docstring names, in order to decide which ones ' + 'will be validated. A prefix "pandas.Series.str.' + 'will make the script validate all the docstrings' + 'of methods starting by this pattern. It is ' + 'ignored if parameter function is provided') + argparser.add_argument('--errors', default=None, help='comma separated ' + 'list of error codes to validate. By default it ' + 'validates all errors (ignored when validating ' + 'a single docstring)') + argparser.add_argument('--ignore_deprecated', default=False, + action='store_true', help='if this flag is set, ' + 'deprecated objects are ignored when validating ' + 'all docstrings') + + args = argparser.parse_args() + sys.exit(main(args.function, args.prefix, + args.errors.split(',') if args.errors else None, + args.format, + args.ignore_deprecated)) diff --git a/scripts/winbuild_py27.bat b/scripts/winbuild_py27.bat deleted file mode 100644 index bec67c7e527ed..0000000000000 --- a/scripts/winbuild_py27.bat +++ /dev/null @@ -1,2 +0,0 @@ -SET PATH=C:\MinGW\bin;C:\Python27;C:\Python27\Scripts;%PATH% -python setup.py build -c mingw32 bdist_wininst diff --git a/scripts/windows_builder/build_27-32.bat b/scripts/windows_builder/build_27-32.bat deleted file mode 100644 index 37eb4d436d567..0000000000000 --- a/scripts/windows_builder/build_27-32.bat +++ /dev/null @@ -1,25 +0,0 @@ -@echo off -echo "starting 27-32" - -setlocal EnableDelayedExpansion -set MSSdk=1 -CALL "C:\Program Files\Microsoft SDKs\Windows\v7.0\Bin\SetEnv.cmd" /x86 /release -set DISTUTILS_USE_SDK=1 - -title 27-32 build -echo "building" -cd "c:\users\Jeff Reback\documents\github\pandas" -C:\python27-32\python.exe setup.py build > build.27-32.log 2>&1 - -title "installing" -C:\python27-32\python.exe setup.py bdist --formats=wininst > install.27-32.log 2>&1 - -echo "testing" -C:\python27-32\scripts\nosetests -A "not slow" build\lib.win32-2.7\pandas > test.27-32.log 2>&1 - -echo "versions" -cd build\lib.win32-2.7 -C:\python27-32\python.exe ../../ci/print_versions.py > ../../versions.27-32.log 2>&1 - -exit - diff --git a/scripts/windows_builder/build_27-64.bat b/scripts/windows_builder/build_27-64.bat deleted file mode 100644 index e76e25d0ef39c..0000000000000 --- a/scripts/windows_builder/build_27-64.bat +++ /dev/null @@ -1,25 +0,0 @@ -@echo off -echo "starting 27-64" - -setlocal EnableDelayedExpansion -set MSSdk=1 -CALL "C:\Program Files\Microsoft SDKs\Windows\v7.0\Bin\SetEnv.cmd" /x64 /release -set DISTUTILS_USE_SDK=1 - -title 27-64 build -echo "building" -cd "c:\users\Jeff Reback\documents\github\pandas" -C:\python27-64\python.exe setup.py build > build.27-64.log 2>&1 - -echo "installing" -C:\python27-64\python.exe setup.py bdist --formats=wininst > install.27-64.log 2>&1 - -echo "testing" -C:\python27-64\scripts\nosetests -A "not slow" build\lib.win-amd64-2.7\pandas > test.27-64.log 2>&1 - -echo "versions" -cd build\lib.win-amd64-2.7 -C:\python27-64\python.exe ../../ci/print_versions.py > ../../versions.27-64.log 2>&1 - -exit - diff --git a/scripts/windows_builder/build_34-32.bat b/scripts/windows_builder/build_34-32.bat deleted file mode 100644 index 8e060e000bc8f..0000000000000 --- a/scripts/windows_builder/build_34-32.bat +++ /dev/null @@ -1,27 +0,0 @@ -@echo off -echo "starting 34-32" - -setlocal EnableDelayedExpansion -set MSSdk=1 -CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x86 /release -set DISTUTILS_USE_SDK=1 - -title 34-32 build -echo "building" -cd "c:\users\Jeff Reback\documents\github\pandas" -C:\python34-32\python.exe setup.py build > build.34-32.log 2>&1 - -echo "installing" -C:\python34-32\python.exe setup.py bdist --formats=wininst > install.34-32.log 2>&1 - -echo "testing" -C:\python34-32\scripts\nosetests -A "not slow" build\lib.win32-3.4\pandas > test.34-32.log 2>&1 - -echo "versions" -cd build\lib.win32-3.4 -C:\python34-32\python.exe ../../ci/print_versions.py > ../../versions.34-32.log 2>&1 - -exit - - - diff --git a/scripts/windows_builder/build_34-64.bat b/scripts/windows_builder/build_34-64.bat deleted file mode 100644 index 3a8512b730346..0000000000000 --- a/scripts/windows_builder/build_34-64.bat +++ /dev/null @@ -1,27 +0,0 @@ -@echo off -echo "starting 34-64" - -setlocal EnableDelayedExpansion -set MSSdk=1 -CALL "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 /release -set DISTUTILS_USE_SDK=1 - -title 34-64 build -echo "building" -cd "c:\users\Jeff Reback\documents\github\pandas" -C:\python34-64\python.exe setup.py build > build.34-64.log 2>&1 - -echo "installing" -C:\python34-64\python.exe setup.py bdist --formats=wininst > install.34-64.log 2>&1 - -echo "testing" -C:\python34-64\scripts\nosetests -A "not slow" build\lib.win-amd64-3.4\pandas > test.34-64.log 2>&1 - -echo "versions" -cd build\lib.win-amd64-3.4 -C:\python34-64\python.exe ../../ci/print_versions.py > ../../versions.34-64.log 2>&1 - -exit - - - diff --git a/scripts/windows_builder/check_and_build.bat b/scripts/windows_builder/check_and_build.bat deleted file mode 100644 index 32be1bde1f7f3..0000000000000 --- a/scripts/windows_builder/check_and_build.bat +++ /dev/null @@ -1,2 +0,0 @@ -set PYTHONPATH=c:/python27-64/lib -c:/python27-64/python.exe c:/Builds/check_and_build.py %1 %2 %3 %4 %4 %6 %7 %8 %9 diff --git a/scripts/windows_builder/check_and_build.py b/scripts/windows_builder/check_and_build.py deleted file mode 100644 index 2eb32fb4265d9..0000000000000 --- a/scripts/windows_builder/check_and_build.py +++ /dev/null @@ -1,194 +0,0 @@ -import datetime -import git -import logging -import os, re, time -import subprocess -import argparse -import pysftp - -# parse the args -parser = argparse.ArgumentParser(description='build, test, and install updated versions of master pandas') -parser.add_argument('-b', '--build', - help='run just this build', - dest='build') -parser.add_argument('-u', '--update', - help='get a git update', - dest='update', - action='store_true', - default=False) -parser.add_argument('-t', '--test', - help='run the tests', - dest='test', - action='store_true', - default=False) -parser.add_argument('-c', '--compare', - help='show the last tests compare', - dest='compare', - action='store_true', - default=False) -parser.add_argument('-v', '--version', - help='show the last versions', - dest='version', - action='store_true', - default=False) -parser.add_argument('-i', '--install', - help='run the install', - dest='install', - action='store_true', - default=False) -parser.add_argument('--dry', - help='dry run', - dest='dry', - action='store_true', - default=False) - -args = parser.parse_args() -dry_run = args.dry - -builds = ['27-32','27-64','34-32','34-64'] -base_dir = "C:\Users\Jeff Reback\Documents\GitHub\pandas" -remote_host='pandas.pydata.org' -username='pandas' -password=############ - -# drop python from our environment to avoid -# passing this onto sub-processes -env = os.environ -del env['PYTHONPATH'] - -# the stdout logger -fmt = '%(asctime)s: %(message)s' -logger = logging.getLogger('check_and_build') -logger.setLevel(logging.DEBUG) -stream_handler = logging.StreamHandler() -stream_handler.setFormatter(logging.Formatter(fmt)) -logger.addHandler(stream_handler) - -def run_all(test=False,compare=False,install=False,version=False,build=None): - # run everything - - for b in builds: - if build is not None and build != b: - continue - if test: - do_rebuild(b) - if compare or test: - try: - do_compare(b) - except (Exception) as e: - logger.info("ERROR COMPARE {0} : {1}".format(b,e)) - if version: - try: - do_version(b) - except (Exception) as e: - logger.info("ERROR VERSION {0} : {1}".format(b,e)) - - if install: - run_install() - -def do_rebuild(build): - # trigger the rebuild - - cmd = "c:/Builds/build_{0}.bat".format(build) - logger.info("rebuild : {0}".format(cmd)) - p = subprocess.Popen("start /wait /min {0}".format(cmd),env=env,shell=True,close_fds=True) - ret = p.wait() - -def do_compare(build): - # print the test outputs - - f = os.path.join(base_dir,"test.{0}.log".format(build)) - with open(f,'r') as fh: - for l in fh: - l = l.rstrip() - if l.startswith('ERROR:'): - logger.info("{0} : {1}".format(build,l)) - if l.startswith('Ran') or l.startswith('OK') or l.startswith('FAIL'): - logger.info("{0} : {1}".format(build,l)) - -def do_version(build): - # print the version strings - - f = os.path.join(base_dir,"versions.{0}.log".format(build)) - with open(f,'r') as fh: - for l in fh: - l = l.rstrip() - logger.info("{0} : {1}".format(build,l)) - -def do_update(is_verbose=True): - # update git; return True if the commit has changed - - repo = git.Repo(base_dir) - master = repo.heads.master - origin = repo.remotes.origin - start_commit = master.commit - - if is_verbose: - logger.info("current commit : {0}".format(start_commit)) - - try: - origin.update() - except (Exception) as e: - logger.info("update exception : {0}".format(e)) - try: - origin.pull() - except (Exception) as e: - logger.info("pull exception : {0}".format(e)) - - result = start_commit != master.commit - if result: - if is_verbose: - logger.info("commits changed : {0} -> {1}".format(start_commit,master.commit)) - return result - -def run_install(): - # send the installation binaries - - repo = git.Repo(base_dir) - master = repo.heads.master - commit = master.commit - short_hash = str(commit)[:7] - - logger.info("sending files : {0}".format(commit)) - d = os.path.join(base_dir,"dist") - files = [ f for f in os.listdir(d) if re.search(short_hash,f) ] - srv = pysftp.Connection(host=remote_host,username=username,password=password) - srv.chdir("www/pandas-build/dev") - - # get current files - remote_files = set(srv.listdir(path='.')) - - for f in files: - if f not in remote_files: - logger.info("sending: {0}".format(f)) - local = os.path.join(d,f) - srv.put(localpath=local) - - srv.close() - logger.info("sending files: done") - -# just perform the action -if args.update or args.test or args.compare or args.install or args.version: - if args.update: - do_update() - run_all(test=args.test,compare=args.compare,install=args.install,version=args.version,build=args.build) - exit(0) - -# file logging -file_handler = logging.FileHandler("C:\Builds\logs\check_and_build.log") -file_handler.setFormatter(logging.Formatter(fmt)) -logger.addHandler(file_handler) - -logger.info("start") - -# main loop -while(True): - - if do_update(): - run_all(test=True,install=False) - - time.sleep(60*60) - -logger.info("exit") -file_handler.close() - diff --git a/scripts/windows_builder/readme.txt b/scripts/windows_builder/readme.txt deleted file mode 100644 index 789e2a9ee0c63..0000000000000 --- a/scripts/windows_builder/readme.txt +++ /dev/null @@ -1,17 +0,0 @@ -This is a collection of windows batch scripts (and a python script) -to rebuild the binaries, test, and upload the binaries for public distribution -upon a commit on github. - -Obviously requires that these be setup on windows -Requires an install of Windows SDK 3.5 and 4.0 -Full python installs for each version with the deps - -Currently supporting - -27-32,27-64,34-32,34-64 - -Note that 34 use the 4.0 SDK, while the other suse 3.5 SDK - -I installed these scripts in C:\Builds - -Installed libaries in C:\Installs diff --git a/setup.cfg b/setup.cfg index 8b32f0f62fe28..84b8f69a83f16 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,8 +12,43 @@ tag_prefix = v parentdir_prefix = pandas- [flake8] -ignore = E731,E402 max-line-length = 79 +ignore = + W503, # line break before binary operator + W504, # line break after binary operator + E402, # module level import not at top of file + E731, # do not assign a lambda expression, use a def + C406, # Unnecessary list literal - rewrite as a dict literal. + C408, # Unnecessary dict call - rewrite as a literal. + C409, # Unnecessary list passed to tuple() - rewrite as a tuple literal. + S001 # found modulo formatter (incorrect picks up mod operations) +exclude = + doc/sphinxext/*.py, + doc/build/*.py, + doc/temp/*.py, + .eggs/*.py, + versioneer.py, + env # exclude asv benchmark environments from linting + +[flake8-rst] +bootstrap = + import numpy as np + import pandas as pd + np # avoiding error when importing again numpy or pandas + pd # (in some cases we want to do it to show users) +ignore = E402, # module level import not at top of file + W503, # line break before binary operator + # Classes/functions in different blocks can generate those errors + E302, # expected 2 blank lines, found 0 + E305, # expected 2 blank lines after class or function definition, found 0 + # We use semicolon at the end to avoid displaying plot objects + E703, # statement ends with a semicolon + E711, # comparison to none should be 'if cond is none:' + +exclude = + doc/source/getting_started/basics.rst + doc/source/development/contributing_docstring.rst + [yapf] based_on_style = pep8 @@ -22,8 +57,119 @@ split_penalty_after_opening_bracket = 1000000 split_penalty_logical_operator = 30 [tool:pytest] -# TODO: Change all yield-based (nose-style) fixutures to pytest fixtures -# Silencing the warning until then +minversion = 4.0.2 testpaths = pandas markers = single: mark a test as single cpu only + slow: mark a test as slow + network: mark a test as network + db: tests requiring a database (mysql or postgres) + high_memory: mark a test as a high-memory only + clipboard: mark a pd.read_clipboard test +doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL +addopts = --strict-data-files +xfail_strict = True + +[coverage:run] +branch = False +omit = */tests/* +plugins = Cython.Coverage + +[coverage:report] +ignore_errors = False +show_missing = True +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + AbstractMethodError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +[coverage:html] +directory = coverage_html_report + +# To be kept consistent with "Import Formatting" section in contributing.rst +[isort] +known_pre_core=pandas._libs,pandas.util._*,pandas.compat,pandas.errors +known_dtypes=pandas.core.dtypes +known_post_core=pandas.tseries,pandas.io,pandas.plotting +sections=FUTURE,STDLIB,THIRDPARTY,PRE_CORE,DTYPES,FIRSTPARTY,POST_CORE,LOCALFOLDER + +known_first_party=pandas +known_third_party=Cython,numpy,dateutil,matplotlib,python-dateutil,pytz,pyarrow,pytest +multi_line_output=4 +force_grid_wrap=0 +combine_as_imports=True +force_sort_within_sections=True +skip= + pandas/core/api.py, + pandas/core/frame.py, + asv_bench/benchmarks/attrs_caching.py, + asv_bench/benchmarks/binary_ops.py, + asv_bench/benchmarks/categoricals.py, + asv_bench/benchmarks/ctors.py, + asv_bench/benchmarks/eval.py, + asv_bench/benchmarks/frame_ctor.py, + asv_bench/benchmarks/frame_methods.py, + asv_bench/benchmarks/gil.py, + asv_bench/benchmarks/groupby.py, + asv_bench/benchmarks/index_object.py, + asv_bench/benchmarks/indexing.py, + asv_bench/benchmarks/inference.py, + asv_bench/benchmarks/io/csv.py, + asv_bench/benchmarks/io/excel.py, + asv_bench/benchmarks/io/hdf.py, + asv_bench/benchmarks/io/json.py, + asv_bench/benchmarks/io/msgpack.py, + asv_bench/benchmarks/io/pickle.py, + asv_bench/benchmarks/io/sql.py, + asv_bench/benchmarks/io/stata.py, + asv_bench/benchmarks/join_merge.py, + asv_bench/benchmarks/multiindex_object.py, + asv_bench/benchmarks/panel_ctor.py, + asv_bench/benchmarks/panel_methods.py, + asv_bench/benchmarks/plotting.py, + asv_bench/benchmarks/reindex.py, + asv_bench/benchmarks/replace.py, + asv_bench/benchmarks/reshape.py, + asv_bench/benchmarks/rolling.py, + asv_bench/benchmarks/series_methods.py, + asv_bench/benchmarks/sparse.py, + asv_bench/benchmarks/stat_ops.py, + asv_bench/benchmarks/timeseries.py + asv_bench/benchmarks/pandas_vb_common.py + asv_bench/benchmarks/offset.py + asv_bench/benchmarks/dtypes.py + asv_bench/benchmarks/strings.py + asv_bench/benchmarks/period.py + pandas/__init__.py + pandas/plotting/__init__.py + pandas/tests/extension/decimal/__init__.py + pandas/tests/extension/base/__init__.py + pandas/io/msgpack/__init__.py + pandas/io/json/__init__.py + pandas/io/clipboard/__init__.py + pandas/io/excel/__init__.py + pandas/compat/__init__.py + pandas/compat/numpy/__init__.py + pandas/core/arrays/__init__.py + pandas/core/groupby/__init__.py + pandas/core/internals/__init__.py + pandas/api/__init__.py + pandas/api/extensions/__init__.py + pandas/api/types/__init__.py + pandas/_libs/__init__.py + pandas/_libs/tslibs/__init__.py + pandas/util/__init__.py + pandas/arrays/__init__.py diff --git a/setup.py b/setup.py index 1b471f76ac5e6..a83e07b50ed57 100755 --- a/setup.py +++ b/setup.py @@ -7,95 +7,66 @@ """ import os +from os.path import join as pjoin + +import pkg_resources +import platform +from distutils.sysconfig import get_config_var import sys import shutil -import warnings -import re -import platform from distutils.version import LooseVersion +from setuptools import setup, Command, find_packages + +# versioning +import versioneer +cmdclass = versioneer.get_cmdclass() + def is_platform_windows(): return sys.platform == 'win32' or sys.platform == 'cygwin' -def is_platform_linux(): - return sys.platform == 'linux2' def is_platform_mac(): return sys.platform == 'darwin' -# versioning -import versioneer -cmdclass = versioneer.get_cmdclass() -min_cython_ver = '0.23' +min_numpy_ver = '1.12.0' +setuptools_kwargs = { + 'install_requires': [ + 'python-dateutil >= 2.5.0', + 'pytz >= 2011k', + 'numpy >= {numpy_ver}'.format(numpy_ver=min_numpy_ver), + ], + 'setup_requires': ['numpy >= {numpy_ver}'.format(numpy_ver=min_numpy_ver)], + 'zip_safe': False, +} + + +min_cython_ver = '0.28.2' try: import Cython ver = Cython.__version__ + from Cython.Build import cythonize _CYTHON_INSTALLED = ver >= LooseVersion(min_cython_ver) except ImportError: _CYTHON_INSTALLED = False + cythonize = lambda x, *args, **kwargs: x # dummy func -try: - import pkg_resources - from setuptools import setup, Command - _have_setuptools = True -except ImportError: - # no setuptools installed - from distutils.core import setup, Command - _have_setuptools = False - -setuptools_kwargs = {} -min_numpy_ver = '1.7.0' -if sys.version_info[0] >= 3: - - setuptools_kwargs = { - 'zip_safe': False, - 'install_requires': ['python-dateutil >= 2', - 'pytz >= 2011k', - 'numpy >= %s' % min_numpy_ver], - 'setup_requires': ['numpy >= %s' % min_numpy_ver], - } - if not _have_setuptools: - sys.exit("need setuptools/distribute for Py3k" - "\n$ pip install distribute") - -else: - setuptools_kwargs = { - 'install_requires': ['python-dateutil', - 'pytz >= 2011k', - 'numpy >= %s' % min_numpy_ver], - 'setup_requires': ['numpy >= %s' % min_numpy_ver], - 'zip_safe': False, - } - - if not _have_setuptools: - try: - import numpy - import dateutil - setuptools_kwargs = {} - except ImportError: - sys.exit("install requires: 'python-dateutil < 2','numpy'." - " use pip or easy_install." - "\n $ pip install 'python-dateutil < 2' 'numpy'") - -from distutils.extension import Extension -from distutils.command.build import build -from distutils.command.build_ext import build_ext as _build_ext +# The import of Extension must be after the import of Cython, otherwise +# we do not get the appropriately patched class. +# See https://cython.readthedocs.io/en/latest/src/reference/compilation.html +from distutils.extension import Extension # noqa:E402 +from distutils.command.build import build # noqa:E402 try: if not _CYTHON_INSTALLED: raise ImportError('No supported version of Cython installed.') - try: - from Cython.Distutils.old_build_ext import old_build_ext as _build_ext - except ImportError: - # Pre 0.25 - from Cython.Distutils import build_ext as _build_ext + from Cython.Distutils.old_build_ext import old_build_ext as _build_ext cython = True except ImportError: + from distutils.command.build_ext import build_ext as _build_ext cython = False - - -if cython: +else: try: try: from Cython import Tempita as tempita @@ -106,20 +77,16 @@ def is_platform_mac(): 'pip install Tempita') -from os.path import join as pjoin - - _pxi_dep_template = { 'algos': ['_libs/algos_common_helper.pxi.in', - '_libs/algos_take_helper.pxi.in', '_libs/algos_rank_helper.pxi.in'], + '_libs/algos_take_helper.pxi.in', + '_libs/algos_rank_helper.pxi.in'], 'groupby': ['_libs/groupby_helper.pxi.in'], - 'join': ['_libs/join_helper.pxi.in', '_libs/join_func_helper.pxi.in'], - 'reshape': ['_libs/reshape_helper.pxi.in'], 'hashtable': ['_libs/hashtable_class_helper.pxi.in', - '_libs/hashtable_func_helper.pxi.in'], + '_libs/hashtable_func_helper.pxi.in'], 'index': ['_libs/index_class_helper.pxi.in'], - 'sparse': ['sparse/sparse_op_helper.pxi.in'], -} + 'sparse': ['_libs/sparse_op_helper.pxi.in'], + 'interval': ['_libs/intervaltree.pxi.in']} _pxifiles = [] _pxi_dep = {} @@ -130,37 +97,41 @@ def is_platform_mac(): class build_ext(_build_ext): - def build_extensions(self): - - # if builing from c files, don't need to - # generate template output - if cython: - for pxifile in _pxifiles: - # build pxifiles first, template extention must be .pxi.in - assert pxifile.endswith('.pxi.in') - outfile = pxifile[:-3] - - if (os.path.exists(outfile) and + @classmethod + def render_templates(cls, pxifiles): + for pxifile in pxifiles: + # build pxifiles first, template extension must be .pxi.in + assert pxifile.endswith('.pxi.in') + outfile = pxifile[:-3] + + if (os.path.exists(outfile) and os.stat(pxifile).st_mtime < os.stat(outfile).st_mtime): - # if .pxi.in is not updated, no need to output .pxi - continue + # if .pxi.in is not updated, no need to output .pxi + continue - with open(pxifile, "r") as f: - tmpl = f.read() - pyxcontent = tempita.sub(tmpl) + with open(pxifile, "r") as f: + tmpl = f.read() + pyxcontent = tempita.sub(tmpl) - with open(outfile, "w") as f: - f.write(pyxcontent) + with open(outfile, "w") as f: + f.write(pyxcontent) + + def build_extensions(self): + # if building from c files, don't need to + # generate template output + if cython: + self.render_templates(_pxifiles) numpy_incl = pkg_resources.resource_filename('numpy', 'core/include') for ext in self.extensions: - if hasattr(ext, 'include_dirs') and not numpy_incl in ext.include_dirs: + if (hasattr(ext, 'include_dirs') and + numpy_incl not in ext.include_dirs): ext.include_dirs.append(numpy_incl) _build_ext.build_extensions(self) -DESCRIPTION = ("Powerful data structures for data analysis, time series," +DESCRIPTION = ("Powerful data structures for data analysis, time series, " "and statistics") LONG_DESCRIPTION = """ **pandas** is a Python package providing fast, flexible, and expressive data @@ -224,10 +195,6 @@ def build_extensions(self): munging and cleaning data, analyzing / modeling it, then organizing the results of the analysis into a form suitable for plotting or tabular display. pandas is the ideal tool for all of these tasks. - -Note ----- -Windows binaries built against NumPy 1.8.1 """ DISTNAME = 'pandas' @@ -245,12 +212,12 @@ def build_extensions(self): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Programming Language :: Cython', - 'Topic :: Scientific/Engineering', -] + 'Topic :: Scientific/Engineering'] + class CleanCommand(Command): """Custom distutils command to clean the .so and .pyc files.""" @@ -262,24 +229,23 @@ def initialize_options(self): self._clean_me = [] self._clean_trees = [] - base = pjoin('pandas','_libs', 'src') - dt = pjoin(base,'datetime') - src = base - util = pjoin('pandas','util') - parser = pjoin(base,'parser') - ujson_python = pjoin(base,'ujson','python') - ujson_lib = pjoin(base,'ujson','lib') - self._clean_exclude = [pjoin(dt,'np_datetime.c'), - pjoin(dt,'np_datetime_strings.c'), - pjoin(src,'period_helper.c'), - pjoin(parser,'tokenizer.c'), - pjoin(parser,'io.c'), - pjoin(ujson_python,'ujson.c'), - pjoin(ujson_python,'objToJSON.c'), - pjoin(ujson_python,'JSONtoObj.c'), - pjoin(ujson_lib,'ultrajsonenc.c'), - pjoin(ujson_lib,'ultrajsondec.c'), - pjoin(util,'move.c'), + base = pjoin('pandas', '_libs', 'src') + tsbase = pjoin('pandas', '_libs', 'tslibs', 'src') + dt = pjoin(tsbase, 'datetime') + util = pjoin('pandas', 'util') + parser = pjoin(base, 'parser') + ujson_python = pjoin(base, 'ujson', 'python') + ujson_lib = pjoin(base, 'ujson', 'lib') + self._clean_exclude = [pjoin(dt, 'np_datetime.c'), + pjoin(dt, 'np_datetime_strings.c'), + pjoin(parser, 'tokenizer.c'), + pjoin(parser, 'io.c'), + pjoin(ujson_python, 'ujson.c'), + pjoin(ujson_python, 'objToJSON.c'), + pjoin(ujson_python, 'JSONtoObj.c'), + pjoin(ujson_lib, 'ultrajsonenc.c'), + pjoin(ujson_lib, 'ultrajsondec.c'), + pjoin(util, 'move.c'), ] for root, dirs, files in os.walk('pandas'): @@ -325,50 +291,71 @@ def run(self): # class as it encodes the version info sdist_class = cmdclass['sdist'] + class CheckSDist(sdist_class): """Custom sdist that ensures Cython has compiled all pyx files to c.""" _pyxfiles = ['pandas/_libs/lib.pyx', 'pandas/_libs/hashtable.pyx', 'pandas/_libs/tslib.pyx', - 'pandas/_libs/period.pyx', 'pandas/_libs/index.pyx', + 'pandas/_libs/internals.pyx', 'pandas/_libs/algos.pyx', 'pandas/_libs/join.pyx', - 'pandas/core/window.pyx', - 'pandas/sparse/sparse.pyx', - 'pandas/util/testing.pyx', - 'pandas/tools/hash.pyx', - 'pandas/io/parsers.pyx', + 'pandas/_libs/indexing.pyx', + 'pandas/_libs/interval.pyx', + 'pandas/_libs/hashing.pyx', + 'pandas/_libs/missing.pyx', + 'pandas/_libs/reduction.pyx', + 'pandas/_libs/testing.pyx', + 'pandas/_libs/skiplist.pyx', + 'pandas/_libs/sparse.pyx', + 'pandas/_libs/ops.pyx', + 'pandas/_libs/parsers.pyx', + 'pandas/_libs/tslibs/ccalendar.pyx', + 'pandas/_libs/tslibs/period.pyx', + 'pandas/_libs/tslibs/strptime.pyx', + 'pandas/_libs/tslibs/np_datetime.pyx', + 'pandas/_libs/tslibs/timedeltas.pyx', + 'pandas/_libs/tslibs/timestamps.pyx', + 'pandas/_libs/tslibs/timezones.pyx', + 'pandas/_libs/tslibs/conversion.pyx', + 'pandas/_libs/tslibs/fields.pyx', + 'pandas/_libs/tslibs/offsets.pyx', + 'pandas/_libs/tslibs/frequencies.pyx', + 'pandas/_libs/tslibs/resolution.pyx', + 'pandas/_libs/tslibs/parsing.pyx', + 'pandas/_libs/writers.pyx', 'pandas/io/sas/sas.pyx'] + _cpp_pyxfiles = ['pandas/_libs/window.pyx', + 'pandas/io/msgpack/_packer.pyx', + 'pandas/io/msgpack/_unpacker.pyx'] + def initialize_options(self): sdist_class.initialize_options(self) - ''' - self._pyxfiles = [] - for root, dirs, files in os.walk('pandas'): - for f in files: - if f.endswith('.pyx'): - self._pyxfiles.append(pjoin(root, f)) - ''' - def run(self): if 'cython' in cmdclass: self.run_command('cython') else: - for pyxfile in self._pyxfiles: - cfile = pyxfile[:-3] + 'c' - msg = "C-source file '%s' not found." % (cfile) +\ - " Run 'setup.py cython' before sdist." - assert os.path.isfile(cfile), msg + # If we are not running cython then + # compile the extensions correctly + pyx_files = [(self._pyxfiles, 'c'), (self._cpp_pyxfiles, 'cpp')] + + for pyxfiles, extension in pyx_files: + for pyxfile in pyxfiles: + sourcefile = pyxfile[:-3] + extension + msg = ("{extension}-source file '{source}' not found.\n" + "Run 'setup.py cython' before sdist.".format( + source=sourcefile, extension=extension)) + assert os.path.isfile(sourcefile), msg sdist_class.run(self) class CheckingBuildExt(build_ext): """ Subclass build_ext to get clearer report if Cython is necessary. - """ def check_cython_extensions(self, extensions): @@ -376,10 +363,10 @@ def check_cython_extensions(self, extensions): for src in ext.sources: if not os.path.exists(src): print("{}: -> [{}]".format(ext.name, ext.sources)) - raise Exception("""Cython-generated file '%s' not found. + raise Exception("""Cython-generated file '{src}' not found. Cython is required to compile pandas from a development branch. Please install Cython or download a release package of pandas. - """ % src) + """.format(src=src)) def build_extensions(self): self.check_cython_extensions(self.extensions) @@ -387,9 +374,11 @@ def build_extensions(self): class CythonCommand(build_ext): - """Custom distutils command subclassed from Cython.Distutils.build_ext + """ + Custom distutils command subclassed from Cython.Distutils.build_ext to compile pyx->c, and stop there. All this does is override the - C-compile method build_extension() with a no-op.""" + C-compile method build_extension() with a no-op. + """ def build_extension(self, ext): pass @@ -408,23 +397,10 @@ def finalize_options(self): def run(self): pass + cmdclass.update({'clean': CleanCommand, 'build': build}) -try: - from wheel.bdist_wheel import bdist_wheel - - class BdistWheel(bdist_wheel): - def get_tag(self): - tag = bdist_wheel.get_tag(self) - repl = 'macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64' - if tag[2] == 'macosx_10_6_intel': - tag = (tag[0], tag[1], repl) - return tag - cmdclass['bdist_wheel'] = BdistWheel -except ImportError: - pass - if cython: suffix = '.pyx' cmdclass['build_ext'] = CheckingBuildExt @@ -434,272 +410,334 @@ def get_tag(self): cmdclass['build_src'] = DummyBuildSrc cmdclass['build_ext'] = CheckingBuildExt -lib_depends = ['reduce', 'inference', 'properties'] +# ---------------------------------------------------------------------- +# Preparation of compiler arguments +if sys.byteorder == 'big': + endian_macro = [('__BIG_ENDIAN__', '1')] +else: + endian_macro = [('__LITTLE_ENDIAN__', '1')] -def srcpath(name=None, suffix='.pyx', subdir='src'): - return pjoin('pandas', subdir, name + suffix) -if suffix == '.pyx': - lib_depends = [srcpath(f, suffix='.pyx', subdir='_libs/src') for f in lib_depends] - lib_depends.append('pandas/_libs/src/util.pxd') +if is_platform_windows(): + extra_compile_args = [] else: - lib_depends = [] - plib_depends = [] + # args to ignore warnings + extra_compile_args = ['-Wno-unused-function'] + + +# For mac, ensure extensions are built for macos 10.9 when compiling on a +# 10.9 system or above, overriding distuitls behaviour which is to target +# the version that python was built for. This may be overridden by setting +# MACOSX_DEPLOYMENT_TARGET before calling setup.py +if is_platform_mac(): + if 'MACOSX_DEPLOYMENT_TARGET' not in os.environ: + current_system = LooseVersion(platform.mac_ver()[0]) + python_target = LooseVersion( + get_config_var('MACOSX_DEPLOYMENT_TARGET')) + if python_target < '10.9' and current_system >= '10.9': + os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.9' + + +# enable coverage by building cython files by setting the environment variable +# "PANDAS_CYTHON_COVERAGE" (with a Truthy value) or by running build_ext +# with `--with-cython-coverage`enabled +linetrace = os.environ.get('PANDAS_CYTHON_COVERAGE', False) +if '--with-cython-coverage' in sys.argv: + linetrace = True + sys.argv.remove('--with-cython-coverage') + +# Note: if not using `cythonize`, coverage can be enabled by +# pinning `ext.cython_directives = directives` to each ext in extensions. +# github.com/cython/cython/wiki/enhancements-compilerdirectives#in-setuppy +directives = {'linetrace': False, + 'language_level': 2} +macros = [] +if linetrace: + # https://pypkg.com/pypi/pytest-cython/f/tests/example-project/setup.py + directives['linetrace'] = True + macros = [('CYTHON_TRACE', '1'), ('CYTHON_TRACE_NOGIL', '1')] + +# in numpy>=1.16.0, silence build warnings about deprecated API usage +# we can't do anything about these warnings because they stem from +# cython+numpy version mismatches. +macros.append(('NPY_NO_DEPRECATED_API', '0')) + + +# ---------------------------------------------------------------------- +# Specification of Dependencies + +# TODO: Need to check to see if e.g. `linetrace` has changed and possibly +# re-compile. +def maybe_cythonize(extensions, *args, **kwargs): + """ + Render tempita templates before calling cythonize + """ + if len(sys.argv) > 1 and 'clean' in sys.argv: + # Avoid running cythonize on `python setup.py clean` + # See https://github.com/cython/cython/issues/1495 + return extensions + if not cython: + # Avoid trying to look up numpy when installing from sdist + # https://github.com/pandas-dev/pandas/issues/25193 + # TODO: See if this can be removed after pyproject.toml added. + return extensions + + numpy_incl = pkg_resources.resource_filename('numpy', 'core/include') + # TODO: Is this really necessary here? + for ext in extensions: + if (hasattr(ext, 'include_dirs') and + numpy_incl not in ext.include_dirs): + ext.include_dirs.append(numpy_incl) -common_include = ['pandas/_libs/src/klib', 'pandas/_libs/src'] + build_ext.render_templates(_pxifiles) + return cythonize(extensions, *args, **kwargs) -def pxd(name): - return os.path.abspath(pjoin('pandas', name + '.pxd')) +def srcpath(name=None, suffix='.pyx', subdir='src'): + return pjoin('pandas', subdir, name + suffix) -# args to ignore warnings -if is_platform_windows(): - extra_compile_args=[] -else: - extra_compile_args=['-Wno-unused-function'] -lib_depends = lib_depends + ['pandas/_libs/src/numpy_helper.h', - 'pandas/_libs/src/parse_helper.h', - 'pandas/_libs/src/compat_helper.h'] +common_include = ['pandas/_libs/src/klib', 'pandas/_libs/src'] +ts_include = ['pandas/_libs/tslibs/src', 'pandas/_libs/tslibs'] + +lib_depends = ['pandas/_libs/src/parse_helper.h', + 'pandas/_libs/src/compat_helper.h'] -tseries_depends = ['pandas/_libs/src/datetime/np_datetime.h', - 'pandas/_libs/src/datetime/np_datetime_strings.h', - 'pandas/_libs/src/datetime_helper.h', - 'pandas/_libs/src/period_helper.h', - 'pandas/_libs/src/datetime.pxd'] +np_datetime_headers = [ + 'pandas/_libs/tslibs/src/datetime/np_datetime.h', + 'pandas/_libs/tslibs/src/datetime/np_datetime_strings.h'] +np_datetime_sources = [ + 'pandas/_libs/tslibs/src/datetime/np_datetime.c', + 'pandas/_libs/tslibs/src/datetime/np_datetime_strings.c'] +tseries_depends = np_datetime_headers -# some linux distros require it -libraries = ['m'] if not is_platform_windows() else [] ext_data = { - '_libs.lib': {'pyxfile': '_libs/lib', - 'depends': lib_depends + tseries_depends}, - '_libs.hashtable': {'pyxfile': '_libs/hashtable', - 'pxdfiles': ['_libs/hashtable'], - 'depends': (['pandas/_libs/src/klib/khash_python.h'] - + _pxi_dep['hashtable'])}, - '_libs.tslib': {'pyxfile': '_libs/tslib', - 'pxdfiles': ['_libs/src/util', '_libs/lib'], - 'depends': tseries_depends, - 'sources': ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c', - 'pandas/_libs/src/period_helper.c']}, - '_libs.period': {'pyxfile': '_libs/period', - 'depends': tseries_depends, - 'sources': ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c', - 'pandas/_libs/src/period_helper.c']}, - '_libs.index': {'pyxfile': '_libs/index', - 'sources': ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c'], - 'pxdfiles': ['_libs/src/util', '_libs/hashtable'], - 'depends': _pxi_dep['index']}, - '_libs.algos': {'pyxfile': '_libs/algos', - 'pxdfiles': ['_libs/src/util', '_libs/algos', '_libs/hashtable'], - 'depends': _pxi_dep['algos']}, - '_libs.groupby': {'pyxfile': '_libs/groupby', - 'pxdfiles': ['_libs/src/util', '_libs/algos'], - 'depends': _pxi_dep['groupby']}, - '_libs.join': {'pyxfile': '_libs/join', - 'pxdfiles': ['_libs/src/util', '_libs/hashtable'], - 'depends': _pxi_dep['join']}, - '_libs.reshape': {'pyxfile': '_libs/reshape', - 'depends': _pxi_dep['reshape']}, - 'core.libwindow': {'pyxfile': 'core/window', - 'pxdfiles': ['_libs/src/skiplist', '_libs/src/util'], - 'depends': ['pandas/_libs/src/skiplist.pyx', - 'pandas/_libs/src/skiplist.h']}, - 'io.libparsers': {'pyxfile': 'io/parsers', - 'depends': ['pandas/_libs/src/parser/tokenizer.h', - 'pandas/_libs/src/parser/io.h', - 'pandas/_libs/src/numpy_helper.h'], - 'sources': ['pandas/_libs/src/parser/tokenizer.c', - 'pandas/_libs/src/parser/io.c']}, - 'sparse.libsparse': {'pyxfile': 'sparse/sparse', - 'depends': (['pandas/sparse/sparse.pyx'] + - _pxi_dep['sparse'])}, - 'util.libtesting': {'pyxfile': 'util/testing', - 'depends': ['pandas/util/testing.pyx']}, - 'tools.libhashing': {'pyxfile': 'tools/hashing', - 'depends': ['pandas/tools/hashing.pyx']}, - 'io.sas.libsas': {'pyxfile': 'io/sas/sas'}, + '_libs.algos': { + 'pyxfile': '_libs/algos', + 'depends': _pxi_dep['algos']}, + '_libs.groupby': { + 'pyxfile': '_libs/groupby', + 'depends': _pxi_dep['groupby']}, + '_libs.hashing': { + 'pyxfile': '_libs/hashing', + 'include': [], + 'depends': []}, + '_libs.hashtable': { + 'pyxfile': '_libs/hashtable', + 'depends': (['pandas/_libs/src/klib/khash_python.h'] + + _pxi_dep['hashtable'])}, + '_libs.index': { + 'pyxfile': '_libs/index', + 'include': common_include + ts_include, + 'depends': _pxi_dep['index'], + 'sources': np_datetime_sources}, + '_libs.indexing': { + 'pyxfile': '_libs/indexing'}, + '_libs.internals': { + 'pyxfile': '_libs/internals'}, + '_libs.interval': { + 'pyxfile': '_libs/interval', + 'depends': _pxi_dep['interval']}, + '_libs.join': { + 'pyxfile': '_libs/join'}, + '_libs.lib': { + 'pyxfile': '_libs/lib', + 'include': common_include + ts_include, + 'depends': lib_depends + tseries_depends}, + '_libs.missing': { + 'pyxfile': '_libs/missing', + 'include': common_include + ts_include, + 'depends': tseries_depends}, + '_libs.parsers': { + 'pyxfile': '_libs/parsers', + 'depends': ['pandas/_libs/src/parser/tokenizer.h', + 'pandas/_libs/src/parser/io.h'], + 'sources': ['pandas/_libs/src/parser/tokenizer.c', + 'pandas/_libs/src/parser/io.c']}, + '_libs.reduction': { + 'pyxfile': '_libs/reduction'}, + '_libs.ops': { + 'pyxfile': '_libs/ops'}, + '_libs.properties': { + 'pyxfile': '_libs/properties', + 'include': []}, + '_libs.reshape': { + 'pyxfile': '_libs/reshape', + 'depends': []}, + '_libs.skiplist': { + 'pyxfile': '_libs/skiplist', + 'depends': ['pandas/_libs/src/skiplist.h']}, + '_libs.sparse': { + 'pyxfile': '_libs/sparse', + 'depends': _pxi_dep['sparse']}, + '_libs.tslib': { + 'pyxfile': '_libs/tslib', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.ccalendar': { + 'pyxfile': '_libs/tslibs/ccalendar', + 'include': []}, + '_libs.tslibs.conversion': { + 'pyxfile': '_libs/tslibs/conversion', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.fields': { + 'pyxfile': '_libs/tslibs/fields', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.frequencies': { + 'pyxfile': '_libs/tslibs/frequencies', + 'include': []}, + '_libs.tslibs.nattype': { + 'pyxfile': '_libs/tslibs/nattype', + 'include': []}, + '_libs.tslibs.np_datetime': { + 'pyxfile': '_libs/tslibs/np_datetime', + 'include': ts_include, + 'depends': np_datetime_headers, + 'sources': np_datetime_sources}, + '_libs.tslibs.offsets': { + 'pyxfile': '_libs/tslibs/offsets', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.parsing': { + 'pyxfile': '_libs/tslibs/parsing', + 'include': []}, + '_libs.tslibs.period': { + 'pyxfile': '_libs/tslibs/period', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.resolution': { + 'pyxfile': '_libs/tslibs/resolution', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.strptime': { + 'pyxfile': '_libs/tslibs/strptime', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.timedeltas': { + 'pyxfile': '_libs/tslibs/timedeltas', + 'include': ts_include, + 'depends': np_datetime_headers, + 'sources': np_datetime_sources}, + '_libs.tslibs.timestamps': { + 'pyxfile': '_libs/tslibs/timestamps', + 'include': ts_include, + 'depends': tseries_depends, + 'sources': np_datetime_sources}, + '_libs.tslibs.timezones': { + 'pyxfile': '_libs/tslibs/timezones', + 'include': []}, + '_libs.testing': { + 'pyxfile': '_libs/testing'}, + '_libs.window': { + 'pyxfile': '_libs/window', + 'language': 'c++', + 'suffix': '.cpp'}, + '_libs.writers': { + 'pyxfile': '_libs/writers'}, + 'io.sas._sas': { + 'pyxfile': 'io/sas/sas'}, + 'io.msgpack._packer': { + 'macros': endian_macro + macros, + 'depends': ['pandas/_libs/src/msgpack/pack.h', + 'pandas/_libs/src/msgpack/pack_template.h'], + 'include': ['pandas/_libs/src/msgpack'] + common_include, + 'language': 'c++', + 'suffix': '.cpp', + 'pyxfile': 'io/msgpack/_packer', + 'subdir': 'io/msgpack'}, + 'io.msgpack._unpacker': { + 'depends': ['pandas/_libs/src/msgpack/unpack.h', + 'pandas/_libs/src/msgpack/unpack_define.h', + 'pandas/_libs/src/msgpack/unpack_template.h'], + 'macros': endian_macro + macros, + 'include': ['pandas/_libs/src/msgpack'] + common_include, + 'language': 'c++', + 'suffix': '.cpp', + 'pyxfile': 'io/msgpack/_unpacker', + 'subdir': 'io/msgpack' } +} extensions = [] for name, data in ext_data.items(): - sources = [srcpath(data['pyxfile'], suffix=suffix, subdir='')] - pxds = [pxd(x) for x in data.get('pxdfiles', [])] - if suffix == '.pyx' and pxds: - sources.extend(pxds) + source_suffix = suffix if suffix == '.pyx' else data.get('suffix', '.c') + + sources = [srcpath(data['pyxfile'], suffix=source_suffix, subdir='')] sources.extend(data.get('sources', [])) include = data.get('include', common_include) - obj = Extension('pandas.%s' % name, + obj = Extension('pandas.{name}'.format(name=name), sources=sources, depends=data.get('depends', []), include_dirs=include, + language=data.get('language', 'c'), + define_macros=data.get('macros', macros), extra_compile_args=extra_compile_args) extensions.append(obj) - -#---------------------------------------------------------------------- -# msgpack - -if sys.byteorder == 'big': - macros = [('__BIG_ENDIAN__', '1')] -else: - macros = [('__LITTLE_ENDIAN__', '1')] - -packer_ext = Extension('pandas.io.msgpack._packer', - depends=['pandas/_libs/src/msgpack/pack.h', - 'pandas/_libs/src/msgpack/pack_template.h'], - sources = [srcpath('_packer', - suffix=suffix if suffix == '.pyx' else '.cpp', - subdir='io/msgpack')], - language='c++', - include_dirs=['pandas/_libs/src/msgpack'] + common_include, - define_macros=macros, - extra_compile_args=extra_compile_args) -unpacker_ext = Extension('pandas.io.msgpack._unpacker', - depends=['pandas/_libs/src/msgpack/unpack.h', - 'pandas/_libs/src/msgpack/unpack_define.h', - 'pandas/_libs/src/msgpack/unpack_template.h'], - sources = [srcpath('_unpacker', - suffix=suffix if suffix == '.pyx' else '.cpp', - subdir='io/msgpack')], - language='c++', - include_dirs=['pandas/_libs/src/msgpack'] + common_include, - define_macros=macros, - extra_compile_args=extra_compile_args) -extensions.append(packer_ext) -extensions.append(unpacker_ext) - -#---------------------------------------------------------------------- +# ---------------------------------------------------------------------- # ujson -if suffix == '.pyx' and 'setuptools' in sys.modules: +if suffix == '.pyx': # undo dumb setuptools bug clobbering .pyx sources back to .c for ext in extensions: - if ext.sources[0].endswith(('.c','.cpp')): + if ext.sources[0].endswith(('.c', '.cpp')): root, _ = os.path.splitext(ext.sources[0]) ext.sources[0] = root + suffix -ujson_ext = Extension('pandas.io.json.libjson', - depends=['pandas/_libs/src/ujson/lib/ultrajson.h', - 'pandas/_libs/src/datetime_helper.h', - 'pandas/_libs/src/numpy_helper.h'], - sources=['pandas/_libs/src/ujson/python/ujson.c', - 'pandas/_libs/src/ujson/python/objToJSON.c', - 'pandas/_libs/src/ujson/python/JSONtoObj.c', - 'pandas/_libs/src/ujson/lib/ultrajsonenc.c', - 'pandas/_libs/src/ujson/lib/ultrajsondec.c', - 'pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c'], +ujson_ext = Extension('pandas._libs.json', + depends=['pandas/_libs/src/ujson/lib/ultrajson.h'], + sources=(['pandas/_libs/src/ujson/python/ujson.c', + 'pandas/_libs/src/ujson/python/objToJSON.c', + 'pandas/_libs/src/ujson/python/JSONtoObj.c', + 'pandas/_libs/src/ujson/lib/ultrajsonenc.c', + 'pandas/_libs/src/ujson/lib/ultrajsondec.c'] + + np_datetime_sources), include_dirs=['pandas/_libs/src/ujson/python', 'pandas/_libs/src/ujson/lib', - 'pandas/_libs/src/datetime'] + common_include, - extra_compile_args=['-D_GNU_SOURCE'] + extra_compile_args) + 'pandas/_libs/src/datetime'], + extra_compile_args=(['-D_GNU_SOURCE'] + + extra_compile_args), + define_macros=macros) extensions.append(ujson_ext) -#---------------------------------------------------------------------- +# ---------------------------------------------------------------------- # util # extension for pseudo-safely moving bytes into mutable buffers _move_ext = Extension('pandas.util._move', depends=[], - sources=['pandas/util/move.c']) + sources=['pandas/util/move.c'], + define_macros=macros) extensions.append(_move_ext) - -if _have_setuptools: - setuptools_kwargs["test_suite"] = "nose.collector" - # The build cache system does string matching below this point. # if you change something, be careful. setup(name=DISTNAME, maintainer=AUTHOR, version=versioneer.get_version(), - packages=['pandas', - 'pandas.api', - 'pandas.api.types', - 'pandas.compat', - 'pandas.compat.numpy', - 'pandas.computation', - 'pandas.core', - 'pandas.indexes', - 'pandas.io', - 'pandas.io.json', - 'pandas.io.sas', - 'pandas.io.msgpack', - 'pandas._libs', - 'pandas.formats', - 'pandas.sparse', - 'pandas.stats', - 'pandas.util', - 'pandas.tests', - 'pandas.tests.api', - 'pandas.tests.computation', - 'pandas.tests.frame', - 'pandas.tests.indexes', - 'pandas.tests.indexes.datetimes', - 'pandas.tests.indexes.timedeltas', - 'pandas.tests.indexes.period', - 'pandas.tests.io', - 'pandas.tests.io.json', - 'pandas.tests.io.parser', - 'pandas.tests.io.sas', - 'pandas.tests.io.msgpack', - 'pandas.tests.groupby', - 'pandas.tests.series', - 'pandas.tests.formats', - 'pandas.tests.scalar', - 'pandas.tests.sparse', - 'pandas.tests.tseries', - 'pandas.tests.tools', - 'pandas.tests.types', - 'pandas.tests.plotting', - 'pandas.tools', - 'pandas.tseries', - 'pandas.types', - 'pandas.util.clipboard' - ], - package_data={'pandas.tests': ['data/*.csv'], - 'pandas.tests.formats': ['data/*.csv'], - 'pandas.tests.indexes': ['data/*.pickle'], - 'pandas.tests.io': ['data/legacy_hdf/*.h5', - 'data/legacy_pickle/*/*.pickle', - 'data/legacy_msgpack/*/*.msgpack', - 'data/*.csv*', - 'data/*.dta', - 'data/*.pickle', - 'data/*.txt', - 'data/*.xls', - 'data/*.xlsx', - 'data/*.xlsm', - 'data/*.table', - 'parser/data/*.csv', - 'parser/data/*.gz', - 'parser/data/*.bz2', - 'parser/data/*.txt', - 'sas/data/*.csv', - 'sas/data/*.xpt', - 'sas/data/*.sas7bdat', - 'data/*.html', - 'data/html_encoding/*.html', - 'json/data/*.json'], - 'pandas.tests.tools': ['data/*.csv'], - 'pandas.tests.tseries': ['data/*.pickle'] - }, - ext_modules=extensions, + packages=find_packages(include=['pandas', 'pandas.*']), + package_data={'': ['templates/*', '_libs/*.dll']}, + ext_modules=maybe_cythonize(extensions, compiler_directives=directives), maintainer_email=EMAIL, description=DESCRIPTION, license=LICENSE, @@ -709,4 +747,5 @@ def pxd(name): long_description=LONG_DESCRIPTION, classifiers=CLASSIFIERS, platforms='any', + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', **setuptools_kwargs) diff --git a/test.bat b/test.bat index 080a1cc163a05..e07c84f257a69 100644 --- a/test.bat +++ b/test.bat @@ -1,3 +1,3 @@ :: test on windows -pytest --skip-slow --skip-network pandas %* +pytest --skip-slow --skip-network pandas -n 2 -r sxX --strict %* diff --git a/test.sh b/test.sh index 23c7ff52d2ce9..1255a39816f78 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ #!/bin/sh command -v coverage >/dev/null && coverage erase command -v python-coverage >/dev/null && python-coverage erase -pytest pandas --cov=pandas +pytest pandas --cov=pandas -r sxX --strict diff --git a/test_fast.bat b/test_fast.bat index 17dc54b580137..f2c4e9fa71fcd 100644 --- a/test_fast.bat +++ b/test_fast.bat @@ -1,3 +1,3 @@ :: test on windows set PYTHONHASHSEED=314159265 -pytest --skip-slow --skip-network -m "not single" -n 4 pandas +pytest --skip-slow --skip-network --skip-db -m "not single" -n 4 -r sXX --strict pandas diff --git a/test_fast.sh b/test_fast.sh index 9b984156a796c..0a47f9de600ea 100755 --- a/test_fast.sh +++ b/test_fast.sh @@ -5,4 +5,4 @@ # https://github.com/pytest-dev/pytest/issues/1075 export PYTHONHASHSEED=$(python -c 'import random; print(random.randint(1, 4294967295))') -pytest pandas --skip-slow --skip-network -m "not single" -n 4 "$@" +pytest pandas --skip-slow --skip-network --skip-db -m "not single" -n 4 -r sxX --strict "$@" diff --git a/test_perf.sh b/test_perf.sh deleted file mode 100755 index 022de25bca8fc..0000000000000 --- a/test_perf.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -CURDIR=$(pwd) -BASEDIR=$(cd "$(dirname "$0")"; pwd) -python "$BASEDIR"/vb_suite/test_perf.py $@ diff --git a/tox.ini b/tox.ini index 85c5d90fde7fb..f055251581a93 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, py35 +envlist = py27, py35, py36 [testenv] deps = @@ -19,6 +19,7 @@ deps = xlrd six sqlalchemy + moto # cd to anything but the default {toxinidir} which # contains the pandas subdirectory and confuses @@ -49,14 +50,14 @@ deps = bigquery {[testenv]deps} -[testenv:py34] +[testenv:py35] deps = - numpy==1.8.0 + numpy==1.10.0 {[testenv]deps} -[testenv:py35] +[testenv:py36] deps = - numpy==1.10.0 + numpy {[testenv]deps} [testenv:openpyxl1] diff --git a/vb_suite/.gitignore b/vb_suite/.gitignore deleted file mode 100644 index cc110f04e1225..0000000000000 --- a/vb_suite/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -benchmarks.db -build/* -source/vbench/* -source/*.rst \ No newline at end of file diff --git a/vb_suite/attrs_caching.py b/vb_suite/attrs_caching.py deleted file mode 100644 index a7e3ed7094ed6..0000000000000 --- a/vb_suite/attrs_caching.py +++ /dev/null @@ -1,20 +0,0 @@ -from vbench.benchmark import Benchmark - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# DataFrame.index / columns property lookup time - -setup = common_setup + """ -df = DataFrame(np.random.randn(10, 6)) -cur_index = df.index -""" -stmt = "foo = df.index" - -getattr_dataframe_index = Benchmark(stmt, setup, - name="getattr_dataframe_index") - -stmt = "df.index = cur_index" -setattr_dataframe_index = Benchmark(stmt, setup, - name="setattr_dataframe_index") diff --git a/vb_suite/binary_ops.py b/vb_suite/binary_ops.py deleted file mode 100644 index 7c821374a83ab..0000000000000 --- a/vb_suite/binary_ops.py +++ /dev/null @@ -1,199 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -SECTION = 'Binary ops' - -#---------------------------------------------------------------------- -# binary ops - -#---------------------------------------------------------------------- -# add - -setup = common_setup + """ -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -""" -frame_add = \ - Benchmark("df + df2", setup, name='frame_add', - start_date=datetime(2012, 1, 1)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_numexpr_threads(1) -""" - -frame_add_st = \ - Benchmark("df + df2", setup, name='frame_add_st',cleanup="expr.set_numexpr_threads()", - start_date=datetime(2013, 2, 26)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_use_numexpr(False) -""" -frame_add_no_ne = \ - Benchmark("df + df2", setup, name='frame_add_no_ne',cleanup="expr.set_use_numexpr(True)", - start_date=datetime(2013, 2, 26)) - -#---------------------------------------------------------------------- -# mult - -setup = common_setup + """ -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -""" -frame_mult = \ - Benchmark("df * df2", setup, name='frame_mult', - start_date=datetime(2012, 1, 1)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_numexpr_threads(1) -""" -frame_mult_st = \ - Benchmark("df * df2", setup, name='frame_mult_st',cleanup="expr.set_numexpr_threads()", - start_date=datetime(2013, 2, 26)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_use_numexpr(False) -""" -frame_mult_no_ne = \ - Benchmark("df * df2", setup, name='frame_mult_no_ne',cleanup="expr.set_use_numexpr(True)", - start_date=datetime(2013, 2, 26)) - -#---------------------------------------------------------------------- -# division - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000, 1000)) -""" -frame_float_div_by_zero = \ - Benchmark("df / 0", setup, name='frame_float_div_by_zero') - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000, 1000)) -""" -frame_float_floor_by_zero = \ - Benchmark("df // 0", setup, name='frame_float_floor_by_zero') - -setup = common_setup + """ -df = DataFrame(np.random.random_integers(np.iinfo(np.int16).min, np.iinfo(np.int16).max, size=(1000, 1000))) -""" -frame_int_div_by_zero = \ - Benchmark("df / 0", setup, name='frame_int_div_by_zero') - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000, 1000)) -df2 = DataFrame(np.random.randn(1000, 1000)) -""" -frame_float_div = \ - Benchmark("df // df2", setup, name='frame_float_div') - -#---------------------------------------------------------------------- -# modulo - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000, 1000)) -df2 = DataFrame(np.random.randn(1000, 1000)) -""" -frame_float_mod = \ - Benchmark("df / df2", setup, name='frame_float_mod') - -setup = common_setup + """ -df = DataFrame(np.random.random_integers(np.iinfo(np.int16).min, np.iinfo(np.int16).max, size=(1000, 1000))) -df2 = DataFrame(np.random.random_integers(np.iinfo(np.int16).min, np.iinfo(np.int16).max, size=(1000, 1000))) -""" -frame_int_mod = \ - Benchmark("df / df2", setup, name='frame_int_mod') - -#---------------------------------------------------------------------- -# multi and - -setup = common_setup + """ -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -""" -frame_multi_and = \ - Benchmark("df[(df>0) & (df2>0)]", setup, name='frame_multi_and', - start_date=datetime(2012, 1, 1)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_numexpr_threads(1) -""" -frame_multi_and_st = \ - Benchmark("df[(df>0) & (df2>0)]", setup, name='frame_multi_and_st',cleanup="expr.set_numexpr_threads()", - start_date=datetime(2013, 2, 26)) - -setup = common_setup + """ -import pandas.computation.expressions as expr -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -expr.set_use_numexpr(False) -""" -frame_multi_and_no_ne = \ - Benchmark("df[(df>0) & (df2>0)]", setup, name='frame_multi_and_no_ne',cleanup="expr.set_use_numexpr(True)", - start_date=datetime(2013, 2, 26)) - -#---------------------------------------------------------------------- -# timeseries - -setup = common_setup + """ -N = 1000000 -halfway = N // 2 - 1 -s = Series(date_range('20010101', periods=N, freq='T')) -ts = s[halfway] -""" - -timestamp_series_compare = Benchmark("ts >= s", setup, - start_date=datetime(2013, 9, 27)) -series_timestamp_compare = Benchmark("s <= ts", setup, - start_date=datetime(2012, 2, 21)) - -setup = common_setup + """ -N = 1000000 -s = Series(date_range('20010101', periods=N, freq='s')) -""" - -timestamp_ops_diff1 = Benchmark("s.diff()", setup, - start_date=datetime(2013, 1, 1)) -timestamp_ops_diff2 = Benchmark("s-s.shift()", setup, - start_date=datetime(2013, 1, 1)) - -#---------------------------------------------------------------------- -# timeseries with tz - -setup = common_setup + """ -N = 10000 -halfway = N // 2 - 1 -s = Series(date_range('20010101', periods=N, freq='T', tz='US/Eastern')) -ts = s[halfway] -""" - -timestamp_tz_series_compare = Benchmark("ts >= s", setup, - start_date=datetime(2013, 9, 27)) -series_timestamp_tz_compare = Benchmark("s <= ts", setup, - start_date=datetime(2012, 2, 21)) - -setup = common_setup + """ -N = 10000 -s = Series(date_range('20010101', periods=N, freq='s', tz='US/Eastern')) -""" - -timestamp_tz_ops_diff1 = Benchmark("s.diff()", setup, - start_date=datetime(2013, 1, 1)) -timestamp_tz_ops_diff2 = Benchmark("s-s.shift()", setup, - start_date=datetime(2013, 1, 1)) diff --git a/vb_suite/categoricals.py b/vb_suite/categoricals.py deleted file mode 100644 index a08d479df20cb..0000000000000 --- a/vb_suite/categoricals.py +++ /dev/null @@ -1,16 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# Series constructors - -setup = common_setup + """ -s = pd.Series(list('aabbcd') * 1000000).astype('category') -""" - -concat_categorical = \ - Benchmark("concat([s, s])", setup=setup, name='concat_categorical', - start_date=datetime(year=2015, month=7, day=15)) diff --git a/vb_suite/ctors.py b/vb_suite/ctors.py deleted file mode 100644 index 8123322383f0a..0000000000000 --- a/vb_suite/ctors.py +++ /dev/null @@ -1,39 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# Series constructors - -setup = common_setup + """ -data = np.random.randn(100) -index = Index(np.arange(100)) -""" - -ctor_series_ndarray = \ - Benchmark("Series(data, index=index)", setup=setup, - name='series_constructor_ndarray') - -setup = common_setup + """ -arr = np.random.randn(100, 100) -""" - -ctor_frame_ndarray = \ - Benchmark("DataFrame(arr)", setup=setup, - name='frame_constructor_ndarray') - -setup = common_setup + """ -data = np.array(['foo', 'bar', 'baz'], dtype=object) -""" - -ctor_index_array_string = Benchmark('Index(data)', setup=setup) - -# index constructors -setup = common_setup + """ -s = Series([Timestamp('20110101'),Timestamp('20120101'),Timestamp('20130101')]*1000) -""" -index_from_series_ctor = Benchmark('Index(s)', setup=setup) - -dtindex_from_series_ctor = Benchmark('DatetimeIndex(s)', setup=setup) diff --git a/vb_suite/eval.py b/vb_suite/eval.py deleted file mode 100644 index bf80aad956184..0000000000000 --- a/vb_suite/eval.py +++ /dev/null @@ -1,150 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -import pandas as pd -df = DataFrame(np.random.randn(20000, 100)) -df2 = DataFrame(np.random.randn(20000, 100)) -df3 = DataFrame(np.random.randn(20000, 100)) -df4 = DataFrame(np.random.randn(20000, 100)) -""" - -setup = common_setup + """ -import pandas.computation.expressions as expr -expr.set_numexpr_threads(1) -""" - -SECTION = 'Eval' - -#---------------------------------------------------------------------- -# binary ops - -#---------------------------------------------------------------------- -# add -eval_frame_add_all_threads = \ - Benchmark("pd.eval('df + df2 + df3 + df4')", common_setup, - name='eval_frame_add_all_threads', - start_date=datetime(2013, 7, 21)) - - - -eval_frame_add_one_thread = \ - Benchmark("pd.eval('df + df2 + df3 + df4')", setup, - name='eval_frame_add_one_thread', - start_date=datetime(2013, 7, 26)) - -eval_frame_add_python = \ - Benchmark("pd.eval('df + df2 + df3 + df4', engine='python')", common_setup, - name='eval_frame_add_python', start_date=datetime(2013, 7, 21)) - -eval_frame_add_python_one_thread = \ - Benchmark("pd.eval('df + df2 + df3 + df4', engine='python')", setup, - name='eval_frame_add_python_one_thread', - start_date=datetime(2013, 7, 26)) -#---------------------------------------------------------------------- -# mult - -eval_frame_mult_all_threads = \ - Benchmark("pd.eval('df * df2 * df3 * df4')", common_setup, - name='eval_frame_mult_all_threads', - start_date=datetime(2013, 7, 21)) - -eval_frame_mult_one_thread = \ - Benchmark("pd.eval('df * df2 * df3 * df4')", setup, - name='eval_frame_mult_one_thread', - start_date=datetime(2013, 7, 26)) - -eval_frame_mult_python = \ - Benchmark("pd.eval('df * df2 * df3 * df4', engine='python')", - common_setup, - name='eval_frame_mult_python', start_date=datetime(2013, 7, 21)) - -eval_frame_mult_python_one_thread = \ - Benchmark("pd.eval('df * df2 * df3 * df4', engine='python')", setup, - name='eval_frame_mult_python_one_thread', - start_date=datetime(2013, 7, 26)) - -#---------------------------------------------------------------------- -# multi and - -eval_frame_and_all_threads = \ - Benchmark("pd.eval('(df > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)')", - common_setup, - name='eval_frame_and_all_threads', - start_date=datetime(2013, 7, 21)) - -eval_frame_and_one_thread = \ - Benchmark("pd.eval('(df > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)')", setup, - name='eval_frame_and_one_thread', - start_date=datetime(2013, 7, 26)) - -eval_frame_and_python = \ - Benchmark("pd.eval('(df > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)', engine='python')", - common_setup, name='eval_frame_and_python', - start_date=datetime(2013, 7, 21)) - -eval_frame_and_one_thread = \ - Benchmark("pd.eval('(df > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)', engine='python')", - setup, - name='eval_frame_and_python_one_thread', - start_date=datetime(2013, 7, 26)) - -#-------------------------------------------------------------------- -# chained comp -eval_frame_chained_cmp_all_threads = \ - Benchmark("pd.eval('df < df2 < df3 < df4')", common_setup, - name='eval_frame_chained_cmp_all_threads', - start_date=datetime(2013, 7, 21)) - -eval_frame_chained_cmp_one_thread = \ - Benchmark("pd.eval('df < df2 < df3 < df4')", setup, - name='eval_frame_chained_cmp_one_thread', - start_date=datetime(2013, 7, 26)) - -eval_frame_chained_cmp_python = \ - Benchmark("pd.eval('df < df2 < df3 < df4', engine='python')", - common_setup, name='eval_frame_chained_cmp_python', - start_date=datetime(2013, 7, 26)) - -eval_frame_chained_cmp_one_thread = \ - Benchmark("pd.eval('df < df2 < df3 < df4', engine='python')", setup, - name='eval_frame_chained_cmp_python_one_thread', - start_date=datetime(2013, 7, 26)) - - -common_setup = """from .pandas_vb_common import * -""" - -setup = common_setup + """ -N = 1000000 -halfway = N // 2 - 1 -index = date_range('20010101', periods=N, freq='T') -s = Series(index) -ts = s.iloc[halfway] -""" - -series_setup = setup + """ -df = DataFrame({'dates': s.values}) -""" - -query_datetime_series = Benchmark("df.query('dates < @ts')", - series_setup, - start_date=datetime(2013, 9, 27)) - -index_setup = setup + """ -df = DataFrame({'a': np.random.randn(N)}, index=index) -""" - -query_datetime_index = Benchmark("df.query('index < @ts')", - index_setup, start_date=datetime(2013, 9, 27)) - -setup = setup + """ -N = 1000000 -df = DataFrame({'a': np.random.randn(N)}) -min_val = df['a'].min() -max_val = df['a'].max() -""" - -query_with_boolean_selection = Benchmark("df.query('(a >= @min_val) & (a <= @max_val)')", - setup, start_date=datetime(2013, 9, 27)) - diff --git a/vb_suite/frame_ctor.py b/vb_suite/frame_ctor.py deleted file mode 100644 index 0d57da7b88d3b..0000000000000 --- a/vb_suite/frame_ctor.py +++ /dev/null @@ -1,123 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime -try: - import pandas.tseries.offsets as offsets -except: - import pandas.core.datetools as offsets - -common_setup = """from .pandas_vb_common import * -try: - from pandas.tseries.offsets import * -except: - from pandas.core.datetools import * -""" - -#---------------------------------------------------------------------- -# Creation from nested dict - -setup = common_setup + """ -N, K = 5000, 50 -index = tm.makeStringIndex(N) -columns = tm.makeStringIndex(K) -frame = DataFrame(np.random.randn(N, K), index=index, columns=columns) - -try: - data = frame.to_dict() -except: - data = frame.toDict() - -some_dict = data.values()[0] -dict_list = [dict(zip(columns, row)) for row in frame.values] -""" - -frame_ctor_nested_dict = Benchmark("DataFrame(data)", setup) - -# From JSON-like stuff -frame_ctor_list_of_dict = Benchmark("DataFrame(dict_list)", setup, - start_date=datetime(2011, 12, 20)) - -series_ctor_from_dict = Benchmark("Series(some_dict)", setup) - -# nested dict, integer indexes, regression described in #621 -setup = common_setup + """ -data = dict((i,dict((j,float(j)) for j in range(100))) for i in xrange(2000)) -""" -frame_ctor_nested_dict_int64 = Benchmark("DataFrame(data)", setup) - -# dynamically generate benchmarks for every offset -# -# get_period_count & get_index_for_offset are there because blindly taking each -# offset times 1000 can easily go out of Timestamp bounds and raise errors. -dynamic_benchmarks = {} -n_steps = [1, 2] -offset_kwargs = {'WeekOfMonth': {'weekday': 1, 'week': 1}, - 'LastWeekOfMonth': {'weekday': 1, 'week': 1}, - 'FY5253': {'startingMonth': 1, 'weekday': 1}, - 'FY5253Quarter': {'qtr_with_extra_week': 1, 'startingMonth': 1, 'weekday': 1}} - -offset_extra_cases = {'FY5253': {'variation': ['nearest', 'last']}, - 'FY5253Quarter': {'variation': ['nearest', 'last']}} - -for offset in offsets.__all__: - for n in n_steps: - kwargs = {} - if offset in offset_kwargs: - kwargs = offset_kwargs[offset] - - if offset in offset_extra_cases: - extras = offset_extra_cases[offset] - else: - extras = {'': ['']} - - for extra_arg in extras: - for extra in extras[extra_arg]: - if extra: - kwargs[extra_arg] = extra - setup = common_setup + """ - -def get_period_count(start_date, off): - ten_offsets_in_days = ((start_date + off * 10) - start_date).days - if ten_offsets_in_days == 0: - return 1000 - else: - return min(9 * ((Timestamp.max - start_date).days // - ten_offsets_in_days), - 1000) - -def get_index_for_offset(off): - start_date = Timestamp('1/1/1900') - return date_range(start_date, - periods=min(1000, get_period_count(start_date, off)), - freq=off) - -idx = get_index_for_offset({}({}, **{})) -df = DataFrame(np.random.randn(len(idx),10), index=idx) -d = dict([ (col,df[col]) for col in df.columns ]) -""".format(offset, n, kwargs) - key = 'frame_ctor_dtindex_{}x{}'.format(offset, n) - if extra: - key += '__{}_{}'.format(extra_arg, extra) - dynamic_benchmarks[key] = Benchmark("DataFrame(d)", setup, name=key) - -# Have to stuff them in globals() so vbench detects them -globals().update(dynamic_benchmarks) - -# from a mi-series -setup = common_setup + """ -mi = MultiIndex.from_tuples([(x,y) for x in range(100) for y in range(100)]) -s = Series(randn(10000), index=mi) -""" -frame_from_series = Benchmark("DataFrame(s)", setup) - -#---------------------------------------------------------------------- -# get_numeric_data - -setup = common_setup + """ -df = DataFrame(randn(10000, 25)) -df['foo'] = 'bar' -df['bar'] = 'baz' -df = df.consolidate() -""" - -frame_get_numeric_data = Benchmark('df._get_numeric_data()', setup, - start_date=datetime(2011, 11, 1)) diff --git a/vb_suite/frame_methods.py b/vb_suite/frame_methods.py deleted file mode 100644 index 46343e9c607fd..0000000000000 --- a/vb_suite/frame_methods.py +++ /dev/null @@ -1,525 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# lookup - -setup = common_setup + """ -df = DataFrame(np.random.randn(10000, 8), columns=list('abcdefgh')) -df['foo'] = 'bar' - -row_labels = list(df.index[::10])[:900] -col_labels = list(df.columns) * 100 -row_labels_all = np.array(list(df.index) * len(df.columns), dtype='object') -col_labels_all = np.array(list(df.columns) * len(df.index), dtype='object') -""" - -frame_fancy_lookup = Benchmark('df.lookup(row_labels, col_labels)', setup, - start_date=datetime(2012, 1, 12)) - -frame_fancy_lookup_all = Benchmark('df.lookup(row_labels_all, col_labels_all)', - setup, - start_date=datetime(2012, 1, 12)) - -#---------------------------------------------------------------------- -# fillna in place - -setup = common_setup + """ -df = DataFrame(randn(10000, 100)) -df.values[::2] = np.nan -""" - -frame_fillna_inplace = Benchmark('df.fillna(0, inplace=True)', setup, - start_date=datetime(2012, 4, 4)) - - -#---------------------------------------------------------------------- -# reindex both axes - -setup = common_setup + """ -df = DataFrame(randn(10000, 10000)) -idx = np.arange(4000, 7000) -""" - -frame_reindex_axis0 = Benchmark('df.reindex(idx)', setup) - -frame_reindex_axis1 = Benchmark('df.reindex(columns=idx)', setup) - -frame_reindex_both_axes = Benchmark('df.reindex(index=idx, columns=idx)', - setup, start_date=datetime(2011, 1, 1)) - -frame_reindex_both_axes_ix = Benchmark('df.ix[idx, idx]', setup, - start_date=datetime(2011, 1, 1)) - -#---------------------------------------------------------------------- -# reindex with upcasts -setup = common_setup + """ -df=DataFrame(dict([(c, { - 0: randint(0, 2, 1000).astype(np.bool_), - 1: randint(0, 1000, 1000).astype(np.int16), - 2: randint(0, 1000, 1000).astype(np.int32), - 3: randint(0, 1000, 1000).astype(np.int64) - }[randint(0, 4)]) for c in range(1000)])) -""" - -frame_reindex_upcast = Benchmark('df.reindex(permutation(range(1200)))', setup) - -#---------------------------------------------------------------------- -# boolean indexing - -setup = common_setup + """ -df = DataFrame(randn(10000, 100)) -bool_arr = np.zeros(10000, dtype=bool) -bool_arr[:1000] = True -""" - -frame_boolean_row_select = Benchmark('df[bool_arr]', setup, - start_date=datetime(2011, 1, 1)) - -#---------------------------------------------------------------------- -# iteritems (monitor no-copying behaviour) - -setup = common_setup + """ -df = DataFrame(randn(10000, 1000)) -df2 = DataFrame(randn(3000,1),columns=['A']) -df3 = DataFrame(randn(3000,1)) - -def f(): - if hasattr(df, '_item_cache'): - df._item_cache.clear() - for name, col in df.iteritems(): - pass - -def g(): - for name, col in df.iteritems(): - pass - -def h(): - for i in range(10000): - df2['A'] - -def j(): - for i in range(10000): - df3[0] - -""" - -# as far back as the earliest test currently in the suite -frame_iteritems = Benchmark('f()', setup, - start_date=datetime(2010, 6, 1)) - -frame_iteritems_cached = Benchmark('g()', setup, - start_date=datetime(2010, 6, 1)) - -frame_getitem_single_column = Benchmark('h()', setup, - start_date=datetime(2010, 6, 1)) - -frame_getitem_single_column2 = Benchmark('j()', setup, - start_date=datetime(2010, 6, 1)) - -#---------------------------------------------------------------------- -# assignment - -setup = common_setup + """ -idx = date_range('1/1/2000', periods=100000, freq='D') -df = DataFrame(randn(100000, 1),columns=['A'],index=idx) -def f(df): - x = df.copy() - x['date'] = x.index -""" - -frame_assign_timeseries_index = Benchmark('f(df)', setup, - start_date=datetime(2013, 10, 1)) - - -#---------------------------------------------------------------------- -# to_string - -setup = common_setup + """ -df = DataFrame(randn(100, 10)) -""" - -frame_to_string_floats = Benchmark('df.to_string()', setup, - start_date=datetime(2010, 6, 1)) - -#---------------------------------------------------------------------- -# to_html - -setup = common_setup + """ -nrows=500 -df = DataFrame(randn(nrows, 10)) -df[0]=period_range("2000","2010",nrows) -df[1]=range(nrows) - -""" - -frame_to_html_mixed = Benchmark('df.to_html()', setup, - start_date=datetime(2011, 11, 18)) - - -# truncated repr_html, single index - -setup = common_setup + """ -nrows=10000 -data=randn(nrows,10) -idx=MultiIndex.from_arrays(np.tile(randn(3,nrows/100),100)) -df=DataFrame(data,index=idx) - -""" - -frame_html_repr_trunc_mi = Benchmark('df._repr_html_()', setup, - start_date=datetime(2013, 11, 25)) - -# truncated repr_html, MultiIndex - -setup = common_setup + """ -nrows=10000 -data=randn(nrows,10) -idx=randn(nrows) -df=DataFrame(data,index=idx) - -""" - -frame_html_repr_trunc_si = Benchmark('df._repr_html_()', setup, - start_date=datetime(2013, 11, 25)) - - -# insert many columns - -setup = common_setup + """ -N = 1000 - -def f(K=500): - df = DataFrame(index=range(N)) - new_col = np.random.randn(N) - for i in range(K): - df[i] = new_col -""" - -frame_insert_500_columns_end = Benchmark('f()', setup, start_date=datetime(2011, 1, 1)) - -setup = common_setup + """ -N = 1000 - -def f(K=100): - df = DataFrame(index=range(N)) - new_col = np.random.randn(N) - for i in range(K): - df.insert(0,i,new_col) -""" - -frame_insert_100_columns_begin = Benchmark('f()', setup, start_date=datetime(2011, 1, 1)) - -#---------------------------------------------------------------------- -# strings methods, #2602 - -setup = common_setup + """ -s = Series(['abcdefg', np.nan]*500000) -""" - -series_string_vector_slice = Benchmark('s.str[:5]', setup, - start_date=datetime(2012, 8, 1)) - -#---------------------------------------------------------------------- -# df.info() and get_dtype_counts() # 2807 - -setup = common_setup + """ -df = pandas.DataFrame(np.random.randn(10,10000)) -""" - -frame_get_dtype_counts = Benchmark('df.get_dtype_counts()', setup, - start_date=datetime(2012, 8, 1)) - -## -setup = common_setup + """ -df = pandas.DataFrame(np.random.randn(10,10000)) -""" - -frame_repr_wide = Benchmark('repr(df)', setup, - start_date=datetime(2012, 8, 1)) - -## -setup = common_setup + """ -df = pandas.DataFrame(np.random.randn(10000, 10)) -""" - -frame_repr_tall = Benchmark('repr(df)', setup, - start_date=datetime(2012, 8, 1)) - -## -setup = common_setup + """ -df = DataFrame(randn(100000, 1)) -""" - -frame_xs_row = Benchmark('df.xs(50000)', setup) - -## -setup = common_setup + """ -df = DataFrame(randn(1,100000)) -""" - -frame_xs_col = Benchmark('df.xs(50000,axis = 1)', setup) - -#---------------------------------------------------------------------- -# nulls/masking - -## masking -setup = common_setup + """ -data = np.random.randn(1000, 500) -df = DataFrame(data) -df = df.where(df > 0) # create nans -bools = df > 0 -mask = isnull(df) -""" - -frame_mask_bools = Benchmark('bools.mask(mask)', setup, - start_date=datetime(2013,1,1)) - -frame_mask_floats = Benchmark('bools.astype(float).mask(mask)', setup, - start_date=datetime(2013,1,1)) - -## isnull -setup = common_setup + """ -data = np.random.randn(1000, 1000) -df = DataFrame(data) -""" -frame_isnull = Benchmark('isnull(df)', setup, - start_date=datetime(2012,1,1)) - -## dropna -dropna_setup = common_setup + """ -data = np.random.randn(10000, 1000) -df = DataFrame(data) -df.ix[50:1000,20:50] = np.nan -df.ix[2000:3000] = np.nan -df.ix[:,60:70] = np.nan -""" -frame_dropna_axis0_any = Benchmark('df.dropna(how="any",axis=0)', dropna_setup, - start_date=datetime(2012,1,1)) -frame_dropna_axis0_all = Benchmark('df.dropna(how="all",axis=0)', dropna_setup, - start_date=datetime(2012,1,1)) - -frame_dropna_axis1_any = Benchmark('df.dropna(how="any",axis=1)', dropna_setup, - start_date=datetime(2012,1,1)) - -frame_dropna_axis1_all = Benchmark('df.dropna(how="all",axis=1)', dropna_setup, - start_date=datetime(2012,1,1)) - -# dropna on mixed dtypes -dropna_mixed_setup = common_setup + """ -data = np.random.randn(10000, 1000) -df = DataFrame(data) -df.ix[50:1000,20:50] = np.nan -df.ix[2000:3000] = np.nan -df.ix[:,60:70] = np.nan -df['foo'] = 'bar' -""" -frame_dropna_axis0_any_mixed_dtypes = Benchmark('df.dropna(how="any",axis=0)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) -frame_dropna_axis0_all_mixed_dtypes = Benchmark('df.dropna(how="all",axis=0)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) - -frame_dropna_axis1_any_mixed_dtypes = Benchmark('df.dropna(how="any",axis=1)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) - -frame_dropna_axis1_all_mixed_dtypes = Benchmark('df.dropna(how="all",axis=1)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) - -## dropna multi -dropna_setup = common_setup + """ -data = np.random.randn(10000, 1000) -df = DataFrame(data) -df.ix[50:1000,20:50] = np.nan -df.ix[2000:3000] = np.nan -df.ix[:,60:70] = np.nan -df.index = MultiIndex.from_tuples(df.index.map(lambda x: (x, x))) -df.columns = MultiIndex.from_tuples(df.columns.map(lambda x: (x, x))) -""" -frame_count_level_axis0_multi = Benchmark('df.count(axis=0, level=1)', dropna_setup, - start_date=datetime(2012,1,1)) - -frame_count_level_axis1_multi = Benchmark('df.count(axis=1, level=1)', dropna_setup, - start_date=datetime(2012,1,1)) - -# dropna on mixed dtypes -dropna_mixed_setup = common_setup + """ -data = np.random.randn(10000, 1000) -df = DataFrame(data) -df.ix[50:1000,20:50] = np.nan -df.ix[2000:3000] = np.nan -df.ix[:,60:70] = np.nan -df['foo'] = 'bar' -df.index = MultiIndex.from_tuples(df.index.map(lambda x: (x, x))) -df.columns = MultiIndex.from_tuples(df.columns.map(lambda x: (x, x))) -""" -frame_count_level_axis0_mixed_dtypes_multi = Benchmark('df.count(axis=0, level=1)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) - -frame_count_level_axis1_mixed_dtypes_multi = Benchmark('df.count(axis=1, level=1)', dropna_mixed_setup, - start_date=datetime(2012,1,1)) - -#---------------------------------------------------------------------- -# apply - -setup = common_setup + """ -s = Series(np.arange(1028.)) -df = DataFrame({ i:s for i in range(1028) }) -""" -frame_apply_user_func = Benchmark('df.apply(lambda x: np.corrcoef(x,s)[0,1])', setup, - name = 'frame_apply_user_func', - start_date=datetime(2012,1,1)) - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,100)) -""" -frame_apply_lambda_mean = Benchmark('df.apply(lambda x: x.sum())', setup, - name = 'frame_apply_lambda_mean', - start_date=datetime(2012,1,1)) -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,100)) -""" -frame_apply_np_mean = Benchmark('df.apply(np.mean)', setup, - name = 'frame_apply_np_mean', - start_date=datetime(2012,1,1)) - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,100)) -""" -frame_apply_pass_thru = Benchmark('df.apply(lambda x: x)', setup, - name = 'frame_apply_pass_thru', - start_date=datetime(2012,1,1)) - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,100)) -""" -frame_apply_axis_1 = Benchmark('df.apply(lambda x: x+1,axis=1)', setup, - name = 'frame_apply_axis_1', - start_date=datetime(2012,1,1)) - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,3),columns=list('ABC')) -""" -frame_apply_ref_by_name = Benchmark('df.apply(lambda x: x["A"] + x["B"],axis=1)', setup, - name = 'frame_apply_ref_by_name', - start_date=datetime(2012,1,1)) - -#---------------------------------------------------------------------- -# dtypes - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000,1000)) -""" -frame_dtypes = Benchmark('df.dtypes', setup, - start_date=datetime(2012,1,1)) - -#---------------------------------------------------------------------- -# equals -setup = common_setup + """ -def make_pair(frame): - df = frame - df2 = df.copy() - df2.ix[-1,-1] = np.nan - return df, df2 - -def test_equal(name): - df, df2 = pairs[name] - return df.equals(df) - -def test_unequal(name): - df, df2 = pairs[name] - return df.equals(df2) - -float_df = DataFrame(np.random.randn(1000, 1000)) -object_df = DataFrame([['foo']*1000]*1000) -nonunique_cols = object_df.copy() -nonunique_cols.columns = ['A']*len(nonunique_cols.columns) - -pairs = dict([(name, make_pair(frame)) - for name, frame in (('float_df', float_df), ('object_df', object_df), ('nonunique_cols', nonunique_cols))]) -""" -frame_float_equal = Benchmark('test_equal("float_df")', setup) -frame_object_equal = Benchmark('test_equal("object_df")', setup) -frame_nonunique_equal = Benchmark('test_equal("nonunique_cols")', setup) - -frame_float_unequal = Benchmark('test_unequal("float_df")', setup) -frame_object_unequal = Benchmark('test_unequal("object_df")', setup) -frame_nonunique_unequal = Benchmark('test_unequal("nonunique_cols")', setup) - -#----------------------------------------------------------------------------- -# interpolate -# this is the worst case, where every column has NaNs. -setup = common_setup + """ -df = DataFrame(randn(10000, 100)) -df.values[::2] = np.nan -""" - -frame_interpolate = Benchmark('df.interpolate()', setup, - start_date=datetime(2014, 2, 7)) - -setup = common_setup + """ -df = DataFrame({'A': np.arange(0, 10000), - 'B': np.random.randint(0, 100, 10000), - 'C': randn(10000), - 'D': randn(10000)}) -df.loc[1::5, 'A'] = np.nan -df.loc[1::5, 'C'] = np.nan -""" - -frame_interpolate_some_good = Benchmark('df.interpolate()', setup, - start_date=datetime(2014, 2, 7)) -frame_interpolate_some_good_infer = Benchmark('df.interpolate(downcast="infer")', - setup, - start_date=datetime(2014, 2, 7)) - - -#------------------------------------------------------------------------- -# frame shift speedup issue-5609 - -setup = common_setup + """ -df = DataFrame(np.random.rand(10000,500)) -# note: df._data.blocks are f_contigous -""" -frame_shift_axis0 = Benchmark('df.shift(1,axis=0)', setup, - start_date=datetime(2014,1,1)) -frame_shift_axis1 = Benchmark('df.shift(1,axis=1)', setup, - name = 'frame_shift_axis_1', - start_date=datetime(2014,1,1)) - - -#----------------------------------------------------------------------------- -# from_records issue-6700 - -setup = common_setup + """ -def get_data(n=100000): - return ((x, x*20, x*100) for x in range(n)) -""" - -frame_from_records_generator = Benchmark('df = DataFrame.from_records(get_data())', - setup, - name='frame_from_records_generator', - start_date=datetime(2013,10,4)) # issue-4911 - -frame_from_records_generator_nrows = Benchmark('df = DataFrame.from_records(get_data(), nrows=1000)', - setup, - name='frame_from_records_generator_nrows', - start_date=datetime(2013,10,04)) # issue-4911 - -#----------------------------------------------------------------------------- -# duplicated - -setup = common_setup + ''' -n = 1 << 20 - -t = date_range('2015-01-01', freq='S', periods=n // 64) -xs = np.random.randn(n // 64).round(2) - -df = DataFrame({'a':np.random.randint(- 1 << 8, 1 << 8, n), - 'b':np.random.choice(t, n), - 'c':np.random.choice(xs, n)}) -''' - -frame_duplicated = Benchmark('df.duplicated()', setup, - name='frame_duplicated') diff --git a/vb_suite/generate_rst_files.py b/vb_suite/generate_rst_files.py deleted file mode 100644 index 92e7cd4d59b71..0000000000000 --- a/vb_suite/generate_rst_files.py +++ /dev/null @@ -1,2 +0,0 @@ -from suite import benchmarks, generate_rst_files -generate_rst_files(benchmarks) diff --git a/vb_suite/gil.py b/vb_suite/gil.py deleted file mode 100644 index df2bd2dcd8db4..0000000000000 --- a/vb_suite/gil.py +++ /dev/null @@ -1,110 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -basic = common_setup + """ -try: - from pandas.util.testing import test_parallel - have_real_test_parallel = True -except ImportError: - have_real_test_parallel = False - def test_parallel(num_threads=1): - def wrapper(fname): - return fname - - return wrapper - -N = 1000000 -ngroups = 1000 -np.random.seed(1234) - -df = DataFrame({'key' : np.random.randint(0,ngroups,size=N), - 'data' : np.random.randn(N) }) - -if not have_real_test_parallel: - raise NotImplementedError -""" - -setup = basic + """ - -def f(): - df.groupby('key')['data'].sum() - -# run consecutivily -def g2(): - for i in range(2): - f() -def g4(): - for i in range(4): - f() -def g8(): - for i in range(8): - f() - -# run in parallel -@test_parallel(num_threads=2) -def pg2(): - f() - -@test_parallel(num_threads=4) -def pg4(): - f() - -@test_parallel(num_threads=8) -def pg8(): - f() - -""" - -nogil_groupby_sum_4 = Benchmark( - 'pg4()', setup, - start_date=datetime(2015, 1, 1)) - -nogil_groupby_sum_8 = Benchmark( - 'pg8()', setup, - start_date=datetime(2015, 1, 1)) - - -#### test all groupby funcs #### - -setup = basic + """ - -@test_parallel(num_threads=2) -def pg2(): - df.groupby('key')['data'].func() - -""" - -for f in ['sum','prod','var','count','min','max','mean','last']: - - name = "nogil_groupby_{f}_2".format(f=f) - bmark = Benchmark('pg2()', setup.replace('func',f), start_date=datetime(2015, 1, 1)) - bmark.name = name - globals()[name] = bmark - -del bmark - - -#### test take_1d #### -setup = basic + """ -from pandas.core import common as com - -N = 1e7 -df = DataFrame({'int64' : np.arange(N,dtype='int64'), - 'float64' : np.arange(N,dtype='float64')}) -indexer = np.arange(100,len(df)-100) - -@test_parallel(num_threads=2) -def take_1d_pg2_int64(): - com.take_1d(df.int64.values,indexer) - -@test_parallel(num_threads=2) -def take_1d_pg2_float64(): - com.take_1d(df.float64.values,indexer) - -""" - -nogil_take1d_float64 = Benchmark('take_1d_pg2_int64()', setup, start_date=datetime(2015, 1, 1)) -nogil_take1d_int64 = Benchmark('take_1d_pg2_float64()', setup, start_date=datetime(2015, 1, 1)) diff --git a/vb_suite/groupby.py b/vb_suite/groupby.py deleted file mode 100644 index 268d71f864823..0000000000000 --- a/vb_suite/groupby.py +++ /dev/null @@ -1,620 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -setup = common_setup + """ -N = 100000 -ngroups = 100 - -def get_test_data(ngroups=100, n=100000): - unique_groups = range(ngroups) - arr = np.asarray(np.tile(unique_groups, n / ngroups), dtype=object) - - if len(arr) < n: - arr = np.asarray(list(arr) + unique_groups[:n - len(arr)], - dtype=object) - - random.shuffle(arr) - return arr - -# aggregate multiple columns -df = DataFrame({'key1' : get_test_data(ngroups=ngroups), - 'key2' : get_test_data(ngroups=ngroups), - 'data1' : np.random.randn(N), - 'data2' : np.random.randn(N)}) -def f(): - df.groupby(['key1', 'key2']).agg(lambda x: x.values.sum()) - -simple_series = Series(np.random.randn(N)) -key1 = df['key1'] -""" - -stmt1 = "df.groupby(['key1', 'key2'])['data1'].agg(lambda x: x.values.sum())" -groupby_multi_python = Benchmark(stmt1, setup, - start_date=datetime(2011, 7, 1)) - -stmt3 = "df.groupby(['key1', 'key2']).sum()" -groupby_multi_cython = Benchmark(stmt3, setup, - start_date=datetime(2011, 7, 1)) - -stmt = "df.groupby(['key1', 'key2'])['data1'].agg(np.std)" -groupby_multi_series_op = Benchmark(stmt, setup, - start_date=datetime(2011, 8, 1)) - -groupby_series_simple_cython = \ - Benchmark('simple_series.groupby(key1).sum()', setup, - start_date=datetime(2011, 3, 1)) - - -stmt4 = "df.groupby('key1').rank(pct=True)" -groupby_series_simple_cython = Benchmark(stmt4, setup, - start_date=datetime(2014, 1, 16)) - -#---------------------------------------------------------------------- -# 2d grouping, aggregate many columns - -setup = common_setup + """ -labels = np.random.randint(0, 100, size=1000) -df = DataFrame(randn(1000, 1000)) -""" - -groupby_frame_cython_many_columns = Benchmark( - 'df.groupby(labels).sum()', setup, - start_date=datetime(2011, 8, 1), - logy=True) - -#---------------------------------------------------------------------- -# single key, long, integer key - -setup = common_setup + """ -data = np.random.randn(100000, 1) -labels = np.random.randint(0, 1000, size=100000) -df = DataFrame(data) -""" - -groupby_frame_singlekey_integer = \ - Benchmark('df.groupby(labels).sum()', setup, - start_date=datetime(2011, 8, 1), logy=True) - -#---------------------------------------------------------------------- -# group with different functions per column - -setup = common_setup + """ -fac1 = np.array(['A', 'B', 'C'], dtype='O') -fac2 = np.array(['one', 'two'], dtype='O') - -df = DataFrame({'key1': fac1.take(np.random.randint(0, 3, size=100000)), - 'key2': fac2.take(np.random.randint(0, 2, size=100000)), - 'value1' : np.random.randn(100000), - 'value2' : np.random.randn(100000), - 'value3' : np.random.randn(100000)}) -""" - -groupby_multi_different_functions = \ - Benchmark("""df.groupby(['key1', 'key2']).agg({'value1' : 'mean', - 'value2' : 'var', - 'value3' : 'sum'})""", - setup, start_date=datetime(2011, 9, 1)) - -groupby_multi_different_numpy_functions = \ - Benchmark("""df.groupby(['key1', 'key2']).agg({'value1' : np.mean, - 'value2' : np.var, - 'value3' : np.sum})""", - setup, start_date=datetime(2011, 9, 1)) - -#---------------------------------------------------------------------- -# size() speed - -setup = common_setup + """ -n = 100000 -offsets = np.random.randint(n, size=n).astype('timedelta64[ns]') -dates = np.datetime64('now') + offsets -df = DataFrame({'key1': np.random.randint(0, 500, size=n), - 'key2': np.random.randint(0, 100, size=n), - 'value1' : np.random.randn(n), - 'value2' : np.random.randn(n), - 'value3' : np.random.randn(n), - 'dates' : dates}) -""" - -groupby_multi_size = Benchmark("df.groupby(['key1', 'key2']).size()", - setup, start_date=datetime(2011, 10, 1)) - -groupby_dt_size = Benchmark("df.groupby(['dates']).size()", - setup, start_date=datetime(2011, 10, 1)) - -groupby_dt_timegrouper_size = Benchmark("df.groupby(TimeGrouper(key='dates', freq='M')).size()", - setup, start_date=datetime(2011, 10, 1)) - -#---------------------------------------------------------------------- -# count() speed - -setup = common_setup + """ -n = 10000 -offsets = np.random.randint(n, size=n).astype('timedelta64[ns]') - -dates = np.datetime64('now') + offsets -dates[np.random.rand(n) > 0.5] = np.datetime64('nat') - -offsets[np.random.rand(n) > 0.5] = np.timedelta64('nat') - -value2 = np.random.randn(n) -value2[np.random.rand(n) > 0.5] = np.nan - -obj = np.random.choice(list('ab'), size=n).astype(object) -obj[np.random.randn(n) > 0.5] = np.nan - -df = DataFrame({'key1': np.random.randint(0, 500, size=n), - 'key2': np.random.randint(0, 100, size=n), - 'dates': dates, - 'value2' : value2, - 'value3' : np.random.randn(n), - 'ints': np.random.randint(0, 1000, size=n), - 'obj': obj, - 'offsets': offsets}) -""" - -groupby_multi_count = Benchmark("df.groupby(['key1', 'key2']).count()", - setup, name='groupby_multi_count', - start_date=datetime(2014, 5, 5)) - -setup = common_setup + """ -n = 10000 - -df = DataFrame({'key1': randint(0, 500, size=n), - 'key2': randint(0, 100, size=n), - 'ints': randint(0, 1000, size=n), - 'ints2': randint(0, 1000, size=n)}) -""" - -groupby_int_count = Benchmark("df.groupby(['key1', 'key2']).count()", - setup, name='groupby_int_count', - start_date=datetime(2014, 5, 6)) -#---------------------------------------------------------------------- -# Series.value_counts - -setup = common_setup + """ -s = Series(np.random.randint(0, 1000, size=100000)) -""" - -series_value_counts_int64 = Benchmark('s.value_counts()', setup, - start_date=datetime(2011, 10, 21)) - -# value_counts on lots of strings - -setup = common_setup + """ -K = 1000 -N = 100000 -uniques = tm.makeStringIndex(K).values -s = Series(np.tile(uniques, N // K)) -""" - -series_value_counts_strings = Benchmark('s.value_counts()', setup, - start_date=datetime(2011, 10, 21)) - -#value_counts on float dtype - -setup = common_setup + """ -s = Series(np.random.randint(0, 1000, size=100000)).astype(float) -""" - -series_value_counts_float64 = Benchmark('s.value_counts()', setup, - start_date=datetime(2015, 8, 17)) - -#---------------------------------------------------------------------- -# pivot_table - -setup = common_setup + """ -fac1 = np.array(['A', 'B', 'C'], dtype='O') -fac2 = np.array(['one', 'two'], dtype='O') - -ind1 = np.random.randint(0, 3, size=100000) -ind2 = np.random.randint(0, 2, size=100000) - -df = DataFrame({'key1': fac1.take(ind1), -'key2': fac2.take(ind2), -'key3': fac2.take(ind2), -'value1' : np.random.randn(100000), -'value2' : np.random.randn(100000), -'value3' : np.random.randn(100000)}) -""" - -stmt = "df.pivot_table(index='key1', columns=['key2', 'key3'])" -groupby_pivot_table = Benchmark(stmt, setup, start_date=datetime(2011, 12, 15)) - - -#---------------------------------------------------------------------- -# dict return values - -setup = common_setup + """ -labels = np.arange(1000).repeat(10) -data = Series(randn(len(labels))) -f = lambda x: {'first': x.values[0], 'last': x.values[-1]} -""" - -groupby_apply_dict_return = Benchmark('data.groupby(labels).apply(f)', - setup, start_date=datetime(2011, 12, 15)) - -#---------------------------------------------------------------------- -# First / last functions - -setup = common_setup + """ -labels = np.arange(10000).repeat(10) -data = Series(randn(len(labels))) -data[::3] = np.nan -data[1::3] = np.nan -data2 = Series(randn(len(labels)),dtype='float32') -data2[::3] = np.nan -data2[1::3] = np.nan -labels = labels.take(np.random.permutation(len(labels))) -""" - -groupby_first_float64 = Benchmark('data.groupby(labels).first()', setup, - start_date=datetime(2012, 5, 1)) - -groupby_first_float32 = Benchmark('data2.groupby(labels).first()', setup, - start_date=datetime(2013, 1, 1)) - -groupby_last_float64 = Benchmark('data.groupby(labels).last()', setup, - start_date=datetime(2012, 5, 1)) - -groupby_last_float32 = Benchmark('data2.groupby(labels).last()', setup, - start_date=datetime(2013, 1, 1)) - -groupby_nth_float64_none = Benchmark('data.groupby(labels).nth(0)', setup, - start_date=datetime(2012, 5, 1)) -groupby_nth_float32_none = Benchmark('data2.groupby(labels).nth(0)', setup, - start_date=datetime(2013, 1, 1)) -groupby_nth_float64_any = Benchmark('data.groupby(labels).nth(0,dropna="all")', setup, - start_date=datetime(2012, 5, 1)) -groupby_nth_float32_any = Benchmark('data2.groupby(labels).nth(0,dropna="all")', setup, - start_date=datetime(2013, 1, 1)) - -# with datetimes (GH7555) -setup = common_setup + """ -df = DataFrame({'a' : date_range('1/1/2011',periods=100000,freq='s'),'b' : range(100000)}) -""" - -groupby_first_datetimes = Benchmark('df.groupby("b").first()', setup, - start_date=datetime(2013, 5, 1)) -groupby_last_datetimes = Benchmark('df.groupby("b").last()', setup, - start_date=datetime(2013, 5, 1)) -groupby_nth_datetimes_none = Benchmark('df.groupby("b").nth(0)', setup, - start_date=datetime(2013, 5, 1)) -groupby_nth_datetimes_any = Benchmark('df.groupby("b").nth(0,dropna="all")', setup, - start_date=datetime(2013, 5, 1)) - -# with object -setup = common_setup + """ -df = DataFrame({'a' : ['foo']*100000,'b' : range(100000)}) -""" - -groupby_first_object = Benchmark('df.groupby("b").first()', setup, - start_date=datetime(2013, 5, 1)) -groupby_last_object = Benchmark('df.groupby("b").last()', setup, - start_date=datetime(2013, 5, 1)) -groupby_nth_object_none = Benchmark('df.groupby("b").nth(0)', setup, - start_date=datetime(2013, 5, 1)) -groupby_nth_object_any = Benchmark('df.groupby("b").nth(0,dropna="any")', setup, - start_date=datetime(2013, 5, 1)) - -#---------------------------------------------------------------------- -# groupby_indices replacement, chop up Series - -setup = common_setup + """ -try: - rng = date_range('1/1/2000', '12/31/2005', freq='H') - year, month, day = rng.year, rng.month, rng.day -except: - rng = date_range('1/1/2000', '12/31/2000', offset=datetools.Hour()) - year = rng.map(lambda x: x.year) - month = rng.map(lambda x: x.month) - day = rng.map(lambda x: x.day) - -ts = Series(np.random.randn(len(rng)), index=rng) -""" - -groupby_indices = Benchmark('len(ts.groupby([year, month, day]))', - setup, start_date=datetime(2012, 1, 1)) - -#---------------------------------------------------------------------- -# median - -#---------------------------------------------------------------------- -# single key, long, integer key - -setup = common_setup + """ -data = np.random.randn(100000, 2) -labels = np.random.randint(0, 1000, size=100000) -df = DataFrame(data) -""" - -groupby_frame_median = \ - Benchmark('df.groupby(labels).median()', setup, - start_date=datetime(2011, 8, 1), logy=True) - - -setup = common_setup + """ -data = np.random.randn(1000000, 2) -labels = np.random.randint(0, 1000, size=1000000) -df = DataFrame(data) -""" - -groupby_simple_compress_timing = \ - Benchmark('df.groupby(labels).mean()', setup, - start_date=datetime(2011, 8, 1)) - - -#---------------------------------------------------------------------- -# DataFrame Apply overhead - -setup = common_setup + """ -N = 10000 -labels = np.random.randint(0, 2000, size=N) -labels2 = np.random.randint(0, 3, size=N) -df = DataFrame({'key': labels, -'key2': labels2, -'value1': randn(N), -'value2': ['foo', 'bar', 'baz', 'qux'] * (N / 4)}) -def f(g): - return 1 -""" - -groupby_frame_apply_overhead = Benchmark("df.groupby('key').apply(f)", setup, - start_date=datetime(2011, 10, 1)) - -groupby_frame_apply = Benchmark("df.groupby(['key', 'key2']).apply(f)", setup, - start_date=datetime(2011, 10, 1)) - - -#---------------------------------------------------------------------- -# DataFrame nth - -setup = common_setup + """ -df = DataFrame(np.random.randint(1, 100, (10000, 2))) -""" - -# Not really a fair test as behaviour has changed! -groupby_frame_nth_none = Benchmark("df.groupby(0).nth(0)", setup, - start_date=datetime(2014, 3, 1)) - -groupby_series_nth_none = Benchmark("df[1].groupby(df[0]).nth(0)", setup, - start_date=datetime(2014, 3, 1)) -groupby_frame_nth_any= Benchmark("df.groupby(0).nth(0,dropna='any')", setup, - start_date=datetime(2014, 3, 1)) - -groupby_series_nth_any = Benchmark("df[1].groupby(df[0]).nth(0,dropna='any')", setup, - start_date=datetime(2014, 3, 1)) - - -#---------------------------------------------------------------------- -# Sum booleans #2692 - -setup = common_setup + """ -N = 500 -df = DataFrame({'ii':range(N),'bb':[True for x in range(N)]}) -""" - -groupby_sum_booleans = Benchmark("df.groupby('ii').sum()", setup) - - -#---------------------------------------------------------------------- -# multi-indexed group sum #9049 - -setup = common_setup + """ -N = 50 -df = DataFrame({'A': range(N) * 2, 'B': range(N*2), 'C': 1}).set_index(["A", "B"]) -""" - -groupby_sum_multiindex = Benchmark("df.groupby(level=[0, 1]).sum()", setup) - - -#---------------------------------------------------------------------- -# Transform testing - -setup = common_setup + """ -n_dates = 400 -n_securities = 250 -n_columns = 3 -share_na = 0.1 - -dates = date_range('1997-12-31', periods=n_dates, freq='B') -dates = Index(map(lambda x: x.year * 10000 + x.month * 100 + x.day, dates)) - -secid_min = int('10000000', 16) -secid_max = int('F0000000', 16) -step = (secid_max - secid_min) // (n_securities - 1) -security_ids = map(lambda x: hex(x)[2:10].upper(), range(secid_min, secid_max + 1, step)) - -data_index = MultiIndex(levels=[dates.values, security_ids], - labels=[[i for i in range(n_dates) for _ in xrange(n_securities)], range(n_securities) * n_dates], - names=['date', 'security_id']) -n_data = len(data_index) - -columns = Index(['factor{}'.format(i) for i in range(1, n_columns + 1)]) - -data = DataFrame(np.random.randn(n_data, n_columns), index=data_index, columns=columns) - -step = int(n_data * share_na) -for column_index in range(n_columns): - index = column_index - while index < n_data: - data.set_value(data_index[index], columns[column_index], np.nan) - index += step - -f_fillna = lambda x: x.fillna(method='pad') -""" - -groupby_transform = Benchmark("data.groupby(level='security_id').transform(f_fillna)", setup) -groupby_transform_ufunc = Benchmark("data.groupby(level='date').transform(np.max)", setup) - -setup = common_setup + """ -np.random.seed(0) - -N = 120000 -N_TRANSITIONS = 1400 - -# generate groups -transition_points = np.random.permutation(np.arange(N))[:N_TRANSITIONS] -transition_points.sort() -transitions = np.zeros((N,), dtype=np.bool) -transitions[transition_points] = True -g = transitions.cumsum() - -df = DataFrame({ 'signal' : np.random.rand(N)}) -""" -groupby_transform_series = Benchmark("df['signal'].groupby(g).transform(np.mean)", setup) - -setup = common_setup + """ -np.random.seed(0) - -df=DataFrame( { 'id' : np.arange( 100000 ) / 3, - 'val': np.random.randn( 100000) } ) -""" - -groupby_transform_series2 = Benchmark("df.groupby('id')['val'].transform(np.mean)", setup) - -setup = common_setup + ''' -np.random.seed(2718281) -n = 20000 -df = DataFrame(np.random.randint(1, n, (n, 3)), - columns=['jim', 'joe', 'jolie']) -''' - -stmt = "df.groupby(['jim', 'joe'])['jolie'].transform('max')"; -groupby_transform_multi_key1 = Benchmark(stmt, setup) -groupby_transform_multi_key2 = Benchmark(stmt, setup + "df['jim'] = df['joe']") - -setup = common_setup + ''' -np.random.seed(2718281) -n = 200000 -df = DataFrame(np.random.randint(1, n / 10, (n, 3)), - columns=['jim', 'joe', 'jolie']) -''' -groupby_transform_multi_key3 = Benchmark(stmt, setup) -groupby_transform_multi_key4 = Benchmark(stmt, setup + "df['jim'] = df['joe']") - -setup = common_setup + ''' -np.random.seed(27182) -n = 100000 -df = DataFrame(np.random.randint(1, n / 100, (n, 3)), - columns=['jim', 'joe', 'jolie']) -''' - -groupby_agg_builtins1 = Benchmark("df.groupby('jim').agg([sum, min, max])", setup) -groupby_agg_builtins2 = Benchmark("df.groupby(['jim', 'joe']).agg([sum, min, max])", setup) - - -setup = common_setup + ''' -arr = np.random.randint(- 1 << 12, 1 << 12, (1 << 17, 5)) -i = np.random.choice(len(arr), len(arr) * 5) -arr = np.vstack((arr, arr[i])) # add sume duplicate rows - -i = np.random.permutation(len(arr)) -arr = arr[i] # shuffle rows - -df = DataFrame(arr, columns=list('abcde')) -df['jim'], df['joe'] = np.random.randn(2, len(df)) * 10 -''' - -groupby_int64_overflow = Benchmark("df.groupby(list('abcde')).max()", setup, - name='groupby_int64_overflow') - - -setup = common_setup + ''' -from itertools import product -from string import ascii_letters, digits - -n = 5 * 7 * 11 * (1 << 9) -alpha = list(map(''.join, product(ascii_letters + digits, repeat=4))) -f = lambda k: np.repeat(np.random.choice(alpha, n // k), k) - -df = DataFrame({'a': f(11), 'b': f(7), 'c': f(5), 'd': f(1)}) -df['joe'] = (np.random.randn(len(df)) * 10).round(3) - -i = np.random.permutation(len(df)) -df = df.iloc[i].reset_index(drop=True).copy() -''' - -groupby_multi_index = Benchmark("df.groupby(list('abcd')).max()", setup, - name='groupby_multi_index') - -#---------------------------------------------------------------------- -# groupby with a variable value for ngroups - - -ngroups_list = [100, 10000] -no_arg_func_list = [ - 'all', - 'any', - 'count', - 'cumcount', - 'cummax', - 'cummin', - 'cumprod', - 'cumsum', - 'describe', - 'diff', - 'first', - 'head', - 'last', - 'mad', - 'max', - 'mean', - 'median', - 'min', - 'nunique', - 'pct_change', - 'prod', - 'rank', - 'sem', - 'size', - 'skew', - 'std', - 'sum', - 'tail', - 'unique', - 'var', - 'value_counts', -] - - -_stmt_template = "df.groupby('value')['timestamp'].%s" -_setup_template = common_setup + """ -np.random.seed(1234) -ngroups = %s -size = ngroups * 2 -rng = np.arange(ngroups) -df = DataFrame(dict( - timestamp=rng.take(np.random.randint(0, ngroups, size=size)), - value=np.random.randint(0, size, size=size) -)) -""" -START_DATE = datetime(2011, 7, 1) - - -def make_large_ngroups_bmark(ngroups, func_name, func_args=''): - bmark_name = 'groupby_ngroups_%s_%s' % (ngroups, func_name) - stmt = _stmt_template % ('%s(%s)' % (func_name, func_args)) - setup = _setup_template % ngroups - bmark = Benchmark(stmt, setup, start_date=START_DATE) - # MUST set name - bmark.name = bmark_name - return bmark - - -def inject_bmark_into_globals(bmark): - if not bmark.name: - raise AssertionError('benchmark must have a name') - globals()[bmark.name] = bmark - - -for ngroups in ngroups_list: - for func_name in no_arg_func_list: - bmark = make_large_ngroups_bmark(ngroups, func_name) - inject_bmark_into_globals(bmark) - -# avoid bmark to be collected as Benchmark object -del bmark diff --git a/vb_suite/hdfstore_bench.py b/vb_suite/hdfstore_bench.py deleted file mode 100644 index 393fd4cc77e66..0000000000000 --- a/vb_suite/hdfstore_bench.py +++ /dev/null @@ -1,278 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -start_date = datetime(2012, 7, 1) - -common_setup = """from .pandas_vb_common import * -import os - -f = '__test__.h5' -def remove(f): - try: - os.remove(f) - except: - pass - -""" - -#---------------------------------------------------------------------- -# get from a store - -setup1 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000)}, - index=index) -remove(f) -store = HDFStore(f) -store.put('df1',df) -""" - -read_store = Benchmark("store.get('df1')", setup1, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a store - -setup2 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000)}, - index=index) -remove(f) -store = HDFStore(f) -""" - -write_store = Benchmark( - "store.put('df2',df)", setup2, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# get from a store (mixed) - -setup3 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000), - 'string1' : ['foo'] * 25000, - 'bool1' : [True] * 25000, - 'int1' : np.random.randint(0, 250000, size=25000)}, - index=index) -remove(f) -store = HDFStore(f) -store.put('df3',df) -""" - -read_store_mixed = Benchmark( - "store.get('df3')", setup3, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a store (mixed) - -setup4 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000), - 'string1' : ['foo'] * 25000, - 'bool1' : [True] * 25000, - 'int1' : np.random.randint(0, 250000, size=25000)}, - index=index) -remove(f) -store = HDFStore(f) -""" - -write_store_mixed = Benchmark( - "store.put('df4',df)", setup4, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# get from a table (mixed) - -setup5 = common_setup + """ -N=10000 -index = tm.makeStringIndex(N) -df = DataFrame({'float1' : randn(N), - 'float2' : randn(N), - 'string1' : ['foo'] * N, - 'bool1' : [True] * N, - 'int1' : np.random.randint(0, N, size=N)}, - index=index) - -remove(f) -store = HDFStore(f) -store.append('df5',df) -""" - -read_store_table_mixed = Benchmark( - "store.select('df5')", setup5, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a table (mixed) - -setup6 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000), - 'string1' : ['foo'] * 25000, - 'bool1' : [True] * 25000, - 'int1' : np.random.randint(0, 25000, size=25000)}, - index=index) -remove(f) -store = HDFStore(f) -""" - -write_store_table_mixed = Benchmark( - "store.append('df6',df)", setup6, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# select from a table - -setup7 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000) }, - index=index) - -remove(f) -store = HDFStore(f) -store.append('df7',df) -""" - -read_store_table = Benchmark( - "store.select('df7')", setup7, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a table - -setup8 = common_setup + """ -index = tm.makeStringIndex(25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000) }, - index=index) -remove(f) -store = HDFStore(f) -""" - -write_store_table = Benchmark( - "store.append('df8',df)", setup8, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# get from a table (wide) - -setup9 = common_setup + """ -df = DataFrame(np.random.randn(25000,100)) - -remove(f) -store = HDFStore(f) -store.append('df9',df) -""" - -read_store_table_wide = Benchmark( - "store.select('df9')", setup9, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a table (wide) - -setup10 = common_setup + """ -df = DataFrame(np.random.randn(25000,100)) - -remove(f) -store = HDFStore(f) -""" - -write_store_table_wide = Benchmark( - "store.append('df10',df)", setup10, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# get from a table (wide) - -setup11 = common_setup + """ -index = date_range('1/1/2000', periods = 25000) -df = DataFrame(np.random.randn(25000,100), index = index) - -remove(f) -store = HDFStore(f) -store.append('df11',df) -""" - -query_store_table_wide = Benchmark( - "store.select('df11', [ ('index', '>', df.index[10000]), ('index', '<', df.index[15000]) ])", setup11, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# query from a table - -setup12 = common_setup + """ -index = date_range('1/1/2000', periods = 25000) -df = DataFrame({'float1' : randn(25000), - 'float2' : randn(25000) }, - index=index) - -remove(f) -store = HDFStore(f) -store.append('df12',df) -""" - -query_store_table = Benchmark( - "store.select('df12', [ ('index', '>', df.index[10000]), ('index', '<', df.index[15000]) ])", setup12, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# select from a panel table - -setup13 = common_setup + """ -p = Panel(randn(20, 1000, 25), items= [ 'Item%03d' % i for i in range(20) ], - major_axis=date_range('1/1/2000', periods=1000), minor_axis = [ 'E%03d' % i for i in range(25) ]) - -remove(f) -store = HDFStore(f) -store.append('p1',p) -""" - -read_store_table_panel = Benchmark( - "store.select('p1')", setup13, cleanup="store.close()", - start_date=start_date) - - -#---------------------------------------------------------------------- -# write to a panel table - -setup14 = common_setup + """ -p = Panel(randn(20, 1000, 25), items= [ 'Item%03d' % i for i in range(20) ], - major_axis=date_range('1/1/2000', periods=1000), minor_axis = [ 'E%03d' % i for i in range(25) ]) - -remove(f) -store = HDFStore(f) -""" - -write_store_table_panel = Benchmark( - "store.append('p2',p)", setup14, cleanup="store.close()", - start_date=start_date) - -#---------------------------------------------------------------------- -# write to a table (data_columns) - -setup15 = common_setup + """ -df = DataFrame(np.random.randn(10000,10),columns = [ 'C%03d' % i for i in range(10) ]) - -remove(f) -store = HDFStore(f) -""" - -write_store_table_dc = Benchmark( - "store.append('df15',df,data_columns=True)", setup15, cleanup="store.close()", - start_date=start_date) - diff --git a/vb_suite/index_object.py b/vb_suite/index_object.py deleted file mode 100644 index 2ab2bc15f3853..0000000000000 --- a/vb_suite/index_object.py +++ /dev/null @@ -1,173 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -SECTION = "Index / MultiIndex objects" - - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# intersection, union - -setup = common_setup + """ -rng = DatetimeIndex(start='1/1/2000', periods=10000, freq=datetools.Minute()) -if rng.dtype == object: - rng = rng.view(Index) -else: - rng = rng.asobject -rng2 = rng[:-1] -""" - -index_datetime_intersection = Benchmark("rng.intersection(rng2)", setup) -index_datetime_union = Benchmark("rng.union(rng2)", setup) - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=10000, freq='T') -rng2 = rng[:-1] -""" - -datetime_index_intersection = Benchmark("rng.intersection(rng2)", setup, - start_date=datetime(2013, 9, 27)) -datetime_index_union = Benchmark("rng.union(rng2)", setup, - start_date=datetime(2013, 9, 27)) - -# integers -setup = common_setup + """ -N = 1000000 -options = np.arange(N) - -left = Index(options.take(np.random.permutation(N)[:N // 2])) -right = Index(options.take(np.random.permutation(N)[:N // 2])) -""" - -index_int64_union = Benchmark('left.union(right)', setup, - start_date=datetime(2011, 1, 1)) - -index_int64_intersection = Benchmark('left.intersection(right)', setup, - start_date=datetime(2011, 1, 1)) - -#---------------------------------------------------------------------- -# string index slicing -setup = common_setup + """ -idx = tm.makeStringIndex(1000000) - -mask = np.arange(1000000) % 3 == 0 -series_mask = Series(mask) -""" -index_str_slice_indexer_basic = Benchmark('idx[:-1]', setup) -index_str_slice_indexer_even = Benchmark('idx[::2]', setup) -index_str_boolean_indexer = Benchmark('idx[mask]', setup) -index_str_boolean_series_indexer = Benchmark('idx[series_mask]', setup) - -#---------------------------------------------------------------------- -# float64 index -#---------------------------------------------------------------------- -# construction -setup = common_setup + """ -baseidx = np.arange(1e6) -""" - -index_float64_construct = Benchmark('Index(baseidx)', setup, - name='index_float64_construct', - start_date=datetime(2014, 4, 13)) - -setup = common_setup + """ -idx = tm.makeFloatIndex(1000000) - -mask = np.arange(idx.size) % 3 == 0 -series_mask = Series(mask) -""" -#---------------------------------------------------------------------- -# getting -index_float64_get = Benchmark('idx[1]', setup, name='index_float64_get', - start_date=datetime(2014, 4, 13)) - - -#---------------------------------------------------------------------- -# slicing -index_float64_slice_indexer_basic = Benchmark('idx[:-1]', setup, - name='index_float64_slice_indexer_basic', - start_date=datetime(2014, 4, 13)) -index_float64_slice_indexer_even = Benchmark('idx[::2]', setup, - name='index_float64_slice_indexer_even', - start_date=datetime(2014, 4, 13)) -index_float64_boolean_indexer = Benchmark('idx[mask]', setup, - name='index_float64_boolean_indexer', - start_date=datetime(2014, 4, 13)) -index_float64_boolean_series_indexer = Benchmark('idx[series_mask]', setup, - name='index_float64_boolean_series_indexer', - start_date=datetime(2014, 4, 13)) - -#---------------------------------------------------------------------- -# arith ops -index_float64_mul = Benchmark('idx * 2', setup, name='index_float64_mul', - start_date=datetime(2014, 4, 13)) -index_float64_div = Benchmark('idx / 2', setup, name='index_float64_div', - start_date=datetime(2014, 4, 13)) - - -# Constructing MultiIndex from cartesian product of iterables -# - -setup = common_setup + """ -iterables = [tm.makeStringIndex(10000), range(20)] -""" - -multiindex_from_product = Benchmark('MultiIndex.from_product(iterables)', - setup, name='multiindex_from_product', - start_date=datetime(2014, 6, 30)) - -#---------------------------------------------------------------------- -# MultiIndex with DatetimeIndex level - -setup = common_setup + """ -level1 = range(1000) -level2 = date_range(start='1/1/2012', periods=100) -mi = MultiIndex.from_product([level1, level2]) -""" - -multiindex_with_datetime_level_full = \ - Benchmark("mi.copy().values", setup, - name='multiindex_with_datetime_level_full', - start_date=datetime(2014, 10, 11)) - - -multiindex_with_datetime_level_sliced = \ - Benchmark("mi[:10].values", setup, - name='multiindex_with_datetime_level_sliced', - start_date=datetime(2014, 10, 11)) - -# multi-index duplicated -setup = common_setup + """ -n, k = 200, 5000 -levels = [np.arange(n), tm.makeStringIndex(n).values, 1000 + np.arange(n)] -labels = [np.random.choice(n, k * n) for lev in levels] -mi = MultiIndex(levels=levels, labels=labels) -""" - -multiindex_duplicated = Benchmark('mi.duplicated()', setup, - name='multiindex_duplicated') - -#---------------------------------------------------------------------- -# repr - -setup = common_setup + """ -dr = pd.date_range('20000101', freq='D', periods=100000) -""" - -datetime_index_repr = \ - Benchmark("dr._is_dates_only", setup, - start_date=datetime(2012, 1, 11)) - -setup = common_setup + """ -n = 3 * 5 * 7 * 11 * (1 << 10) -low, high = - 1 << 12, 1 << 12 -f = lambda k: np.repeat(np.random.randint(low, high, n // k), k) - -i = np.random.permutation(n) -mi = MultiIndex.from_arrays([f(11), f(7), f(5), f(3), f(1)])[i] -""" - -multiindex_sortlevel_int64 = Benchmark('mi.sortlevel()', setup, - name='multiindex_sortlevel_int64') diff --git a/vb_suite/indexing.py b/vb_suite/indexing.py deleted file mode 100644 index 3d95d52dccd71..0000000000000 --- a/vb_suite/indexing.py +++ /dev/null @@ -1,292 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -SECTION = 'Indexing and scalar value access' - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# Series.__getitem__, get_value, __getitem__(slice) - -setup = common_setup + """ -tm.N = 1000 -ts = tm.makeTimeSeries() -dt = ts.index[500] -""" -statement = "ts[dt]" -bm_getitem = Benchmark(statement, setup, ncalls=100000, - name='time_series_getitem_scalar') - -setup = common_setup + """ -index = tm.makeStringIndex(1000) -s = Series(np.random.rand(1000), index=index) -idx = index[100] -""" -statement = "s.get_value(idx)" -bm_get_value = Benchmark(statement, setup, - name='series_get_value', - start_date=datetime(2011, 11, 12)) - - -setup = common_setup + """ -index = tm.makeStringIndex(1000000) -s = Series(np.random.rand(1000000), index=index) -""" -series_getitem_pos_slice = Benchmark("s[:800000]", setup, - name="series_getitem_pos_slice") - - -setup = common_setup + """ -index = tm.makeStringIndex(1000000) -s = Series(np.random.rand(1000000), index=index) -lbl = s.index[800000] -""" -series_getitem_label_slice = Benchmark("s[:lbl]", setup, - name="series_getitem_label_slice") - - -#---------------------------------------------------------------------- -# DataFrame __getitem__ - -setup = common_setup + """ -index = tm.makeStringIndex(1000) -columns = tm.makeStringIndex(30) -df = DataFrame(np.random.rand(1000, 30), index=index, - columns=columns) -idx = index[100] -col = columns[10] -""" -statement = "df[col][idx]" -bm_df_getitem = Benchmark(statement, setup, - name='dataframe_getitem_scalar') - -setup = common_setup + """ -try: - klass = DataMatrix -except: - klass = DataFrame - -index = tm.makeStringIndex(1000) -columns = tm.makeStringIndex(30) -df = klass(np.random.rand(1000, 30), index=index, columns=columns) -idx = index[100] -col = columns[10] -""" -statement = "df[col][idx]" -bm_df_getitem2 = Benchmark(statement, setup, - name='datamatrix_getitem_scalar') - - -#---------------------------------------------------------------------- -# ix get scalar - -setup = common_setup + """ -index = tm.makeStringIndex(1000) -columns = tm.makeStringIndex(30) -df = DataFrame(np.random.randn(1000, 30), index=index, columns=columns) -idx = index[100] -col = columns[10] -""" - -indexing_frame_get_value_ix = Benchmark("df.ix[idx,col]", setup, - name='indexing_frame_get_value_ix', - start_date=datetime(2011, 11, 12)) - -indexing_frame_get_value = Benchmark("df.get_value(idx,col)", setup, - name='indexing_frame_get_value', - start_date=datetime(2011, 11, 12)) - -setup = common_setup + """ -mi = MultiIndex.from_tuples([(x,y) for x in range(1000) for y in range(1000)]) -s = Series(np.random.randn(1000000), index=mi) -""" - -series_xs_mi_ix = Benchmark("s.ix[999]", setup, - name='series_xs_mi_ix', - start_date=datetime(2013, 1, 1)) - -setup = common_setup + """ -mi = MultiIndex.from_tuples([(x,y) for x in range(1000) for y in range(1000)]) -s = Series(np.random.randn(1000000), index=mi) -df = DataFrame(s) -""" - -frame_xs_mi_ix = Benchmark("df.ix[999]", setup, - name='frame_xs_mi_ix', - start_date=datetime(2013, 1, 1)) - -#---------------------------------------------------------------------- -# Boolean DataFrame row selection - -setup = common_setup + """ -df = DataFrame(np.random.randn(10000, 4), columns=['A', 'B', 'C', 'D']) -indexer = df['B'] > 0 -obj_indexer = indexer.astype('O') -""" -indexing_dataframe_boolean_rows = \ - Benchmark("df[indexer]", setup, name='indexing_dataframe_boolean_rows') - -indexing_dataframe_boolean_rows_object = \ - Benchmark("df[obj_indexer]", setup, - name='indexing_dataframe_boolean_rows_object') - -setup = common_setup + """ -df = DataFrame(np.random.randn(50000, 100)) -df2 = DataFrame(np.random.randn(50000, 100)) -""" -indexing_dataframe_boolean = \ - Benchmark("df > df2", setup, name='indexing_dataframe_boolean', - start_date=datetime(2012, 1, 1)) - -setup = common_setup + """ -try: - import pandas.computation.expressions as expr -except: - expr = None - -if expr is None: - raise NotImplementedError -df = DataFrame(np.random.randn(50000, 100)) -df2 = DataFrame(np.random.randn(50000, 100)) -expr.set_numexpr_threads(1) -""" - -indexing_dataframe_boolean_st = \ - Benchmark("df > df2", setup, name='indexing_dataframe_boolean_st',cleanup="expr.set_numexpr_threads()", - start_date=datetime(2013, 2, 26)) - - -setup = common_setup + """ -try: - import pandas.computation.expressions as expr -except: - expr = None - -if expr is None: - raise NotImplementedError -df = DataFrame(np.random.randn(50000, 100)) -df2 = DataFrame(np.random.randn(50000, 100)) -expr.set_use_numexpr(False) -""" - -indexing_dataframe_boolean_no_ne = \ - Benchmark("df > df2", setup, name='indexing_dataframe_boolean_no_ne',cleanup="expr.set_use_numexpr(True)", - start_date=datetime(2013, 2, 26)) -#---------------------------------------------------------------------- -# MultiIndex sortlevel - -setup = common_setup + """ -a = np.repeat(np.arange(100), 1000) -b = np.tile(np.arange(1000), 100) -midx = MultiIndex.from_arrays([a, b]) -midx = midx.take(np.random.permutation(np.arange(100000))) -""" -sort_level_zero = Benchmark("midx.sortlevel(0)", setup, - start_date=datetime(2012, 1, 1)) -sort_level_one = Benchmark("midx.sortlevel(1)", setup, - start_date=datetime(2012, 1, 1)) - -#---------------------------------------------------------------------- -# Panel subset selection - -setup = common_setup + """ -p = Panel(np.random.randn(100, 100, 100)) -inds = range(0, 100, 10) -""" - -indexing_panel_subset = Benchmark('p.ix[inds, inds, inds]', setup, - start_date=datetime(2012, 1, 1)) - -#---------------------------------------------------------------------- -# Iloc - -setup = common_setup + """ -df = DataFrame({'A' : [0.1] * 3000, 'B' : [1] * 3000}) -idx = np.array(range(30)) * 99 -df2 = DataFrame({'A' : [0.1] * 1000, 'B' : [1] * 1000}) -df2 = concat([df2, 2*df2, 3*df2]) -""" - -frame_iloc_dups = Benchmark('df2.iloc[idx]', setup, - start_date=datetime(2013, 1, 1)) - -frame_loc_dups = Benchmark('df2.loc[idx]', setup, - start_date=datetime(2013, 1, 1)) - -setup = common_setup + """ -df = DataFrame(dict( A = [ 'foo'] * 1000000)) -""" - -frame_iloc_big = Benchmark('df.iloc[:100,0]', setup, - start_date=datetime(2013, 1, 1)) - -#---------------------------------------------------------------------- -# basic tests for [], .loc[], .iloc[] and .ix[] - -setup = common_setup + """ -s = Series(np.random.rand(1000000)) -""" - -series_getitem_scalar = Benchmark("s[800000]", setup) -series_getitem_slice = Benchmark("s[:800000]", setup) -series_getitem_list_like = Benchmark("s[[800000]]", setup) -series_getitem_array = Benchmark("s[np.arange(10000)]", setup) - -series_loc_scalar = Benchmark("s.loc[800000]", setup) -series_loc_slice = Benchmark("s.loc[:800000]", setup) -series_loc_list_like = Benchmark("s.loc[[800000]]", setup) -series_loc_array = Benchmark("s.loc[np.arange(10000)]", setup) - -series_iloc_scalar = Benchmark("s.iloc[800000]", setup) -series_iloc_slice = Benchmark("s.iloc[:800000]", setup) -series_iloc_list_like = Benchmark("s.iloc[[800000]]", setup) -series_iloc_array = Benchmark("s.iloc[np.arange(10000)]", setup) - -series_ix_scalar = Benchmark("s.ix[800000]", setup) -series_ix_slice = Benchmark("s.ix[:800000]", setup) -series_ix_list_like = Benchmark("s.ix[[800000]]", setup) -series_ix_array = Benchmark("s.ix[np.arange(10000)]", setup) - - -# multi-index slicing -setup = common_setup + """ -np.random.seed(1234) -idx=pd.IndexSlice -n=100000 -mdt = pandas.DataFrame() -mdt['A'] = np.random.choice(range(10000,45000,1000), n) -mdt['B'] = np.random.choice(range(10,400), n) -mdt['C'] = np.random.choice(range(1,150), n) -mdt['D'] = np.random.choice(range(10000,45000), n) -mdt['x'] = np.random.choice(range(400), n) -mdt['y'] = np.random.choice(range(25), n) - - -test_A = 25000 -test_B = 25 -test_C = 40 -test_D = 35000 - -eps_A = 5000 -eps_B = 5 -eps_C = 5 -eps_D = 5000 -mdt2 = mdt.set_index(['A','B','C','D']).sortlevel() -""" - -multiindex_slicers = Benchmark('mdt2.loc[idx[test_A-eps_A:test_A+eps_A,test_B-eps_B:test_B+eps_B,test_C-eps_C:test_C+eps_C,test_D-eps_D:test_D+eps_D],:]', setup, - start_date=datetime(2015, 1, 1)) - -#---------------------------------------------------------------------- -# take - -setup = common_setup + """ -s = Series(np.random.rand(100000)) -ts = Series(np.random.rand(100000), - index=date_range('2011-01-01', freq='S', periods=100000)) -indexer = [True, False, True, True, False] * 20000 -""" - -series_take_intindex = Benchmark("s.take(indexer)", setup) -series_take_dtindex = Benchmark("ts.take(indexer)", setup) diff --git a/vb_suite/inference.py b/vb_suite/inference.py deleted file mode 100644 index aaa51aa5163ce..0000000000000 --- a/vb_suite/inference.py +++ /dev/null @@ -1,36 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime -import sys - -# from GH 7332 - -setup = """from .pandas_vb_common import * -import pandas as pd -N = 500000 -df_int64 = DataFrame(dict(A = np.arange(N,dtype='int64'), B = np.arange(N,dtype='int64'))) -df_int32 = DataFrame(dict(A = np.arange(N,dtype='int32'), B = np.arange(N,dtype='int32'))) -df_uint32 = DataFrame(dict(A = np.arange(N,dtype='uint32'), B = np.arange(N,dtype='uint32'))) -df_float64 = DataFrame(dict(A = np.arange(N,dtype='float64'), B = np.arange(N,dtype='float64'))) -df_float32 = DataFrame(dict(A = np.arange(N,dtype='float32'), B = np.arange(N,dtype='float32'))) -df_datetime64 = DataFrame(dict(A = pd.to_datetime(np.arange(N,dtype='int64'),unit='ms'), - B = pd.to_datetime(np.arange(N,dtype='int64'),unit='ms'))) -df_timedelta64 = DataFrame(dict(A = df_datetime64['A']-df_datetime64['B'], - B = df_datetime64['B'])) -""" - -dtype_infer_int64 = Benchmark('df_int64["A"] + df_int64["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_int32 = Benchmark('df_int32["A"] + df_int32["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_uint32 = Benchmark('df_uint32["A"] + df_uint32["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_float64 = Benchmark('df_float64["A"] + df_float64["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_float32 = Benchmark('df_float32["A"] + df_float32["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_datetime64 = Benchmark('df_datetime64["A"] - df_datetime64["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_timedelta64_1 = Benchmark('df_timedelta64["A"] + df_timedelta64["B"]', setup, - start_date=datetime(2014, 1, 1)) -dtype_infer_timedelta64_2 = Benchmark('df_timedelta64["A"] + df_timedelta64["A"]', setup, - start_date=datetime(2014, 1, 1)) diff --git a/vb_suite/io_bench.py b/vb_suite/io_bench.py deleted file mode 100644 index af5f6076515cc..0000000000000 --- a/vb_suite/io_bench.py +++ /dev/null @@ -1,150 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -from io import StringIO -""" - -#---------------------------------------------------------------------- -# read_csv - -setup1 = common_setup + """ -index = tm.makeStringIndex(10000) -df = DataFrame({'float1' : randn(10000), - 'float2' : randn(10000), - 'string1' : ['foo'] * 10000, - 'bool1' : [True] * 10000, - 'int1' : np.random.randint(0, 100000, size=10000)}, - index=index) -df.to_csv('__test__.csv') -""" - -read_csv_standard = Benchmark("read_csv('__test__.csv')", setup1, - start_date=datetime(2011, 9, 15)) - -#---------------------------------- -# skiprows - -setup1 = common_setup + """ -index = tm.makeStringIndex(20000) -df = DataFrame({'float1' : randn(20000), - 'float2' : randn(20000), - 'string1' : ['foo'] * 20000, - 'bool1' : [True] * 20000, - 'int1' : np.random.randint(0, 200000, size=20000)}, - index=index) -df.to_csv('__test__.csv') -""" - -read_csv_skiprows = Benchmark("read_csv('__test__.csv', skiprows=10000)", setup1, - start_date=datetime(2011, 9, 15)) - -#---------------------------------------------------------------------- -# write_csv - -setup2 = common_setup + """ -index = tm.makeStringIndex(10000) -df = DataFrame({'float1' : randn(10000), - 'float2' : randn(10000), - 'string1' : ['foo'] * 10000, - 'bool1' : [True] * 10000, - 'int1' : np.random.randint(0, 100000, size=10000)}, - index=index) -""" - -write_csv_standard = Benchmark("df.to_csv('__test__.csv')", setup2, - start_date=datetime(2011, 9, 15)) - -#---------------------------------- -setup = common_setup + """ -df = DataFrame(np.random.randn(3000, 30)) -""" -frame_to_csv = Benchmark("df.to_csv('__test__.csv')", setup, - start_date=datetime(2011, 1, 1)) -#---------------------------------- - -setup = common_setup + """ -df=DataFrame({'A':range(50000)}) -df['B'] = df.A + 1.0 -df['C'] = df.A + 2.0 -df['D'] = df.A + 3.0 -""" -frame_to_csv2 = Benchmark("df.to_csv('__test__.csv')", setup, - start_date=datetime(2011, 1, 1)) - -#---------------------------------- -setup = common_setup + """ -from pandas import concat, Timestamp - -def create_cols(name): - return [ "%s%03d" % (name,i) for i in range(5) ] -df_float = DataFrame(np.random.randn(5000, 5),dtype='float64',columns=create_cols('float')) -df_int = DataFrame(np.random.randn(5000, 5),dtype='int64',columns=create_cols('int')) -df_bool = DataFrame(True,index=df_float.index,columns=create_cols('bool')) -df_object = DataFrame('foo',index=df_float.index,columns=create_cols('object')) -df_dt = DataFrame(Timestamp('20010101'),index=df_float.index,columns=create_cols('date')) - -# add in some nans -df_float.ix[30:500,1:3] = np.nan - -df = concat([ df_float, df_int, df_bool, df_object, df_dt ], axis=1) - -""" -frame_to_csv_mixed = Benchmark("df.to_csv('__test__.csv')", setup, - start_date=datetime(2012, 6, 1)) - -#---------------------------------------------------------------------- -# parse dates, ISO8601 format - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=1000) -data = '\\n'.join(rng.map(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"))) -""" - -stmt = ("read_csv(StringIO(data), header=None, names=['foo'], " - " parse_dates=['foo'])") -read_parse_dates_iso8601 = Benchmark(stmt, setup, - start_date=datetime(2012, 3, 1)) - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=1000) -data = DataFrame(rng, index=rng) -""" - -stmt = ("data.to_csv('__test__.csv', date_format='%Y%m%d')") - -frame_to_csv_date_formatting = Benchmark(stmt, setup, - start_date=datetime(2013, 9, 1)) - -#---------------------------------------------------------------------- -# infer datetime format - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=1000) -data = '\\n'.join(rng.map(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"))) -""" - -stmt = ("read_csv(StringIO(data), header=None, names=['foo'], " - " parse_dates=['foo'], infer_datetime_format=True)") - -read_csv_infer_datetime_format_iso8601 = Benchmark(stmt, setup) - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=1000) -data = '\\n'.join(rng.map(lambda x: x.strftime("%Y%m%d"))) -""" - -stmt = ("read_csv(StringIO(data), header=None, names=['foo'], " - " parse_dates=['foo'], infer_datetime_format=True)") - -read_csv_infer_datetime_format_ymd = Benchmark(stmt, setup) - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=1000) -data = '\\n'.join(rng.map(lambda x: x.strftime("%m/%d/%Y %H:%M:%S.%f"))) -""" - -stmt = ("read_csv(StringIO(data), header=None, names=['foo'], " - " parse_dates=['foo'], infer_datetime_format=True)") - -read_csv_infer_datetime_format_custom = Benchmark(stmt, setup) diff --git a/vb_suite/io_sql.py b/vb_suite/io_sql.py deleted file mode 100644 index ba8367e7e356b..0000000000000 --- a/vb_suite/io_sql.py +++ /dev/null @@ -1,126 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -import sqlite3 -import sqlalchemy -from sqlalchemy import create_engine - -engine = create_engine('sqlite:///:memory:') -con = sqlite3.connect(':memory:') -""" - -sdate = datetime(2014, 6, 1) - - -#------------------------------------------------------------------------------- -# to_sql - -setup = common_setup + """ -index = tm.makeStringIndex(10000) -df = DataFrame({'float1' : randn(10000), - 'float2' : randn(10000), - 'string1' : ['foo'] * 10000, - 'bool1' : [True] * 10000, - 'int1' : np.random.randint(0, 100000, size=10000)}, - index=index) -""" - -sql_write_sqlalchemy = Benchmark("df.to_sql('test1', engine, if_exists='replace')", - setup, start_date=sdate) - -sql_write_fallback = Benchmark("df.to_sql('test1', con, if_exists='replace')", - setup, start_date=sdate) - - -#------------------------------------------------------------------------------- -# read_sql - -setup = common_setup + """ -index = tm.makeStringIndex(10000) -df = DataFrame({'float1' : randn(10000), - 'float2' : randn(10000), - 'string1' : ['foo'] * 10000, - 'bool1' : [True] * 10000, - 'int1' : np.random.randint(0, 100000, size=10000)}, - index=index) -df.to_sql('test2', engine, if_exists='replace') -df.to_sql('test2', con, if_exists='replace') -""" - -sql_read_query_sqlalchemy = Benchmark("read_sql_query('SELECT * FROM test2', engine)", - setup, start_date=sdate) - -sql_read_query_fallback = Benchmark("read_sql_query('SELECT * FROM test2', con)", - setup, start_date=sdate) - -sql_read_table_sqlalchemy = Benchmark("read_sql_table('test2', engine)", - setup, start_date=sdate) - - -#------------------------------------------------------------------------------- -# type specific write - -setup = common_setup + """ -df = DataFrame({'float' : randn(10000), - 'string' : ['foo'] * 10000, - 'bool' : [True] * 10000, - 'datetime' : date_range('2000-01-01', periods=10000, freq='s')}) -df.loc[1000:3000, 'float'] = np.nan -""" - -sql_float_write_sqlalchemy = \ - Benchmark("df[['float']].to_sql('test_float', engine, if_exists='replace')", - setup, start_date=sdate) - -sql_float_write_fallback = \ - Benchmark("df[['float']].to_sql('test_float', con, if_exists='replace')", - setup, start_date=sdate) - -sql_string_write_sqlalchemy = \ - Benchmark("df[['string']].to_sql('test_string', engine, if_exists='replace')", - setup, start_date=sdate) - -sql_string_write_fallback = \ - Benchmark("df[['string']].to_sql('test_string', con, if_exists='replace')", - setup, start_date=sdate) - -sql_datetime_write_sqlalchemy = \ - Benchmark("df[['datetime']].to_sql('test_datetime', engine, if_exists='replace')", - setup, start_date=sdate) - -#sql_datetime_write_fallback = \ -# Benchmark("df[['datetime']].to_sql('test_datetime', con, if_exists='replace')", -# setup3, start_date=sdate) - -#------------------------------------------------------------------------------- -# type specific read - -setup = common_setup + """ -df = DataFrame({'float' : randn(10000), - 'datetime' : date_range('2000-01-01', periods=10000, freq='s')}) -df['datetime_string'] = df['datetime'].map(str) - -df.to_sql('test_type', engine, if_exists='replace') -df[['float', 'datetime_string']].to_sql('test_type', con, if_exists='replace') -""" - -sql_float_read_query_sqlalchemy = \ - Benchmark("read_sql_query('SELECT float FROM test_type', engine)", - setup, start_date=sdate) - -sql_float_read_table_sqlalchemy = \ - Benchmark("read_sql_table('test_type', engine, columns=['float'])", - setup, start_date=sdate) - -sql_float_read_query_fallback = \ - Benchmark("read_sql_query('SELECT float FROM test_type', con)", - setup, start_date=sdate) - -sql_datetime_read_as_native_sqlalchemy = \ - Benchmark("read_sql_table('test_type', engine, columns=['datetime'])", - setup, start_date=sdate) - -sql_datetime_read_and_parse_sqlalchemy = \ - Benchmark("read_sql_table('test_type', engine, columns=['datetime_string'], parse_dates=['datetime_string'])", - setup, start_date=sdate) diff --git a/vb_suite/join_merge.py b/vb_suite/join_merge.py deleted file mode 100644 index 238a129552e90..0000000000000 --- a/vb_suite/join_merge.py +++ /dev/null @@ -1,270 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -setup = common_setup + """ -level1 = tm.makeStringIndex(10).values -level2 = tm.makeStringIndex(1000).values -label1 = np.arange(10).repeat(1000) -label2 = np.tile(np.arange(1000), 10) - -key1 = np.tile(level1.take(label1), 10) -key2 = np.tile(level2.take(label2), 10) - -shuf = np.arange(100000) -random.shuffle(shuf) -try: - index2 = MultiIndex(levels=[level1, level2], labels=[label1, label2]) - index3 = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], - labels=[np.arange(10).repeat(10000), - np.tile(np.arange(100).repeat(100), 10), - np.tile(np.tile(np.arange(100), 100), 10)]) - df_multi = DataFrame(np.random.randn(len(index2), 4), index=index2, - columns=['A', 'B', 'C', 'D']) -except: # pre-MultiIndex - pass - -try: - DataFrame = DataMatrix -except: - pass - -df = pd.DataFrame({'data1' : np.random.randn(100000), - 'data2' : np.random.randn(100000), - 'key1' : key1, - 'key2' : key2}) - - -df_key1 = pd.DataFrame(np.random.randn(len(level1), 4), index=level1, - columns=['A', 'B', 'C', 'D']) -df_key2 = pd.DataFrame(np.random.randn(len(level2), 4), index=level2, - columns=['A', 'B', 'C', 'D']) - -df_shuf = df.reindex(df.index[shuf]) -""" - -#---------------------------------------------------------------------- -# DataFrame joins on key - -join_dataframe_index_single_key_small = \ - Benchmark("df.join(df_key1, on='key1')", setup, - name='join_dataframe_index_single_key_small') - -join_dataframe_index_single_key_bigger = \ - Benchmark("df.join(df_key2, on='key2')", setup, - name='join_dataframe_index_single_key_bigger') - -join_dataframe_index_single_key_bigger_sort = \ - Benchmark("df_shuf.join(df_key2, on='key2', sort=True)", setup, - name='join_dataframe_index_single_key_bigger_sort', - start_date=datetime(2012, 2, 5)) - -join_dataframe_index_multi = \ - Benchmark("df.join(df_multi, on=['key1', 'key2'])", setup, - name='join_dataframe_index_multi', - start_date=datetime(2011, 10, 20)) - -#---------------------------------------------------------------------- -# Joins on integer keys -setup = common_setup + """ -df = pd.DataFrame({'key1': np.tile(np.arange(500).repeat(10), 2), - 'key2': np.tile(np.arange(250).repeat(10), 4), - 'value': np.random.randn(10000)}) -df2 = pd.DataFrame({'key1': np.arange(500), 'value2': randn(500)}) -df3 = df[:5000] -""" - - -join_dataframe_integer_key = Benchmark("merge(df, df2, on='key1')", setup, - start_date=datetime(2011, 10, 20)) -join_dataframe_integer_2key = Benchmark("merge(df, df3)", setup, - start_date=datetime(2011, 10, 20)) - -#---------------------------------------------------------------------- -# DataFrame joins on index - - -#---------------------------------------------------------------------- -# Merges -setup = common_setup + """ -N = 10000 - -indices = tm.makeStringIndex(N).values -indices2 = tm.makeStringIndex(N).values -key = np.tile(indices[:8000], 10) -key2 = np.tile(indices2[:8000], 10) - -left = pd.DataFrame({'key' : key, 'key2':key2, - 'value' : np.random.randn(80000)}) -right = pd.DataFrame({'key': indices[2000:], 'key2':indices2[2000:], - 'value2' : np.random.randn(8000)}) -""" - -merge_2intkey_nosort = Benchmark('merge(left, right, sort=False)', setup, - start_date=datetime(2011, 10, 20)) - -merge_2intkey_sort = Benchmark('merge(left, right, sort=True)', setup, - start_date=datetime(2011, 10, 20)) - -#---------------------------------------------------------------------- -# Appending DataFrames - -setup = common_setup + """ -df1 = pd.DataFrame(np.random.randn(10000, 4), columns=['A', 'B', 'C', 'D']) -df2 = df1.copy() -df2.index = np.arange(10000, 20000) -mdf1 = df1.copy() -mdf1['obj1'] = 'bar' -mdf1['obj2'] = 'bar' -mdf1['int1'] = 5 -try: - mdf1.consolidate(inplace=True) -except: - pass -mdf2 = mdf1.copy() -mdf2.index = df2.index -""" - -stmt = "df1.append(df2)" -append_frame_single_homogenous = \ - Benchmark(stmt, setup, name='append_frame_single_homogenous', - ncalls=500, repeat=1) - -stmt = "mdf1.append(mdf2)" -append_frame_single_mixed = Benchmark(stmt, setup, - name='append_frame_single_mixed', - ncalls=500, repeat=1) - -#---------------------------------------------------------------------- -# data alignment - -setup = common_setup + """n = 1000000 -# indices = tm.makeStringIndex(n) -def sample(values, k): - sampler = np.random.permutation(len(values)) - return values.take(sampler[:k]) -sz = 500000 -rng = np.arange(0, 10000000000000, 10000000) -stamps = np.datetime64(datetime.now()).view('i8') + rng -idx1 = np.sort(sample(stamps, sz)) -idx2 = np.sort(sample(stamps, sz)) -ts1 = Series(np.random.randn(sz), idx1) -ts2 = Series(np.random.randn(sz), idx2) -""" -stmt = "ts1 + ts2" -series_align_int64_index = \ - Benchmark(stmt, setup, - name="series_align_int64_index", - start_date=datetime(2010, 6, 1), logy=True) - -stmt = "ts1.align(ts2, join='left')" -series_align_left_monotonic = \ - Benchmark(stmt, setup, - name="series_align_left_monotonic", - start_date=datetime(2011, 12, 1), logy=True) - -#---------------------------------------------------------------------- -# Concat Series axis=1 - -setup = common_setup + """ -n = 1000 -indices = tm.makeStringIndex(1000) -s = Series(n, index=indices) -pieces = [s[i:-i] for i in range(1, 10)] -pieces = pieces * 50 -""" - -concat_series_axis1 = Benchmark('concat(pieces, axis=1)', setup, - start_date=datetime(2012, 2, 27)) - -setup = common_setup + """ -df = pd.DataFrame(randn(5, 4)) -""" - -concat_small_frames = Benchmark('concat([df] * 1000)', setup, - start_date=datetime(2012, 1, 1)) - - -#---------------------------------------------------------------------- -# Concat empty - -setup = common_setup + """ -df = pd.DataFrame(dict(A = range(10000)),index=date_range('20130101',periods=10000,freq='s')) -empty = pd.DataFrame() -""" - -concat_empty_frames1 = Benchmark('concat([df,empty])', setup, - start_date=datetime(2012, 1, 1)) -concat_empty_frames2 = Benchmark('concat([empty,df])', setup, - start_date=datetime(2012, 1, 1)) - - -#---------------------------------------------------------------------- -# Ordered merge - -setup = common_setup + """ -groups = tm.makeStringIndex(10).values - -left = pd.DataFrame({'group': groups.repeat(5000), - 'key' : np.tile(np.arange(0, 10000, 2), 10), - 'lvalue': np.random.randn(50000)}) - -right = pd.DataFrame({'key' : np.arange(10000), - 'rvalue' : np.random.randn(10000)}) - -""" - -stmt = "ordered_merge(left, right, on='key', left_by='group')" - -#---------------------------------------------------------------------- -# outer join of non-unique -# GH 6329 - -setup = common_setup + """ -date_index = date_range('01-Jan-2013', '23-Jan-2013', freq='T') -daily_dates = date_index.to_period('D').to_timestamp('S','S') -fracofday = date_index.view(np.ndarray) - daily_dates.view(np.ndarray) -fracofday = fracofday.astype('timedelta64[ns]').astype(np.float64)/864e11 -fracofday = TimeSeries(fracofday, daily_dates) -index = date_range(date_index.min().to_period('A').to_timestamp('D','S'), - date_index.max().to_period('A').to_timestamp('D','E'), - freq='D') -temp = TimeSeries(1.0, index) -""" - -join_non_unique_equal = Benchmark('fracofday * temp[fracofday.index]', setup, - start_date=datetime(2013, 1, 1)) - - -setup = common_setup + ''' -np.random.seed(2718281) -n = 50000 - -left = pd.DataFrame(np.random.randint(1, n/500, (n, 2)), - columns=['jim', 'joe']) - -right = pd.DataFrame(np.random.randint(1, n/500, (n, 2)), - columns=['jolie', 'jolia']).set_index('jolie') -''' - -left_outer_join_index = Benchmark("left.join(right, on='jim')", setup, - name='left_outer_join_index') - - -setup = common_setup + """ -low, high, n = -1 << 10, 1 << 10, 1 << 20 -left = pd.DataFrame(np.random.randint(low, high, (n, 7)), - columns=list('ABCDEFG')) -left['left'] = left.sum(axis=1) - -i = np.random.permutation(len(left)) -right = left.iloc[i].copy() -right.columns = right.columns[:-1].tolist() + ['right'] -right.index = np.arange(len(right)) -right['right'] *= -1 -""" - -i8merge = Benchmark("merge(left, right, how='outer')", setup, - name='i8merge') diff --git a/vb_suite/make.py b/vb_suite/make.py deleted file mode 100755 index 5a8a8215db9a4..0000000000000 --- a/vb_suite/make.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python - -""" -Python script for building documentation. - -To build the docs you must have all optional dependencies for statsmodels -installed. See the installation instructions for a list of these. - -Note: currently latex builds do not work because of table formats that are not -supported in the latex generation. - -Usage ------ -python make.py clean -python make.py html -""" - -import glob -import os -import shutil -import sys -import sphinx - -os.environ['PYTHONPATH'] = '..' - -SPHINX_BUILD = 'sphinxbuild' - - -def upload(): - 'push a copy to the site' - os.system('cd build/html; rsync -avz . pandas@pandas.pydata.org' - ':/usr/share/nginx/pandas/pandas-docs/vbench/ -essh') - - -def clean(): - if os.path.exists('build'): - shutil.rmtree('build') - - if os.path.exists('source/generated'): - shutil.rmtree('source/generated') - - -def html(): - check_build() - if os.system('sphinx-build -P -b html -d build/doctrees ' - 'source build/html'): - raise SystemExit("Building HTML failed.") - - -def check_build(): - build_dirs = [ - 'build', 'build/doctrees', 'build/html', - 'build/plots', 'build/_static', - 'build/_templates'] - for d in build_dirs: - try: - os.mkdir(d) - except OSError: - pass - - -def all(): - clean() - html() - - -def auto_update(): - msg = '' - try: - clean() - html() - upload() - sendmail() - except (Exception, SystemExit), inst: - msg += str(inst) + '\n' - sendmail(msg) - - -def sendmail(err_msg=None): - from_name, to_name = _get_config() - - if err_msg is None: - msgstr = 'Daily vbench uploaded successfully' - subject = "VB: daily update successful" - else: - msgstr = err_msg - subject = "VB: daily update failed" - - import smtplib - from email.MIMEText import MIMEText - msg = MIMEText(msgstr) - msg['Subject'] = subject - msg['From'] = from_name - msg['To'] = to_name - - server_str, port, login, pwd = _get_credentials() - server = smtplib.SMTP(server_str, port) - server.ehlo() - server.starttls() - server.ehlo() - - server.login(login, pwd) - try: - server.sendmail(from_name, to_name, msg.as_string()) - finally: - server.close() - - -def _get_dir(subdir=None): - import getpass - USERNAME = getpass.getuser() - if sys.platform == 'darwin': - HOME = '/Users/%s' % USERNAME - else: - HOME = '/home/%s' % USERNAME - - if subdir is None: - subdir = '/code/scripts' - conf_dir = '%s%s' % (HOME, subdir) - return conf_dir - - -def _get_credentials(): - tmp_dir = _get_dir() - cred = '%s/credentials' % tmp_dir - with open(cred, 'r') as fh: - server, port, un, domain = fh.read().split(',') - port = int(port) - login = un + '@' + domain + '.com' - - import base64 - with open('%s/cron_email_pwd' % tmp_dir, 'r') as fh: - pwd = base64.b64decode(fh.read()) - - return server, port, login, pwd - - -def _get_config(): - tmp_dir = _get_dir() - with open('%s/addresses' % tmp_dir, 'r') as fh: - from_name, to_name = fh.read().split(',') - return from_name, to_name - -funcd = { - 'html': html, - 'clean': clean, - 'upload': upload, - 'auto_update': auto_update, - 'all': all, -} - -small_docs = False - -# current_dir = os.getcwd() -# os.chdir(os.path.dirname(os.path.join(current_dir, __file__))) - -if len(sys.argv) > 1: - for arg in sys.argv[1:]: - func = funcd.get(arg) - if func is None: - raise SystemExit('Do not know how to handle %s; valid args are %s' % ( - arg, funcd.keys())) - func() -else: - small_docs = False - all() -# os.chdir(current_dir) diff --git a/vb_suite/measure_memory_consumption.py b/vb_suite/measure_memory_consumption.py deleted file mode 100755 index bb73cf5da4302..0000000000000 --- a/vb_suite/measure_memory_consumption.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import print_function - -"""Short one-line summary - -long summary -""" - - -def main(): - import shutil - import tempfile - import warnings - - from pandas import Series - - from vbench.api import BenchmarkRunner - from suite import (REPO_PATH, BUILD, DB_PATH, PREPARE, - dependencies, benchmarks) - - from memory_profiler import memory_usage - - warnings.filterwarnings('ignore', category=FutureWarning) - - try: - TMP_DIR = tempfile.mkdtemp() - runner = BenchmarkRunner( - benchmarks, REPO_PATH, REPO_PATH, BUILD, DB_PATH, - TMP_DIR, PREPARE, always_clean=True, - # run_option='eod', start_date=START_DATE, - module_dependencies=dependencies) - results = {} - for b in runner.benchmarks: - k = b.name - try: - vs = memory_usage((b.run,)) - v = max(vs) - # print(k, v) - results[k] = v - except Exception as e: - print("Exception caught in %s\n" % k) - print(str(e)) - - s = Series(results) - s.sort() - print((s)) - - finally: - shutil.rmtree(TMP_DIR) - - -if __name__ == "__main__": - main() diff --git a/vb_suite/miscellaneous.py b/vb_suite/miscellaneous.py deleted file mode 100644 index da2c736e79ea7..0000000000000 --- a/vb_suite/miscellaneous.py +++ /dev/null @@ -1,32 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# cache_readonly - -setup = common_setup + """ -from pandas.util.decorators import cache_readonly - -class Foo: - - @cache_readonly - def prop(self): - return 5 -obj = Foo() -""" -misc_cache_readonly = Benchmark("obj.prop", setup, name="misc_cache_readonly", - ncalls=2000000) - -#---------------------------------------------------------------------- -# match - -setup = common_setup + """ -uniques = tm.makeStringIndex(1000).values -all = uniques.repeat(10) -""" - -match_strings = Benchmark("match(all, uniques)", setup, - start_date=datetime(2012, 5, 12)) diff --git a/vb_suite/packers.py b/vb_suite/packers.py deleted file mode 100644 index 69ec10822b392..0000000000000 --- a/vb_suite/packers.py +++ /dev/null @@ -1,252 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -start_date = datetime(2013, 5, 1) - -common_setup = """from .pandas_vb_common import * -import os -import pandas as pd -from pandas.core import common as com -from pandas.compat import BytesIO -from random import randrange - -f = '__test__.msg' -def remove(f): - try: - os.remove(f) - except: - pass - -N=100000 -C=5 -index = date_range('20000101',periods=N,freq='H') -df = DataFrame(dict([ ("float{0}".format(i),randn(N)) for i in range(C) ]), - index=index) - -N=100000 -C=5 -index = date_range('20000101',periods=N,freq='H') -df2 = DataFrame(dict([ ("float{0}".format(i),randn(N)) for i in range(C) ]), - index=index) -df2['object'] = ['%08x'%randrange(16**8) for _ in range(N)] -remove(f) -""" - -#---------------------------------------------------------------------- -# msgpack - -setup = common_setup + """ -df2.to_msgpack(f) -""" - -packers_read_pack = Benchmark("pd.read_msgpack(f)", setup, start_date=start_date) - -setup = common_setup + """ -""" - -packers_write_pack = Benchmark("df2.to_msgpack(f)", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# pickle - -setup = common_setup + """ -df2.to_pickle(f) -""" - -packers_read_pickle = Benchmark("pd.read_pickle(f)", setup, start_date=start_date) - -setup = common_setup + """ -""" - -packers_write_pickle = Benchmark("df2.to_pickle(f)", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# csv - -setup = common_setup + """ -df.to_csv(f) -""" - -packers_read_csv = Benchmark("pd.read_csv(f)", setup, start_date=start_date) - -setup = common_setup + """ -""" - -packers_write_csv = Benchmark("df.to_csv(f)", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# hdf store - -setup = common_setup + """ -df2.to_hdf(f,'df') -""" - -packers_read_hdf_store = Benchmark("pd.read_hdf(f,'df')", setup, start_date=start_date) - -setup = common_setup + """ -""" - -packers_write_hdf_store = Benchmark("df2.to_hdf(f,'df')", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# hdf table - -setup = common_setup + """ -df2.to_hdf(f,'df',format='table') -""" - -packers_read_hdf_table = Benchmark("pd.read_hdf(f,'df')", setup, start_date=start_date) - -setup = common_setup + """ -""" - -packers_write_hdf_table = Benchmark("df2.to_hdf(f,'df',table=True)", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# sql - -setup = common_setup + """ -import sqlite3 -from sqlalchemy import create_engine -engine = create_engine('sqlite:///:memory:') - -df2.to_sql('table', engine, if_exists='replace') -""" - -packers_read_sql= Benchmark("pd.read_sql_table('table', engine)", setup, start_date=start_date) - -setup = common_setup + """ -import sqlite3 -from sqlalchemy import create_engine -engine = create_engine('sqlite:///:memory:') -""" - -packers_write_sql = Benchmark("df2.to_sql('table', engine, if_exists='replace')", setup, start_date=start_date) - -#---------------------------------------------------------------------- -# json - -setup_int_index = """ -import numpy as np -df.index = np.arange(N) -""" - -setup = common_setup + """ -df.to_json(f,orient='split') -""" -packers_read_json_date_index = Benchmark("pd.read_json(f, orient='split')", setup, start_date=start_date) -setup = setup + setup_int_index -packers_read_json = Benchmark("pd.read_json(f, orient='split')", setup, start_date=start_date) - -setup = common_setup + """ -""" -packers_write_json_date_index = Benchmark("df.to_json(f,orient='split')", setup, cleanup="remove(f)", start_date=start_date) - -setup = setup + setup_int_index -packers_write_json = Benchmark("df.to_json(f,orient='split')", setup, cleanup="remove(f)", start_date=start_date) -packers_write_json_T = Benchmark("df.to_json(f,orient='columns')", setup, cleanup="remove(f)", start_date=start_date) - -setup = common_setup + """ -from numpy.random import randint -from collections import OrderedDict - -cols = [ - lambda i: ("{0}_timedelta".format(i), [pd.Timedelta('%d seconds' % randrange(1e6)) for _ in range(N)]), - lambda i: ("{0}_int".format(i), randint(1e8, size=N)), - lambda i: ("{0}_timestamp".format(i), [pd.Timestamp( 1418842918083256000 + randrange(1e9, 1e18, 200)) for _ in range(N)]) - ] -df_mixed = DataFrame(OrderedDict([cols[i % len(cols)](i) for i in range(C)]), - index=index) -""" -packers_write_json_mixed_delta_int_tstamp = Benchmark("df_mixed.to_json(f,orient='split')", setup, cleanup="remove(f)", start_date=start_date) - -setup = common_setup + """ -from numpy.random import randint -from collections import OrderedDict -cols = [ - lambda i: ("{0}_float".format(i), randn(N)), - lambda i: ("{0}_int".format(i), randint(1e8, size=N)) - ] -df_mixed = DataFrame(OrderedDict([cols[i % len(cols)](i) for i in range(C)]), - index=index) -""" -packers_write_json_mixed_float_int = Benchmark("df_mixed.to_json(f,orient='index')", setup, cleanup="remove(f)", start_date=start_date) -packers_write_json_mixed_float_int_T = Benchmark("df_mixed.to_json(f,orient='columns')", setup, cleanup="remove(f)", start_date=start_date) - -setup = common_setup + """ -from numpy.random import randint -from collections import OrderedDict -cols = [ - lambda i: ("{0}_float".format(i), randn(N)), - lambda i: ("{0}_int".format(i), randint(1e8, size=N)), - lambda i: ("{0}_str".format(i), ['%08x'%randrange(16**8) for _ in range(N)]) - ] -df_mixed = DataFrame(OrderedDict([cols[i % len(cols)](i) for i in range(C)]), - index=index) -""" -packers_write_json_mixed_float_int_str = Benchmark("df_mixed.to_json(f,orient='split')", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# stata - -setup = common_setup + """ -df.to_stata(f, {'index': 'tc'}) -""" -packers_read_stata = Benchmark("pd.read_stata(f)", setup, start_date=start_date) - -packers_write_stata = Benchmark("df.to_stata(f, {'index': 'tc'})", setup, cleanup="remove(f)", start_date=start_date) - -setup = common_setup + """ -df['int8_'] = [randint(np.iinfo(np.int8).min, np.iinfo(np.int8).max - 27) for _ in range(N)] -df['int16_'] = [randint(np.iinfo(np.int16).min, np.iinfo(np.int16).max - 27) for _ in range(N)] -df['int32_'] = [randint(np.iinfo(np.int32).min, np.iinfo(np.int32).max - 27) for _ in range(N)] -df['float32_'] = np.array(randn(N), dtype=np.float32) -df.to_stata(f, {'index': 'tc'}) -""" - -packers_read_stata_with_validation = Benchmark("pd.read_stata(f)", setup, start_date=start_date) - -packers_write_stata_with_validation = Benchmark("df.to_stata(f, {'index': 'tc'})", setup, cleanup="remove(f)", start_date=start_date) - -#---------------------------------------------------------------------- -# Excel - alternative writers -setup = common_setup + """ -bio = BytesIO() -""" - -excel_writer_bench = """ -bio.seek(0) -writer = pd.io.excel.ExcelWriter(bio, engine='{engine}') -df[:2000].to_excel(writer) -writer.save() -""" - -benchmark_xlsxwriter = excel_writer_bench.format(engine='xlsxwriter') - -packers_write_excel_xlsxwriter = Benchmark(benchmark_xlsxwriter, setup) - -benchmark_openpyxl = excel_writer_bench.format(engine='openpyxl') - -packers_write_excel_openpyxl = Benchmark(benchmark_openpyxl, setup) - -benchmark_xlwt = excel_writer_bench.format(engine='xlwt') - -packers_write_excel_xlwt = Benchmark(benchmark_xlwt, setup) - - -#---------------------------------------------------------------------- -# Excel - reader - -setup = common_setup + """ -bio = BytesIO() -writer = pd.io.excel.ExcelWriter(bio, engine='xlsxwriter') -df[:2000].to_excel(writer) -writer.save() -""" - -benchmark_read_excel=""" -bio.seek(0) -pd.read_excel(bio) -""" - -packers_read_excel = Benchmark(benchmark_read_excel, setup) diff --git a/vb_suite/pandas_vb_common.py b/vb_suite/pandas_vb_common.py deleted file mode 100644 index bd2e8a1c1d504..0000000000000 --- a/vb_suite/pandas_vb_common.py +++ /dev/null @@ -1,30 +0,0 @@ -from pandas import * -import pandas as pd -from datetime import timedelta -from numpy.random import randn -from numpy.random import randint -from numpy.random import permutation -import pandas.util.testing as tm -import random -import numpy as np -try: - from pandas.compat import range -except ImportError: - pass - -np.random.seed(1234) -try: - import pandas._tseries as lib -except: - import pandas._libs.lib as lib - -try: - Panel = WidePanel -except Exception: - pass - -# didn't add to namespace until later -try: - from pandas.core.index import MultiIndex -except ImportError: - pass diff --git a/vb_suite/panel_ctor.py b/vb_suite/panel_ctor.py deleted file mode 100644 index 9f497e7357a61..0000000000000 --- a/vb_suite/panel_ctor.py +++ /dev/null @@ -1,76 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# Panel.from_dict homogenization time - -START_DATE = datetime(2011, 6, 1) - -setup_same_index = common_setup + """ -# create 100 dataframes with the same index -dr = np.asarray(DatetimeIndex(start=datetime(1990,1,1), end=datetime(2012,1,1), - freq=datetools.Day(1))) -data_frames = {} -for x in range(100): - df = DataFrame({"a": [0]*len(dr), "b": [1]*len(dr), - "c": [2]*len(dr)}, index=dr) - data_frames[x] = df -""" - -panel_from_dict_same_index = \ - Benchmark("Panel.from_dict(data_frames)", - setup_same_index, name='panel_from_dict_same_index', - start_date=START_DATE, repeat=1, logy=True) - -setup_equiv_indexes = common_setup + """ -data_frames = {} -for x in range(100): - dr = np.asarray(DatetimeIndex(start=datetime(1990,1,1), end=datetime(2012,1,1), - freq=datetools.Day(1))) - df = DataFrame({"a": [0]*len(dr), "b": [1]*len(dr), - "c": [2]*len(dr)}, index=dr) - data_frames[x] = df -""" - -panel_from_dict_equiv_indexes = \ - Benchmark("Panel.from_dict(data_frames)", - setup_equiv_indexes, name='panel_from_dict_equiv_indexes', - start_date=START_DATE, repeat=1, logy=True) - -setup_all_different_indexes = common_setup + """ -data_frames = {} -start = datetime(1990,1,1) -end = datetime(2012,1,1) -for x in range(100): - end += timedelta(days=1) - dr = np.asarray(date_range(start, end)) - df = DataFrame({"a": [0]*len(dr), "b": [1]*len(dr), - "c": [2]*len(dr)}, index=dr) - data_frames[x] = df -""" -panel_from_dict_all_different_indexes = \ - Benchmark("Panel.from_dict(data_frames)", - setup_all_different_indexes, - name='panel_from_dict_all_different_indexes', - start_date=START_DATE, repeat=1, logy=True) - -setup_two_different_indexes = common_setup + """ -data_frames = {} -start = datetime(1990,1,1) -end = datetime(2012,1,1) -for x in range(100): - if x == 50: - end += timedelta(days=1) - dr = np.asarray(date_range(start, end)) - df = DataFrame({"a": [0]*len(dr), "b": [1]*len(dr), - "c": [2]*len(dr)}, index=dr) - data_frames[x] = df -""" -panel_from_dict_two_different_indexes = \ - Benchmark("Panel.from_dict(data_frames)", - setup_two_different_indexes, - name='panel_from_dict_two_different_indexes', - start_date=START_DATE, repeat=1, logy=True) diff --git a/vb_suite/panel_methods.py b/vb_suite/panel_methods.py deleted file mode 100644 index 28586422a66e3..0000000000000 --- a/vb_suite/panel_methods.py +++ /dev/null @@ -1,28 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# shift - -setup = common_setup + """ -index = date_range(start="2000", freq="D", periods=1000) -panel = Panel(np.random.randn(100, len(index), 1000)) -""" - -panel_shift = Benchmark('panel.shift(1)', setup, - start_date=datetime(2012, 1, 12)) - -panel_shift_minor = Benchmark('panel.shift(1, axis="minor")', setup, - start_date=datetime(2012, 1, 12)) - -panel_pct_change_major = Benchmark('panel.pct_change(1, axis="major")', setup, - start_date=datetime(2014, 4, 19)) - -panel_pct_change_minor = Benchmark('panel.pct_change(1, axis="minor")', setup, - start_date=datetime(2014, 4, 19)) - -panel_pct_change_items = Benchmark('panel.pct_change(1, axis="items")', setup, - start_date=datetime(2014, 4, 19)) diff --git a/vb_suite/parser_vb.py b/vb_suite/parser_vb.py deleted file mode 100644 index bb9ccbdb5e854..0000000000000 --- a/vb_suite/parser_vb.py +++ /dev/null @@ -1,112 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -from pandas import read_csv, read_table -""" - -setup = common_setup + """ -import os -N = 10000 -K = 8 -df = DataFrame(np.random.randn(N, K) * np.random.randint(100, 10000, (N, K))) -df.to_csv('test.csv', sep='|') -""" - -read_csv_vb = Benchmark("read_csv('test.csv', sep='|')", setup, - cleanup="os.remove('test.csv')", - start_date=datetime(2012, 5, 7)) - - -setup = common_setup + """ -import os -N = 10000 -K = 8 -format = lambda x: '{:,}'.format(x) -df = DataFrame(np.random.randn(N, K) * np.random.randint(100, 10000, (N, K))) -df = df.applymap(format) -df.to_csv('test.csv', sep='|') -""" - -read_csv_thou_vb = Benchmark("read_csv('test.csv', sep='|', thousands=',')", - setup, - cleanup="os.remove('test.csv')", - start_date=datetime(2012, 5, 7)) - -setup = common_setup + """ -data = ['A,B,C'] -data = data + ['1,2,3 # comment'] * 100000 -data = '\\n'.join(data) -""" - -stmt = "read_csv(StringIO(data), comment='#')" -read_csv_comment2 = Benchmark(stmt, setup, - start_date=datetime(2011, 11, 1)) - -setup = common_setup + """ -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -import os -N = 10000 -K = 8 -data = '''\ -KORD,19990127, 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127, 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127, 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127, 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127, 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -''' -data = data * 200 -""" -cmd = ("read_table(StringIO(data), sep=',', header=None, " - "parse_dates=[[1,2], [1,3]])") -sdate = datetime(2012, 5, 7) -read_table_multiple_date = Benchmark(cmd, setup, start_date=sdate) - -setup = common_setup + """ -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -import os -N = 10000 -K = 8 -data = '''\ -KORD,19990127 19:00:00, 18:56:00, 0.8100, 2.8100, 7.2000, 0.0000, 280.0000 -KORD,19990127 20:00:00, 19:56:00, 0.0100, 2.2100, 7.2000, 0.0000, 260.0000 -KORD,19990127 21:00:00, 20:56:00, -0.5900, 2.2100, 5.7000, 0.0000, 280.0000 -KORD,19990127 21:00:00, 21:18:00, -0.9900, 2.0100, 3.6000, 0.0000, 270.0000 -KORD,19990127 22:00:00, 21:56:00, -0.5900, 1.7100, 5.1000, 0.0000, 290.0000 -''' -data = data * 200 -""" -cmd = "read_table(StringIO(data), sep=',', header=None, parse_dates=[1])" -sdate = datetime(2012, 5, 7) -read_table_multiple_date_baseline = Benchmark(cmd, setup, start_date=sdate) - -setup = common_setup + """ -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -data = '''\ -0.1213700904466425978256438611,0.0525708283766902484401839501,0.4174092731488769913994474336 -0.4096341697147408700274695547,0.1587830198973579909349496119,0.1292545832485494372576795285 -0.8323255650024565799327547210,0.9694902427379478160318626578,0.6295047811546814475747169126 -0.4679375305798131323697930383,0.2963942381834381301075609371,0.5268936082160610157032465394 -0.6685382761849776311890991564,0.6721207066140679753374342908,0.6519975277021627935170045020 -''' -data = data * 200 -""" -cmd = "read_csv(StringIO(data), sep=',', header=None, float_precision=None)" -sdate = datetime(2014, 8, 20) -read_csv_default_converter = Benchmark(cmd, setup, start_date=sdate) -cmd = "read_csv(StringIO(data), sep=',', header=None, float_precision='high')" -read_csv_precise_converter = Benchmark(cmd, setup, start_date=sdate) -cmd = "read_csv(StringIO(data), sep=',', header=None, float_precision='round_trip')" -read_csv_roundtrip_converter = Benchmark(cmd, setup, start_date=sdate) diff --git a/vb_suite/perf_HEAD.py b/vb_suite/perf_HEAD.py deleted file mode 100755 index 143d943b9eadf..0000000000000 --- a/vb_suite/perf_HEAD.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from __future__ import print_function - -"""Run all the vbenches in `suite`, and post the results as a json blob to gist - -""" - -import urllib2 -from contextlib import closing -from urllib2 import urlopen -import json - -import pandas as pd - -WEB_TIMEOUT = 10 - - -def get_travis_data(): - """figure out what worker we're running on, and the number of jobs it's running - """ - import os - jobid = os.environ.get("TRAVIS_JOB_ID") - if not jobid: - return None, None - - with closing(urlopen("https://api.travis-ci.org/workers/")) as resp: - workers = json.loads(resp.read()) - - host = njobs = None - for item in workers: - host = item.get("host") - id = ((item.get("payload") or {}).get("job") or {}).get("id") - if id and str(id) == str(jobid): - break - if host: - njobs = len( - [x for x in workers if host in x['host'] and x['payload']]) - - return host, njobs - - -def get_utcdatetime(): - try: - from datetime import datetime - return datetime.utcnow().isoformat(" ") - except: - pass - - -def dump_as_gist(data, desc="The Commit", njobs=None): - host, njobs2 = get_travis_data()[:2] - - if njobs: # be slightly more reliable - njobs = max(njobs, njobs2) - - content = dict(version="0.1.1", - timings=data, - datetime=get_utcdatetime(), # added in 0.1.1 - hostname=host, # added in 0.1.1 - njobs=njobs # added in 0.1.1, a measure of load on the travis box - ) - - payload = dict(description=desc, - public=True, - files={'results.json': dict(content=json.dumps(content))}) - try: - with closing(urlopen("https://api.github.com/gists", - json.dumps(payload), timeout=WEB_TIMEOUT)) as r: - if 200 <= r.getcode() < 300: - print("\n\n" + "-" * 80) - - gist = json.loads(r.read()) - file_raw_url = gist['files'].items()[0][1]['raw_url'] - print("[vbench-gist-raw_url] %s" % file_raw_url) - print("[vbench-html-url] %s" % gist['html_url']) - print("[vbench-api-url] %s" % gist['url']) - - print("-" * 80 + "\n\n") - else: - print("api.github.com returned status %d" % r.getcode()) - except: - print("Error occured while dumping to gist") - - -def main(): - import warnings - from suite import benchmarks - - exit_code = 0 - warnings.filterwarnings('ignore', category=FutureWarning) - - host, njobs = get_travis_data()[:2] - results = [] - for b in benchmarks: - try: - d = b.run() - d.update(dict(name=b.name)) - results.append(d) - msg = "{name:<40}: {timing:> 10.4f} [ms]" - print(msg.format(name=results[-1]['name'], - timing=results[-1]['timing'])) - - except Exception as e: - exit_code = 1 - if (type(e) == KeyboardInterrupt or - 'KeyboardInterrupt' in str(d)): - raise KeyboardInterrupt() - - msg = "{name:<40}: ERROR:\n<-------" - print(msg.format(name=b.name)) - if isinstance(d, dict): - if d['succeeded']: - print("\nException:\n%s\n" % str(e)) - else: - for k, v in sorted(d.iteritems()): - print("{k}: {v}".format(k=k, v=v)) - - print("------->\n") - - dump_as_gist(results, "testing", njobs=njobs) - - return exit_code - - -if __name__ == "__main__": - import sys - sys.exit(main()) - -##################################################### -# functions for retrieving and processing the results - - -def get_vbench_log(build_url): - with closing(urllib2.urlopen(build_url)) as r: - if not (200 <= r.getcode() < 300): - return - - s = json.loads(r.read()) - s = [x for x in s['matrix'] if "VBENCH" in ((x.get('config', {}) - or {}).get('env', {}) or {})] - # s=[x for x in s['matrix']] - if not s: - return - id = s[0]['id'] # should be just one for now - with closing(urllib2.urlopen("https://api.travis-ci.org/jobs/%s" % id)) as r2: - if not 200 <= r.getcode() < 300: - return - s2 = json.loads(r2.read()) - return s2.get('log') - - -def get_results_raw_url(build): - "Taks a Travis a build number, retrieves the build log and extracts the gist url" - import re - log = get_vbench_log("https://api.travis-ci.org/builds/%s" % build) - if not log: - return - l = [x.strip( - ) for x in log.split("\n") if re.match(".vbench-gist-raw_url", x)] - if l: - s = l[0] - m = re.search("(https://[^\s]+)", s) - if m: - return m.group(0) - - -def convert_json_to_df(results_url): - """retrieve json results file from url and return df - - df contains timings for all successful vbenchmarks - """ - - with closing(urlopen(results_url)) as resp: - res = json.loads(resp.read()) - timings = res.get("timings") - if not timings: - return - res = [x for x in timings if x.get('succeeded')] - df = pd.DataFrame(res) - df = df.set_index("name") - return df - - -def get_build_results(build): - "Returns a df with the results of the VBENCH job associated with the travis build" - r_url = get_results_raw_url(build) - if not r_url: - return - - return convert_json_to_df(r_url) - - -def get_all_results(repo_id=53976): # travis pandas-dev/pandas id - """Fetches the VBENCH results for all travis builds, and returns a list of result df - - unsuccesful individual vbenches are dropped. - """ - from collections import OrderedDict - - def get_results_from_builds(builds): - dfs = OrderedDict() - for build in builds: - build_id = build['id'] - build_number = build['number'] - print(build_number) - res = get_build_results(build_id) - if res is not None: - dfs[build_number] = res - return dfs - - base_url = 'https://api.travis-ci.org/builds?url=%2Fbuilds&repository_id={repo_id}' - url = base_url.format(repo_id=repo_id) - url_after = url + '&after_number={after}' - dfs = OrderedDict() - - while True: - with closing(urlopen(url)) as r: - if not (200 <= r.getcode() < 300): - break - builds = json.loads(r.read()) - res = get_results_from_builds(builds) - if not res: - break - last_build_number = min(res.keys()) - dfs.update(res) - url = url_after.format(after=last_build_number) - - return dfs - - -def get_all_results_joined(repo_id=53976): - def mk_unique(df): - for dupe in df.index.get_duplicates(): - df = df.ix[df.index != dupe] - return df - dfs = get_all_results(repo_id) - for k in dfs: - dfs[k] = mk_unique(dfs[k]) - ss = [pd.Series(v.timing, name=k) for k, v in dfs.iteritems()] - results = pd.concat(reversed(ss), 1) - return results diff --git a/vb_suite/plotting.py b/vb_suite/plotting.py deleted file mode 100644 index 79e81e9eea8f4..0000000000000 --- a/vb_suite/plotting.py +++ /dev/null @@ -1,25 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * - -try: - from pandas import date_range -except ImportError: - def date_range(start=None, end=None, periods=None, freq=None): - return DatetimeIndex(start, end, periods=periods, offset=freq) - -""" - -#----------------------------------------------------------------------------- -# Timeseries plotting - -setup = common_setup + """ -N = 2000 -M = 5 -df = DataFrame(np.random.randn(N,M), index=date_range('1/1/1975', periods=N)) -""" - -plot_timeseries_period = Benchmark("df.plot()", setup=setup, - name='plot_timeseries_period') - diff --git a/vb_suite/reindex.py b/vb_suite/reindex.py deleted file mode 100644 index 443eb43835745..0000000000000 --- a/vb_suite/reindex.py +++ /dev/null @@ -1,225 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# DataFrame reindex columns - -setup = common_setup + """ -df = DataFrame(index=range(10000), data=np.random.rand(10000,30), - columns=range(30)) -""" -statement = "df.reindex(columns=df.columns[1:5])" - -frame_reindex_columns = Benchmark(statement, setup) - -#---------------------------------------------------------------------- - -setup = common_setup + """ -rng = DatetimeIndex(start='1/1/1970', periods=10000, freq=datetools.Minute()) -df = DataFrame(np.random.rand(10000, 10), index=rng, - columns=range(10)) -df['foo'] = 'bar' -rng2 = Index(rng[::2]) -""" -statement = "df.reindex(rng2)" -dataframe_reindex = Benchmark(statement, setup) - -#---------------------------------------------------------------------- -# multiindex reindexing - -setup = common_setup + """ -N = 1000 -K = 20 - -level1 = tm.makeStringIndex(N).values.repeat(K) -level2 = np.tile(tm.makeStringIndex(K).values, N) -index = MultiIndex.from_arrays([level1, level2]) - -s1 = Series(np.random.randn(N * K), index=index) -s2 = s1[::2] -""" -statement = "s1.reindex(s2.index)" -reindex_multi = Benchmark(statement, setup, - name='reindex_multiindex', - start_date=datetime(2011, 9, 1)) - -#---------------------------------------------------------------------- -# Pad / backfill - -def pad(source_series, target_index): - try: - source_series.reindex(target_index, method='pad') - except: - source_series.reindex(target_index, fillMethod='pad') - -def backfill(source_series, target_index): - try: - source_series.reindex(target_index, method='backfill') - except: - source_series.reindex(target_index, fillMethod='backfill') - -setup = common_setup + """ -rng = date_range('1/1/2000', periods=100000, freq=datetools.Minute()) - -ts = Series(np.random.randn(len(rng)), index=rng) -ts2 = ts[::2] -ts3 = ts2.reindex(ts.index) -ts4 = ts3.astype('float32') - -def pad(source_series, target_index): - try: - source_series.reindex(target_index, method='pad') - except: - source_series.reindex(target_index, fillMethod='pad') -def backfill(source_series, target_index): - try: - source_series.reindex(target_index, method='backfill') - except: - source_series.reindex(target_index, fillMethod='backfill') -""" - -statement = "pad(ts2, ts.index)" -reindex_daterange_pad = Benchmark(statement, setup, - name="reindex_daterange_pad") - -statement = "backfill(ts2, ts.index)" -reindex_daterange_backfill = Benchmark(statement, setup, - name="reindex_daterange_backfill") - -reindex_fillna_pad = Benchmark("ts3.fillna(method='pad')", setup, - name="reindex_fillna_pad", - start_date=datetime(2011, 3, 1)) - -reindex_fillna_pad_float32 = Benchmark("ts4.fillna(method='pad')", setup, - name="reindex_fillna_pad_float32", - start_date=datetime(2013, 1, 1)) - -reindex_fillna_backfill = Benchmark("ts3.fillna(method='backfill')", setup, - name="reindex_fillna_backfill", - start_date=datetime(2011, 3, 1)) -reindex_fillna_backfill_float32 = Benchmark("ts4.fillna(method='backfill')", setup, - name="reindex_fillna_backfill_float32", - start_date=datetime(2013, 1, 1)) - -#---------------------------------------------------------------------- -# align on level - -setup = common_setup + """ -index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], - labels=[np.arange(10).repeat(10000), - np.tile(np.arange(100).repeat(100), 10), - np.tile(np.tile(np.arange(100), 100), 10)]) -random.shuffle(index.values) -df = DataFrame(np.random.randn(len(index), 4), index=index) -df_level = DataFrame(np.random.randn(100, 4), index=index.levels[1]) -""" - -reindex_frame_level_align = \ - Benchmark("df.align(df_level, level=1, copy=False)", setup, - name='reindex_frame_level_align', - start_date=datetime(2011, 12, 27)) - -reindex_frame_level_reindex = \ - Benchmark("df_level.reindex(df.index, level=1)", setup, - name='reindex_frame_level_reindex', - start_date=datetime(2011, 12, 27)) - - -#---------------------------------------------------------------------- -# sort_index, drop_duplicates - -# pathological, but realistic -setup = common_setup + """ -N = 10000 -K = 10 - -key1 = tm.makeStringIndex(N).values.repeat(K) -key2 = tm.makeStringIndex(N).values.repeat(K) - -df = DataFrame({'key1' : key1, 'key2' : key2, - 'value' : np.random.randn(N * K)}) -col_array_list = list(df.values.T) -""" -statement = "df.sort_index(by=['key1', 'key2'])" -frame_sort_index_by_columns = Benchmark(statement, setup, - start_date=datetime(2011, 11, 1)) - -# drop_duplicates - -statement = "df.drop_duplicates(['key1', 'key2'])" -frame_drop_duplicates = Benchmark(statement, setup, - start_date=datetime(2011, 11, 15)) - -statement = "df.drop_duplicates(['key1', 'key2'], inplace=True)" -frame_drop_dup_inplace = Benchmark(statement, setup, - start_date=datetime(2012, 5, 16)) - -lib_fast_zip = Benchmark('lib.fast_zip(col_array_list)', setup, - name='lib_fast_zip', - start_date=datetime(2012, 1, 1)) - -setup = setup + """ -df.ix[:10000, :] = np.nan -""" -statement2 = "df.drop_duplicates(['key1', 'key2'])" -frame_drop_duplicates_na = Benchmark(statement2, setup, - start_date=datetime(2012, 5, 15)) - -lib_fast_zip_fillna = Benchmark('lib.fast_zip_fillna(col_array_list)', setup, - start_date=datetime(2012, 5, 15)) - -statement2 = "df.drop_duplicates(['key1', 'key2'], inplace=True)" -frame_drop_dup_na_inplace = Benchmark(statement2, setup, - start_date=datetime(2012, 5, 16)) - -setup = common_setup + """ -s = Series(np.random.randint(0, 1000, size=10000)) -s2 = Series(np.tile(tm.makeStringIndex(1000).values, 10)) -""" - -series_drop_duplicates_int = Benchmark('s.drop_duplicates()', setup, - start_date=datetime(2012, 11, 27)) - -series_drop_duplicates_string = \ - Benchmark('s2.drop_duplicates()', setup, - start_date=datetime(2012, 11, 27)) - -#---------------------------------------------------------------------- -# fillna, many columns - - -setup = common_setup + """ -values = np.random.randn(1000, 1000) -values[::2] = np.nan -df = DataFrame(values) -""" - -frame_fillna_many_columns_pad = Benchmark("df.fillna(method='pad')", - setup, - start_date=datetime(2011, 3, 1)) - -#---------------------------------------------------------------------- -# blog "pandas escaped the zoo" - -setup = common_setup + """ -n = 50000 -indices = tm.makeStringIndex(n) - -def sample(values, k): - from random import shuffle - sampler = np.arange(len(values)) - shuffle(sampler) - return values.take(sampler[:k]) - -subsample_size = 40000 - -x = Series(np.random.randn(50000), indices) -y = Series(np.random.randn(subsample_size), - index=sample(indices, subsample_size)) -""" - -series_align_irregular_string = Benchmark("x + y", setup, - start_date=datetime(2010, 6, 1)) diff --git a/vb_suite/replace.py b/vb_suite/replace.py deleted file mode 100644 index 9326aa5becca9..0000000000000 --- a/vb_suite/replace.py +++ /dev/null @@ -1,36 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -from datetime import timedelta - -N = 1000000 - -try: - rng = date_range('1/1/2000', periods=N, freq='min') -except NameError: - rng = DatetimeIndex('1/1/2000', periods=N, offset=datetools.Minute()) - date_range = DateRange - -ts = Series(np.random.randn(N), index=rng) -""" - -large_dict_setup = """from .pandas_vb_common import * -from pandas.compat import range -n = 10 ** 6 -start_value = 10 ** 5 -to_rep = dict((i, start_value + i) for i in range(n)) -s = Series(np.random.randint(n, size=10 ** 3)) -""" - -replace_fillna = Benchmark('ts.fillna(0., inplace=True)', common_setup, - name='replace_fillna', - start_date=datetime(2012, 4, 4)) -replace_replacena = Benchmark('ts.replace(np.nan, 0., inplace=True)', - common_setup, - name='replace_replacena', - start_date=datetime(2012, 5, 15)) -replace_large_dict = Benchmark('s.replace(to_rep, inplace=True)', - large_dict_setup, - name='replace_large_dict', - start_date=datetime(2014, 4, 6)) diff --git a/vb_suite/reshape.py b/vb_suite/reshape.py deleted file mode 100644 index daab96103f2c5..0000000000000 --- a/vb_suite/reshape.py +++ /dev/null @@ -1,65 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -index = MultiIndex.from_arrays([np.arange(100).repeat(100), - np.roll(np.tile(np.arange(100), 100), 25)]) -df = DataFrame(np.random.randn(10000, 4), index=index) -""" - -reshape_unstack_simple = Benchmark('df.unstack(1)', common_setup, - start_date=datetime(2011, 10, 1)) - -setup = common_setup + """ -udf = df.unstack(1) -""" - -reshape_stack_simple = Benchmark('udf.stack()', setup, - start_date=datetime(2011, 10, 1)) - -setup = common_setup + """ -def unpivot(frame): - N, K = frame.shape - data = {'value' : frame.values.ravel('F'), - 'variable' : np.asarray(frame.columns).repeat(N), - 'date' : np.tile(np.asarray(frame.index), K)} - return DataFrame(data, columns=['date', 'variable', 'value']) -index = date_range('1/1/2000', periods=10000, freq='h') -df = DataFrame(randn(10000, 50), index=index, columns=range(50)) -pdf = unpivot(df) -f = lambda: pdf.pivot('date', 'variable', 'value') -""" - -reshape_pivot_time_series = Benchmark('f()', setup, - start_date=datetime(2012, 5, 1)) - -# Sparse key space, re: #2278 - -setup = common_setup + """ -NUM_ROWS = 1000 -for iter in range(10): - df = DataFrame({'A' : np.random.randint(50, size=NUM_ROWS), - 'B' : np.random.randint(50, size=NUM_ROWS), - 'C' : np.random.randint(-10,10, size=NUM_ROWS), - 'D' : np.random.randint(-10,10, size=NUM_ROWS), - 'E' : np.random.randint(10, size=NUM_ROWS), - 'F' : np.random.randn(NUM_ROWS)}) - idf = df.set_index(['A', 'B', 'C', 'D', 'E']) - if len(idf.index.unique()) == NUM_ROWS: - break -""" - -unstack_sparse_keyspace = Benchmark('idf.unstack()', setup, - start_date=datetime(2011, 10, 1)) - -# Melt - -setup = common_setup + """ -from pandas.core.reshape import melt -df = DataFrame(np.random.randn(10000, 3), columns=['A', 'B', 'C']) -df['id1'] = np.random.randint(0, 10, 10000) -df['id2'] = np.random.randint(100, 1000, 10000) -""" - -melt_dataframe = Benchmark("melt(df, id_vars=['id1', 'id2'])", setup, - start_date=datetime(2012, 8, 1)) diff --git a/vb_suite/run_suite.py b/vb_suite/run_suite.py deleted file mode 100755 index 43bf24faae43a..0000000000000 --- a/vb_suite/run_suite.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -from vbench.api import BenchmarkRunner -from suite import * - - -def run_process(): - runner = BenchmarkRunner(benchmarks, REPO_PATH, REPO_URL, - BUILD, DB_PATH, TMP_DIR, PREPARE, - always_clean=True, - run_option='eod', start_date=START_DATE, - module_dependencies=dependencies) - runner.run() - -if __name__ == '__main__': - run_process() diff --git a/vb_suite/series_methods.py b/vb_suite/series_methods.py deleted file mode 100644 index c545f419c2dec..0000000000000 --- a/vb_suite/series_methods.py +++ /dev/null @@ -1,39 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -setup = common_setup + """ -s1 = Series(np.random.randn(10000)) -s2 = Series(np.random.randint(1, 10, 10000)) -s3 = Series(np.random.randint(1, 10, 100000)).astype('int64') -values = [1,2] -s4 = s3.astype('object') -""" - -series_nlargest1 = Benchmark("s1.nlargest(3, keep='last');" - "s1.nlargest(3, keep='first')", - setup, - start_date=datetime(2014, 1, 25)) -series_nlargest2 = Benchmark("s2.nlargest(3, keep='last');" - "s2.nlargest(3, keep='first')", - setup, - start_date=datetime(2014, 1, 25)) - -series_nsmallest2 = Benchmark("s1.nsmallest(3, keep='last');" - "s1.nsmallest(3, keep='first')", - setup, - start_date=datetime(2014, 1, 25)) - -series_nsmallest2 = Benchmark("s2.nsmallest(3, keep='last');" - "s2.nsmallest(3, keep='first')", - setup, - start_date=datetime(2014, 1, 25)) - -series_isin_int64 = Benchmark('s3.isin(values)', - setup, - start_date=datetime(2014, 1, 25)) -series_isin_object = Benchmark('s4.isin(values)', - setup, - start_date=datetime(2014, 1, 25)) diff --git a/vb_suite/source/conf.py b/vb_suite/source/conf.py deleted file mode 100644 index d83448fd97d09..0000000000000 --- a/vb_suite/source/conf.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -# -# pandas documentation build configuration file, created by -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.append(os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../sphinxext')) - -sys.path.extend([ - - # numpy standard doc extensions - os.path.join(os.path.dirname(__file__), - '..', '../..', - 'sphinxext') - -]) - -# -- General configuration ----------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. sphinxext. - -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates', '_templates/autosummary'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -# source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'pandas' -copyright = u'2008-2011, the pandas development team' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -import pandas - -# version = '%s r%s' % (pandas.__version__, svn_version()) -version = '%s' % (pandas.__version__) - -# The full version, including alpha/beta/rc tags. -release = version - -# JP: added from sphinxdocs -autosummary_generate = True - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -# unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'agogo' - -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -# html_style = 'statsmodels.css' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = 'Vbench performance benchmarks for pandas' - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -html_use_modindex = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'performance' - - -# -- Options for LaTeX output -------------------------------------------- - -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'performance.tex', - u'pandas vbench Performance Benchmarks', - u'Wes McKinney', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_use_modindex = True - - -# Example configuration for intersphinx: refer to the Python standard library. -# intersphinx_mapping = {'http://docs.scipy.org/': None} -import glob -autosummary_generate = glob.glob("*.rst") diff --git a/vb_suite/source/themes/agogo/layout.html b/vb_suite/source/themes/agogo/layout.html deleted file mode 100644 index cd0f3d7ffc9c7..0000000000000 --- a/vb_suite/source/themes/agogo/layout.html +++ /dev/null @@ -1,95 +0,0 @@ -{# - agogo/layout.html - ~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the agogo theme, originally written - by Andi Albrecht. - - :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{% extends "basic/layout.html" %} - -{% block header %} -
    -
    - {%- if logo %} - - {%- endif %} - {%- block headertitle %} -

    {{ shorttitle|e }}

    - {%- endblock %} -
    - {%- for rellink in rellinks|reverse %} - {{ rellink[3] }} - {%- if not loop.last %}{{ reldelim2 }}{% endif %} - {%- endfor %} -
    -
    -
    -{% endblock %} - -{% block content %} -
    -
    - -
    - {%- block document %} - {{ super() }} - {%- endblock %} -
    -
    -
    -
    -{% endblock %} - -{% block footer %} - -{% endblock %} - -{% block relbar1 %}{% endblock %} -{% block relbar2 %}{% endblock %} diff --git a/vb_suite/source/themes/agogo/static/agogo.css_t b/vb_suite/source/themes/agogo/static/agogo.css_t deleted file mode 100644 index ef909b72e20f6..0000000000000 --- a/vb_suite/source/themes/agogo/static/agogo.css_t +++ /dev/null @@ -1,476 +0,0 @@ -/* - * agogo.css_t - * ~~~~~~~~~~~ - * - * Sphinx stylesheet -- agogo theme. - * - * :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -* { - margin: 0px; - padding: 0px; -} - -body { - font-family: {{ theme_bodyfont }}; - line-height: 1.4em; - color: black; - background-color: {{ theme_bgcolor }}; -} - - -/* Page layout */ - -div.header, div.content, div.footer { - max-width: {{ theme_pagewidth }}; - margin-left: auto; - margin-right: auto; -} - -div.header-wrapper { - background: {{ theme_headerbg }}; - padding: 1em 1em 0; - border-bottom: 3px solid #2e3436; - min-height: 0px; -} - - -/* Default body styles */ -a { - color: {{ theme_linkcolor }}; -} - -div.bodywrapper a, div.footer a { - text-decoration: underline; -} - -.clearer { - clear: both; -} - -.left { - float: left; -} - -.right { - float: right; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -h1, h2, h3, h4 { - font-family: {{ theme_headerfont }}; - font-weight: normal; - color: {{ theme_headercolor2 }}; - margin-bottom: .8em; -} - -h1 { - color: {{ theme_headercolor1 }}; -} - -h2 { - padding-bottom: .5em; - border-bottom: 1px solid {{ theme_headercolor2 }}; -} - -a.headerlink { - visibility: hidden; - color: #dddddd; - padding-left: .3em; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -img { - border: 0; -} - -pre { - background-color: #EEE; - padding: 0.5em; -} - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 2px 7px 1px 7px; - border-left: 0.2em solid black; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -dt:target, .highlighted { - background-color: #fbe54e; -} - -/* Header */ - -/* -div.header { - padding-top: 10px; - padding-bottom: 10px; -} -*/ - -div.header {} - -div.header h1 { - font-family: {{ theme_headerfont }}; - font-weight: normal; - font-size: 180%; - letter-spacing: .08em; -} - -div.header h1 a { - color: white; -} - -div.header div.rel { - text-decoration: none; -} -/* margin-top: 1em; */ - -div.header div.rel a { - margin-top: 1em; - color: {{ theme_headerlinkcolor }}; - letter-spacing: .1em; - text-transform: uppercase; - padding: 3px 1em; -} - -p.logo { - float: right; -} - -img.logo { - border: 0; -} - - -/* Content */ -div.content-wrapper { - background-color: white; - padding: 1em; -} -/* - padding-top: 20px; - padding-bottom: 20px; -*/ - -/* float: left; */ - -div.document { - max-width: {{ theme_documentwidth }}; -} - -div.body { - padding-right: 2em; - text-align: {{ theme_textalign }}; -} - -div.document ul { - margin: 1.5em; - list-style-type: square; -} - -div.document dd { - margin-left: 1.2em; - margin-top: .4em; - margin-bottom: 1em; -} - -div.document .section { - margin-top: 1.7em; -} -div.document .section:first-child { - margin-top: 0px; -} - -div.document div.highlight { - padding: 3px; - background-color: #eeeeec; - border-top: 2px solid #dddddd; - border-bottom: 2px solid #dddddd; - margin-top: .8em; - margin-bottom: .8em; -} - -div.document h2 { - margin-top: .7em; -} - -div.document p { - margin-bottom: .5em; -} - -div.document li.toctree-l1 { - margin-bottom: 1em; -} - -div.document .descname { - font-weight: bold; -} - -div.document .docutils.literal { - background-color: #eeeeec; - padding: 1px; -} - -div.document .docutils.xref.literal { - background-color: transparent; - padding: 0px; -} - -div.document blockquote { - margin: 1em; -} - -div.document ol { - margin: 1.5em; -} - - -/* Sidebar */ - - -div.sidebar { - width: {{ theme_sidebarwidth }}; - padding: 0 1em; - float: right; - font-size: .93em; -} - -div.sidebar a, div.header a { - text-decoration: none; -} - -div.sidebar a:hover, div.header a:hover { - text-decoration: underline; -} - -div.sidebar h3 { - color: #2e3436; - text-transform: uppercase; - font-size: 130%; - letter-spacing: .1em; -} - -div.sidebar ul { - list-style-type: none; -} - -div.sidebar li.toctree-l1 a { - display: block; - padding: 1px; - border: 1px solid #dddddd; - background-color: #eeeeec; - margin-bottom: .4em; - padding-left: 3px; - color: #2e3436; -} - -div.sidebar li.toctree-l2 a { - background-color: transparent; - border: none; - margin-left: 1em; - border-bottom: 1px solid #dddddd; -} - -div.sidebar li.toctree-l3 a { - background-color: transparent; - border: none; - margin-left: 2em; - border-bottom: 1px solid #dddddd; -} - -div.sidebar li.toctree-l2:last-child a { - border-bottom: none; -} - -div.sidebar li.toctree-l1.current a { - border-right: 5px solid {{ theme_headerlinkcolor }}; -} - -div.sidebar li.toctree-l1.current li.toctree-l2 a { - border-right: none; -} - - -/* Footer */ - -div.footer-wrapper { - background: {{ theme_footerbg }}; - border-top: 4px solid #babdb6; - padding-top: 10px; - padding-bottom: 10px; - min-height: 80px; -} - -div.footer, div.footer a { - color: #888a85; -} - -div.footer .right { - text-align: right; -} - -div.footer .left { - text-transform: uppercase; -} - - -/* Styles copied from basic theme */ - -img.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - clear: both; - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -/* -- viewcode extension ---------------------------------------------------- */ - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family:: {{ theme_bodyfont }}; -} - -div.viewcode-block:target { - margin: -1px -3px; - padding: 0 3px; - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} - -th.field-name { - white-space: nowrap; -} diff --git a/vb_suite/source/themes/agogo/static/bgfooter.png b/vb_suite/source/themes/agogo/static/bgfooter.png deleted file mode 100644 index 9ce5bdd902943fdf8b0c0ca6a545297e1e2cc665..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 434 zcmV;j0ZsmiP)Px#24YJ`L;%wO*8tD73qoQ5000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXD> z2Q(2CT#42I000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0003ENklR?sq9~H`=l5UI-{JW_f9!)=Hwush3JC}Y z1gFM&r>$lJNPt^*1k!w;l|obx>lr$2IOaI$n=(gBBaj^I0=y%@K5N&GIU&-%OE_~V zX=m=_j7d`hvubQRuF+xT63vIfWnC3%kKN*T3l7ob3nEC2R->wU1Y)4)(7_t^thiqb zj$CO7xBn9gg`*!MY$}SI|_*)!a*&V0w7h>cUb&$Grh37iJ=C%Yn c>}w1E0Z4f>1OEiDlmGw#07*qoM6N<$g4BwtIsgCw diff --git a/vb_suite/source/themes/agogo/static/bgtop.png b/vb_suite/source/themes/agogo/static/bgtop.png deleted file mode 100644 index a0d4709bac8f79943a817195c086461c8c4d5419..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 430 zcmV;f0a5;mP)Px#24YJ`L;zI)R{&FzA;Z4_000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXD> z2Q3AZhV-)l000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}0003ANklMo8vqN`cM=KwSQV|n zk}naE+VzlN;kK@Ej${PSkI$-R6-Yfp`zA;^O$`)7`gRi{-0i?owGIbX{p>Nc##93U z;sA|ayOYkG%F9M0iEMUM*s3NDYSS=KN2ht8Rv|7nv77i{NTO47R)}V_+2H~mL-nTR z_8j}*%6Qm8?#7NU2kM$#gcP&kO?iw|n}ynz+r-~FA9nKcZnfixWvZ&d28Cc_6&_Pe zMpbjI>9r+<=}NIDz4mCd3U++H?rrHcYxH&eeB|)>mnv*N#44ILM2zL6yU!VVWSrgp Y0Yu&#qm)=by8r+H07*qoM6N<$f@HC)j{pDw diff --git a/vb_suite/source/themes/agogo/theme.conf b/vb_suite/source/themes/agogo/theme.conf deleted file mode 100644 index 3fc88580f1ab4..0000000000000 --- a/vb_suite/source/themes/agogo/theme.conf +++ /dev/null @@ -1,19 +0,0 @@ -[theme] -inherit = basic -stylesheet = agogo.css -pygments_style = tango - -[options] -bodyfont = "Verdana", Arial, sans-serif -headerfont = "Georgia", "Times New Roman", serif -pagewidth = 70em -documentwidth = 50em -sidebarwidth = 20em -bgcolor = #eeeeec -headerbg = url(bgtop.png) top left repeat-x -footerbg = url(bgfooter.png) top left repeat-x -linkcolor = #ce5c00 -headercolor1 = #204a87 -headercolor2 = #3465a4 -headerlinkcolor = #fcaf3e -textalign = justify \ No newline at end of file diff --git a/vb_suite/sparse.py b/vb_suite/sparse.py deleted file mode 100644 index 53e2778ee0865..0000000000000 --- a/vb_suite/sparse.py +++ /dev/null @@ -1,65 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- - -setup = common_setup + """ -from pandas.core.sparse import SparseSeries, SparseDataFrame - -K = 50 -N = 50000 -rng = np.asarray(date_range('1/1/2000', periods=N, - freq='T')) - -# rng2 = np.asarray(rng).astype('M8[ns]').astype('i8') - -series = {} -for i in range(1, K + 1): - data = np.random.randn(N)[:-i] - this_rng = rng[:-i] - data[100:] = np.nan - series[i] = SparseSeries(data, index=this_rng) -""" -stmt = "SparseDataFrame(series)" - -bm_sparse1 = Benchmark(stmt, setup, name="sparse_series_to_frame", - start_date=datetime(2011, 6, 1)) - - -setup = common_setup + """ -from pandas.core.sparse import SparseDataFrame -""" - -stmt = "SparseDataFrame(columns=np.arange(100), index=np.arange(1000))" - -sparse_constructor = Benchmark(stmt, setup, name="sparse_frame_constructor", - start_date=datetime(2012, 6, 1)) - - -setup = common_setup + """ -s = pd.Series([np.nan] * 10000) -s[0] = 3.0 -s[100] = -1.0 -s[999] = 12.1 -s.index = pd.MultiIndex.from_product((range(10), range(10), range(10), range(10))) -ss = s.to_sparse() -""" - -stmt = "ss.to_coo(row_levels=[0, 1], column_levels=[2, 3], sort_labels=True)" - -sparse_series_to_coo = Benchmark(stmt, setup, name="sparse_series_to_coo", - start_date=datetime(2015, 1, 3)) - -setup = common_setup + """ -import scipy.sparse -import pandas.sparse.series -A = scipy.sparse.coo_matrix(([3.0, 1.0, 2.0], ([1, 0, 0], [0, 2, 3])), shape=(100, 100)) -""" - -stmt = "ss = pandas.sparse.series.SparseSeries.from_coo(A)" - -sparse_series_from_coo = Benchmark(stmt, setup, name="sparse_series_from_coo", - start_date=datetime(2015, 1, 3)) diff --git a/vb_suite/stat_ops.py b/vb_suite/stat_ops.py deleted file mode 100644 index 8d7c30dc9fdcf..0000000000000 --- a/vb_suite/stat_ops.py +++ /dev/null @@ -1,126 +0,0 @@ -from vbench.benchmark import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -""" - -#---------------------------------------------------------------------- -# nanops - -setup = common_setup + """ -s = Series(np.random.randn(100000), index=np.arange(100000)) -s[::2] = np.nan -""" - -stat_ops_series_std = Benchmark("s.std()", setup) - -#---------------------------------------------------------------------- -# ops by level - -setup = common_setup + """ -index = MultiIndex(levels=[np.arange(10), np.arange(100), np.arange(100)], - labels=[np.arange(10).repeat(10000), - np.tile(np.arange(100).repeat(100), 10), - np.tile(np.tile(np.arange(100), 100), 10)]) -random.shuffle(index.values) -df = DataFrame(np.random.randn(len(index), 4), index=index) -df_level = DataFrame(np.random.randn(100, 4), index=index.levels[1]) -""" - -stat_ops_level_frame_sum = \ - Benchmark("df.sum(level=1)", setup, - start_date=datetime(2011, 11, 15)) - -stat_ops_level_frame_sum_multiple = \ - Benchmark("df.sum(level=[0, 1])", setup, repeat=1, - start_date=datetime(2011, 11, 15)) - -stat_ops_level_series_sum = \ - Benchmark("df[1].sum(level=1)", setup, - start_date=datetime(2011, 11, 15)) - -stat_ops_level_series_sum_multiple = \ - Benchmark("df[1].sum(level=[0, 1])", setup, repeat=1, - start_date=datetime(2011, 11, 15)) - -sum_setup = common_setup + """ -df = DataFrame(np.random.randn(100000, 4)) -dfi = DataFrame(np.random.randint(1000, size=df.shape)) -""" - -stat_ops_frame_sum_int_axis_0 = \ - Benchmark("dfi.sum()", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_sum_float_axis_0 = \ - Benchmark("df.sum()", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_mean_int_axis_0 = \ - Benchmark("dfi.mean()", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_mean_float_axis_0 = \ - Benchmark("df.mean()", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_sum_int_axis_1 = \ - Benchmark("dfi.sum(1)", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_sum_float_axis_1 = \ - Benchmark("df.sum(1)", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_mean_int_axis_1 = \ - Benchmark("dfi.mean(1)", sum_setup, start_date=datetime(2013, 7, 25)) - -stat_ops_frame_mean_float_axis_1 = \ - Benchmark("df.mean(1)", sum_setup, start_date=datetime(2013, 7, 25)) - -#---------------------------------------------------------------------- -# rank - -setup = common_setup + """ -values = np.concatenate([np.arange(100000), - np.random.randn(100000), - np.arange(100000)]) -s = Series(values) -""" - -stats_rank_average = Benchmark('s.rank()', setup, - start_date=datetime(2011, 12, 12)) - -stats_rank_pct_average = Benchmark('s.rank(pct=True)', setup, - start_date=datetime(2014, 1, 16)) -stats_rank_pct_average_old = Benchmark('s.rank() / len(s)', setup, - start_date=datetime(2014, 1, 16)) -setup = common_setup + """ -values = np.random.randint(0, 100000, size=200000) -s = Series(values) -""" - -stats_rank_average_int = Benchmark('s.rank()', setup, - start_date=datetime(2011, 12, 12)) - -setup = common_setup + """ -df = DataFrame(np.random.randn(5000, 50)) -""" - -stats_rank2d_axis1_average = Benchmark('df.rank(1)', setup, - start_date=datetime(2011, 12, 12)) - -stats_rank2d_axis0_average = Benchmark('df.rank()', setup, - start_date=datetime(2011, 12, 12)) - -# rolling functions - -setup = common_setup + """ -arr = np.random.randn(100000) -""" - -stats_rolling_mean = Benchmark('rolling_mean(arr, 100)', setup, - start_date=datetime(2011, 6, 1)) - -# spearman correlation - -setup = common_setup + """ -df = DataFrame(np.random.randn(1000, 30)) -""" - -stats_corr_spearman = Benchmark("df.corr(method='spearman')", setup, - start_date=datetime(2011, 12, 4)) diff --git a/vb_suite/strings.py b/vb_suite/strings.py deleted file mode 100644 index 0948df5673a0d..0000000000000 --- a/vb_suite/strings.py +++ /dev/null @@ -1,59 +0,0 @@ -from vbench.api import Benchmark - -common_setup = """from .pandas_vb_common import * -""" - -setup = common_setup + """ -import string -import itertools as IT - -def make_series(letters, strlen, size): - return Series( - [str(x) for x in np.fromiter(IT.cycle(letters), count=size*strlen, dtype='|S1') - .view('|S{}'.format(strlen))]) - -many = make_series('matchthis'+string.ascii_uppercase, strlen=19, size=10000) # 31% matches -few = make_series('matchthis'+string.ascii_uppercase*42, strlen=19, size=10000) # 1% matches -""" - -strings_cat = Benchmark("many.str.cat(sep=',')", setup) -strings_title = Benchmark("many.str.title()", setup) -strings_count = Benchmark("many.str.count('matchthis')", setup) -strings_contains_many = Benchmark("many.str.contains('matchthis')", setup) -strings_contains_few = Benchmark("few.str.contains('matchthis')", setup) -strings_contains_many_noregex = Benchmark( - "many.str.contains('matchthis', regex=False)", setup) -strings_contains_few_noregex = Benchmark( - "few.str.contains('matchthis', regex=False)", setup) -strings_startswith = Benchmark("many.str.startswith('matchthis')", setup) -strings_endswith = Benchmark("many.str.endswith('matchthis')", setup) -strings_lower = Benchmark("many.str.lower()", setup) -strings_upper = Benchmark("many.str.upper()", setup) -strings_replace = Benchmark("many.str.replace(r'(matchthis)', r'\1\1')", setup) -strings_repeat = Benchmark( - "many.str.repeat(list(IT.islice(IT.cycle(range(1,4)),len(many))))", setup) -strings_match = Benchmark("many.str.match(r'mat..this')", setup) -strings_extract = Benchmark("many.str.extract(r'(\w*)matchthis(\w*)')", setup) -strings_join_split = Benchmark("many.str.join(r'--').str.split('--')", setup) -strings_join_split_expand = Benchmark("many.str.join(r'--').str.split('--',expand=True)", setup) -strings_len = Benchmark("many.str.len()", setup) -strings_findall = Benchmark("many.str.findall(r'[A-Z]+')", setup) -strings_pad = Benchmark("many.str.pad(100, side='both')", setup) -strings_center = Benchmark("many.str.center(100)", setup) -strings_slice = Benchmark("many.str.slice(5,15,2)", setup) -strings_strip = Benchmark("many.str.strip('matchthis')", setup) -strings_lstrip = Benchmark("many.str.lstrip('matchthis')", setup) -strings_rstrip = Benchmark("many.str.rstrip('matchthis')", setup) -strings_get = Benchmark("many.str.get(0)", setup) - -setup = setup + """ -s = make_series(string.ascii_uppercase, strlen=10, size=10000).str.join('|') -""" -strings_get_dummies = Benchmark("s.str.get_dummies('|')", setup) - -setup = common_setup + """ -import pandas.util.testing as testing -ser = Series(testing.makeUnicodeIndex()) -""" - -strings_encode_decode = Benchmark("ser.str.encode('utf-8').str.decode('utf-8')", setup) diff --git a/vb_suite/suite.py b/vb_suite/suite.py deleted file mode 100644 index 45053b6610896..0000000000000 --- a/vb_suite/suite.py +++ /dev/null @@ -1,164 +0,0 @@ -from vbench.api import Benchmark, GitRepo -from datetime import datetime - -import os - -modules = ['attrs_caching', - 'binary_ops', - 'ctors', - 'frame_ctor', - 'frame_methods', - 'groupby', - 'index_object', - 'indexing', - 'io_bench', - 'io_sql', - 'inference', - 'hdfstore_bench', - 'join_merge', - 'gil', - 'miscellaneous', - 'panel_ctor', - 'packers', - 'parser_vb', - 'panel_methods', - 'plotting', - 'reindex', - 'replace', - 'sparse', - 'strings', - 'reshape', - 'stat_ops', - 'timeseries', - 'timedelta', - 'eval'] - -by_module = {} -benchmarks = [] - -for modname in modules: - ref = __import__(modname) - by_module[modname] = [v for v in ref.__dict__.values() - if isinstance(v, Benchmark)] - benchmarks.extend(by_module[modname]) - -for bm in benchmarks: - assert(bm.name is not None) - -import getpass -import sys - -USERNAME = getpass.getuser() - -if sys.platform == 'darwin': - HOME = '/Users/%s' % USERNAME -else: - HOME = '/home/%s' % USERNAME - -try: - import ConfigParser - - config = ConfigParser.ConfigParser() - config.readfp(open(os.path.expanduser('~/.vbenchcfg'))) - - REPO_PATH = config.get('setup', 'repo_path') - REPO_URL = config.get('setup', 'repo_url') - DB_PATH = config.get('setup', 'db_path') - TMP_DIR = config.get('setup', 'tmp_dir') -except: - REPO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) - REPO_URL = 'git@github.com:pandas-dev/pandas.git' - DB_PATH = os.path.join(REPO_PATH, 'vb_suite/benchmarks.db') - TMP_DIR = os.path.join(HOME, 'tmp/vb_pandas') - -PREPARE = """ -python setup.py clean -""" -BUILD = """ -python setup.py build_ext --inplace -""" -dependencies = ['pandas_vb_common.py'] - -START_DATE = datetime(2010, 6, 1) - -# repo = GitRepo(REPO_PATH) - -RST_BASE = 'source' - -# HACK! - -# timespan = [datetime(2011, 1, 1), datetime(2012, 1, 1)] - - -def generate_rst_files(benchmarks): - import matplotlib as mpl - mpl.use('Agg') - import matplotlib.pyplot as plt - - vb_path = os.path.join(RST_BASE, 'vbench') - fig_base_path = os.path.join(vb_path, 'figures') - - if not os.path.exists(vb_path): - print('creating %s' % vb_path) - os.makedirs(vb_path) - - if not os.path.exists(fig_base_path): - print('creating %s' % fig_base_path) - os.makedirs(fig_base_path) - - for bmk in benchmarks: - print('Generating rst file for %s' % bmk.name) - rst_path = os.path.join(RST_BASE, 'vbench/%s.txt' % bmk.name) - - fig_full_path = os.path.join(fig_base_path, '%s.png' % bmk.name) - - # make the figure - plt.figure(figsize=(10, 6)) - ax = plt.gca() - bmk.plot(DB_PATH, ax=ax) - - start, end = ax.get_xlim() - - plt.xlim([start - 30, end + 30]) - plt.savefig(fig_full_path, bbox_inches='tight') - plt.close('all') - - fig_rel_path = 'vbench/figures/%s.png' % bmk.name - rst_text = bmk.to_rst(image_path=fig_rel_path) - with open(rst_path, 'w') as f: - f.write(rst_text) - - with open(os.path.join(RST_BASE, 'index.rst'), 'w') as f: - print >> f, """ -Performance Benchmarks -====================== - -These historical benchmark graphs were produced with `vbench -`__. - -The ``.pandas_vb_common`` setup script can be found here_ - -.. _here: https://github.com/pandas-dev/pandas/tree/master/vb_suite - -Produced on a machine with - - - Intel Core i7 950 processor - - (K)ubuntu Linux 12.10 - - Python 2.7.2 64-bit (Enthought Python Distribution 7.1-2) - - NumPy 1.6.1 - -.. toctree:: - :hidden: - :maxdepth: 3 -""" - for modname, mod_bmks in sorted(by_module.items()): - print >> f, ' vb_%s' % modname - modpath = os.path.join(RST_BASE, 'vb_%s.rst' % modname) - with open(modpath, 'w') as mh: - header = '%s\n%s\n\n' % (modname, '=' * len(modname)) - print >> mh, header - - for bmk in mod_bmks: - print >> mh, bmk.name - print >> mh, '-' * len(bmk.name) - print >> mh, '.. include:: vbench/%s.txt\n' % bmk.name diff --git a/vb_suite/test.py b/vb_suite/test.py deleted file mode 100644 index da30c3e1a5f76..0000000000000 --- a/vb_suite/test.py +++ /dev/null @@ -1,67 +0,0 @@ -from pandas import * -import matplotlib.pyplot as plt - -import sqlite3 - -from vbench.git import GitRepo - - -REPO_PATH = '/home/adam/code/pandas' -repo = GitRepo(REPO_PATH) - -con = sqlite3.connect('vb_suite/benchmarks.db') - -bmk = '36900a889961162138c140ce4ae3c205' -# bmk = '9d7b8c04b532df6c2d55ef497039b0ce' -bmk = '4481aa4efa9926683002a673d2ed3dac' -bmk = '00593cd8c03d769669d7b46585161726' -bmk = '3725ab7cd0a0657d7ae70f171c877cea' -bmk = '3cd376d6d6ef802cdea49ac47a67be21' -bmk2 = '459225186023853494bc345fd180f395' -bmk = 'c22ca82e0cfba8dc42595103113c7da3' -bmk = 'e0e651a8e9fbf0270ab68137f8b9df5f' -bmk = '96bda4b9a60e17acf92a243580f2a0c3' - - -def get_results(bmk): - results = con.execute( - "select * from results where checksum='%s'" % bmk).fetchall() - x = Series(dict((t[1], t[3]) for t in results)) - x.index = x.index.map(repo.timestamps.get) - x = x.sort_index() - return x - -x = get_results(bmk) - - -def graph1(): - dm_getitem = get_results('459225186023853494bc345fd180f395') - dm_getvalue = get_results('c22ca82e0cfba8dc42595103113c7da3') - - plt.figure() - ax = plt.gca() - - dm_getitem.plot(label='df[col][idx]', ax=ax) - dm_getvalue.plot(label='df.get_value(idx, col)', ax=ax) - - plt.ylabel('ms') - plt.legend(loc='best') - - -def graph2(): - bm = get_results('96bda4b9a60e17acf92a243580f2a0c3') - plt.figure() - ax = plt.gca() - - bm.plot(ax=ax) - plt.ylabel('ms') - -bm = get_results('36900a889961162138c140ce4ae3c205') -fig = plt.figure() -ax = plt.gca() -bm.plot(ax=ax) -fig.autofmt_xdate() - -plt.xlim([bm.dropna().index[0] - datetools.MonthEnd(), - bm.dropna().index[-1] + datetools.MonthEnd()]) -plt.ylabel('ms') diff --git a/vb_suite/test_perf.py b/vb_suite/test_perf.py deleted file mode 100755 index be546b72f9465..0000000000000 --- a/vb_suite/test_perf.py +++ /dev/null @@ -1,616 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -What ----- -vbench is a library which can be used to benchmark the performance -of a codebase over time. -Although vbench can collect data over many commites, generate plots -and other niceties, for Pull-Requests the important thing is the -performance of the HEAD commit against a known-good baseline. - -This script tries to automate the process of comparing these -two commits, and is meant to run out of the box on a fresh -clone. - -How ---- -These are the steps taken: -1) create a temp directory into which vbench will clone the temporary repo. -2) instantiate a vbench runner, using the local repo as the source repo. -3) perform a vbench run for the baseline commit, then the target commit. -4) pull the results for both commits from the db. use pandas to align -everything and calculate a ration for the timing information. -5) print the results to the log file and to stdout. - -""" - -# IMPORTANT NOTE -# -# This script should run on pandas versions at least as far back as 0.9.1. -# devs should be able to use the latest version of this script with -# any dusty old commit and expect it to "just work". -# One way in which this is useful is when collecting historical data, -# where writing some logic around this script may prove easier -# in some cases then running vbench directly (think perf bisection). -# -# *please*, when you modify this script for whatever reason, -# make sure you do not break its functionality when running under older -# pandas versions. -# Note that depreaction warnings are turned off in main(), so there's -# no need to change the actual code to supress such warnings. - -import shutil -import os -import sys -import argparse -import tempfile -import time -import re - -import random -import numpy as np - -import pandas as pd -from pandas import DataFrame, Series - -from suite import REPO_PATH -VB_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DEFAULT_MIN_DURATION = 0.01 -HEAD_COL="head[ms]" -BASE_COL="base[ms]" - -try: - import git # gitpython -except Exception: - print("Error: Please install the `gitpython` package\n") - sys.exit(1) - -class RevParseAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - import subprocess - cmd = 'git rev-parse --short -verify {0}^{{commit}}'.format(values) - rev_parse = subprocess.check_output(cmd, shell=True) - setattr(namespace, self.dest, rev_parse.strip()) - - -parser = argparse.ArgumentParser(description='Use vbench to measure and compare the performance of commits.') -parser.add_argument('-H', '--head', - help='Execute vbenches using the currently checked out copy.', - dest='head', - action='store_true', - default=False) -parser.add_argument('-b', '--base-commit', - help='The commit serving as performance baseline ', - type=str, action=RevParseAction) -parser.add_argument('-t', '--target-commit', - help='The commit to compare against the baseline (default: HEAD).', - type=str, action=RevParseAction) -parser.add_argument('--base-pickle', - help='name of pickle file with timings data generated by a former `-H -d FILE` run. '\ - 'filename must be of the form -*.* or specify --base-commit seperately', - type=str) -parser.add_argument('--target-pickle', - help='name of pickle file with timings data generated by a former `-H -d FILE` run '\ - 'filename must be of the form -*.* or specify --target-commit seperately', - type=str) -parser.add_argument('-m', '--min-duration', - help='Minimum duration (in ms) of baseline test for inclusion in report (default: %.3f).' % DEFAULT_MIN_DURATION, - type=float, - default=0.01) -parser.add_argument('-o', '--output', - metavar="", - dest='log_file', - help='Path of file in which to save the textual report (default: vb_suite.log).') -parser.add_argument('-d', '--outdf', - metavar="FNAME", - dest='outdf', - default=None, - help='Name of file to df.save() the result table into. Will overwrite') -parser.add_argument('-r', '--regex', - metavar="REGEX", - dest='regex', - default="", - help='Regex pat, only tests whose name matches the regext will be run.') -parser.add_argument('-s', '--seed', - metavar="SEED", - dest='seed', - default=1234, - type=int, - help='Integer value to seed PRNG with') -parser.add_argument('-n', '--repeats', - metavar="N", - dest='repeats', - default=3, - type=int, - help='Number of times to run each vbench, result value is the best of') -parser.add_argument('-c', '--ncalls', - metavar="N", - dest='ncalls', - default=3, - type=int, - help='Number of calls to in each repetition of a vbench') -parser.add_argument('-N', '--hrepeats', - metavar="N", - dest='hrepeats', - default=1, - type=int, - help='implies -H, number of times to run the vbench suite on the head commit.\n' - 'Each iteration will yield another column in the output' ) -parser.add_argument('-a', '--affinity', - metavar="a", - dest='affinity', - default=1, - type=int, - help='set processor affinity of process by default bind to cpu/core #1 only. ' - 'Requires the "affinity" or "psutil" python module, will raise Warning otherwise') -parser.add_argument('-u', '--burnin', - metavar="u", - dest='burnin', - default=1, - type=int, - help='Number of extra iteration per benchmark to perform first, then throw away. ' ) - -parser.add_argument('-S', '--stats', - default=False, - action='store_true', - help='when specified with -N, prints the output of describe() per vbench results. ' ) - -parser.add_argument('--temp-dir', - metavar="PATH", - default=None, - help='Specify temp work dir to use. ccache depends on builds being invoked from consistent directory.' ) - -parser.add_argument('-q', '--quiet', - default=False, - action='store_true', - help='Suppress report output to stdout. ' ) - -def get_results_df(db, rev): - """Takes a git commit hash and returns a Dataframe of benchmark results - """ - bench = DataFrame(db.get_benchmarks()) - results = DataFrame(map(list,db.get_rev_results(rev).values())) - - # Sinch vbench.db._reg_rev_results returns an unlabeled dict, - # we have to break encapsulation a bit. - results.columns = db._results.c.keys() - results = results.join(bench['name'], on='checksum').set_index("checksum") - return results - - -def prprint(s): - print("*** %s" % s) - -def pre_hook(): - import gc - gc.disable() - -def post_hook(): - import gc - gc.enable() - -def profile_comparative(benchmarks): - - from vbench.api import BenchmarkRunner - from vbench.db import BenchmarkDB - from vbench.git import GitRepo - from suite import BUILD, DB_PATH, PREPARE, dependencies - - TMP_DIR = args.temp_dir or tempfile.mkdtemp() - - try: - - prprint("Opening DB at '%s'...\n" % DB_PATH) - db = BenchmarkDB(DB_PATH) - - prprint("Initializing Runner...") - - # all in a good cause... - GitRepo._parse_commit_log = _parse_wrapper(args.base_commit) - - runner = BenchmarkRunner( - benchmarks, REPO_PATH, REPO_PATH, BUILD, DB_PATH, - TMP_DIR, PREPARE, always_clean=True, - # run_option='eod', start_date=START_DATE, - module_dependencies=dependencies) - - repo = runner.repo # (steal the parsed git repo used by runner) - h_head = args.target_commit or repo.shas[-1] - h_baseline = args.base_commit - - # ARGH. reparse the repo, without discarding any commits, - # then overwrite the previous parse results - # prprint("Slaughtering kittens...") - (repo.shas, repo.messages, - repo.timestamps, repo.authors) = _parse_commit_log(None,REPO_PATH, - args.base_commit) - - prprint('Target [%s] : %s\n' % (h_head, repo.messages.get(h_head, ""))) - prprint('Baseline [%s] : %s\n' % (h_baseline, - repo.messages.get(h_baseline, ""))) - - prprint("Removing any previous measurements for the commits.") - db.delete_rev_results(h_baseline) - db.delete_rev_results(h_head) - - # TODO: we could skip this, but we need to make sure all - # results are in the DB, which is a little tricky with - # start dates and so on. - prprint("Running benchmarks for baseline [%s]" % h_baseline) - runner._run_and_write_results(h_baseline) - - prprint("Running benchmarks for target [%s]" % h_head) - runner._run_and_write_results(h_head) - - prprint('Processing results...') - - head_res = get_results_df(db, h_head) - baseline_res = get_results_df(db, h_baseline) - - report_comparative(head_res,baseline_res) - - finally: - # print("Disposing of TMP_DIR: %s" % TMP_DIR) - shutil.rmtree(TMP_DIR) - -def prep_pickle_for_total(df, agg_name='median'): - """ - accepts a datafram resulting from invocation with -H -d o.pickle - If multiple data columns are present (-N was used), the - `agg_name` attr of the datafram will be used to reduce - them to a single value per vbench, df.median is used by defa - ult. - - Returns a datadrame of the form expected by prep_totals - """ - def prep(df): - agg = getattr(df,agg_name) - df = DataFrame(agg(1)) - cols = list(df.columns) - cols[0]='timing' - df.columns=cols - df['name'] = list(df.index) - return df - - return prep(df) - -def prep_totals(head_res, baseline_res): - """ - Each argument should be a dataframe with 'timing' and 'name' columns - where name is the name of the vbench. - - returns a 'totals' dataframe, suitable as input for print_report. - """ - head_res, baseline_res = head_res.align(baseline_res) - ratio = head_res['timing'] / baseline_res['timing'] - totals = DataFrame({HEAD_COL:head_res['timing'], - BASE_COL:baseline_res['timing'], - 'ratio':ratio, - 'name':baseline_res.name}, - columns=[HEAD_COL, BASE_COL, "ratio", "name"]) - totals = totals.ix[totals[HEAD_COL] > args.min_duration] - # ignore below threshold - totals = totals.dropna( - ).sort("ratio").set_index('name') # sort in ascending order - return totals - -def report_comparative(head_res,baseline_res): - try: - r=git.Repo(VB_DIR) - except: - import pdb - pdb.set_trace() - - totals = prep_totals(head_res,baseline_res) - - h_head = args.target_commit - h_baseline = args.base_commit - h_msg = b_msg = "Unknown" - try: - h_msg = r.commit(h_head).message.strip() - except git.exc.BadObject: - pass - try: - b_msg = r.commit(h_baseline).message.strip() - except git.exc.BadObject: - pass - - - print_report(totals,h_head=h_head,h_msg=h_msg, - h_baseline=h_baseline,b_msg=b_msg) - - if args.outdf: - prprint("The results DataFrame was written to '%s'\n" % args.outdf) - totals.save(args.outdf) - -def profile_head_single(benchmark): - import gc - results = [] - - # just in case - gc.collect() - - try: - from ctypes import cdll, CDLL - cdll.LoadLibrary("libc.so.6") - libc = CDLL("libc.so.6") - libc.malloc_trim(0) - except: - pass - - - N = args.hrepeats + args.burnin - - results = [] - try: - for i in range(N): - gc.disable() - d=dict() - - try: - d = benchmark.run() - - except KeyboardInterrupt: - raise - except Exception as e: # if a single vbench bursts into flames, don't die. - err="" - try: - err = d.get("traceback") - if err is None: - err = str(e) - except: - pass - print("%s died with:\n%s\nSkipping...\n" % (benchmark.name, err)) - - results.append(d.get('timing',np.nan)) - gc.enable() - gc.collect() - - finally: - gc.enable() - - if results: - # throw away the burn_in - results = results[args.burnin:] - sys.stdout.write('.') - sys.stdout.flush() - return Series(results, name=benchmark.name) - - # df = DataFrame(results) - # df.columns = ["name",HEAD_COL] - # return df.set_index("name")[HEAD_COL] - -def profile_head(benchmarks): - print( "Performing %d benchmarks (%d runs each)" % ( len(benchmarks), args.hrepeats)) - - ss= [profile_head_single(b) for b in benchmarks] - print("\n") - - results = DataFrame(ss) - results.columns=[ "#%d" %i for i in range(args.hrepeats)] - # results.index = ["#%d" % i for i in range(len(ss))] - # results = results.T - - shas, messages, _,_ = _parse_commit_log(None,REPO_PATH,base_commit="HEAD^") - print_report(results,h_head=shas[-1],h_msg=messages[-1]) - - - if args.outdf: - prprint("The results DataFrame was written to '%s'\n" % args.outdf) - DataFrame(results).save(args.outdf) - -def print_report(df,h_head=None,h_msg="",h_baseline=None,b_msg=""): - - name_width=45 - col_width = 10 - - hdr = ("{:%s}" % name_width).format("Test name") - hdr += ("|{:^%d}" % col_width)* len(df.columns) - hdr += "|" - hdr = hdr.format(*df.columns) - hdr = "-"*len(hdr) + "\n" + hdr + "\n" + "-"*len(hdr) + "\n" - ftr=hdr - s = "\n" - s+= "Invoked with :\n" - s+= "--ncalls: %s\n" % (args.ncalls or 'Auto') - s+= "--repeats: %s\n" % (args.repeats) - s+= "\n\n" - - s += hdr - # import ipdb - # ipdb.set_trace() - for i in range(len(df)): - lfmt = ("{:%s}" % name_width) - lfmt += ("| {:%d.4f} " % (col_width-2))* len(df.columns) - lfmt += "|\n" - s += lfmt.format(df.index[i],*list(df.iloc[i].values)) - - s+= ftr + "\n" - - s += "Ratio < 1.0 means the target commit is faster then the baseline.\n" - s += "Seed used: %d\n\n" % args.seed - - if h_head: - s += 'Target [%s] : %s\n' % (h_head, h_msg) - if h_baseline: - s += 'Base [%s] : %s\n\n' % ( - h_baseline, b_msg) - - stats_footer = "\n" - if args.stats : - try: - pd.options.display.expand_frame_repr=False - except: - pass - stats_footer += str(df.T.describe().T) + "\n\n" - - s+= stats_footer - logfile = open(args.log_file, 'w') - logfile.write(s) - logfile.close() - - if not args.quiet: - prprint(s) - - if args.stats and args.quiet: - prprint(stats_footer) - - prprint("Results were also written to the logfile at '%s'" % - args.log_file) - - - -def main(): - from suite import benchmarks - - if not args.log_file: - args.log_file = os.path.abspath( - os.path.join(REPO_PATH, 'vb_suite.log')) - - saved_dir = os.path.curdir - if args.outdf: - # not bullet-proof but enough for us - args.outdf = os.path.realpath(args.outdf) - - if args.log_file: - # not bullet-proof but enough for us - args.log_file = os.path.realpath(args.log_file) - - random.seed(args.seed) - np.random.seed(args.seed) - - if args.base_pickle and args.target_pickle: - baseline_res = prep_pickle_for_total(pd.load(args.base_pickle)) - target_res = prep_pickle_for_total(pd.load(args.target_pickle)) - - report_comparative(target_res, baseline_res) - sys.exit(0) - - if args.affinity is not None: - try: # use psutil rather then stale affinity module. Thanks @yarikoptic - import psutil - if hasattr(psutil.Process, 'set_cpu_affinity'): - psutil.Process(os.getpid()).set_cpu_affinity([args.affinity]) - print("CPU affinity set to %d" % args.affinity) - except ImportError: - print("-a/--affinity specified, but the 'psutil' module is not available, aborting.\n") - sys.exit(1) - - print("\n") - prprint("LOG_FILE = %s" % args.log_file) - if args.outdf: - prprint("PICKE_FILE = %s" % args.outdf) - - print("\n") - - # move away from the pandas root dir, to avoid possible import - # surprises - os.chdir(os.path.dirname(os.path.abspath(__file__))) - - benchmarks = [x for x in benchmarks if re.search(args.regex,x.name)] - - for b in benchmarks: - b.repeat = args.repeats - if args.ncalls: - b.ncalls = args.ncalls - - if benchmarks: - if args.head: - profile_head(benchmarks) - else: - profile_comparative(benchmarks) - else: - print( "No matching benchmarks") - - os.chdir(saved_dir) - -# hack , vbench.git ignores some commits, but we -# need to be able to reference any commit. -# modified from vbench.git -def _parse_commit_log(this,repo_path,base_commit=None): - from vbench.git import _convert_timezones - from pandas import Series - from dateutil import parser as dparser - - git_cmd = 'git --git-dir=%s/.git --work-tree=%s ' % (repo_path, repo_path) - githist = git_cmd + ('log --graph --pretty=format:'+ - '\"::%h::%cd::%s::%an\"'+ - ('%s..' % base_commit)+ - '> githist.txt') - os.system(githist) - githist = open('githist.txt').read() - os.remove('githist.txt') - - shas = [] - timestamps = [] - messages = [] - authors = [] - for line in githist.split('\n'): - if '*' not in line.split("::")[0]: # skip non-commit lines - continue - - _, sha, stamp, message, author = line.split('::', 4) - - # parse timestamp into datetime object - stamp = dparser.parse(stamp) - - shas.append(sha) - timestamps.append(stamp) - messages.append(message) - authors.append(author) - - # to UTC for now - timestamps = _convert_timezones(timestamps) - - shas = Series(shas, timestamps) - messages = Series(messages, shas) - timestamps = Series(timestamps, shas) - authors = Series(authors, shas) - return shas[::-1], messages[::-1], timestamps[::-1], authors[::-1] - -# even worse, monkey patch vbench -def _parse_wrapper(base_commit): - def inner(repo_path): - return _parse_commit_log(repo_path,base_commit) - return inner - -if __name__ == '__main__': - args = parser.parse_args() - if (not args.head - and not (args.base_commit and args.target_commit) - and not (args.base_pickle and args.target_pickle)): - parser.print_help() - sys.exit(1) - elif ((args.base_pickle or args.target_pickle) and not - (args.base_pickle and args.target_pickle)): - print("Must specify Both --base-pickle and --target-pickle.") - sys.exit(1) - - if ((args.base_pickle or args.target_pickle) and not - (args.base_commit and args.target_commit)): - if not args.base_commit: - print("base_commit not specified, Assuming base_pickle is named -foo.*") - args.base_commit = args.base_pickle.split('-')[0] - if not args.target_commit: - print("target_commit not specified, Assuming target_pickle is named -foo.*") - args.target_commit = args.target_pickle.split('-')[0] - - import warnings - warnings.filterwarnings('ignore',category=FutureWarning) - warnings.filterwarnings('ignore',category=DeprecationWarning) - - if args.base_commit and args.target_commit: - print("Verifying specified commits exist in repo...") - r=git.Repo(VB_DIR) - for c in [ args.base_commit, args.target_commit ]: - try: - msg = r.commit(c).message.strip() - except git.BadObject: - print("The commit '%s' was not found, aborting..." % c) - sys.exit(1) - else: - print("%s: %s" % (c,msg)) - - main() diff --git a/vb_suite/timedelta.py b/vb_suite/timedelta.py deleted file mode 100644 index 378968ea1379a..0000000000000 --- a/vb_suite/timedelta.py +++ /dev/null @@ -1,32 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime - -common_setup = """from .pandas_vb_common import * -from pandas import to_timedelta -""" - -#---------------------------------------------------------------------- -# conversion - -setup = common_setup + """ -arr = np.random.randint(0,1000,size=10000) -""" - -stmt = "to_timedelta(arr,unit='s')" -timedelta_convert_int = Benchmark(stmt, setup, start_date=datetime(2014, 1, 1)) - -setup = common_setup + """ -arr = np.random.randint(0,1000,size=10000) -arr = [ '{0} days'.format(i) for i in arr ] -""" - -stmt = "to_timedelta(arr)" -timedelta_convert_string = Benchmark(stmt, setup, start_date=datetime(2014, 1, 1)) - -setup = common_setup + """ -arr = np.random.randint(0,60,size=10000) -arr = [ '00:00:{0:02d}'.format(i) for i in arr ] -""" - -stmt = "to_timedelta(arr)" -timedelta_convert_string_seconds = Benchmark(stmt, setup, start_date=datetime(2014, 1, 1)) diff --git a/vb_suite/timeseries.py b/vb_suite/timeseries.py deleted file mode 100644 index 15bc89d62305f..0000000000000 --- a/vb_suite/timeseries.py +++ /dev/null @@ -1,445 +0,0 @@ -from vbench.api import Benchmark -from datetime import datetime -from pandas import * - -N = 100000 -try: - rng = date_range(start='1/1/2000', periods=N, freq='min') -except NameError: - rng = DatetimeIndex(start='1/1/2000', periods=N, freq='T') - def date_range(start=None, end=None, periods=None, freq=None): - return DatetimeIndex(start=start, end=end, periods=periods, offset=freq) - - -common_setup = """from .pandas_vb_common import * -from datetime import timedelta -N = 100000 - -rng = date_range(start='1/1/2000', periods=N, freq='T') - -if hasattr(Series, 'convert'): - Series.resample = Series.convert - -ts = Series(np.random.randn(N), index=rng) -""" - -#---------------------------------------------------------------------- -# Lookup value in large time series, hash map population - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=1500000, freq='S') -ts = Series(1, index=rng) -""" - -stmt = "ts[ts.index[len(ts) // 2]]; ts.index._cleanup()" -timeseries_large_lookup_value = Benchmark(stmt, setup, - start_date=datetime(2012, 1, 1)) - -#---------------------------------------------------------------------- -# Test slice minutely series - -timeseries_slice_minutely = Benchmark('ts[:10000]', common_setup) - -#---------------------------------------------------------------------- -# Test conversion - -setup = common_setup + """ - -""" - -timeseries_1min_5min_ohlc = Benchmark( - "ts[:10000].resample('5min', how='ohlc')", - common_setup, - start_date=datetime(2012, 5, 1)) - -timeseries_1min_5min_mean = Benchmark( - "ts[:10000].resample('5min', how='mean')", - common_setup, - start_date=datetime(2012, 5, 1)) - -#---------------------------------------------------------------------- -# Irregular alignment - -setup = common_setup + """ -lindex = np.random.permutation(N)[:N // 2] -rindex = np.random.permutation(N)[:N // 2] -left = Series(ts.values.take(lindex), index=ts.index.take(lindex)) -right = Series(ts.values.take(rindex), index=ts.index.take(rindex)) -""" - -timeseries_add_irregular = Benchmark('left + right', setup) - -#---------------------------------------------------------------------- -# Sort large irregular time series - -setup = common_setup + """ -N = 100000 -rng = date_range(start='1/1/2000', periods=N, freq='s') -rng = rng.take(np.random.permutation(N)) -ts = Series(np.random.randn(N), index=rng) -""" - -timeseries_sort_index = Benchmark('ts.sort_index()', setup, - start_date=datetime(2012, 4, 1)) - -#---------------------------------------------------------------------- -# Shifting, add offset - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=10000, freq='T') -""" - -datetimeindex_add_offset = Benchmark('rng + timedelta(minutes=2)', setup, - start_date=datetime(2012, 4, 1)) - -setup = common_setup + """ -N = 10000 -rng = date_range(start='1/1/1990', periods=N, freq='53s') -ts = Series(np.random.randn(N), index=rng) -dates = date_range(start='1/1/1990', periods=N * 10, freq='5s') -""" -timeseries_asof_single = Benchmark('ts.asof(dates[0])', setup, - start_date=datetime(2012, 4, 27)) - -timeseries_asof = Benchmark('ts.asof(dates)', setup, - start_date=datetime(2012, 4, 27)) - -setup = setup + 'ts[250:5000] = np.nan' - -timeseries_asof_nan = Benchmark('ts.asof(dates)', setup, - start_date=datetime(2012, 4, 27)) - -#---------------------------------------------------------------------- -# Time zone - -setup = common_setup + """ -rng = date_range(start='1/1/2000', end='3/1/2000', tz='US/Eastern') -""" - -timeseries_timestamp_tzinfo_cons = \ - Benchmark('rng[0]', setup, start_date=datetime(2012, 5, 5)) - -#---------------------------------------------------------------------- -# Resampling period - -setup = common_setup + """ -rng = period_range(start='1/1/2000', end='1/1/2001', freq='T') -ts = Series(np.random.randn(len(rng)), index=rng) -""" - -timeseries_period_downsample_mean = \ - Benchmark("ts.resample('D', how='mean')", setup, - start_date=datetime(2012, 4, 25)) - -setup = common_setup + """ -rng = date_range(start='1/1/2000', end='1/1/2001', freq='T') -ts = Series(np.random.randn(len(rng)), index=rng) -""" - -timeseries_timestamp_downsample_mean = \ - Benchmark("ts.resample('D', how='mean')", setup, - start_date=datetime(2012, 4, 25)) - -# GH 7754 -setup = common_setup + """ -rng = date_range(start='2000-01-01 00:00:00', - end='2000-01-01 10:00:00', freq='555000U') -int_ts = Series(5, rng, dtype='int64') -ts = int_ts.astype('datetime64[ns]') -""" - -timeseries_resample_datetime64 = Benchmark("ts.resample('1S', how='last')", setup) - -#---------------------------------------------------------------------- -# to_datetime - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=20000, freq='H') -strings = [x.strftime('%Y-%m-%d %H:%M:%S') for x in rng] -""" - -timeseries_to_datetime_iso8601 = \ - Benchmark('to_datetime(strings)', setup, - start_date=datetime(2012, 7, 11)) - -timeseries_to_datetime_iso8601_format = \ - Benchmark("to_datetime(strings, format='%Y-%m-%d %H:%M:%S')", setup, - start_date=datetime(2012, 7, 11)) - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=10000, freq='D') -strings = Series(rng.year*10000+rng.month*100+rng.day,dtype=np.int64).apply(str) -""" - -timeseries_to_datetime_YYYYMMDD = \ - Benchmark('to_datetime(strings,format="%Y%m%d")', setup, - start_date=datetime(2012, 7, 1)) - -setup = common_setup + """ -s = Series(['19MAY11','19MAY11:00:00:00']*100000) -""" -timeseries_with_format_no_exact = Benchmark("to_datetime(s,format='%d%b%y',exact=False)", \ - setup, start_date=datetime(2014, 11, 26)) -timeseries_with_format_replace = Benchmark("to_datetime(s.str.replace(':\S+$',''),format='%d%b%y')", \ - setup, start_date=datetime(2014, 11, 26)) - -# ---- infer_freq -# infer_freq - -setup = common_setup + """ -from pandas.tseries.frequencies import infer_freq -rng = date_range(start='1/1/1700', freq='D', periods=100000) -a = rng[:50000].append(rng[50002:]) -""" - -timeseries_infer_freq = \ - Benchmark('infer_freq(a)', setup, start_date=datetime(2012, 7, 1)) - -# setitem PeriodIndex - -setup = common_setup + """ -rng = period_range(start='1/1/1990', freq='S', periods=20000) -df = DataFrame(index=range(len(rng))) -""" - -period_setitem = \ - Benchmark("df['col'] = rng", setup, - start_date=datetime(2012, 8, 1)) - -setup = common_setup + """ -rng = date_range(start='1/1/2000 9:30', periods=10000, freq='S', tz='US/Eastern') -""" - -datetimeindex_normalize = \ - Benchmark('rng.normalize()', setup, - start_date=datetime(2012, 9, 1)) - -setup = common_setup + """ -from pandas.tseries.offsets import Second -s1 = date_range(start='1/1/2000', periods=100, freq='S') -curr = s1[-1] -slst = [] -for i in range(100): - slst.append(curr + Second()), periods=100, freq='S') - curr = slst[-1][-1] -""" - -# dti_append_tz = \ -# Benchmark('s1.append(slst)', setup, start_date=datetime(2012, 9, 1)) - - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=1000, freq='H') -df = DataFrame(np.random.randn(len(rng), 2), rng) -""" - -dti_reset_index = \ - Benchmark('df.reset_index()', setup, start_date=datetime(2012, 9, 1)) - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=1000, freq='H', - tz='US/Eastern') -df = DataFrame(np.random.randn(len(rng), 2), index=rng) -""" - -dti_reset_index_tz = \ - Benchmark('df.reset_index()', setup, start_date=datetime(2012, 9, 1)) - -setup = common_setup + """ -rng = date_range(start='1/1/2000', periods=1000, freq='T') -index = rng.repeat(10) -""" - -datetimeindex_unique = Benchmark('index.unique()', setup, - start_date=datetime(2012, 7, 1)) - -# tz_localize with infer argument. This is an attempt to emulate the results -# of read_csv with duplicated data. Not passing infer_dst will fail -setup = common_setup + """ -dst_rng = date_range(start='10/29/2000 1:00:00', - end='10/29/2000 1:59:59', freq='S') -index = date_range(start='10/29/2000', end='10/29/2000 00:59:59', freq='S') -index = index.append(dst_rng) -index = index.append(dst_rng) -index = index.append(date_range(start='10/29/2000 2:00:00', - end='10/29/2000 3:00:00', freq='S')) -""" - -datetimeindex_infer_dst = \ -Benchmark('index.tz_localize("US/Eastern", infer_dst=True)', - setup, start_date=datetime(2013, 9, 30)) - - -#---------------------------------------------------------------------- -# Resampling: fast-path various functions - -setup = common_setup + """ -rng = date_range(start='20130101',periods=100000,freq='50L') -df = DataFrame(np.random.randn(100000,2),index=rng) -""" - -dataframe_resample_mean_string = \ - Benchmark("df.resample('1s', how='mean')", setup) - -dataframe_resample_mean_numpy = \ - Benchmark("df.resample('1s', how=np.mean)", setup) - -dataframe_resample_min_string = \ - Benchmark("df.resample('1s', how='min')", setup) - -dataframe_resample_min_numpy = \ - Benchmark("df.resample('1s', how=np.min)", setup) - -dataframe_resample_max_string = \ - Benchmark("df.resample('1s', how='max')", setup) - -dataframe_resample_max_numpy = \ - Benchmark("df.resample('1s', how=np.max)", setup) - - -#---------------------------------------------------------------------- -# DatetimeConverter - -setup = common_setup + """ -from pandas.tseries.converter import DatetimeConverter -""" - -datetimeindex_converter = \ - Benchmark('DatetimeConverter.convert(rng, None, None)', - setup, start_date=datetime(2013, 1, 1)) - -# Adding custom business day -setup = common_setup + """ -import datetime as dt -import pandas as pd -try: - import pandas.tseries.holiday -except ImportError: - pass -import numpy as np - -date = dt.datetime(2011,1,1) -dt64 = np.datetime64('2011-01-01 09:00Z') -hcal = pd.tseries.holiday.USFederalHolidayCalendar() - -day = pd.offsets.Day() -year = pd.offsets.YearBegin() -cday = pd.offsets.CustomBusinessDay() -cmb = pd.offsets.CustomBusinessMonthBegin(calendar=hcal) -cme = pd.offsets.CustomBusinessMonthEnd(calendar=hcal) - -cdayh = pd.offsets.CustomBusinessDay(calendar=hcal) -""" -timeseries_day_incr = Benchmark("date + day",setup) - -timeseries_day_apply = Benchmark("day.apply(date)",setup) - -timeseries_year_incr = Benchmark("date + year",setup) - -timeseries_year_apply = Benchmark("year.apply(date)",setup) - -timeseries_custom_bday_incr = \ - Benchmark("date + cday",setup) - -timeseries_custom_bday_decr = \ - Benchmark("date - cday",setup) - -timeseries_custom_bday_apply = \ - Benchmark("cday.apply(date)",setup) - -timeseries_custom_bday_apply_dt64 = \ - Benchmark("cday.apply(dt64)",setup) - -timeseries_custom_bday_cal_incr = \ - Benchmark("date + 1 * cdayh",setup) - -timeseries_custom_bday_cal_decr = \ - Benchmark("date - 1 * cdayh",setup) - -timeseries_custom_bday_cal_incr_n = \ - Benchmark("date + 10 * cdayh",setup) - -timeseries_custom_bday_cal_incr_neg_n = \ - Benchmark("date - 10 * cdayh",setup) - -# Increment custom business month -timeseries_custom_bmonthend_incr = \ - Benchmark("date + cme",setup) - -timeseries_custom_bmonthend_incr_n = \ - Benchmark("date + 10 * cme",setup) - -timeseries_custom_bmonthend_decr_n = \ - Benchmark("date - 10 * cme",setup) - -timeseries_custom_bmonthbegin_incr_n = \ - Benchmark("date + 10 * cmb",setup) - -timeseries_custom_bmonthbegin_decr_n = \ - Benchmark("date - 10 * cmb",setup) - - -#---------------------------------------------------------------------- -# month/quarter/year start/end accessors - -setup = common_setup + """ -N = 10000 -rng = date_range(start='1/1/1', periods=N, freq='B') -""" - -timeseries_is_month_start = Benchmark('rng.is_month_start', setup, - start_date=datetime(2014, 4, 1)) - -#---------------------------------------------------------------------- -# iterate over DatetimeIndex/PeriodIndex -setup = common_setup + """ -N = 1000000 -M = 10000 -idx1 = date_range(start='20140101', freq='T', periods=N) -idx2 = period_range(start='20140101', freq='T', periods=N) - -def iter_n(iterable, n=None): - i = 0 - for _ in iterable: - i += 1 - if n is not None and i > n: - break -""" - -timeseries_iter_datetimeindex = Benchmark('iter_n(idx1)', setup) - -timeseries_iter_periodindex = Benchmark('iter_n(idx2)', setup) - -timeseries_iter_datetimeindex_preexit = Benchmark('iter_n(idx1, M)', setup) - -timeseries_iter_periodindex_preexit = Benchmark('iter_n(idx2, M)', setup) - - -#---------------------------------------------------------------------- -# apply an Offset to a DatetimeIndex -setup = common_setup + """ -N = 100000 -idx1 = date_range(start='20140101', freq='T', periods=N) -delta_offset = pd.offsets.Day() -fast_offset = pd.offsets.DateOffset(months=2, days=2) -slow_offset = pd.offsets.BusinessDay() - -""" - -timeseries_datetimeindex_offset_delta = Benchmark('idx1 + delta_offset', setup) -timeseries_datetimeindex_offset_fast = Benchmark('idx1 + fast_offset', setup) -timeseries_datetimeindex_offset_slow = Benchmark('idx1 + slow_offset', setup) - -# apply an Offset to a Series containing datetime64 values -setup = common_setup + """ -N = 100000 -s = Series(date_range(start='20140101', freq='T', periods=N)) -delta_offset = pd.offsets.Day() -fast_offset = pd.offsets.DateOffset(months=2, days=2) -slow_offset = pd.offsets.BusinessDay() - -""" - -timeseries_series_offset_delta = Benchmark('s + delta_offset', setup) -timeseries_series_offset_fast = Benchmark('s + fast_offset', setup) -timeseries_series_offset_slow = Benchmark('s + slow_offset', setup) diff --git a/versioneer.py b/versioneer.py index 104e8e97c6bd6..01adaa248dbd4 100644 --- a/versioneer.py +++ b/versioneer.py @@ -12,7 +12,7 @@ * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy * [![Latest Version] (https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) +](https://pypi.org/project/versioneer/) * [![Build Status] (https://travis-ci.org/warner/python-versioneer.png?branch=master) ](https://travis-ci.org/warner/python-versioneer) @@ -352,7 +352,7 @@ import sys -class VersioneerConfig: +class VersioneerConfig(object): pass @@ -464,7 +464,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): print("unable to run %s (error)" % dispcmd) return None return stdout -LONG_VERSION_PY['git'] = ''' + + +LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -606,11 +608,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d @@ -619,7 +621,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs-tags)) if verbose: @@ -960,11 +962,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -973,7 +975,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs-tags)) if verbose:

  • +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�Ugq`J{x#amKtph0o(p{7h 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 literal 0 HcmV?d00001 diff --git a/doc/cheatsheet/README.txt b/doc/cheatsheet/README.txt index e2f6ec042e9cc..d32fe5bcd05a6 100644 --- a/doc/cheatsheet/README.txt +++ b/doc/cheatsheet/README.txt @@ -2,3 +2,7 @@ The Pandas Cheat Sheet was created using Microsoft Powerpoint 2013. To create the PDF version, within Powerpoint, simply do a "Save As" and pick "PDF' as the format. +This cheat sheet was inspired by the RstudioData Wrangling Cheatsheet[1], written by Irv Lustig, Princeton Consultants[2]. + +[1]: https://www.rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf +[2]: http://www.princetonoptimization.com/ diff --git a/doc/make.py b/doc/make.py index 30cd2ad8b61c9..6ffbd3ef86e68 100755 --- a/doc/make.py +++ b/doc/make.py @@ -1,476 +1,341 @@ #!/usr/bin/env python - """ Python script for building documentation. To build the docs you must have all optional dependencies for pandas installed. See the installation instructions for a list of these. -Note: currently latex builds do not work because of table formats that are not -supported in the latex generation. - -2014-01-30: Latex has some issues but 'latex_forced' works ok for 0.13.0-400 or so - Usage ----- -python make.py clean -python make.py html + $ python make.py clean + $ python make.py html + $ python make.py latex """ -from __future__ import print_function - -import io -import glob # noqa +import importlib +import sys import os import shutil -import sys -from contextlib import contextmanager - -import sphinx # noqa +import csv +import subprocess import argparse -import jinja2 # noqa - -os.environ['PYTHONPATH'] = '..' - -SPHINX_BUILD = 'sphinxbuild' - - -def upload_dev(user='pandas'): - 'push a copy to the pydata dev directory' - if os.system('cd build/html; rsync -avz . {0}@pandas.pydata.org' - ':/usr/share/nginx/pandas/pandas-docs/dev/ -essh'.format(user)): - raise SystemExit('Upload to Pydata Dev failed') - - -def upload_dev_pdf(user='pandas'): - 'push a copy to the pydata dev directory' - if os.system('cd build/latex; scp pandas.pdf {0}@pandas.pydata.org' - ':/usr/share/nginx/pandas/pandas-docs/dev/'.format(user)): - raise SystemExit('PDF upload to Pydata Dev failed') - - -def upload_stable(user='pandas'): - 'push a copy to the pydata stable directory' - if os.system('cd build/html; rsync -avz . {0}@pandas.pydata.org' - ':/usr/share/nginx/pandas/pandas-docs/stable/ -essh'.format(user)): - raise SystemExit('Upload to stable failed') - - -def upload_stable_pdf(user='pandas'): - 'push a copy to the pydata dev directory' - if os.system('cd build/latex; scp pandas.pdf {0}@pandas.pydata.org' - ':/usr/share/nginx/pandas/pandas-docs/stable/'.format(user)): - raise SystemExit('PDF upload to stable failed') - - -def upload_prev(ver, doc_root='./', user='pandas'): - 'push a copy of older release to appropriate version directory' - local_dir = doc_root + 'build/html' - remote_dir = '/usr/share/nginx/pandas/pandas-docs/version/%s/' % ver - cmd = 'cd %s; rsync -avz . %s@pandas.pydata.org:%s -essh' - cmd = cmd % (local_dir, user, remote_dir) - print(cmd) - if os.system(cmd): - raise SystemExit( - 'Upload to %s from %s failed' % (remote_dir, local_dir)) - - local_dir = doc_root + 'build/latex' - pdf_cmd = 'cd %s; scp pandas.pdf %s@pandas.pydata.org:%s' - pdf_cmd = pdf_cmd % (local_dir, user, remote_dir) - if os.system(pdf_cmd): - raise SystemExit('Upload PDF to %s from %s failed' % (ver, doc_root)) - -def build_pandas(): - os.chdir('..') - os.system('python setup.py clean') - os.system('python setup.py build_ext --inplace') - os.chdir('doc') - -def build_prev(ver): - if os.system('git checkout v%s' % ver) != 1: - os.chdir('..') - os.system('python setup.py clean') - os.system('python setup.py build_ext --inplace') - os.chdir('doc') - os.system('python make.py clean') - os.system('python make.py html') - os.system('python make.py latex') - os.system('git checkout master') - - -def clean(): - if os.path.exists('build'): - shutil.rmtree('build') - - if os.path.exists('source/generated'): - shutil.rmtree('source/generated') - +import webbrowser +import docutils +import docutils.parsers.rst -@contextmanager -def cleanup_nb(nb): - try: - yield - finally: - try: - os.remove(nb + '.executed') - except OSError: - pass +DOC_PATH = os.path.dirname(os.path.abspath(__file__)) +SOURCE_PATH = os.path.join(DOC_PATH, 'source') +BUILD_PATH = os.path.join(DOC_PATH, 'build') +REDIRECTS_FILE = os.path.join(DOC_PATH, 'redirects.csv') -def get_kernel(): - """Find the kernel name for your python version""" - return 'python%s' % sys.version_info.major - -def execute_nb(src, dst, allow_errors=False, timeout=1000, kernel_name=''): - """ - Execute notebook in `src` and write the output to `dst` - - Parameters - ---------- - src, dst: str - path to notebook - allow_errors: bool - timeout: int - kernel_name: str - defualts to value set in notebook metadata - - Returns - ------- - dst: str +class DocBuilder: """ - import nbformat - from nbconvert.preprocessors import ExecutePreprocessor - - with io.open(src, encoding='utf-8') as f: - nb = nbformat.read(f, as_version=4) - - ep = ExecutePreprocessor(allow_errors=allow_errors, - timeout=timeout, - kernel_name=kernel_name) - ep.preprocess(nb, resources={}) + Class to wrap the different commands of this script. - with io.open(dst, 'wt', encoding='utf-8') as f: - nbformat.write(nb, f) - return dst - - -def convert_nb(src, dst, to='html', template_file='basic'): - """ - Convert a notebook `src`. - - Parameters - ---------- - src, dst: str - filepaths - to: {'rst', 'html'} - format to export to - template_file: str - name of template file to use. Default 'basic' + All public methods of this class can be called as parameters of the + script. """ - from nbconvert import HTMLExporter, RSTExporter - - dispatch = {'rst': RSTExporter, 'html': HTMLExporter} - exporter = dispatch[to.lower()](template_file=template_file) - - (body, resources) = exporter.from_filename(src) - with io.open(dst, 'wt', encoding='utf-8') as f: - f.write(body) - return dst - - -def html(): - check_build() - - notebooks = [ - 'source/html-styling.ipynb', - ] + def __init__(self, num_jobs=0, include_api=True, single_doc=None, + verbosity=0, warnings_are_errors=False): + self.num_jobs = num_jobs + self.verbosity = verbosity + self.warnings_are_errors = warnings_are_errors + + if single_doc: + single_doc = self._process_single_doc(single_doc) + include_api = False + os.environ['SPHINX_PATTERN'] = single_doc + elif not include_api: + os.environ['SPHINX_PATTERN'] = '-api' + + self.single_doc_html = None + if single_doc and single_doc.endswith('.rst'): + self.single_doc_html = os.path.splitext(single_doc)[0] + '.html' + elif single_doc: + self.single_doc_html = 'reference/api/pandas.{}.html'.format( + single_doc) + + def _process_single_doc(self, single_doc): + """ + Make sure the provided value for --single is a path to an existing + .rst/.ipynb file, or a pandas object that can be imported. + + For example, categorial.rst or pandas.DataFrame.head. For the latter, + return the corresponding file path + (e.g. reference/api/pandas.DataFrame.head.rst). + """ + base_name, extension = os.path.splitext(single_doc) + if extension in ('.rst', '.ipynb'): + if os.path.exists(os.path.join(SOURCE_PATH, single_doc)): + return single_doc + else: + raise FileNotFoundError('File {} not found'.format(single_doc)) - for nb in notebooks: - with cleanup_nb(nb): + elif single_doc.startswith('pandas.'): try: - print("Converting %s" % nb) - kernel_name = get_kernel() - executed = execute_nb(nb, nb + '.executed', allow_errors=True, - kernel_name=kernel_name) - convert_nb(executed, nb.rstrip('.ipynb') + '.html') - except (ImportError, IndexError) as e: - print(e) - print("Failed to convert %s" % nb) - - if os.system('sphinx-build -P -b html -d build/doctrees ' - 'source build/html'): - raise SystemExit("Building HTML failed.") - try: - # remove stale file - os.remove('source/html-styling.html') - os.remove('build/html/pandas.zip') - except: - pass - - -def zip_html(): - try: - print("\nZipping up HTML docs...") - # just in case the wonky build box doesn't have zip - # don't fail this. - os.system('cd build; rm -f html/pandas.zip; zip html/pandas.zip -r -q html/* ') - print("\n") - except: - pass - -def latex(): - check_build() - if sys.platform != 'win32': - # LaTeX format. - if os.system('sphinx-build -j 2 -b latex -d build/doctrees ' - 'source build/latex'): - raise SystemExit("Building LaTeX failed.") - # Produce pdf. - - os.chdir('build/latex') - - # Call the makefile produced by sphinx... - if os.system('make'): - print("Rendering LaTeX failed.") - print("You may still be able to get a usable PDF file by going into 'build/latex'") - print("and executing 'pdflatex pandas.tex' for the requisite number of passes.") - print("Or using the 'latex_forced' target") - raise SystemExit - - os.chdir('../..') - else: - print('latex build has not been tested on windows') - -def latex_forced(): - check_build() - if sys.platform != 'win32': - # LaTeX format. - if os.system('sphinx-build -j 2 -b latex -d build/doctrees ' - 'source build/latex'): - raise SystemExit("Building LaTeX failed.") - # Produce pdf. - - os.chdir('build/latex') - - # Manually call pdflatex, 3 passes should ensure latex fixes up - # all the required cross-references and such. - os.system('pdflatex -interaction=nonstopmode pandas.tex') - os.system('pdflatex -interaction=nonstopmode pandas.tex') - os.system('pdflatex -interaction=nonstopmode pandas.tex') - raise SystemExit("You should check the file 'build/latex/pandas.pdf' for problems.") - - os.chdir('../..') - else: - print('latex build has not been tested on windows') - - -def check_build(): - build_dirs = [ - 'build', 'build/doctrees', 'build/html', - 'build/latex', 'build/plots', 'build/_static', - 'build/_templates'] - for d in build_dirs: - try: - os.mkdir(d) - except OSError: - pass - - -def all(): - # clean() - html() - - -def auto_dev_build(debug=False): - msg = '' - try: - step = 'clean' - clean() - step = 'html' - html() - step = 'upload dev' - upload_dev() - if not debug: - sendmail(step) - - step = 'latex' - latex() - step = 'upload pdf' - upload_dev_pdf() - if not debug: - sendmail(step) - except (Exception, SystemExit) as inst: - msg = str(inst) + '\n' - sendmail(step, '[ERROR] ' + msg) - - -def sendmail(step=None, err_msg=None): - from_name, to_name = _get_config() - - if step is None: - step = '' - - if err_msg is None or '[ERROR]' not in err_msg: - msgstr = 'Daily docs %s completed successfully' % step - subject = "DOC: %s successful" % step - else: - msgstr = err_msg - subject = "DOC: %s failed" % step - - import smtplib - from email.MIMEText import MIMEText - msg = MIMEText(msgstr) - msg['Subject'] = subject - msg['From'] = from_name - msg['To'] = to_name - - server_str, port, login, pwd = _get_credentials() - server = smtplib.SMTP(server_str, port) - server.ehlo() - server.starttls() - server.ehlo() - - server.login(login, pwd) - try: - server.sendmail(from_name, to_name, msg.as_string()) - finally: - server.close() - - -def _get_dir(subdir=None): - import getpass - USERNAME = getpass.getuser() - if sys.platform == 'darwin': - HOME = '/Users/%s' % USERNAME - else: - HOME = '/home/%s' % USERNAME - - if subdir is None: - subdir = '/code/scripts/config' - conf_dir = '%s/%s' % (HOME, subdir) - return conf_dir - - -def _get_credentials(): - tmp_dir = _get_dir() - cred = '%s/credentials' % tmp_dir - with open(cred, 'r') as fh: - server, port, un, domain = fh.read().split(',') - port = int(port) - login = un + '@' + domain + '.com' - - import base64 - with open('%s/cron_email_pwd' % tmp_dir, 'r') as fh: - pwd = base64.b64decode(fh.read()) - - return server, port, login, pwd - - -def _get_config(): - tmp_dir = _get_dir() - with open('%s/addresses' % tmp_dir, 'r') as fh: - from_name, to_name = fh.read().split(',') - return from_name, to_name - -funcd = { - 'html': html, - 'zip_html': zip_html, - 'upload_dev': upload_dev, - 'upload_stable': upload_stable, - 'upload_dev_pdf': upload_dev_pdf, - 'upload_stable_pdf': upload_stable_pdf, - 'latex': latex, - 'latex_forced': latex_forced, - 'clean': clean, - 'auto_dev': auto_dev_build, - 'auto_debug': lambda: auto_dev_build(True), - 'build_pandas': build_pandas, - 'all': all, -} - -small_docs = False - -# current_dir = os.getcwd() -# os.chdir(os.path.dirname(os.path.join(current_dir, __file__))) - -import argparse -argparser = argparse.ArgumentParser(description=""" -pandas documentation builder -""".strip()) - -# argparser.add_argument('-arg_name', '--arg_name', -# metavar='label for arg help', -# type=str|etc, -# nargs='N|*|?|+|argparse.REMAINDER', -# required=False, -# #choices='abc', -# help='help string', -# action='store|store_true') - -# args = argparser.parse_args() - -#print args.accumulate(args.integers) - -def generate_index(api=True, single=False, **kwds): - from jinja2 import Template - with open("source/index.rst.template") as f: - t = Template(f.read()) - - with open("source/index.rst","w") as f: - f.write(t.render(api=api,single=single,**kwds)) + obj = pandas # noqa: F821 + for name in single_doc.split('.'): + obj = getattr(obj, name) + except AttributeError: + raise ImportError('Could not import {}'.format(single_doc)) + else: + return single_doc[len('pandas.'):] + else: + raise ValueError(('--single={} not understood. Value should be a ' + 'valid path to a .rst or .ipynb file, or a ' + 'valid pandas object (e.g. categorical.rst or ' + 'pandas.DataFrame.head)').format(single_doc)) + + @staticmethod + def _run_os(*args): + """ + Execute a command as a OS terminal. + + Parameters + ---------- + *args : list of str + Command and parameters to be executed + + Examples + -------- + >>> DocBuilder()._run_os('python', '--version') + """ + subprocess.check_call(args, stdout=sys.stdout, stderr=sys.stderr) + + def _sphinx_build(self, kind): + """ + Call sphinx to build documentation. + + Attribute `num_jobs` from the class is used. + + Parameters + ---------- + kind : {'html', 'latex'} + + Examples + -------- + >>> DocBuilder(num_jobs=4)._sphinx_build('html') + """ + if kind not in ('html', 'latex'): + raise ValueError('kind must be html or latex, ' + 'not {}'.format(kind)) + + cmd = ['sphinx-build', '-b', kind] + if self.num_jobs: + cmd += ['-j', str(self.num_jobs)] + if self.warnings_are_errors: + cmd += ['-W', '--keep-going'] + if self.verbosity: + cmd.append('-{}'.format('v' * self.verbosity)) + cmd += ['-d', os.path.join(BUILD_PATH, 'doctrees'), + SOURCE_PATH, os.path.join(BUILD_PATH, kind)] + return subprocess.call(cmd) + + def _open_browser(self, single_doc_html): + """ + Open a browser tab showing single + """ + url = os.path.join('file://', DOC_PATH, 'build', 'html', + single_doc_html) + webbrowser.open(url, new=2) + + def _get_page_title(self, page): + """ + Open the rst file `page` and extract its title. + """ + fname = os.path.join(SOURCE_PATH, '{}.rst'.format(page)) + option_parser = docutils.frontend.OptionParser( + components=(docutils.parsers.rst.Parser,)) + doc = docutils.utils.new_document( + '', + option_parser.get_default_values()) + with open(fname) as f: + data = f.read() + + parser = docutils.parsers.rst.Parser() + # do not generate any warning when parsing the rst + with open(os.devnull, 'a') as f: + doc.reporter.stream = f + parser.parse(data, doc) + + section = next(node for node in doc.children + if isinstance(node, docutils.nodes.section)) + title = next(node for node in section.children + if isinstance(node, docutils.nodes.title)) + + return title.astext() + + def _add_redirects(self): + """ + Create in the build directory an html file with a redirect, + for every row in REDIRECTS_FILE. + """ + html = ''' + + + + + +